模型與欄位

Model 類別、Field 實例和模型實例都對應到資料庫概念

物件

對應到…

模型類別

資料庫表格

欄位實例

表格上的欄位

模型實例

資料庫表格中的列

以下程式碼顯示您定義資料庫連線和模型類別的典型方式。

import datetime
from peewee import *

db = SqliteDatabase('my_app.db')

class BaseModel(Model):
    class Meta:
        database = db

class User(BaseModel):
    username = CharField(unique=True)

class Tweet(BaseModel):
    user = ForeignKeyField(User, backref='tweets')
    message = TextField()
    created_date = DateTimeField(default=datetime.datetime.now)
    is_published = BooleanField(default=True)
  1. 建立 Database 的實例。

    db = SqliteDatabase('my_app.db')
    

    db 物件將用於管理與 Sqlite 資料庫的連線。在此範例中,我們使用 SqliteDatabase,但您也可以使用其他資料庫引擎之一。

  2. 建立指定資料庫的基礎模型類別。

    class BaseModel(Model):
        class Meta:
            database = db
    

    定義建立資料庫連線的基礎模型類別是一種好的做法。這可以使您的程式碼 DRY,因為您不必為後續模型指定資料庫。

    模型設定會保留在名為 Meta 的特殊類別中。此慣例是借用自 Django。Meta 設定會傳遞給子類別,因此我們專案的模型都將繼承 BaseModel。您可以使用 Model.Meta 設定許多不同的屬性

  3. 定義模型類別。

    class User(BaseModel):
        username = CharField(unique=True)
    

    模型定義使用其他流行的 ORM(例如 SQLAlchemy 或 Django)中看到的宣告式樣式。請注意,我們正在擴充 BaseModel 類別,因此 User 模型將繼承資料庫連線。

    我們已明確定義具有唯一約束的單個 username 欄位。因為我們沒有指定主鍵,peewee 將自動新增一個名為 id 的自動遞增整數主鍵欄位。

注意

如果您想開始使用 peewee 處理現有的資料庫,可以使用 pwiz,一個模型產生器 自動產生模型定義。

欄位

Field 類別用於描述 Model 屬性到資料庫欄位的對應。每個欄位類型都有對應的 SQL 儲存類別(即 varchar、int),並且 python 資料類型和底層儲存之間的轉換會以透明方式處理。

建立 Model 類別時,欄位會定義為類別屬性。這對於 django 框架的使用者來說應該很熟悉。以下是一個範例

class User(Model):
    username = CharField()
    join_date = DateTimeField()
    about_me = TextField()

在上面的範例中,由於沒有任何欄位使用 primary_key=True 初始化,因此將自動建立一個自動遞增的主鍵並命名為「id」。Peewee 使用 AutoField 來表示自動遞增的整數主鍵,這表示 primary_key=True

有一種特殊的欄位類型,ForeignKeyField,可讓您以直觀的方式表示模型之間的外鍵關聯

class Message(Model):
    user = ForeignKeyField(User, backref='messages')
    body = TextField()
    send_date = DateTimeField(default=datetime.datetime.now)

這可讓您編寫如下所示的程式碼

>>> print(some_message.user.username)
Some User

>>> for message in some_user.messages:
...     print(message.body)
some message
another message
yet another message

注意

有關外鍵、聯結和模型之間關聯的深入討論,請參閱關聯和聯結文件。

有關欄位的完整文件,請參閱欄位 API 說明

欄位類型表

欄位類型

Sqlite

Postgresql

MySQL

AutoField

integer

serial

integer

BigAutoField

integer

bigserial

bigint

IntegerField

integer

integer

integer

BigIntegerField

integer

bigint

bigint

SmallIntegerField

integer

smallint

smallint

IdentityField

不支援

int identity

不支援

FloatField

real

real

real

DoubleField

real

double precision

double precision

DecimalField

decimal

numeric

numeric

CharField

varchar

varchar

varchar

FixedCharField

char

char

char

TextField

text

text

text

BlobField

blob

bytea

blob

BitField

integer

bigint

bigint

BigBitField

blob

bytea

blob

UUIDField

text

uuid

varchar(40)

BinaryUUIDField

blob

bytea

varbinary(16)

DateTimeField

datetime

timestamp

datetime

DateField

date

date

date

TimeField

time

time

time

TimestampField

integer

integer

integer

IPField

integer

bigint

bigint

BooleanField

integer

boolean

bool

BareField

未設定類型

不支援

不支援

ForeignKeyField

integer

integer

integer

注意

在上面的表格中找不到您要找的欄位?您可以輕鬆建立自訂欄位類型並將其與您的模型一起使用。

欄位初始化參數

所有欄位類型接受的參數及其預設值

  • null = False – 允許空值

  • index = False – 在此欄位上建立索引

  • unique = False – 在此欄位上建立唯一索引。另請參閱新增複合索引

  • column_name = None – 明確指定資料庫中的欄位名稱。

  • default = None – 任何值或可呼叫物件,用作未初始化模型的預設值

  • primary_key = False – 表格的主鍵

  • constraints = None - 一個或多個約束,例如 [Check('price > 0')]

  • sequence = None – 序列名稱(如果後端支援)

  • collation = None – 用於排序欄位/索引的定序

  • unindexed = False – 指示虛擬表格上的欄位不應建立索引 (僅限 SQLite)

  • choices = None – 可選的可迭代物件,包含 valuedisplay 的 2 元組

  • help_text = None – 代表此欄位任何說明文字的字串

  • verbose_name = None – 代表此欄位「使用者友善」名稱的字串

  • index_type = None – 指定自訂索引類型,例如,對於 Postgres,您可能會指定 'BRIN''GIN' 索引。

某些欄位會採用特殊參數…

欄位類型

特殊參數

CharField

max_length

FixedCharField

max_length

DateTimeField

formats

DateField

formats

TimeField

formats

TimestampField

resolutionutc

DecimalField

max_digitsdecimal_placesauto_roundrounding

ForeignKeyField

modelfieldbackrefon_deleteon_updatedeferrable lazy_load

BareField

adapt

注意

可以在資料庫層級將 defaultchoices 分別實作為 DEFAULTCHECK CONSTRAINT,但是任何應用程式變更都需要進行結構描述變更。因此,default 純粹在 python 中實作,而 choices 不會驗證,但僅用於元數據目的。

若要新增資料庫(伺服器端)約束,請使用 constraints 參數。

預設欄位值

Peewee 可以在建立物件時為欄位提供預設值。例如,若要讓 IntegerField 預設為零而不是 NULL,您可以宣告具有預設值的欄位

class Message(Model):
    context = TextField()
    read_count = IntegerField(default=0)

在某些情況下,將預設值設定為動態值可能更有意義。常見的例子是使用目前的日期和時間。Peewee 允許你在這些情況下指定一個函數,該函數的返回值將在建立物件時使用。請注意,我們僅提供函數,而實際上並不會呼叫它。

class Message(Model):
    context = TextField()
    timestamp = DateTimeField(default=datetime.datetime.now)

注意

如果你正在使用接受可變類型(listdict 等)的欄位,並且想要提供預設值,建議將預設值包裝在一個簡單的函數中,這樣多個模型實例就不會共享對同一個底層物件的參考。

def house_defaults():
    return {'beds': 0, 'baths': 0}

class House(Model):
    number = TextField()
    street = TextField()
    attributes = JSONField(default=house_defaults)

資料庫也可以為欄位提供預設值。雖然 peewee 沒有明確提供 API 來設定伺服器端預設值,但你可以使用 constraints 參數來指定伺服器預設值。

class Message(Model):
    context = TextField()
    timestamp = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')])

注意

請記住:當使用 default 參數時,這些值是由 Peewee 設定,而不是實際表格和欄位定義的一部分。

ForeignKeyField

ForeignKeyField 是一種特殊的欄位類型,允許一個模型參考另一個模型。通常,外來鍵會包含其關聯模型的主鍵(但你可以通過指定 field 來指定特定的欄位)。

外來鍵允許資料被正規化。在我們的範例模型中,有一個從 TweetUser 的外來鍵。這表示所有的使用者都儲存在自己的表格中,推文也是如此,而從推文到使用者的外來鍵允許每個推文指向特定的使用者物件。

注意

有關外來鍵、聯結和模型之間關係的深入討論,請參閱關係和聯結文件。

在 peewee 中,存取 ForeignKeyField 的值會傳回整個相關的物件,例如:

tweets = (Tweet
          .select(Tweet, User)
          .join(User)
          .order_by(Tweet.created_date.desc()))
for tweet in tweets:
    print(tweet.user.username, tweet.message)

注意

在上面的範例中,User 資料是作為查詢的一部分被選取的。有關此技術的更多範例,請參閱避免 N+1 問題文件。

但是,如果我們沒有選取 User,則會發出額外的查詢來獲取相關的 User 資料。

tweets = Tweet.select().order_by(Tweet.created_date.desc())
for tweet in tweets:
    # WARNING: an additional query will be issued for EACH tweet
    # to fetch the associated User data.
    print(tweet.user.username, tweet.message)

有時你只需要外來鍵欄位中相關的主鍵值。在這種情況下,Peewee 遵循 Django 建立的慣例,允許你通過在外來鍵欄位的名稱後附加 "_id" 來存取原始的外來鍵值。

tweets = Tweet.select()
for tweet in tweets:
    # Instead of "tweet.user", we will just get the raw ID value stored
    # in the column.
    print(tweet.user_id, tweet.message)

為了防止意外地解析外來鍵並觸發額外的查詢,ForeignKeyField 支援一個初始化參數 lazy_load,當禁用時,其行為類似於 "_id" 屬性。例如:

class Tweet(Model):
    # ... same fields, except we declare the user FK to have
    # lazy-load disabled:
    user = ForeignKeyField(User, backref='tweets', lazy_load=False)

for tweet in Tweet.select():
    print(tweet.user, tweet.message)

# With lazy-load disabled, accessing tweet.user will not perform an extra
# query and the user ID value is returned instead.
# e.g.:
# 1  tweet from user1
# 1  another from user1
# 2  tweet from user2

# However, if we eagerly load the related user object, then the user
# foreign key will behave like usual:
for tweet in Tweet.select(Tweet, User).join(User):
    print(tweet.user.username, tweet.message)

# user1  tweet from user1
# user1  another from user1
# user2  tweet from user1

ForeignKeyField 反向參考

ForeignKeyField 允許將反向參考屬性綁定到目標模型。隱含地,此屬性將被命名為 classname_set,其中 classname 是類別的小寫名稱,但可以使用參數 backref 覆蓋。

class Message(Model):
    from_user = ForeignKeyField(User, backref='outbox')
    to_user = ForeignKeyField(User, backref='inbox')
    text = TextField()

for message in some_user.outbox:
    # We are iterating over all Messages whose from_user is some_user.
    print(message)

for message in some_user.inbox:
    # We are iterating over all Messages whose to_user is some_user
    print(message)

DateTimeField、DateField 和 TimeField

這三個專門用於處理日期和時間的欄位具有特殊的屬性,可以存取年、月、小時等。

DateField 具有以下屬性:

  • year

  • month

  • day

TimeField 具有以下屬性:

  • hour

  • minute

  • second

DateTimeField 具有以上所有屬性。

這些屬性可以像任何其他運算式一樣使用。假設我們有一個活動日曆,並且想要突出顯示當前月份中所有附加了活動的日期:

# Get the current time.
now = datetime.datetime.now()

# Get days that have events for the current month.
Event.select(Event.event_date.day.alias('day')).where(
    (Event.event_date.year == now.year) &
    (Event.event_date.month == now.month))

注意

SQLite 沒有原生的日期類型,因此日期以格式化的文字欄位儲存。為了確保比較正常運作,日期需要格式化,以便按字典順序排序。這就是為什麼它們預設儲存為 YYYY-MM-DD HH:MM:SS

BitField 和 BigBitField

BitFieldBigBitField 是從 3.0.0 開始新增的。前者提供了 IntegerField 的子類別,適用於將功能開關儲存為整數位元遮罩。後者適用於儲存大型資料集的點陣圖,例如,表示成員資格或點陣圖型資料。

作為使用 BitField 的範例,假設我們有一個 *Post* 模型,並且希望儲存有關貼文的一些 True/False 旗標。我們可以將所有這些功能開關儲存在它們自己的 BooleanField 物件中,或者我們可以使用 BitField 來代替。

class Post(Model):
    content = TextField()
    flags = BitField()

    is_favorite = flags.flag(1)
    is_sticky = flags.flag(2)
    is_minimized = flags.flag(4)
    is_deleted = flags.flag(8)

使用這些旗標非常簡單:

>>> p = Post()
>>> p.is_sticky = True
>>> p.is_minimized = True
>>> print(p.flags)  # Prints 4 | 2 --> "6"
6
>>> p.is_favorite
False
>>> p.is_sticky
True

我們還可以在 Post 類別上使用這些旗標來建構查詢中的運算式:

# Generates a WHERE clause that looks like:
# WHERE (post.flags & 1 != 0)
favorites = Post.select().where(Post.is_favorite)

# Query for sticky + favorite posts:
sticky_faves = Post.select().where(Post.is_sticky & Post.is_favorite)

由於 BitField 儲存在整數中,因此最多可以表示 64 個旗標(64 位元是整數欄位的常見大小)。為了儲存任意大小的點陣圖,你可以改用 BigBitField,它使用以 BlobField 儲存的自動管理的位元組緩衝區。

當大量更新 BitField 中的一個或多個位元時,你可以使用位元運算子來設定或清除一個或多個位元。

# Set the 4th bit on all Post objects.
Post.update(flags=Post.flags | 8).execute()

# Clear the 1st and 3rd bits on all Post objects.
Post.update(flags=Post.flags & ~(1 | 4)).execute()

對於簡單的操作,這些旗標提供了方便的 set()clear() 方法,用於設定或清除單個位元:

# Set the "is_deleted" bit on all posts.
Post.update(flags=Post.is_deleted.set()).execute()

# Clear the "is_deleted" bit on all posts.
Post.update(flags=Post.is_deleted.clear()).execute()

範例用法:

class Bitmap(Model):
    data = BigBitField()

bitmap = Bitmap()

# Sets the ith bit, e.g. the 1st bit, the 11th bit, the 63rd, etc.
bits_to_set = (1, 11, 63, 31, 55, 48, 100, 99)
for bit_idx in bits_to_set:
    bitmap.data.set_bit(bit_idx)

# We can test whether a bit is set using "is_set":
assert bitmap.data.is_set(11)
assert not bitmap.data.is_set(12)

# We can clear a bit:
bitmap.data.clear_bit(11)
assert not bitmap.data.is_set(11)

# We can also "toggle" a bit. Recall that the 63rd bit was set earlier.
assert bitmap.data.toggle_bit(63) is False
assert bitmap.data.toggle_bit(63) is True
assert bitmap.data.is_set(63)

# BigBitField supports item accessor by bit-number, e.g.:
assert bitmap.data[63]
bitmap.data[0] = 1
del bitmap.data[0]

# We can also combine bitmaps using bitwise operators, e.g.
b = Bitmap(data=b'\x01')
b.data |= b'\x02'
assert list(b.data) == [1, 1, 0, 0, 0, 0, 0, 0]
assert len(b.data) == 1

BareField

BareField 類別僅適用於 SQLite。由於 SQLite 使用動態類型並且不強制執行資料類型,因此宣告不具有任何資料類型的欄位是完全可以的。在這些情況下,你可以使用 BareField。SQLite 虛擬表格也通常使用元欄或未類型化的欄位,因此在這些情況下,你也可能希望使用未類型化的欄位(儘管對於全文搜尋,你應該改用 SearchField!)。

BareField 接受一個特殊參數 adapt。此參數是一個函數,它接受來自資料庫的值並將其轉換為適當的 Python 類型。例如,如果你的虛擬表格具有未類型化的欄位,但你知道它將傳回 int 物件,則可以指定 adapt=int

範例:

db = SqliteDatabase(':memory:')

class Junk(Model):
    anything = BareField()

    class Meta:
        database = db

# Store multiple data-types in the Junk.anything column:
Junk.create(anything='a string')
Junk.create(anything=12345)
Junk.create(anything=3.14159)

建立自訂欄位

在 peewee 中新增對自訂欄位類型的支援很容易。在此範例中,我們將為 postgresql 建立一個 UUID 欄位(它具有原生的 UUID 欄位類型)。

若要新增自訂欄位類型,你首先需要確定欄位資料將儲存在哪種欄位類型中。如果你只想在十進位欄位之上新增 Python 行為(例如,建立貨幣欄位),你只需子類別化 DecimalField。另一方面,如果資料庫提供自訂欄位類型,你需要讓 peewee 知道。這由 Field.field_type 屬性控制。

注意

Peewee 隨附 UUIDField,以下程式碼僅作為範例。

讓我們先定義 UUID 欄位:

class UUIDField(Field):
    field_type = 'uuid'

我們將 UUID 儲存在原生的 UUID 欄位中。由於 psycopg2 預設將資料視為字串,因此我們將為該欄位新增兩個方法來處理:

  • 從資料庫傳出的資料,以便在我們的應用程式中使用:

  • 我們的 Python 應用程式的資料會進入資料庫

import uuid

class UUIDField(Field):
    field_type = 'uuid'

    def db_value(self, value):
        return value.hex  # convert UUID to hex string.

    def python_value(self, value):
        return uuid.UUID(value) # convert hex string to UUID

此步驟為選用。 預設情況下,field_type 值將用於資料庫結構描述中的欄位資料類型。如果您需要支援對欄位資料使用不同資料類型的多個資料庫,我們需要讓資料庫知道如何將這個 uuid 標籤對應到資料庫中的實際 uuid 欄位類型。請在 Database 建構函式中指定覆寫。

# Postgres, we use UUID data-type.
db = PostgresqlDatabase('my_db', field_types={'uuid': 'uuid'})

# Sqlite doesn't have a UUID type, so we use text type.
db = SqliteDatabase('my_db', field_types={'uuid': 'text'})

就是這樣!某些欄位可能支援特殊操作,例如 PostgreSQL 的 HStore 欄位就像一個鍵/值儲存區,並且具有自訂運算子,用於執行「包含」和「更新」之類的操作。您也可以指定自訂操作。如需程式碼範例,請查看 HStoreFieldplayhouse.postgres_ext 中的原始程式碼。

欄位命名衝突

Model 類別實作了許多類別方法和實例方法,例如 Model.save()Model.create()。如果您宣告的欄位名稱與模型方法重疊,可能會導致問題。請考慮

class LogEntry(Model):
    event = TextField()
    create = TimestampField()  # Uh-oh.
    update = TimestampField()  # Uh-oh.

為了避免這個問題,同時仍然在資料庫結構描述中使用所需的欄位名稱,請在為欄位屬性提供替代名稱時,明確指定 column_name

class LogEntry(Model):
    event = TextField()
    create_ = TimestampField(column_name='create')
    update_ = TimestampField(column_name='update')

建立模型表格

為了開始使用我們的模型,必須先開啟與資料庫的連線並建立表格。Peewee 將執行必要的 CREATE TABLE 查詢,並額外建立任何限制和索引。

# Connect to our database.
db.connect()

# Create the tables.
db.create_tables([User, Tweet])

注意

嚴格來說,沒有必要呼叫 connect(),但明確地呼叫是良好的習慣。這樣,如果發生錯誤,錯誤會在連線步驟中發生,而不是在稍後的隨意時間發生。

注意

預設情況下,Peewee 在建立表格時會包含 IF NOT EXISTS 子句。如果您想要停用此功能,請指定 safe=False

在您建立表格之後,如果您選擇修改您的資料庫結構描述 (透過新增、移除或以其他方式變更欄位),您將需要執行下列其中一項操作:

  • 捨棄表格並重新建立它。

  • 執行一個或多個 ALTER TABLE 查詢。Peewee 隨附一個結構描述移轉工具,可大幅簡化此過程。請查看 結構描述移轉 文件以取得詳細資訊。

模型選項和表格中繼資料

為了避免污染模型命名空間,模型特定的組態設定會放置在一個名為 Meta 的特殊類別中 (此為借鑒自 Django 框架的慣例)。

from peewee import *

contacts_db = SqliteDatabase('contacts.db')

class Person(Model):
    name = CharField()

    class Meta:
        database = contacts_db

這會指示 Peewee 在針對 Person 執行查詢時,使用 contacts 資料庫。

注意

看看範例模型 - 您會注意到我們建立了一個定義資料庫的 BaseModel,然後加以擴充。這是定義資料庫和建立模型的慣用方法。

一旦定義了類別,您就不應該存取 ModelClass.Meta,而是應該使用 ModelClass._meta

>>> Person.Meta
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Person' has no attribute 'Meta'

>>> Person._meta
<peewee.ModelOptions object at 0x7f51a2f03790>

ModelOptions 類別實作了數種方法,可用於擷取模型中繼資料 (例如欄位清單、外來鍵關聯等等)。

>>> Person._meta.fields
{'id': <peewee.AutoField object at 0x7f51a2e92750>,
 'name': <peewee.CharField object at 0x7f51a2f0a510>}

>>> Person._meta.primary_key
<peewee.AutoField object at 0x7f51a2e92750>

>>> Person._meta.database
<peewee.SqliteDatabase object at 0x7f519bff6dd0>

您可以將數個選項指定為 Meta 屬性。雖然大多數選項都是可繼承的,但有些選項是表格特定的,不會由子類別繼承。

選項

意義

可繼承?

database

模型的資料庫

table_name

用於儲存資料的表格名稱

table_function

動態產生表格名稱的函式

indexes

要建立索引的欄位清單

primary_key

CompositeKey 實例

constraints

表格限制清單

schema

模型的資料庫結構描述

only_save_dirty

當呼叫 model.save() 時,僅儲存已變更的欄位

options

用於建立表格擴充功能的選項字典

table_settings

在右括號後方的設定字串清單

temporary

指示暫時表格

legacy_table_names

使用舊版表格名稱產生 (預設啟用)

depends_on

指示此表格依賴另一個表格來建立

without_rowid

指示表格不應有 rowid (僅限 SQLite)

strict_tables

指示嚴格的資料類型 (僅限 SQLite,3.37 以上版本)

以下範例顯示了可繼承和不可繼承的屬性

>>> db = SqliteDatabase(':memory:')
>>> class ModelOne(Model):
...     class Meta:
...         database = db
...         table_name = 'model_one_tbl'
...
>>> class ModelTwo(ModelOne):
...     pass
...
>>> ModelOne._meta.database is ModelTwo._meta.database
True
>>> ModelOne._meta.table_name == ModelTwo._meta.table_name
False

Meta.primary_key

Meta.primary_key 屬性用於指定 CompositeKey,或指示模型沒有主索引鍵。複合主索引鍵在此處有更詳細的討論:複合主索引鍵

若要指示模型不應具有主索引鍵,請設定 primary_key = False

範例

class BlogToTag(Model):
    """A simple "through" table for many-to-many relationship."""
    blog = ForeignKeyField(Blog)
    tag = ForeignKeyField(Tag)

    class Meta:
        primary_key = CompositeKey('blog', 'tag')

class NoPrimaryKey(Model):
    data = IntegerField()

    class Meta:
        primary_key = False

表格名稱

預設情況下,Peewee 會根據您的模型類別名稱自動產生表格名稱。產生表格名稱的方式取決於 Meta.legacy_table_names 的值。預設情況下,legacy_table_names=True,以避免破壞回溯相容性。但是,如果您想要使用新的改良版表格名稱產生,您可以指定 legacy_table_names=False

此表格顯示模型名稱如何根據 legacy_table_names 的值轉換為 SQL 表格名稱的差異。

模型名稱

legacy_table_names=True

legacy_table_names=False (新的)

User

user

user

UserProfile

userprofile

user_profile

APIResponse

apiresponse

api_response

WebHTTPRequest

webhttprequest

web_http_request

mixedCamelCase

mixedcamelcase

mixed_camel_case

Name2Numbers3XYZ

name2numbers3xyz

name2_numbers3_xyz

注意

為了保持回溯相容性,目前的版本 (Peewee 3.x) 預設會指定 legacy_table_names=True

在下一個主要版本 (Peewee 4.0) 中,legacy_table_names 的預設值將為 False

若要明確指定模型類別的表格名稱,請使用 table_name Meta 選項。此功能對於處理可能使用笨拙命名慣例的現有資料庫結構描述很有用。

class UserProfile(Model):
    class Meta:
        table_name = 'user_profile_tbl'

如果您想要實作自己的命名慣例,您可以指定 table_function Meta 選項。此函式將使用您的模型類別呼叫,且應以字串形式傳回所需的表格名稱。假設我們公司規定表格名稱應為小寫,並以「_tbl」結尾,我們可以將此實作為表格函式。

def make_table_name(model_class):
    model_name = model_class.__name__
    return model_name.lower() + '_tbl'

class BaseModel(Model):
    class Meta:
        table_function = make_table_name

class User(BaseModel):
    # table_name will be "user_tbl".

class UserProfile(BaseModel):
    # table_name will be "userprofile_tbl".

索引和限制

Peewee 可以在單個或多個欄位上建立索引,也可以選擇性地包含 UNIQUE 限制。Peewee 也支援在模型和欄位上定義使用者定義的限制。

單欄索引和限制

單欄索引是使用欄位初始化參數定義的。以下範例在 username 欄位上新增一個唯一索引,並在 email 欄位上新增一個一般索引。

class User(Model):
    username = CharField(unique=True)
    email = CharField(index=True)

若要在欄位上新增使用者定義的限制,可以使用 constraints 參數傳入。您可能希望指定結構描述的預設值,或新增 CHECK 限制,例如

class Product(Model):
    name = CharField(unique=True)
    price = DecimalField(constraints=[Check('price < 10000')])
    created = DateTimeField(
        constraints=[SQL("DEFAULT (datetime('now'))")])

多欄索引

多欄索引可以使用巢狀元組定義為 Meta 屬性。每個資料庫索引都是一個 2 元組,其第一部分是欄位名稱元組,第二部分是布林值,指出索引是否應為唯一。

class Transaction(Model):
    from_acct = CharField()
    to_acct = CharField()
    amount = DecimalField()
    date = DateTimeField()

    class Meta:
        indexes = (
            # create a unique on from/to/date
            (('from_acct', 'to_acct', 'date'), True),

            # create a non-unique on from/to
            (('from_acct', 'to_acct'), False),
        )

注意

如果您的索引元組僅包含一個項目,請記得新增尾隨逗號

class Meta:
    indexes = (
        (('first_name', 'last_name'), True),  # Note the trailing comma!
    )

進階索引建立

Peewee 支援更結構化的 API,可使用 Model.add_index() 方法或直接使用 ModelIndex 協助程式類別,在模型上宣告索引。

範例

class Article(Model):
    name = TextField()
    timestamp = TimestampField()
    status = IntegerField()
    flags = IntegerField()

# Add an index on "name" and "timestamp" columns.
Article.add_index(Article.name, Article.timestamp)

# Add a partial index on name and timestamp where status = 1.
Article.add_index(Article.name, Article.timestamp,
                  where=(Article.status == 1))

# Create a unique index on timestamp desc, status & 4.
idx = Article.index(
    Article.timestamp.desc(),
    Article.flags.bin_and(4),
    unique=True)
Article.add_index(idx)

警告

SQLite 不支援參數化的 CREATE INDEX 查詢。這表示當使用 SQLite 建立涉及運算式或純量的索引時,您需要使用 SQL 協助程式宣告索引。

# SQLite does not support parameterized CREATE INDEX queries, so
# we declare it manually.
Article.add_index(SQL('CREATE INDEX ...'))

請參閱 add_index() 以取得詳細資訊。

如需詳細資訊,請參閱

表格限制

Peewee 可讓您將任意限制新增至您的 Model,這將成為建立結構描述時表格定義的一部分。

例如,假設您有一個 people 表格,其中包含兩個欄位的複合主索引鍵,即人員的名和姓。您希望有另一個表格與 people 表格相關聯,為此,您需要定義外來鍵限制。

class Person(Model):
    first = CharField()
    last = CharField()

    class Meta:
        primary_key = CompositeKey('first', 'last')

class Pet(Model):
    owner_first = CharField()
    owner_last = CharField()
    pet_name = CharField()

    class Meta:
        constraints = [SQL('FOREIGN KEY(owner_first, owner_last) '
                           'REFERENCES person(first, last)')]

您也可以在表格層級實作 CHECK 限制。

class Product(Model):
    name = CharField(unique=True)
    price = DecimalField()

    class Meta:
        constraints = [Check('price < 10000')]

主鍵、複合鍵和其他技巧

AutoField 用於識別自動遞增的整數主鍵。如果您沒有指定主鍵,Peewee 將自動建立一個名為「id」的自動遞增主鍵。

若要使用不同的欄位名稱指定自動遞增 ID,您可以寫成

class Event(Model):
    event_id = AutoField()  # Event.event_id will be auto-incrementing PK.
    name = CharField()
    timestamp = DateTimeField(default=datetime.datetime.now)
    metadata = BlobField()

您可以將不同的欄位識別為主鍵,這樣就不會建立「id」欄位。在此範例中,我們將使用人員的電子郵件地址作為主鍵

class Person(Model):
    email = CharField(primary_key=True)
    name = TextField()
    dob = DateField()

警告

我經常看到有人寫出以下程式碼,期望得到自動遞增的整數主鍵

class MyModel(Model):
    id = IntegerField(primary_key=True)

Peewee 將上述模型宣告理解為具有整數主鍵的模型,但該 ID 的值由應用程式決定。若要建立自動遞增的整數主鍵,您應該改為寫成

class MyModel(Model):
    id = AutoField()  # primary_key=True is implied.

可以使用 CompositeKey 宣告複合主鍵。請注意,這樣做可能會導致 ForeignKeyField 出現問題,因為 Peewee 不支援「複合外鍵」的概念。因此,我發現只建議在少數情況下使用複合主鍵,例如簡單的多對多連接表

class Image(Model):
    filename = TextField()
    mimetype = CharField()

class Tag(Model):
    label = CharField()

class ImageTag(Model):  # Many-to-many relationship.
    image = ForeignKeyField(Image)
    tag = ForeignKeyField(Tag)

    class Meta:
        primary_key = CompositeKey('image', 'tag')

在極為罕見的情況下,您希望宣告一個沒有主鍵的模型,您可以在模型 Meta 選項中指定 primary_key = False

非整數主鍵

如果您想使用非整數主鍵(我通常不建議這樣做),您可以在建立欄位時指定 primary_key=True。當您希望使用非自動遞增主鍵為模型建立新實例時,您需要確保您 save() 指定 force_insert=True

from peewee import *

class UUIDModel(Model):
    id = UUIDField(primary_key=True)

自動遞增 ID,顧名思義,是在您將新列插入資料庫時自動產生的。當您呼叫 save() 時,peewee 會根據主鍵值的存在來判斷執行 INSERT 還是 UPDATE。由於在我們的 uuid 範例中,資料庫驅動程式不會產生新的 ID,我們需要手動指定它。當我們第一次呼叫 save() 時,傳入 force_insert = True

# This works because .create() will specify `force_insert=True`.
obj1 = UUIDModel.create(id=uuid.uuid4())

# This will not work, however. Peewee will attempt to do an update:
obj2 = UUIDModel(id=uuid.uuid4())
obj2.save() # WRONG

obj2.save(force_insert=True) # CORRECT

# Once the object has been created, you can call save() normally.
obj2.save()

注意

任何指向具有非整數主鍵的模型的外鍵都將使用與其相關的主鍵相同的底層儲存類型來 ForeignKeyField

複合主鍵

Peewee 對複合鍵的支援非常基本。若要使用複合鍵,您必須將模型選項的 primary_key 屬性設定為 CompositeKey 實例

class BlogToTag(Model):
    """A simple "through" table for many-to-many relationship."""
    blog = ForeignKeyField(Blog)
    tag = ForeignKeyField(Tag)

    class Meta:
        primary_key = CompositeKey('blog', 'tag')

警告

Peewee 不支援指向定義 CompositeKey 主鍵的模型的外鍵。如果您希望將外鍵新增至具有複合主鍵的模型,請複製相關模型上的欄位並新增自訂存取器(例如屬性)。

手動指定主鍵

有時您不希望資料庫自動產生主鍵的值,例如在大量載入關聯資料時。若要一次性地處理此問題,您可以簡單地告訴 peewee 在匯入期間關閉 auto_increment

data = load_user_csv() # load up a bunch of data

User._meta.auto_increment = False # turn off auto incrementing IDs
with db.atomic():
    for row in data:
        u = User(id=row[0], username=row[1])
        u.save(force_insert=True) # <-- force peewee to insert row

User._meta.auto_increment = True

雖然更好的方法是在不求助於 hack 的情況下完成上述操作,但還是可以使用 Model.insert_many() API

data = load_user_csv()
fields = [User.id, User.username]
with db.atomic():
    User.insert_many(data, fields=fields).execute()

如果您始終想要控制主鍵,只需不要使用 AutoField 欄位類型,而是使用一般的 IntegerField(或其他欄位類型)

class User(BaseModel):
    id = IntegerField(primary_key=True)
    username = CharField()

>>> u = User.create(id=999, username='somebody')
>>> u.id
999
>>> User.get(User.username == 'somebody').id
999

沒有主鍵的模型

如果您希望建立一個沒有主鍵的模型,您可以在內部的 Meta 類別中指定 primary_key = False

class MyData(BaseModel):
    timestamp = DateTimeField()
    value = IntegerField()

    class Meta:
        primary_key = False

這將產生以下的 DDL

CREATE TABLE "mydata" (
  "timestamp" DATETIME NOT NULL,
  "value" INTEGER NOT NULL
)

警告

某些模型 API 可能無法正確適用於沒有主鍵的模型,例如 save()delete_instance()(您可以改為使用 insert()update()delete())。

自我參照外鍵

在建立階層式結構時,有必要建立一個自我參照外鍵,將子物件連結到其父物件。由於在您實例化自我參照外鍵時,尚未定義模型類別,因此請使用特殊字串 'self' 來表示自我參照外鍵

class Category(Model):
    name = CharField()
    parent = ForeignKeyField('self', null=True, backref='children')

如您所見,外鍵向上指向父物件,而反向參照名為children

注意

自我參照外鍵應該始終為 null=True

在查詢包含自我參照外鍵的模型時,有時您可能需要執行自我聯結。在這種情況下,您可以使用 Model.alias() 來建立表格參照。以下說明如何使用自我聯結查詢類別和父模型

Parent = Category.alias()
GrandParent = Category.alias()
query = (Category
         .select(Category, Parent)
         .join(Parent, on=(Category.parent == Parent.id))
         .join(GrandParent, on=(Parent.parent == GrandParent.id))
         .where(GrandParent.name == 'some category')
         .order_by(Category.name))

循環外鍵依賴性

有時您會在兩個表格之間建立循環依賴性。

注意

我個人認為循環外鍵是一種程式碼異味,應該重構(例如,透過新增中介表格)。

使用 peewee 新增循環外鍵有點棘手,因為在您定義任一外鍵時,它指向的模型尚未定義,會導致 NameError

class User(Model):
    username = CharField()
    favorite_tweet = ForeignKeyField(Tweet, null=True)  # NameError!!

class Tweet(Model):
    message = TextField()
    user = ForeignKeyField(User, backref='tweets')

一種選擇是簡單地使用 IntegerField 來儲存原始 ID

class User(Model):
    username = CharField()
    favorite_tweet_id = IntegerField(null=True)

透過使用 DeferredForeignKey,我們可以解決問題並仍然使用外鍵欄位

class User(Model):
    username = CharField()
    # Tweet has not been defined yet so use the deferred reference.
    favorite_tweet = DeferredForeignKey('Tweet', null=True)

class Tweet(Model):
    message = TextField()
    user = ForeignKeyField(User, backref='tweets')

# Now that Tweet is defined, "favorite_tweet" has been converted into
# a ForeignKeyField.
print(User.favorite_tweet)
# <ForeignKeyField: "user"."favorite_tweet">

但是,還有一點需要注意。當您呼叫 create_table 時,我們會再次遇到相同的問題。因此,peewee 不會自動為任何延遲外鍵建立外鍵約束。

若要建立表格外鍵約束,您可以使用 SchemaManager.create_foreign_key() 方法在建立表格後建立約束

# Will create the User and Tweet tables, but does *not* create a
# foreign-key constraint on User.favorite_tweet.
db.create_tables([User, Tweet])

# Create the foreign-key constraint:
User._schema.create_foreign_key(User.favorite_tweet)

注意

由於 SQLite 對變更表格的支援有限,因此無法在建立表格後將外鍵約束新增至表格。