[Django]Django model for pg,看完你還不用 pg 算我輸

postgres 是 django 官方推薦使用的數據庫。爲何使用 postgres 以及 mysql 和 postgres 各有什麼優劣不是這篇文章的重點,若是感興趣能夠參考下面這些文章:javascript

django 的 model有一些只針對於 postgres 的 fields,這篇文章就簡要地介紹一下這些 pg specific fields,而後還會追蹤到 pg 相對於的 feature(由於這一切都是以 pg 強大的特性做爲支持的)。html

本文的例子所有來源於django的官方文檔java

Field 類型一覽:python

  • ArrayField
  • JSONField
  • HStoreField
  • Range Field

ArrayField

定義

class ArrayField(base_field, size=None, **options)
複製代碼

base_field參數

有一個必選的參數base_field,挺好理解,一個 Array 得指定元素類型。因此你能夠傳入IntegerFieldCharFieldTextField,可是不能傳ForeignKey, OneToOneField , ManyToManyFieldArrayField還能實現嵌套列表的功能! 請看下面這個例子:mysql

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)[]類型的字段:sql

能夠這樣插入數據:數據庫

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

這裏有一點是須要注意的,那就是傳入的嵌套列表長度要是同樣的,否則會觸發異常:express

django.db.utils.DataError: multidimensional arrays must have array expressions with matching dimensions
複製代碼

這是由於 pg 自己對於multidimensional array類型數據作了這個限制:django

上圖來源於pg官方文檔:8.15. Arraysjson

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 *一下:

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 爲:

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>]>
複製代碼

Index transforms

查詢列表的某個特定元素(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 []>
複製代碼

Slice transforms

Index transforms相似,可是不是針對某個元素,而是一個 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類型數據:

插入數據示例:

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

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,仍是字符串。

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
  • has_any_keys
  • 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 複製代碼

查詢

Key lookups

根據某個 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 很強大,很是強大,不僅是一個關係型數據庫,能實現的功能不少不少。尤爲是內置的數據類型極其豐富。

pg 還有自帶的全文搜索功能,你甚至不須要額外使用 elasticsearch;pg 針對 json 類型的數據作了索引優化,能實現 mongo 等非關係型數據庫的功能。這也難怪 django 官方首推的數據庫是 pg 了。

若是你像我同樣真正熱愛計算機科學,喜歡研究底層邏輯,歡迎關注個人微信公衆號:

相關文章
相關標籤/搜索