singularity 安装教程

[TOC]

首先是下载源码,singularity下载地址:http://singularity.lbl.gov/all-releases

下载到本地之后,解压

1
2
3
4
5
6
tar xvf singularity-2.2.1.tar.gz
mkdir /gensoft/singularity
cd singularity-2.2.1/
./configure
make
make install

到这里其实singularity已经安装完毕,下面是参照官网说明做的一个镜像

1
2
3
4
5
BootStrap: yum
OSVersion: 7
MirrorURL: http://mirror.centos.org/centos/7/os/x86_64/
Include: yum

[yhu@master ~]$sudo

[root@7-2 singularity-2.2.1]#singularity shell –contain /tmp/Centos7.img

Singularity: Invoking an interactive shell within container…

Singularity.Centos7.img> id

uid=0(root) gid=0(root) groups=0(root)

Singularity.Centos7.img> ls /

bin dev etc lib lost+found mnt proc run singularity sys usr

boot environment home lib64 media opt root sbin srv tmp var

Singularity.Centos7.img> exit

exit

singularity制作镜像也可以直接从docker的镜像中获取,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
[yhu@master ~]$sudo singularity import /tmp/tensorflow.img docker://tensorflow/tensorflow:latest
tensorflow/tensorflow:latest
Downloading layer: sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
...

Adding Docker CMD as Singularity runscript...
Bootstrap initialization
No bootstrap definition passed, updating container
Executing Prebootstrap module
Executing Postbootstrap module
Done.

或者

1
2
3
4
5
6
7
8
9
10
Bootstrap: docker
From: tensorflow/tensorflow:latest

%runscript

exec /usr/bin/python "$@"

%post

echo "Post install stuffs!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#测试结果
[yhu@master ~]$sudo ./singularity shell /tmp/tensorflow.img
Singularity: Invoking an interactive shell within container...

Singularity.tensorflow.img> ls
anaconda-ks.cfg easy-rsa-release-2.x.zip singularity-2.2.1
easy-rsa-release-2.x rpmbuild singularity-2.2.1.tar.gz
Singularity.tensorflow.img> python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()
Singularity.tensorflow.img> pwd
/root
Singularity.tensorflow.img> exit


singularity在官网还有众多详细资料

官方资料:

singularity下载地址:http://singularity.lbl.gov/all-releases

安装教程:http://singularity.lbl.gov/quickstart

使用docker制作镜像, http://singularity.lbl.gov/docs-docker#use-a-spec-file

在centos上构建Ubuntu系统的镜像, http://singularity.lbl.gov/building-ubuntu-rhel-host

singularity使用手册

[TOC]

制作容器

选项一 导入docker容器

使用现成的docker容器,比如docker hub上面的tensorflow:

1
2
sudo singularity create --size 4000 tensorflow.img
sudo singularity import tensorflow.img docker://tensorflow/tensorflow:latest

选项二 自己制作singularity容器

制作一个singularity容器需要两步,需要在任何一台有root权限的linux计算机上进行:

1 创建空白镜像文件

sudo singularity create --size 1200 keras2.0_cpu.img

其中 size用来制定大小,单位是MB,上面将创建一个1.2GB的空白镜像文件。

2 接下来需要在这个镜像中写入系统和预装的软件

创建 keras2.0_cpu.def 文件,内容通过符号%分割成几个部分,分别代表不同的功能。

我们主要要写的内容是1) %setup; 2)%post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 基于docker的ubuntu镜像
BootStrap: docker
From: ubuntu:16.04

%runscript
echo "This is what happens when you run the container..."
bash
%setup
# 1)这一步可以先设置好环境变量,通过wget等下载好必要的软件库等,注意这些指令都只是在host机器上运行,不是container里面。
# 内容请参考https://git.oschina.net/sg-ai/singularityimages/blob/master/keras-tf-1.0.1-gpu.setup
%post
# non interactive debian
DEBIAN_FRONTEND=noninteractive
# Install the necessary packages (from repo)
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates

# 2)这些指令都在container里面运行,在这里写你要安装和设置的东西,可以使用apt install之类的安装需要的软件,这样就会被安装到容器里面去
# 内容请参考https://git.oschina.net/sg-ai/singularityimages/blob/master/keras-tf-1.0.1-gpu.post

接着运行:

1
2
sudo singularity bootstrap keras2.0_cpu.img keras2.0_cpu.def

这样keras2.0_cpu.img 这个文件就可以用了,把它拷贝到集群上,就可以通过singularity使用。

使用singularity容器

运行容器环境

singularity容器中提供隔离的软件环境,方便各种测试、开发、甚至产品发布,同时它专门为集群设计,因为可以不需要root权限即可运行。

常用的singularity指令有2中shell和exec, 比如,以下指令可以打开一个互动的shell:

1
singularity shell /atlas/containers/keras2.0_cpu.img

你会看到:

1
2
3
4
5
[wouyang@login images_singularity]$ singularity shell ./keras2.0_cpu.img

Singularity: Invoking an interactive shell within container...

Singularity.keras-2.0.3-py3-tf-gpu.img>

接下来你就可以在这个容器里面运行任何指令了, 但是注意这个容器是只读的,你不能安装新的东西。

如果你不是想敲单条指令,你可以直接使用exec指令来运行你的python文件,例如

1
2
singularity exec ./keras2.0_cpu.img python3 hello.py

添加新软件/修改容器

使用过程中,如果需要安装新的软件,可以使用–writable 或者 -w 参数来对容器进行修改,运行以下指令:

1
2
sudo singularity shell --writable ./keras2.0_cpu.img

接下来打开的shell里面,可以使用apt-get、pip等安装新的软件,修改会直接写入到容器中。

singularity帮助使用

直接输入singularity即可查看帮助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[yhu@node7 ~]$singularity 
USAGE: singularity [global options...] <command> [command options...] ...

GLOBAL OPTIONS:
-d --debug Print debugging information
-h --help Display usage summary
-q --quiet Only print errors
--version Show application version
-v --verbose Increase verbosity +1
-x --sh-debug Print shell wrapper debugging information

GENERAL COMMANDS:
help Show additional help for a command

CONTAINER USAGE COMMANDS:
exec Execute a command within container
run Launch a runscript within container
shell Run a Bourne shell within container
test Execute any test code defined within container

CONTAINER MANAGEMENT COMMANDS (requires root):
bootstrap Bootstrap a new Singularity image from scratch
copy Copy files from your host into the container
create Create a new container image
expand Grow the container image
export Export the contents of a container via a tar pipe
import Import/add container contents via a tar pipe
mount Mount a Singularity container image

如果要看command的具体使用,可使用如下命令,例如要查看 singularity shell 如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[yhu@node7 ~]$singularity help shell 
USAGE: singularity [...] shell [shell options...] <container path>

Obtain a shell (/bin/sh) within the container image.

note: When invoking a shell within a container, the container image is
by default writable.


SHELL OPTIONS:
-B/--bind <spec> A user-bind path specification. spec can either be a
path or a src:dest pair, specifying the bind mount to
perform (can be called multiple times)
-c/--contain This option disables the automatic sharing of writable
filesystems on your host (e.g. $HOME and /tmp).
-C/--containall Contain not only file systems, but also PID and IPC
-H/--home <path> Path to a different home directory to virtualize within
the container
-i/--ipc Run container in a new IPC namespace
-p/--pid Run container in a new PID namespace (creates child)
--pwd Initial working directory for payload process inside
the container
-S/--scratch <path> Include a scratch directory within the container that
is linked to a temporary dir (use -W to force location)
-s/--shell <shell> Path to program to use for interactive shell
-u/--user Try to run completely unprivileged (only works on very
new kernels/distros)
-W/--workdir Working directory to be used for /tmp, /var/tmp and
$HOME (if -c/--contain was also used)
-w/--writable By default all Singularity containers are available as
read only. This option makes the file system accessible
as read/write.


NOTE:
If there is a daemon process running inside the container, then
subsequent container commands will all run within the same namespaces.
This means that the --writable and --contain options will not be
honored as the namespaces have already been configured by the
'singularity start' command.


EXAMPLES:

$ singularity shell /tmp/Debian.img
Singularity/Debian.img> pwd
/home/gmk/test
Singularity/Debian.img> exit
$ singularity shell -C /tmp/Debian.img
Singularity/Debian.img> pwd
/home/gmk
Singularity/Debian.img> ls -l
total 0
Singularity/Debian.img> exit
$ sudo singularity shell -w /tmp/Debian.img

注意一点,singularity进入容器后,会同服务器共享一个家目录.那如果要在容器中运行项目上的程序,而程序又不在你的家目录比如在/atlas/project/MD 该怎么办呢?

可以这样,加一个参数 -H 接上路径,这个路径就会共享了.例:

1
2
singularity shell -H /atlas/projects/MD /atlas/containers/k-n-g.img

增:后来发现同样也可用 -B参数来实现

1
2
singularity shell -B /atlas/projects/MD:/mnt  /atlas/containers/k-n-g.img

这个命令意思是,当你进入容器的/mnt目录的时候,看到的是 /atlas/projects/MD 中的内容

在集群上使用slurm和singularity

在集群上,可以通过slurm系统直接将singularity发送到计算节点上,只需要在原先的指令前加slurm相关指令和参数即可,例如:

1
srun -p common singularity exec ./keras2.0_cpu.img python3 hello.py
1
2
3
# 指定使用GPU节点
srun --gres=gpu -p fast singularity exec /atlas/containers/keras-2.0.3-py3-tf-gpu.img python3 mnist_cnn.py

运行互动脚本环境–pty选项

运行可以交互的shell脚本,一般的srun指令下,是不支持脚本互动的,如果要在某个节点上使用shell、bash、ipython等互动脚本环境,需要加上–pty选项

例如

1
2
3
4
5
6
7
8
9
10
# 在集群计算节点上执行python互动
[wouyang@login ~]$ srun --pty -p common python

Python 2.7.5 (default, Nov 20 2015, 02:00:19)

[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2

Type "help", "copyright", "credits" or "license" for more information.

>>>

同理,如果要使用singularity容器里面的shell,应该加上–pty

1
2
# 在集群计算节点上执行shell互动
[wouyang@login ~]$ srun --pty -p common singularity shell /atlas/containers/keras-2.0.3-py3-tf-gpu-notebook-nodejs.img

实例一 集群上使用运行ipython环境

1
2
3
4
5
# 在login节点上测试,注意,仅供测试,不要做长时间运算
[wouyang@login ~]$ singularity exec /atlas/containers/keras-2.0.3-py3-tf-gpu-notebook-nodejs.img ipython

# 另外,计算量大的需要发送到计算节点上进行,并且要及时关闭,否则会一直占用节点资源
[wouyang@login ~]$ srun --pty -p common singularity exec /atlas/containers/keras-2.0.3-py3-tf-gpu-notebook-nodejs.img ipython

实例二 在集群上运行jupyter notebook

【注意】不推荐在集群上使用jupyter notebook,因为它会长时间占用节点和GPU资源,因此仅供短时间测试使用。

singularity容器会自动带入当前用户的所有环境变量,但是对于jupyter notebook,目前集群上的$XDG_RUNTIME_DIR 指向/run/user文件夹,这个文件夹在容器里面不存在,所以会导致问题,为了防止singularity自动传入当前环境变量到容器,可以通过-c选项来进行:

1
2
# 在集群上运行包含jupyter notebook的容器, -c选项防止将当前的环境变量传入到容器
srun -p common singularity exec /atlas/containers/keras-2.0.3-py3-tf-gpu-notebook-nodejs.img -c jupyter notebook --ip=0.0.0.0 --port=8888

另外, –ip=0.0.0.0 选项确保在容器外部能访问内部网络。

docker repository使用方法

[TOC]

1.查看repository中的image

假设集群上docker 私有仓库做到了master节点,172.16.10.10:5000。

获取已有image

私有仓库目前已有images如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$curl http://172.16.10.10:5000/v2/_catalog|python2 -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 289 100 289 0 0 1125 0 --:--:-- --:--:-- --:--:-- 1128
{
"repositories": [
"centos",
"gitlab/gitlab-ce",
"gitlab/gitlab-runner",
"gitlab-runner",
"keras",
"mongo",
"nvidia/cuda",
"nvidia/digits",
"paddledev/paddle",
"paddlepaddle/book",
"paddlepaddle/paddle",
"redis",
"registry",
"superman/registry",
"ubuntu",
"yhu/centos",
"yhu/gitlab-runner",
"yhu/registry"
]
}

获取制定image的所有版本号

如果想要查看对应的images拥有哪些版本, 例如paddlepaddle/paddle 这个镜像的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$curl http://172.16.10.10:5000/v2/paddlepaddle/paddle/tags/list|python2 -m json.tool
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 84 100 84 0 0 20771 0 --:--:-- --:--:-- --:--:-- 28000
{
"name": "paddlepaddle/paddle",
"tags": [
"latest",
"0.10.0",
"0.10.0-dev",
"0.10.0-gpu"
]
}

只需要修改中间对应的名字即可

2.上传和下载image

下面简单介绍一下如何从私有仓库下载和上传镜像。

找到对应的镜像和版本之后,执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$docker pull 172.16.10.10:5000/paddlepaddle/paddle:0.10.0  #下载images
...
$docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
python 2.7 26bddf7dbe1b 6 days ago 679MB
$docker tag python:2.7 172.16.10.10:5000/python:2.7
$docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
172.16.10.10:5000/python 2.7 26bddf7dbe1b 6 days ago 679MB
python 2.7 26bddf7dbe1b 6 days ago 679MB
$docker push 172.16.10.10:5000/python:2.7 #上传images
The push refers to a repository [172.16.10.10:5000/python]
c89798427b75: Pushed
...
18f9b4e2e1bc: Pushed
2.7: digest: sha256:d3ecae8689444b025ff6dfd0972d98e4c0f9e8b9ca5762f044447dfd654c1b53 size: 2011
[superman@node7 ~]$curl http://172.16.10.10:5000/v2/_catalog|python2 -m json.tool
{
"repositories": [
...
"python",
...
]
}

docker镜像上增加安装包

[TOC]

前提

有时候我们会遇到这样的问题,一个镜像能满足我们的大部分需求,想在此基础上添加一部分功能.有三种方法

1.dockerfile构建

找到这个镜像的dockerfile,修改后build.

2.作为基础镜像

也是写一个dockerfile,只不过这次把这个有较多功能的镜像作为基础镜像,添加所需功能.然后build.

3.导出container为image

直接在运行的container中添加所需功能,然后commit成image.

第一步

使用root用户进入一个新容器,不要用--rm(否则当你退出容器的时候,容器没有了 你添加的功能自然就不复存在了)

1
docker run --user 0 -it --name superman sgdockerfilebox/mitosis_cpu:latest bash

第二步

在container中直接添加你要的功能

1
2
3
4
5
apt update
apt install ...
npm install -g n
n stable ...
pip3 install ...

然后退出容器 exit

第三步

新的容器 commit成新的image
docker commit superman sgdockerfilebox/wewo_cpu:v1

(这里的sgdockerfilebox/wewo_cpu:v1 名字和版本号自己定义)

查看一下commitimage

1
2
3
root@gyw:~# docker images 
REPOSITORY TAG IMAGE ID CREATED SIZE
sgdockerfilebox/wewo_cpu v1 e4f2c829d1eb 23 minutes ago 4.42GB

没有问题就可以删除 之前创建的容器了,
docker rm superman

第四步

将新的image 保存成tar压缩文件,给其他同事使用,统一环境

docker save sgdockerfilebox/wewo_cpu:v1 -o wewo_cpu.tar

wewo_cpu.tar 给其他人吧。

第五步

其他同事拿到 wewo_cpu.tar 之后使用如下命令即可:
docker load -i wewo_cpu.tar

验证导入的image

1
2
3
root@gyw:~# docker images 
REPOSITORY TAG IMAGE ID CREATED SIZE
sgdockerfilebox/wewo_cpu v1 e4f2c829d1eb 23 minutes ago 4.42GB

docker设置devicemapper存储驱动

[TOC]

背景

在 Ubuntu/Debian 上有 UnionFS 可以使用,如 aufs 或者 overlay2,而 CentOS 和 RHEL 的内核中没有相关驱动。因此对于这类系统,一般使用 devicemapper 驱动利用 LVM 的一些机制来模拟分层存储。这样的做法除了性能比较差外,稳定性一般也不好,而且配置相对复杂。Docker 安装在 CentOS/RHEL 上后,会默认选择 devicemapper,但是为了简化配置,其 devicemapper 是跑在一个稀疏文件模拟的块设备上,也被称为 loop-lvm。这样的选择是因为不需要额外配置就可以运行 Docker,这是自动配置唯一能做到的事情。但是 loop-lvm 的做法非常不好,其稳定性、性能更差,无论是日志还是 docker info 中都会看到警告信息。官方文档有明确的文章讲解了如何配置块设备给 devicemapper 驱动做存储层的做法,这类做法也被称为配置 direct-lvm

除了前面说到的问题外,devicemapper + loop-lvm 还有一个缺陷,因为它是稀疏文件,所以它会不断增长。用户在使用过程中会注意到 /var/lib/docker/devicemapper/devicemapper/data 不断增长,而且无法控制。这个稀疏文件的空间释放后基本不进行垃圾回收的问题。因此往往会出现即使删除了文件内容,空间却无法回收,随着使用这个稀疏文件一直在不断增长。

所以对于 CentOS/RHEL 的用户来说,在没有办法使用 UnionFS 的情况下,一定要配置 direct-lvmdevicemapper,无论是为了性能、稳定性还是空间利用率。

配置过程

1.直接修改daemon.json

官方文档里面有两种方法,第一种就是直接修改/etc/docker/daemon.json 这个文档,然后重启docker即可.

1
2
3
4
5
6
7
8
9
10
11
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.directlvm_device=/dev/xdf", #这里修改成主机存储谁被,可以整个硬盘,或者一个分区
"dm.thinp_percent=95",
"dm.thinp_metapercent=1",
"dm.thinp_autoextend_threshold=80",
"dm.thinp_autoextend_percent=20",
"dm.directlvm_device_force=false"
]
}

修改完了之后,重启docker即可

1
2
3
4
5
6
7
8
9
10
11
sudo systemctl restart docker
docker info

Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.03.1-ce
Storage Driver: devicemapper #看这里
....

但是有些时候修改了配置,并不会生效.需要手动修改添加逻辑卷等.

2.手动修改配置

a.安装依赖包

1
yum install lvm2

b.创建逻辑卷,以官方的/dev/xvdf为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ sudo pvcreate /dev/xvdf  #创建物理卷
Physical volume "/dev/xvdf" successfully created.
$ sudo vgcreate docker /dev/xvdf #创建卷组
Volume group "docker" successfully created
$ sudo lvcreate --wipesignatures y -n thinpool docker -l 95%VG
Logical volume "thinpool" created.
$ sudo lvcreate --wipesignatures y -n thinpoolmeta docker -l 1%VG
Logical volume "thinpoolmeta" created.
$ sudo lvconvert -y \
--zero n \
-c 512K \
--thinpool docker/thinpool \
--poolmetadata docker/thinpoolmeta

WARNING: Converting logical volume docker/thinpool and docker/thinpoolmeta to
thin pool's data and metadata volumes with metadata wiping.
THIS WILL DESTROY CONTENT OF LOGICAL VOLUME (filesystem etc.)
Converted docker/thinpool to thin pool.

$ sudo vi /etc/lvm/profile/docker-thinpool.profile #配置比例,其实不写也可以
activation {
thin_pool_autoextend_threshold=80
thin_pool_autoextend_percent=20
}

$ sudo lvchange --metadataprofile docker-thinpool docker/thinpool
Logical volume docker/thinpool changed.

$ sudo lvs -o+seg_monitor
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert Monitor
thinpool docker twi-a-t--- 95.00g 0.00 0.01 monitored

c.修改配置

1
2
3
4
5
6
7
8
9
10
11
$ mkdir /var/lib/docker.bk  #删除或备份/var/lib/docker
$ mv /var/lib/docker/* /var/lib/docker.bk
$ cat /etc/docker/daemon.json #手动创建了这些设备,启动docker的时候就按照这个配置自动挂在
{
"storage-driver": "devicemapper",
"storage-opts": [
"dm.thinpooldev=/dev/mapper/docker-thinpool",
"dm.use_deferred_removal=true",
"dm.use_deferred_deletion=true"
]
}

d.启动看效果

1
2
3
4
5
6
7
8
9
10
11
12
$ sudo systemctl start docker
$ docker info

Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.03.1-ce
Storage Driver: devicemapper
Pool Name: docker-thinpool

docker常用命令

[TOC]

1.基本概念

Docker 包括三个基本概念

  • 镜像(Image

    Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会改变

  • 容器(Container

    容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

  • 仓库(Repository

    一个集中的存储、分发镜像的服务。

理解了这三个概念,就理解了 Docker 的整个生命周期。

2.docker安装

官方安装教程

nvidia-docker安装

阿里云镜像源docker安装方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ubuntu
sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
sudo apt-get -y update
sudo apt-get -y install docker-ce

#CentOS
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo yum makecache fast
sudo yum -y install docker-ce
sudo service docker start

3.docker命令

a、镜像相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker image --help

Usage: docker image COMMAND
Commands:
ls List images
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rm Remove one or more images
save Save one or more images to a tar archive (streamed to STDOUT by default)
load Load an image from a tar archive or STDIN
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
build Build an image from a Dockerfile

Run 'docker image COMMAND --help' for more information on a command.

1.docker images

列出主机上已存在镜像

1
2
3
4
5
6
7
$ docker images 
REPOSITORY TAG IMAGE ID CREATED SIZE
172.16.10.10:5000/deepchem cpu 75105fd5d68d 11 days ago 6.98GB
172.16.10.10:5000/deepchem 2.0.0-cpu-worker 2b024a901b22 2 weeks ago 6.75GB
172.16.10.10:5000/python-3.5.4-alpine v3 954d2aa346fb 2 months ago 309MB
deepchemio/deepchem 2.0.0-cpu d0b0679dd7ce 3 months ago 6.02GB
crossbario/crossbar latest 01c84a0d6626 3 months ago 170MB

2.docker pull

下载新的镜像文件, 公共的image可在docker hub上查找

1
2
3
4
5
6
7
$ docker pull deepchemio/deepchem:2.0.0-cpu
2.0.0-cpu: Pulling from deepchemio/deepchem
1be7f2b886e8: Pull complete
...
e0bd96390dd4: Pull complete
Digest: sha256:80cfd1de4023a857566759688ba3ef9f719f6062c022a95dd78309156892610f
Status: Downloaded newer image for deepchemio/deepchem:2.0.0-cpu

3.docker push

推送本地主机上的镜像(image)到仓库(Repository)中 。

1
$ docker push hooby/deepchem:2.0.0-cpu

4.docker rmi

删除本地镜像

1
docker rmi hooby/deepchem:2.0.0-cpu 

5.docker save

将本地image保存为一个压缩包

1
docker save hooby/deepchem:2.0.0-cpu -o deepchem.tar

6.docker load

导入image

1
docker load -i deepchem.tar

7.docker history

查看image构建过程中的命令历史

1
2
3
4
5
docker history deepchemio/deepchem:2.0.0-cpu 
IMAGE CREATED CREATED BY SIZE COMMENT
d0b0679dd7ce 3 months ago /bin/sh -c cd deepchem && git clean -fX 0B
<missing> 3 months ago /bin/sh -c export LANG=en_US.UTF-8 && gi… 4.31GB
...

8.docker inspect

查看image的元数据信息

1
2
3
4
5
6
7
8
$ docker inspect deepchemio/deepchem:2.0.0-cpu 
[
{
"Id": "sha256:d0b0679dd7cee07b1ad4e95bff9ac01d79f1f742e04bce01dc9e7e459f76475d",
"RepoTags": [
"deepchemio/deepchem:2.0.0-cpu"
],
...

9.docker tag

从现有的image中创建一个新的tag标签

1
docker image tag deepchemio/deepchem:2.0.0-cpu hooby/deepchem-cpu

这样 deepchemio/deepchem:2.0.0-cpuhooby/deepchem-cpu 其实内容是一样的,只是名字不一样而已。

10.docker build

通过dockerfile,构建image.

1
docker build -t alex/deepchem:v1.0 . 

###b、容器相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker container --help 

Usage: docker container COMMAND
Commands:
ls List containers
run Run a command in a new container
start Start one or more stopped containers
restart Restart one or more containers
exec Run a command in a running container
rm Remove one or more containers
kill Kill one or more running containers
stop Stop one or more running containers
logs Fetch the logs of a container
rename Rename a container
commit Create a new image from a container's changes

Run 'docker container COMMAND --help' for more information on a command.

常用参数

1
2
3
4
5
6
7
8
-it                #以交互模式执行
-d/--detach #后台执行
--rm #container运行结束后自动删除
--name #为container指定名称
-p/--publish #映射主机端口到container中
-v/--volume #挂载主机文件到container中
-u/--user #以指定用户运行container
-w /--workdir #指定在container中执行命令时所在位置

1.docker ps

显示container

1
2
3
4
5
6
7
8
9
10
$ docker ps #显示正在运行状态下的container
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10cf2cd2d507 172.16.10.10:5000/deepchem:cpu "bash" 9 days ago Up 6 days deepchem-cpu
18b35ff44321 crossbario/crossbar "crossbar start --cb…" 9 days ago Up 6 days 8000/tcp, 0.0.0.0:8080->8080/tcp crossbar-test
$ docker ps -a #显示所有container
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10cf2cd2d507 172.16.10.10:5000/deepchem:cpu "bash" 9 days ago Up 6 days deepchem-cpu
18b35ff44321 crossbario/crossbar "crossbar start --cb…" 9 days ago Up 6 days 8000/tcp, 0.0.0.0:8080->8080/tcp crossbar-test
5c0416e9e3d1 crossbario/crossbar:latest "crossbar start --cb…" 9 days ago Exited (1) 9 days ago crossbar_test
8caf97e2c03f 172.16.10.10:5000/deepchem:2.0.0-cpu-worker "bash" 11 days ago Exited (0) 10 days ago deepchem

2.docker run

从image启动,运行一个container

1
2
3
docker run deepchemio/deepchem:2.0.0-cpu 
docker run -d --name deepchem deepchemio/deepchem:2.0.0-cpu
docker run -d --name crossbar --rm -v node_config:/node -p 8080:8080 crossbario/crossbar

3.docker stop

停掉一个正在运行的container

1
docker stop deepchem

4.docker rm

删除停掉的container

1
docker rm deepchem

5.docker start

启动停掉的container

1
docker start deepchem 

6.docker restart

重启或者启动停掉的container

1
docker restart deepchem

7.docker exec

在运行中的container中执行命令

1
2
docker exec -it --user 0 deepchem bash 
docker exec -w /src/backend/ deepchem python3 dl_start.py

8.docker commit

将container构建成一个image

1
docker commit deepchem alex_deepchem

9.docker rename

修改container的name

1
docker rename deepchem deepchem-cpu 

10.docker port

显示container映射的端口

1
2
$ docker port crossbar-test 
8080/tcp -> 0.0.0.0:8080

11.docker cp

复制主机文件到container中,或者从container中复制文件到主机目录。

1
2
docker cp test.file deepchem:/tmp
docker cp deepchem-cpu:/config.conf ./

12.docker logs

查看container运行输出信息

1
2
3
4
5
docker logs crossbar 
2018-06-05T05:16:10+0000 [Controller 1] __ __ __ __ __ __ __ __
2018-06-05T05:16:10+0000 [Controller 1] / `|__)/ \/__`/__`|__) /\ |__) |/ \
2018-06-05T05:16:10+0000 [Controller 1] \__,| \\__/.__/.__/|__)/~~\| \. |\__/
.....

13.docker top

查看docker中的进程

1
2
3
4
5
$ docker top deepchem-cpu 
UID PID PPID C STIME TTY TIME CMD
root 30358 30340 0 6月05 pts/0 00:00:00 bash
$ ps aux |grep 30358
root 30358 0.0 0.0 20080 3736 pts/0 Ss+ Jun05 0:00 bash

14.docker container prune

移除所有停掉的container

4.Dockerfile定制镜像

Dockerfile 是一个文本文件,其内包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

以定制 nginx 镜像为例,在一个空白目录中,建立一个文本文件,并命名为 Dockerfile

1
2
3
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

Dockerfile内容为:

1
2
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROMRUN

再使用构建image命令,生成mynginx:v1 镜像

1
$ docker build -t mynginx:v1 . 

对于最后这个. 的理解可以参考这里。简单来说就是除了Dockerfile和构建image需要用的文件,不要在这里放其他文件。

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM 就是指定基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令

Docker Store 上有非常多的高质量的官方镜像,可以直接拿来使用或者作为基础镜像。

RUN 执行命令

RUN 指令是用来执行命令行命令的。RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

COPY 复制文件

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

1
COPY package.json /usr/src/app/

格式:

  • COPY <源路径>... <目标路径>
  • COPY ["<源路径1>",... "<目标路径>"]

CMD 容器启动命令

在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

ENV 设置环境变量

设置环境变量,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

1
ENV VERSION=1.0 DEBUG=on NAME="Happy Feet"

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

USER 指定当前用户

USER 切换到指定用户,这个用户必须是事先建立好的,否则无法切换。

1
2
3
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

格式:USER <用户名>

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

5.集群docker使用

好处

1、速度快
2、节省带宽
3、属于私有,可以不公开

查看集群已经拥有的docker镜像

1
curl http://172.16.10.10:5000/v2/_catalog|python2 -m json.tool

查看集群已拥有的docker镜像有哪些tag

1
curl http://172.16.10.10:5000/v2/paddlepaddle/paddle/tags/list|python2 -m json.tool

集群docker镜像下载方法

1
docker pull 172.16.10.10:5000/paddlepaddle/paddle:0.10.0

集群docker镜像上传方法

1
docker push 172.16.10.10:5000/python:2.7 

6.一些补充

构建出来的image有些不是基于centos 或者ubuntu 的,而是Alpine 。

docker pull 的时候注意查看tag,例如nvidia/cuda 就有众多不同的tags: https://hub.docker.com/r/nvidia/cuda/tags/

不同系统安装软件包的命令也有所不同:

1
2
3
centos: yum install app
ubuntu: apt install app
alpine: apk add app

相关资源

django的使用3-编写视图

[TOC]

编写视图

一个视图函数(或简称为视图)是一个 Python 函数,它接受 Web 请求并返回一个 Web 响应。这个响应可以是 Web 页面的 HTML 内容,或者重定向,或者404错误,或者 XML 文档,或一个图片…或是任何内容。视图本身包含返回响应所需的任何逻辑。这个代码可以存在任何地方,只要它在你的 Python 路径上就行。可以说,不需要其他东西,这里并没有魔法。为了将代码放置在某处,约定将视图放在名为 views.py 的文件里,这个文件放置在项目或应用目录里。

一个简单的视图

参考:

django的使用3-URL调度器

[TOC]

Django 允许你自由地设计你的URL,不受框架束缚。

概况

对于高质量的Web 应用来说,使用简洁、优雅的URL 模式是一个非常值得重视的细节。

为了给一个应用设计URL,你需要创建一个Python 模块,通常被称为URLconf(URL configuration)。这个模块是纯粹的Python 代码,包含URL 模式(简单的正则表达式)到Python 函数(你的视图)的简单映射。

映射可短可长,随便你。它可以引用其它的映射。而且,因为它是纯粹的Python 代码,它可以动态构造。

Django 如何处理一个请求

当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪个Python 代码使用的算法:

  1. Django 确定使用根 URLconf 模块。通常,这是 ROOT_URLCONF 设置的值,但如果传入 HttpRequest 对象拥有 urlconf 属性(通过中间件设置),它的值将被用来代替 ROOT_URLCONF 设置。
  2. Django 加载该 Python 模块并寻找可用的 urlpatterns 。它是 django.urls.path() 和(或) django.urls.re_path() 实例的序列(sequence)。
  3. Django 会按顺序遍历每个 URL 模式,然后会在所请求的URL匹配到第一个模式后停止,并与 path_info 匹配。
  4. 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图,这个视图是一个Python 函数(或基于类的视图 class-based view )。视图会获得如下参数:
    • 一个 HttpRequest 实例。
    • 如果匹配的 URL 包含未命名组,那么来自正则表达式中的匹配项将作为位置参数提供。
    • 关键字参数由路径表达式匹配的任何命名部分组成,并由 django.urls.path() 或 django.urls.re_path() 的可选 kwargs 参数中指定的任何参数覆盖。
  5. 如果没有 URL 被匹配,或者匹配过程中出现了异常,Django 会调用一个适当的错误处理视图。

实例

下面是一个简单的 URLconf:

1
2
3
4
5
6
7
8
9
10
from django.urls import path

from . import views

urlpatterns = [
path('articles/2003/', views.special_case_2003),
path('articles/<int:year>/', views.year_archive),
path('articles/<int:year>/<int:month>/', views.month_archive),
path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail),
]

注意:

  • 要从 URL 中取值,使用尖括号。
  • 捕获的值可以选择性地包含转换器类型。比如,使用 <int:name> 来捕获整型参数。如果不包含转换器,则会匹配除了 / 外的任何字符。
  • 这里不需要添加反斜杠,因为每个 URL 都有。比如,应该是 articles 而不是 /articles

一些请求的例子:

  • /articles/2005/03/ 会匹配 URL 列表中的第三项。Django 会调用函数 views.month_archive(request, year=2005, month=3)
  • /articles/2003/ 会匹配 URL 列表中的第一项。Django 会调用函数 views.special_case_2003(request)
  • /articles/2003 没有匹配项,因为没有/结尾。
  • /articles/2003/03/building-a-django-site/ 会匹配 URL 列表中的最后一项。Django 会调用函数 views.article_detail(request, year=2003, month=3, slug="building-a-django-site")

路径转换器

下面的路径转换器在默认情况下是有效的:

  • str - 匹配除了 '/' 之外的非空字符串。如果表达式内不包含转换器,则会默认匹配字符串。
  • int - 匹配 0 或任何正整数。返回一个 int
  • slug - 匹配任意由 ASCII 字母或数字以及连字符和下划线组成的短标签。比如,building-your-1st-django-site
  • uuid - 匹配一个格式化的 UUID 。为了防止多个 URL 映射到同一个页面,必须包含破折号并且字符都为小写。比如,075194d3-6885-417e-a8a8-6c931e272f00
  • path - 匹配非空字段,包括路径分隔符 '/' 。它允许你匹配完整的 URL 路径而不是像 str 那样匹配 URL 的一部分。

使用正则表达式

如果路径和转化器语法不能很好的定义你的 URL 模式,你可以可以使用正则表达式。如果要这样做,请使用 re_path() 而不是 path() 。

在 Python 正则表达式中,命名正则表达式组的语法是 (?P<name>pattern) ,其中 name 是组名,pattern 是要匹配的模式。

这里是先前 URLconf 的一些例子,现在用正则表达式重写一下:

1
2
3
4
5
6
7
8
9
10
from django.urls import path, re_path

from . import views

urlpatterns = [
path('articles/2003/', views.special_case_2003),
re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$', views.article_detail),
]

当从使用 path() 切换到 re_path() (反之亦然),视图参数类型可能发生变化,可能需要调整你的视图。

URLconf查找

请求的URL被看做是一个普通的Python 字符串, URLconf在其上查找并匹配。进行匹配时将不包括GET或POST请求方式的参数以及域名。URLconf 不检查使用了哪种请求方法。

指定视图参数的默认值

有一个方便的小技巧是指定视图参数的默认值。 下面是一个URLconf 和视图的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# URLconf
from django.urls import path

from . import views

urlpatterns = [
path('blog/', views.page),
path('blog/page<int:num>/', views.page),
]

# View (in blog/views.py)
def page(request, num=1):
# Output the appropriate page of blog entries, according to num.
...

在上面的例子中,两个URL模式都指向了相同的视图—— views.page 但是第一个样式不能在URL中捕获到任意东西。如果第一个URL模式去匹配URL,page() 函数会使用它默认参数 num=1。如果第二个URL模式去匹配URL,page() 函数都会使用捕获到的任意 num参数。

错误处理

当 Django 找不到所匹配的请求 URL 时,或引发了异常时,Django 会调用一个错误处理视图。

这些情况发生时使用的视图通过4个变量指定。它们的默认值应该满足大部分项目,但是通过赋值给它们以进一步的自定义也是可以的。

这些值得在你的根URLconf 中设置。在其它URLconf 中设置这些变量将不会生效果。

它们的值必须是可调用的或者是表示视图的Python 完整导入路径的字符串,可以方便地调用它们来处理错误情况。

这些值是:

  • handler400
  • handler403
  • handler404
  • handler500

包含其它的URLconfs

在任何时候,你的 urlpatterns 都可以 “include” 其它URLconf 模块。这实际上将一部分URL 放置于其它URL 下面。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.urls import include, path

from apps.main import views as main_views
from credit import views as credit_views

extra_patterns = [
path('reports/', credit_views.report),
path('reports/<int:id>/', credit_views.report),
path('charge/', credit_views.charge),
]

urlpatterns = [
path('', main_views.homepage),
path('help/', include('apps.help.urls')),
path('credit/', include(extra_patterns)),
]

每当 Django 遇到 include() ,它会将匹配到该点的URLconf的任何部分切掉,并将剩余的字符串发送到包含的URLconf进行进一步处理。

在这个例子中, /credit/reports/ URL将被 credit.views.report() 这个Django 视图处理。

这种方法可以用来去除URLconf 中的冗余,其中某个模式前缀被重复使用。例如,考虑这个URLconf:

1
2
3
4
5
6
7
8
9
from django.urls import path
from . import views

urlpatterns = [
path('<page_slug>-<page_id>/history/', views.history),
path('<page_slug>-<page_id>/edit/', views.edit),
path('<page_slug>-<page_id>/discuss/', views.discuss),
path('<page_slug>-<page_id>/permissions/', views.permissions),
]

我们可以改进它,通过只声明共同的路径前缀一次并将后面的部分分组:

1
2
3
4
5
6
7
8
9
10
11
from django.urls import include, path
from . import views

urlpatterns = [
path('<page_slug>-<page_id>/', include([
path('history/', views.history),
path('edit/', views.edit),
path('discuss/', views.discuss),
path('permissions/', views.permissions),
])),
]

捕获的参数

被包含的URLconf 会收到来自父URLconf 捕获的任何参数,所以下面的例子是合法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# In settings/urls/main.py
from django.urls import include, path

urlpatterns = [
path('<username>/blog/', include('foo.urls.blog')),
]

# In foo/urls/blog.py
from django.urls import path
from . import views

urlpatterns = [
path('', views.blog.index),
path('archive/', views.blog.archive),
]

在上面的例子中,捕获的 "username" 变量将被如期传递给include()指向的URLconf。

传递额外选项给视图函数

URLconfs 有钩子来允许你把其他参数作为 Python 字典来传递给视图函数。path() 函数可带有可选的第三参数(必须是字典),传递到视图函数里。

1
2
3
4
5
6
from django.urls import path
from . import views

urlpatterns = [
path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),
]

URL 的反向解析

在 Django 项目中,一个常见需求是获取最终形式的 URL,比如用于嵌入生成的内容中(视图和资源网址,给用户展示网址等)或用户服务器端的导航处理(重定向等)。

强烈建议不要硬编码 URL。

Django 提供执行反转 URL 的工具,这些工具与需要 URL 的不同层匹配:

  • 在模板里:使用 url模板标签。
  • 在 Python 编码:使用 reverse() 函数。
  • 在与 Django 模型实例的 URL 处理相关的高级代码中: get_absolute_url() 方法。

示例

再次考虑这个 URLconf 条目:

1
2
3
4
5
6
7
8
9
from django.urls import path

from . import views

urlpatterns = [
#...
path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
#...
]

根据这个设计,与 year nnnn 相对应的 URL 是 /articles/<nnnn>/

你可以使用以下方式在模板代码中来获取它们:

1
2
3
4
5
6
7
<a href="{% url 'news-year-archive' 2012 %}">2012 Archive</a>
{# Or with the year in a template context variable: #}
<ul>
{% for yearvar in year_list %}
<li><a href="{% url 'news-year-archive' yearvar %}">{{ yearvar }} Archive</a></li>
{% endfor %}
</ul>

或在 Python 代码里:

1
2
3
4
5
6
7
8
from django.http import HttpResponseRedirect
from django.urls import reverse

def redirect_to_year(request):
# ...
year = 2006
# ...
return HttpResponseRedirect(reverse('news-year-archive', args=(year,)))

因为某些原因,如果决定改变每年已发布的文章存档内容的 URL ,你只需要改变 URLconf 中的条目即可。

命名 URL 模式

为了完成反向解析 URL ,你需要像上面那样使用 ** 命名 URL 模式 ** 。用于命名 URL 的字符串可以包含任意字符,并不仅限于 Python 里合法的命名。

当命名 URL 模式时,请选择名称与其他应用名称冲突的名字。如果你命名的 URL 模式 comment 和其他应用程序做了同样的事情,reverse()查询出的 URL 会是在项目的 urlpatterns 列表里靠后的那个。

在 URL 名称前加入前缀,可以来自app名称(比如 myapp-comment 而不是 comment ),这样可以减少冲突。

URL 命名空间

介绍

URL 命名空间允许你使用唯一的反向命名URL模式,即便不同应用程序使用相同的 URL 名称。同样,如果已部署了应用程序的多个实例,它也允许你反向解析 URL。换句话说,因为单个应用的多个实例会分享已命名的 URL,命名空间提供了区分这些已命名 URL 的方法。

URL 命名空间分为两部分,它们都是字符串:

  • 应用程序命名空间

    这描述了正在部署的程序名。单个应用的每个实例拥有相同的命名空间。比如,Django admin 应用有可预测的应用命名空间 'admin'

  • 实例命名空间

    这标识了应用程序的特定实例。实例命名空间应该是完整项目唯一的。但是实例命名空间可以和应用命名空间相同。这常用来指定应用的默认实例。比如,默认Django admin 实例拥有名为 'admin' 的实例命名空间。

被指定的命名空间 URL 使用 ':' 操作符。比如,使用 'admin:index' 引用admin 应用的首页。这表明命名空间为 'admin' ,命名 URL 为'index'

命名空间也可以嵌套。命名 URL 'sports:polls:index' 将在命名空间 'polls' 中寻找命名为 'index' 的模式,该模式是在顶层命名空间 'sports' 中定义的。

反向命名空间

当给定一个命名空间 URL(例如 'polls:index' )解析时,Django 会将完全限定的名称拆分成多个部分,然后尝试下面的查询:

  1. 首先,Django 查找匹配 application namespace(这个例子里是 'polls' )。这将产生应用实例列表。

  2. 如果定义了当前应用程序,Django 会为这个实例查找并返回 URL 解析器。可以用 reverse() 函数的 current_app 参数来指定当前应用程序。

    url 模板标签使用当前已解析的视图命名空间当作 RequestContext 中的应用程序。你可以通过设置在 request.current_app 属性上的当前应用来覆盖这个默认配置。

  3. 如果当前没有应用程序,Django 会寻找默认的应用实例。默认应用程序实例是具有与实例命名空间匹配的应用程序命名空间的实例(比如, polls 实例被称为 'polls' )。

  4. 如果没有默认的应用程序实例,Django 将会引用最后一次部署的应用程序实例,无论其实例命名是什么。

  5. 如果提供的命名空间无法在第一步里匹配应用程序命名空间( application namespace ),Django 会尝试直接寻找命名空间来作为实例命名空间( instance namespace )。

如果有嵌套的命名空间,则会对命名空间的每个部分重复这些步骤,直到视图名不被解析为止,然后视图名称将被解析为已找到的命名空间中的一个 URL 。

例如

为了展示这个解决策略的实际作用,请考虑教程里 polls 应用程序的两个实例案例:分别被称为 'author-polls''publisher-polls' 。假设我们已经增强了这个应用程序,以便会在创建和显示 polls 时考虑实例命名空间

urls.py

1
2
3
4
5
6
from django.urls import include, path

urlpatterns = [
path('author-polls/', include('polls.urls', namespace='author-polls')),
path('publisher-polls/', include('polls.urls', namespace='publisher-polls')),
]

polls/urls.py

1
2
3
4
5
6
7
8
9
10
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
...
]

使用这步后,可以进行以下查找:

  • 如果其中一个实例是最新的- 例如,如果我们在实例 'author-polls' 中渲染详情页 - 'polls:index' 将解析为 'author-polls' 的首页;比如下面两种都将触发 "/author-polls/"

    在基于类的视图里的方法:

    1
    reverse('polls:index', current_app=self.request.resolver_match.namespace)

    以及在模板中:

    1
    {% url 'polls:index' %}
  • 如果其中一个实例是最新的 - 例如,如果我们在站点某处渲染一个页面 - 'polls:index' 将被解析为 polls 的最后一个注册实例。因为这里没有默认实例( 'polls' 的实例命名空间),所以将使用 polls 的最后一个注册实例。这将是 'publisher-polls' ,因为它是在 urlpatterns 的最后面声明的。

  • 'author-polls:index' 会一直被解析为实例 'author-polls' 的首页(对于 'publisher-polls' 同样如此)。

如果还有一个默认实例 - 例如,一个叫 'polls' 的实例 - 唯一的变化就是没有当前实例(上面列表中的第二项)。在这个例子 'polls:index' 将解析为默认实例的首页而不是在 urlpatterns 中最后声明的实例。

URL 命名空间和包含的 URLconfs

有两种办法指定包含的URLconfs应用程序空间。

首先,你可以在包含的 URLcon 模块中设置一个 app_name 属性,在相同层作为 urlpatterns 属性。你必须传递实际的模块或对该模块的一个字符串引用传递给 include() ,而不是 urlpatterns 本身的列表。

polls/urls.py

1
2
3
4
5
6
7
8
9
10
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
...
]

urls.py

1
2
3
4
5
from django.urls import include, path

urlpatterns = [
path('polls/', include('polls.urls')),
]

polls.urls 里的 URLs 定义将具有应用程序命名空间 polls

其次,你可以包括一个包含嵌入式命名空间数据的对象。如果你 include() 了一个 path()re_path() 实例的列表,那个对象里包含的 URLs 将被添加到全局命名空间内。但是,你也可以 include() 一个包含以下内容的2元组:

1
(<list of path()/re_path() instances>, <application namespace>)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
from django.urls import include, path

from . import views

polls_patterns = ([
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
], 'polls')

urlpatterns = [
path('polls/', include(polls_patterns)),
]

这会将指定的 URL 模式包含到给定的应用程序命名空间里。

使用 include()namespace 参数来指定实例命名空间。如果实例命名空间没有被指定,会默认已被导入的 URLconf 的应用程序命名空间。这意味着它也将成为那个命名空间的默认实例。

参考:

django的使用3-聚合

[TOC]

聚合

Django 数据库抽象 API 描述了使用 Django queries 来增删查改单个对象的方法。 然而,有时候你要获取的值需要根据一组对象聚合后才能得到。

模型示例: 作者,出版社,书,商店。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.db import models

class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()

class Publisher(models.Model):
name = models.CharField(max_length=300)

class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
pubdate = models.DateField()

class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)

常见的聚合查询

  • expressions

  • output_field

  • filter

  • Avg

  • Count

  • Max

  • Min

  • Sum

  • Variance: 返回给定表达式中数据的方差

  • StdDev: 返回给定表达式中数据的标准差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Total number of books.
>>> Book.objects.count()
2452

# Total number of books with publisher=BaloneyPress
>>> Book.objects.filter(publisher__name='BaloneyPress').count()
73

# Average price across all books.
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg('price'))
{'price__avg': 34.35}

# Max price across all books.
>>> from django.db.models import Max
>>> Book.objects.all().aggregate(Max('price'))
{'price__max': Decimal('81.20')}

# Difference between the highest priced book and the average price of all books.
>>> from django.db.models import FloatField
>>> Book.objects.aggregate(
... price_diff=Max('price', output_field=FloatField()) - Avg('price'))
{'price_diff': 46.85}

# All the following queries involve traversing the Book<->Publisher
# foreign key relationship backwards.

# Each publisher, each with a count of books as a "num_books" attribute.
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count('book'))
>>> pubs
<QuerySet [<Publisher: BaloneyPress>, <Publisher: SalamiPress>, ...]>
>>> pubs[0].num_books
73

# Each publisher, with a separate count of books with a rating above and below 5
>>> from django.db.models import Q
>>> above_5 = Count('book', filter=Q(book__rating__gt=5))
>>> below_5 = Count('book', filter=Q(book__rating__lte=5))
>>> pubs = Publisher.objects.annotate(below_5=below_5).annotate(above_5=above_5)
>>> pubs[0].above_5
23
>>> pubs[0].below_5
12

# The top 5 publishers, in order by number of books.
>>> pubs = Publisher.objects.annotate(num_books=Count('book')).order_by('-num_books')[:5]
>>> pubs[0].num_books
1323

aggregate

Django 提供了两种生成聚合的方法。第一种方法是从整个 QuerySet 生成汇总值。比如你想要计算所有在售书的平均价格。Django 的查询语法提供了一种用来描述所有图书集合的方法:

1
>>> Book.objects.all()

可以通过在 QuerySet 后添加 aggregate() 子句来计算 QuerySet 对象的汇总值。

1
2
3
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg('price'))
{'price__avg': 34.35}

本例中的 all() 是多余的,所以可以简化成这样的:

1
2
>>> Book.objects.aggregate(Avg('price'))
{'price__avg': 34.35}

传递给 aggregate() 的参数描述了我们想要计算的聚合值。在这个例子里,要计算的就是 Book 模型上的 price 字段的平均值。

aggregate()QuerySet 的一个终端子句,使用后将返回“名称-值”的字典,其中“名称”就是聚合值的标志,“值”就是计算出的聚合结果。“名称”是根据字段名和聚合函数而自动生成的。如果你想指定一个聚合值的名称,你可以在指定聚合子句的时候提供指定的名称:

1
2
>>> Book.objects.aggregate(average_price=Avg('price'))
{'average_price': 34.35}

如果你想生成更多的聚合内容,你需要在 aggregate() 子句中加入其它参数即可。所以,如果我们也想知道所有书中最高和最低的价格,我们可以写这样的查询:

1
2
3
>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}

annotate

生成值的汇总的另一个办法是为 QuerySet的每一个对象生成独立汇总。

使用 annotate() 子句可以生成每一个对象的汇总。当指定 annotate() 子句,QuerySet 中的每一个对象将对指定值进行汇总。

这些汇总语法规则与 aggregate() 子句的规则相同。annotate() 的每一个参数描述了一个要计算的聚合。比如,注解(annotate)所有书的所有作者:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Build an annotated queryset
>>> from django.db.models import Count
>>> q = Book.objects.annotate(Count('authors'))
# Interrogate the first object in the queryset
>>> q[0]
<Book: The Definitive Guide to Django>
>>> q[0].authors__count
2
# Interrogate the second object in the queryset
>>> q[1]
<Book: Practical Django Projects>
>>> q[1].authors__count
1

aggregate() 一样,注解的名称是根据聚合函数和被聚合的字段名自动生成的。当你在指定注解的时候,你可以通过提供一个别名重写这个默认名。

1
2
3
4
5
>>> q = Book.objects.annotate(num_authors=Count('authors'))
>>> q[0].num_authors
2
>>> q[1].num_authors
1

aggregate() 不同的是,annotate() 不是终端子句。annotate() 子句的输出就是 QuerySet;这个 QuerySet 被其他 QuerySet 操作进行修改,包括 filter(), order_by(),甚至可以对annotate()` 进行额外调用

组合多个聚合

使用 annotate() 组合多个聚合将产生错误的结果,因为它使用连接(joins)而不是子查询:

1
2
3
4
5
6
7
8
9
10
>>> book = Book.objects.first()
>>> book.authors.count()
2
>>> book.store_set.count()
3
>>> q = Book.objects.annotate(Count('authors'), Count('store'))
>>> q[0].authors__count
6
>>> q[0].store__count
6

对大部分聚合来说,没办法避免这个问题,但是,Count聚合可以使用 distinct 参数来避免:

1
2
3
4
5
>>> q = Book.objects.annotate(Count('authors', distinct=True), Count('store', distinct=True))
>>> q[0].authors__count
2
>>> q[0].store__count
3

连接(Joins)和聚合

有时候想聚合的值属于你正在查询模型的关联模型。在聚合函数里面指定聚合的字段时,Django 允许你在过滤相关字段的时候使用相同的双下划线表示法。Django 将处理任何需要检索和聚合的关联值的表连接(table joins)。

比如,要寻找每个书店提供的书籍价格区间,你可以使用这个注解(annotation):

1
2
>>> from django.db.models import Max, Min
>>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))

这告诉 Django 去检索 Store 模型,连接(通过多对多关系) Book 模型,并且聚合书籍模型的价格字段来获取最大最小值。

相同规则应用于 aggregate() 从句。如果你想知道任何店铺正在销售的任何书籍的最低最高价,你可以使用这个聚合:

1
>>> Store.objects.aggregate(min_price=Min('books__price'), max_price=Max('books__price'))

Join 链可以根据你的需求尽可能深。比如,要提取所出售的书籍中最年轻的作者年龄,你可以写这样的查询:

1
>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))

参考:

django的使用3-数据库抽象API

[TOC]

数据库抽象API

一旦创建数据模型后,Django 自动给予你一套数据库抽象 API,允许你创建,检索,更新和删除对象。下面介绍这些API。

先创建三个模型:博客,作者,记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django.db import models

class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()

def __str__(self):
return self.name

class Author(models.Model):
name = models.CharField(max_length=200)
email = models.EmailField()

def __str__(self):
return self.name

class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=255)
body_text = models.TextField()
pub_date = models.DateField()
mod_date = models.DateField()
authors = models.ManyToManyField(Author)
number_of_comments = models.IntegerField()
number_of_pingbacks = models.IntegerField()
rating = models.IntegerField()

def __str__(self):
return self.headline

创建新数据

一个模型类代表一张数据表,一个模型类的实例代表数据库表中的一行记录。要创建一个对象,用关键字参数初始化它,然后调用 save() 将其存入数据库。

1
2
3
>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

注意: 必须执行了save() 方法之后,才会把数据更新到数据库。

更新数据

更新数据有两种情况:

  • ForeignKey
  • ManyToManyField

ForeignKey

更新 ForeignKey 字段的方式与保存普通字段的方式相同——只需将正确类型的实例分配给相关字段。如下entry

1
2
3
4
5
>>> from blog.models import Blog, Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

ManyToManyField

更新 ManyToManyField 字段有点不同——在字段上使用 add() 方法为关联关系添加一条记录。如下entry.authors

1
2
3
>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

查询数据

要从数据库检索对象,要通过模型类的 Manager 构建一个 QuerySet。一个 QuerySet 代表来自数据库中对象的一个集合。它可以有 0 个,1 个或者多个 filters. Filters,可以根据给定参数缩小查询结果量。

你能通过模型的 Manager 获取 QuerySet。每个模型至少有一个 Manager,默认名称是 objects。像这样直接通过模型类使用它:

1
2
3
4
5
6
7
>>> Blog.objects
<django.db.models.manager.Manager object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
...
AttributeError: "Manager isn't accessible via Blog instances."

检索全部对象数据

调用 all() 方法即可。

1
all_entries = Entry.objects.all()

过滤器检索

两种最常见的方式:

  • filter(**kwargs)
  • exclude(**kwargs)

filter(**kwargs) 返回一个新的 QuerySet,包含的对象满足给定查询参数。
exclude(**kwargs) 返回一个新的 QuerySet,包含的对象 不 满足给定查询参数。

例子如下:

1
2
Entry.objects.filter(pub_date__year=2006)
Entry.objects.all().filter(pub_date__year=2006)

链式过滤器

结果本身还是一个 QuerySet,所以能串联精炼过程。例子:

1
2
3
4
5
6
7
>>> Entry.objects.filter(
... headline__startswith='What'
... ).exclude(
... pub_date__gte=datetime.date.today()
... ).filter(
... pub_date__gte=datetime.date(2005, 1, 30)
... )

这个先获取包含数据库所有条目(entry)的 QuerySet,然后排除一些,再进入另一个过滤器。最终的 QuerySet 包含标题以 “What” 开头的,发布日期介于 2005 年 1 月 30 日与今天之间的所有条目。

注意:QuerySet 是惰性的

1
2
3
4
>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q = q.exclude(body_text__icontains="food")
>>> print(q)

虽然这看起来像是三次数据库操作,实际上只在最后一行 (print(q)) 做了一次。

检索单个对象

若你知道只会有一个对象满足查询条件,你可以在 Manager 上使用 get() 方法,它会直接返回这个对象:

1
>>> one_entry = Entry.objects.get(pk=1)

限制 QuerySet 条目数

利用 Python 的数组切片语法将 QuerySet 切成指定长度。这等价于 SQL 的 LIMIT 和 OFFSET 子句。

1
>>> Entry.objects.all()[:5]

字段查询

基本的查询关键字参数遵照 field__lookuptype=value。(有个双下划线)。例如:

1
>>> Entry.objects.filter(pub_date__lte='2006-01-01')

转换为 SQL 语句大致如下:

1
SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

常见的查询

exact(默认):

1
2
>>> Entry.objects.get(headline__exact="Cat bites dog")
SQL: SELECT ... WHERE headline = 'Cat bites dog';

若关键字参数未包含双下划线 —— 查询类型会被指定为 exact。因为 exact 查询是最常见的。

iexact:不分大小写的匹配,查询语句:

1
>>> Blog.objects.get(name__iexact="beatles blog")

contains: 大小写敏感的包含。例子:

1
2
Entry.objects.get(headline__contains='Lennon')
SQL: SELECT ... WHERE headline LIKE '%Lennon%';

icontains: 大小写不敏感的包含查找。

startswith: 以……开头的查找

endswith: 以……结尾的查找

istartswith: 大小写不敏感的, 以……开头的查找

iendswith: 大小写不敏感的, 以……结尾的查找

跨关系查询

Django 提供了一种强大而直观的方式来“追踪”查询中的关系,在幕后自动为你处理 SQL JOIN 关系。为了跨越关系,跨模型使用关联字段名,字段名由双下划线分割,直到拿到想要的字段。

1
>>> Entry.objects.filter(blog__name='Beatles Blog')

本例检索出所有的 Entry 对象,其 Blogname'Beatles Blog' .

1
>>> Blog.objects.filter(entry__headline__contains='Lennon')

本例检索的所有 Blog 对象均拥有少一个 标题 含有 'Lennon' 的条目.

要筛选出所有关联条目同时满足标题含有 “Lennon” 发布于 2008 (同一个条目,同时满足两个条件)年的博客,这样写:

1
Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

要筛选所有条目标题包含 “Lennon” 条目发布于 2008 年的博客,这样写:

1
Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

过滤器指定字段

Django 提供了 F 表达式, 实现将模型字段值与同一模型中的另一字段做比较.

例如,要查出所有评论数大于 pingbacks 的博客条目.

1
2
>>> from django.db.models import F
>>> Entry.objects.filter(number_of_comments__gt=F('number_of_pingbacks'))

F 表达式特点:

  • Django 支持对 F() 对象进行加、减、乘、除、求余和次方.

  • 用双下划线在 F() 对象中通过关联关系查询。

  • F() 对象通过 .bitand(), .bitor(), .bitxor(),.bitrightshift() 和 .bitleftshift() 支持位操作。

  • 对于 date 和 date/time 字段,你可以加上或减去一个 timedelta 对象。

主键 (pk) 查询快捷方式

出于方便的目的,Django 提供了一种 pk 查询快捷方式, pk 表示主键 “primary key”。

1
2
3
4
5
6
7
8
>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])
# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

pk 查找也支持跨连接。

1
2
3
>>> Entry.objects.filter(blog__id__exact=3) # Explicit form
>>> Entry.objects.filter(blog__id=3) # __exact is implied
>>> Entry.objects.filter(blog__pk=3) # __pk implies __id__exact

缓存和 QuerySet

每个 QuerySet 都带有缓存,尽量减少数据库访问。

例如,以下会创建两个 QuerySet,计算它们,丢掉它们:

1
2
>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

两个结果:

  • 同样的数据库查询会被执行两次,实际加倍了数据库负载。
  • 有可能这两个列表不包含同样的记录,因为在两次请求间,可能有 Entry 被添加或删除了。

要避免此问题,保存 QuerySet 并复用它:

1
2
3
>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation.

什么时候不会缓存?

当仅计算查询结果集的 部分 时,会校验缓存,若没有填充缓存,则后续查询返回的项目不会被缓存。使用数组切片或索引的 限制查询结果集 不会填充缓存。

例如:

1
2
3
>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # Queries the database
>>> print(queryset[5]) # Queries the database again

若全部查询结果集已被检出,就会去检查缓存:

1
2
3
4
>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print(queryset[5]) # Uses cache
>>> print(queryset[5]) # Uses cache

触发计算结果并缓存的操作包括如下几个方法:

1
2
3
4
>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

JSONField

JSONField 里的查找实现是不一样的,主要因为存在key转换。为了演示,我们将使用下面这个例子:

1
2
3
4
5
6
7
8
from django.db import models

class Dog(models.Model):
name = models.CharField(max_length=200)
data = models.JSONField(null=True)

def __str__(self):
return self.name

保存和查询 None

如果 None 被保存在列表或字典中,它将始终被解释为JSON的 null 值。当查询时,None 值将一直被解释为JSON的null。要查询SQL的NULL,需要使用 isnull:

1
2
3
4
5
6
7
8
9
10
11
12
>>> Dog.objects.create(name='Max', data=None)  # SQL NULL.
<Dog: Max>
>>> Dog.objects.create(name='Archie', data=Value('null')) # JSON null.
<Dog: Archie>
>>> Dog.objects.filter(data=None)
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=Value('null'))
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
<QuerySet [<Dog: Max>]>
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>

Key, index, 和路径转换

为了查询给定的字典键,可以将该键作为查询名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> Dog.objects.create(name='Rufus', data={
... 'breed': 'labrador',
... 'owner': {
... 'name': 'Bob',
... 'other_pets': [{
... 'name': 'Fishy',
... }],
... },
... })
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': None})
<Dog: Meg>
>>> Dog.objects.filter(data__breed='collie')
<QuerySet [<Dog: Meg>]>


>>> Dog.objects.filter(data__owner__name='Bob')
<QuerySet [<Dog: Rufus>]>

# 如果键是个整型,那么它将在数组中被解释成一个索引:
>>> Dog.objects.filter(data__owner__other_pets__0__name='Fishy')
<QuerySet [<Dog: Rufus>]>

如果查询时缺少键名,请使用 isnull 查询:

1
2
3
4
>>> Dog.objects.create(name='Shep', data={'breed': 'collie'})
<Dog: Shep>
>>> Dog.objects.filter(data__owner__isnull=True)
<QuerySet [<Dog: Shep>]>

包含与键查找

contains

给定的键值对都包含在顶级字段中的对象

1
2
3
4
5
6
7
8
9
10
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contains={'owner': 'Bob'})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>
>>> Dog.objects.filter(data__contains={'breed': 'collie'})
<QuerySet [<Dog: Meg>]>

contained_by

这是 contains 查找逆过程——返回的对象将是那些传递的值中的子集在对象上的键值对。例如:

1
2
3
4
5
6
7
8
9
10
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie', 'owner': 'Bob'})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
<QuerySet [<Dog: Fred>]>

has_key

返回给定的键位于数据顶层的对象。例如:

1
2
3
4
5
6
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_key='owner')
<QuerySet [<Dog: Meg>]>

has_keys

返回所有给定的键位于数据顶层的对象。例如:

1
2
3
4
5
6
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_keys=['breed', 'owner'])
<QuerySet [<Dog: Meg>]>

has_any_keys

返回任何给定的键位于数据顶层的对象。例如

1
2
3
4
5
6
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_any_keys=['owner', 'breed'])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

通过 Q 对象完成复杂查询

在类似 filter() 中,查询使用的关键字参数是通过 “AND” 连接起来的。如果你要执行更复杂的查询(例如,由 OR 语句连接的查询),你可以使用 Q 对象。

一个 Q 对象 (django.db.models.Q) 用于压缩关键字参数集合。这些关键字参数由前文 “Field lookups” 指定。

例如,该 Q 对象压缩了一个 LIKE 查询:

1
2
from django.db.models import Q
Q(question__startswith='What')

Q 对象能通过 &| 操作符连接起来。当操作符被用于两个 Q 对象之间时会生成一个新的 Q 对象。

例如,该语句生成一个 Q 对象,表示两个 "question_startswith" 查询语句之间的 “OR” 关系:

1
Q(question__startswith='Who') | Q(question__startswith='What')

这等价于以下 SQL WHERE 字句:

1
WHERE question LIKE 'Who%' OR question LIKE 'What%'

你能通过 &| 操作符和括号分组,组合任意复杂度的语句。当然, Q 对象也可通过 ~ 操作符反转,允许在组合查询中组合普通查询或反向 (NOT) 查询:

1
Q(question__startswith='Who') | ~Q(pub_date__year=2005)

每个接受关键字参数的查询函数 (例如 filter(), exclude(), get()) 也同时接受一个或多个 Q 对象作为位置(未命名的)参数。若你为查询函数提供了多个 Q 对象参数,这些参数会通过 “AND” 连接。例子:

1
2
3
4
Poll.objects.get(
Q(question__startswith='Who'),
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

…粗略地转为 SQL:

1
2
SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

查询函数能混合使用 Q 对象和关键字参数。所有提供给查询函数的参数(即关键字参数或 Q 对象)均通过 “AND” 连接。然而,若提供了 Q 对象,那么它必须位于所有关键字参数之前。例子:

1
2
3
4
Poll.objects.get(
Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
question__startswith='Who',
)

……会是一个有效的查询,等效于前文的例子;

比较对象

要比较两个模型实例,使用标准的 Python 比较操作符,两个等号: ==。实际上,这比较了两个模型实例的主键值。比较时总会使用主键,不管它叫啥。

删除对象

通常,删除方法被命名为 delete()。该方法立刻删除对象,并返回被删除的对象数量和一个包含了每个被删除对象类型的数量的字典。例子:

1
2
3
4
5
6
>>> e.delete()
(1, {'weblog.Entry': 1})

# 批量删除对象。
>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

某个对象被删除时,关联对象也会被删除。例子:

1
2
3
b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

复制模型实例

pk 设为 None

1
2
3
4
5
blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save() # blog.pk == 1

blog.pk = None
blog.save() # blog.pk == 2

一次修改多个对象

1
2
3
4
5
6
7
# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')
>>> b = Blog.objects.get(pk=1)

# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.all().update(blog=b)

执行原生查询

若管理器方法 raw() 能用于执行原生 SQL 查询,就会返回模型实例:

Manager.raw(raw_query, params=None, translations=None)
该方法接受一个原生 SQL 查询语句,执行它,并返回一个 django.db.models.query.RawQuerySet 实例。这个 RawQuerySet 能像普通的 QuerySet 一样被迭代获取对象实例。

最好用例子来解释。假设你有以下模型:

1
2
3
4
5
6
7
8
9
class Person(models.Model):
first_name = models.CharField(...)
last_name = models.CharField(...)
birth_date = models.DateField(...)

>>> for p in Person.objects.raw('SELECT * FROM myapp_person'):
... print(p)
John Smith
Jane Jones

参考: