[Django]Django model for pg,看完你还不用 pg 算我输

本文详细介绍了Django中针对PostgreSQL数据库的特殊字段,包括ArrayField、JSONField、HStoreField和Range Field的定义、参数及查询方法。通过示例展示了如何在模型中使用这些字段,并探讨了PostgreSQL相对于其他数据库的优势,如丰富的数据类型和强大的查询能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

640?wx_fmt=png

postgres 是 django 官方推荐使用的数据库。为什么使用 postgres 以及 mysql 和 postgres 各有什么优劣不是这篇文章的重点,如果感兴趣可以参考下面这些文章:

  • What are pros and cons of PostgreSQL and MySQL? With respect to reliability, speed, scalability, and features

  • PostgreSQL vs MySQL

  • PostgreSQL Vs. MySQL: Differences In Performance, Syntax, And Features

  • Why I Choose PostgreSQL Over MySQL/MariaDB

  • PostgreSQL 与 MySQL 相比,优势何在?

django 的 model有一些只针对于 postgres 的 fields,这篇文章就简要地介绍一下这些 pg specific fields,然后还会追踪到 pg 相对于的 feature(因为这一切都是以 pg 强大的特性作为支持的)。

本文的例子全部来源于django的官方文档。

Field 类型一览:

  • ArrayField

  • JSONField

  • HStoreField

  • Range Field

ArrayField

定义

class ArrayField(base_field, size=None, **options)

base_field参数

有一个必选的参数 base_field,挺好理解,一个 Array 得指定元素类型。所以你可以传入 IntegerFieldCharFieldTextField,但是不能传 ForeignKey, OneToOneField , ManyToManyFieldArrayField还能实现嵌套列表的功能! 请看下面这个例子:

from django.contrib.postgres.fields import ArrayField	
from django.db import models	
class ChessBoard(models.Model):	
    board = ArrayField(	
        ArrayField(	
            models.CharField(max_length=10, blank=True),	
            size=8,	
        ),	
        size=8,	
    )

这个会在数据库中生成一个 character varying(10)[]类型的字段:

640?wx_fmt=png

可以这样插入数据:

c = ChessBoard()	
c.board = [["a", "b", "c"], ["d", "e", "f"]]	
c.save()

这里有一点是需要注意的,那就是传入的嵌套列表长度要是一样的,不然会触发异常:

django.db.utils.DataError: multidimensional arrays must have array expressions with matching dimensions

这是因为 pg 本身对于 multidimensional array类型数据做了这个限制:

640?wx_fmt=png

上图来源于pg官方文档:8.15. Arrays

size参数

可选,指定 Array的最大长度。但是事实上pg 并不会做强制限制,如果你插入的列表长度超过了 size,不会报错,还是能成功执行:

c = ChessBoard()	
c.board = [	
    ["a", "b", "c"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
    ["d", "e", "f"],	
]	
c.save()

select*一下:

640?wx_fmt=png

query查询

以这个 model 为例:

from django.contrib.postgres.fields import ArrayField	
from django.db import models	
class Post(models.Model):	
    name = models.CharField(max_length=200)	
    tags = ArrayField(models.CharField(max_length=200), blank=True)	
    def __str__(self):	
        return self.name

生成的 table 为:

640?wx_fmt=png

contains

插入三条数据:

Post.objects.create(name='First post', tags=['thoughts', 'django'])	
Post.objects.create(name='Second post', tags=['thoughts'])	
Post.objects.create(name='Third post', tags=['tutorial', 'django'])

过滤tags包含某个 tag 的数据:

tags__contains传入的是一个列表

>>> Post.objects.filter(tags__contains=['thoughts'])	
<QuerySet [<Post: First post>, <Post: Second post>]>	
>>> Post.objects.filter(tags__contains=['django'])	
<QuerySet [<Post: First post>, <Post: Third post>]>	
>>> Post.objects.filter(tags__contains=['django', 'thoughts'])	
<QuerySet [<Post: First post>]>

contained_by

contains相反,这个查询的是 tags 是传入数据的 subset。

>>> Post.objects.filter(tags__contained_by=['thoughts', 'django'])	
<QuerySet [<Post: First post>, <Post: Second post>]>	
>>> Post.objects.filter(tags__contained_by=['thoughts', 'django', 'tutorial'])	
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

overlap

只要包含其中一个就行了,也就是你传入的列表范围越大,查询到的数据可能性就越多。而前面的 contains 传入的列表越长,得到的数据可能就越少。

>>> Post.objects.filter(tags__overlap=['thoughts'])	
<QuerySet [<Post: First post>, <Post: Second post>]>	
>>> Post.objects.filter(tags__overlap=['thoughts', 'tutorial'])	
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

len

根据 ArrayField的长度进行查询。

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])	
>>> Post.objects.create(name='Second post', tags=['thoughts'])	
>>> Post.objects.filter(tags__len=1)	
<QuerySet [<Post: Second post>]>

Indextransforms

查询列表的某个特定元素(pg是不是有点强大~),任何非负数都可以,如果超过了 size 也不会报错

>>> Post.objects.filter(tags__0='thoughts')	
<QuerySet [<Post: First post>, <Post: Second post>]>	
>>> Post.objects.filter(tags__1__iexact='Django')	
<QuerySet [<Post: First post>]>	
>>> Post.objects.filter(tags__276='javascript')	
<QuerySet []>

Slicetransforms

Indextransforms类似,但是不是针对某个元素,而是一个 slice:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])	
>>> Post.objects.create(name='Second post', tags=['thoughts'])	
>>> Post.objects.create(name='Third post', tags=['django', 'python', 'thoughts'])	
>>> Post.objects.filter(tags__0_1=['thoughts'])	
<QuerySet [<Post: First post>, <Post: Second post>]>	
>>> Post.objects.filter(tags__0_2__contains=['thoughts'])	
<QuerySet [<Post: First post>, <Post: Second post>]>

JSONField

定义

class JSONField(encoder=None, **options)

Python的这些native format都可以用:dictionaries, lists, strings, numbers, booleans, None.

下文的示例用的是这个 model:

class Dog(models.Model):	
    name = models.CharField(max_length=200)	
    data = JSONField()	
    def __str__(self):	
        return self.name

data字段是一个 jsonb类型数据:

640?wx_fmt=png

插入数据示例:

Dog.objects.create(name='Rufus', data={	
    'breed': 'labrador',	
    'owner': {	
        'name': 'Bob',	
        'other_pets': [{	
            'name': 'Fishy',	
        }],	
    }	
})

640?wx_fmt=png

encoder 参数

可选,什么时候有用呢?当你的数据不是 Python native 类型的时候,比如 uuid、datetime等。 这个时候可以用 DjangoJSONEncoder或任何满足需求的 json.JSONEncoder子类。

上面那个 model 插入非 python native 对象的时候就会报错,比如插入 datetime.datetime.now()会提示:

TypeError: Object of type datetime is not JSON serializable

如果你非要插入datetime 类型的数据,可以使用DjangoJSONEncoder(详细的官方文档在这里),也就是:

from django.core.serializers.json import DjangoJSONEncoder	
class Dog(models.Model):	
    name = models.CharField(max_length=200)	
    data = JSONField(encoder=DjangoJSONEncoder)

然后执行下面这条插入语句不会报错了:

Dog.objects.create(name='Rufus', data={	
    'breed': 'labrador',	
    'owner': {	
        'name': 'Bob',	
        'other_pets': [{	
            'name': 'Fishy',	
        }],	
    },	
    'birthday': datetime.datetime.now()	
})

需要注意的是,pg 真正存储的时候,还是用字符串存的,而且取出来的时候,不会自动转回 datetime,还是字符串。

640?wx_fmt=png

dog = Dog.objects.filter(name='Rufus')[1]	
print(type(dog.data['birthday']))
<class 'str'>

我想,django 不为你自动转换的原因,应该是考虑到存进去是字符串,有可能只是恰好那个字符串长得像 datetime 格式,强行转换可能并不是你想要的结果。而你要比 django 更清楚数据是什么类型的,什么时候需要转换什么时候不需要。

查询数据

插入测试数据:

Dog.objects.create(name='Rufus', data={	
    'breed': 'labrador',	
    'owner': {	
        'name': 'Bob',	
        'other_pets': [{	
            'name': 'Fishy',	
        }]	
    }	
})

Key, index, and path lookups

Dog.objects.filter(data__owner=None)	
Dog.objects.filter(data__breed='collie')	
Dog.objects.filter(data__owner__name='Bob')	
Dog.objects.filter(data__owner__other_pets__0__name='Fishy')	
# 查询 missing 的 key,使用 isnull	
>>> Dog.objects.create(name='Shep', data={'breed': 'collie'})	
>>> Dog.objects.filter(data__owner__isnull=True)	
<QuerySet [<Dog: Shep>]>

是不是和 mongo 对 json 类型数据的支持一样强大!

其他

和下面要讲的 HStoreField一样有下面几个查询方法:

  • contains

  • contained_by

  • has_key

  • hasanykeys

  • has_keys

HStoreField

定义

class HStoreField(**options)

用来存储键值对类型数据,对应 Python 数据类型为 dict,但是 key 必须是字符串,value 必须是字符串或者 null。

如果要使用这个 field,还需要做两步额外的事情:

  • 将 django.contrib.postgres添加到 INSTALLED_APPS

  • 开启 PG 的 hstore extension

第二步是要修改一个 migrations文件:

比如这样的一个model:

class Dog(models.Model):	
    name = models.CharField(max_length=200)	
    data = HStoreField()	
    def __str__(self):	
        return self.name

原始的 migrations 文件是这样的:

import django.contrib.postgres.fields.hstore	
from django.db import migrations, models	
class Migration(migrations.Migration):	
    dependencies = [	
        ('goods', '0004_auto_20190514_1502'),	
    ]	
    operations = [	
        migrations.CreateModel(	
            name='Dog',	
            fields=[	
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),	
                ('name', models.CharField(max_length=200)),	
                ('data', django.contrib.postgres.fields.hstore.HStoreField()),	
            ],	
        ),	
    ]

我们需要做一点点修改:在operations的最前面添加一个 HStoreExtension()

from django.contrib.postgres.operations import HStoreExtension	
class Migration(migrations.Migration):	
    ...	
    operations = [	
        HStoreExtension(),	
        ...	
    ]

最终的 migrations 文件是这样的:

import django.contrib.postgres.fields.hstore	
from django.db import migrations, models	
from django.contrib.postgres.operations import HStoreExtension	
class Migration(migrations.Migration):	
    dependencies = [	
        ('goods', '0004_auto_20190514_1502'),	
    ]	
    operations = [	
        HStoreExtension(),	
        migrations.CreateModel(	
            name='Dog',	
            fields=[	
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),	
                ('name', models.CharField(max_length=200)),	
                ('data', django.contrib.postgres.fields.hstore.HStoreField()),	
            ],	
        ),	
    ]

关于在 migrations 里面添加数据库插件的功能情看官方文档。

这个第二步是不能少的。 不然会报以下错误:

can't adapt type 'dict' if you skip the first step, or type "hstore" does not exist

查询

Keylookups

根据某个 key 的值查询:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})	
>>> Dog.objects.create(name='Meg', data={'breed': 'collie'})	
>>> Dog.objects.filter(data__breed='collie')	
<QuerySet [<Dog: Meg>]>

还可以链式调用其他的查询方法:

>>> Dog.objects.filter(data__breed__contains='l')	
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

contains

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})	
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})	
>>> Dog.objects.create(name='Fred', data={})	
>>> Dog.objects.filter(data__contains={'owner': 'Bob'})	
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>	
>>> Dog.objects.filter(data__contains={'breed': 'collie'})	
<QuerySet [<Dog: Meg>]>

contained_by

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})	
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})	
>>> Dog.objects.create(name='Fred', data={})	
>>> 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

根据是否包含某个 key 作为查询条件。

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

has_any_keys

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})	
>>> Dog.objects.create(name='Meg', data={'owner': 'Bob'})	
>>> Dog.objects.create(name='Fred', data={})	
>>> Dog.objects.filter(data__has_any_keys=['owner', 'breed'])	
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

has_keys

>>> Dog.objects.create(name='Rufus', data={})	
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})	
>>> Dog.objects.filter(data__has_keys=['breed', 'owner'])	
<QuerySet [<Dog: Meg>]>

keys

>>> Dog.objects.create(name='Rufus', data={'toy': 'bone'})	
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})	
>>> Dog.objects.filter(data__keys__overlap=['breed', 'toy'])	
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

values

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})	
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})	
>>> Dog.objects.filter(data__values__contains=['collie'])	
<QuerySet [<Dog: Meg>]>

Range Fields

pg 还支持范围类型的数据。比如 IntegerRangeField, BigIntegerRangeField, DecimalRangeField等,篇幅有限,这里就不讲了。有兴趣的情看官方文档。

总结一下

pg 很强大,非常强大,不只是一个关系型数据库,能实现的功能很多很多。尤其是内置的数据类型极其丰富。

640?wx_fmt=jpeg

pg 还有自带的全文搜索功能,你甚至不需要额外使用 elasticsearch;pg 针对 json 类型的数据做了索引优化,能实现 mongo 等非关系型数据库的功能。这也难怪 django 官方首推的数据库是 pg 了。

640?wx_fmt=jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值