模型和字段
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)
创建
Database
的一个实例。db = SqliteDatabase('my_app.db')
db
对象将用于管理与 Sqlite 数据库的连接。在此示例中,我们使用SqliteDatabase
,但你也可以使用其他 数据库引擎。创建一个指定我们数据库的基本模型类。
定义一个模型类。
class User(BaseModel): username = CharField(unique=True)
模型定义使用声明式风格,这在 SQLAlchemy 或 Django 等其他流行的 ORM 中可见。请注意,我们正在扩展 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 |
---|---|---|---|
|
integer |
serial |
integer |
|
integer |
bigserial |
bigint |
|
integer |
integer |
integer |
|
integer |
bigint |
bigint |
|
integer |
smallint |
smallint |
|
不支持 |
int identity |
不支持 |
|
real |
real |
real |
|
real |
double precision |
double precision |
|
decimal |
numeric |
numeric |
|
varchar |
varchar |
varchar |
|
char |
char |
char |
|
text |
text |
text |
|
blob |
bytea |
blob |
|
integer |
bigint |
bigint |
|
blob |
bytea |
blob |
|
text |
uuid |
varchar(40) |
|
blob |
bytea |
varbinary(16) |
|
datetime |
timestamp |
datetime |
|
date |
date |
date |
|
time |
time |
time |
|
integer |
integer |
integer |
|
integer |
bigint |
bigint |
|
integer |
boolean |
bool |
|
untyped |
不支持 |
不支持 |
|
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
– 包含value
、display
的 2 元组的可选可迭代对象help_text = None
– 表示此字段的任何帮助文本的字符串verbose_name = None
– 表示此字段的“用户友好”名称的字符串index_type = None
– 指定自定义索引类型,例如,对于 Postgres,你可以指定'BRIN'
或'GIN'
索引。
某些字段采用特殊参数…
字段类型 |
特殊参数 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注意
default
和 choices
都可以在数据库级别分别作为 DEFAULT 和 CHECK 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)
注意
如果你正在使用接受可变类型(list、dict 等)的字段,并且想要提供一个默认值,最好将你的默认值包装在一个简单函数中,以便多个模型实例不共享对同一底层对象的引用
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
来指定特定列)。
外键允许数据被 规范化。在我们的示例模型中,有一个从 Tweet
到 User
的外键。这意味着所有用户都存储在它们自己的表中,推文也是如此,从推文到用户的这个外键允许每条推文指向特定的用户对象。
注意
请参阅 关系和联接 文档,以深入了解外键、联接和模型之间的关系。
在 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
月
日
TimeField
具有以下属性:
小时
分钟
秒
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
BitField
和 BigBitField
是 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 字段的行为类似于键/值存储,并且具有用于 包含 和 更新 等内容的自定义运算符。您还可以指定 自定义操作。例如代码,请查看 HStoreField
的源代码,在 playhouse.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
属性。虽然大多数选项都是可继承的,但有些选项是特定于表的,并且不会被子类继承。
选项 |
含义 |
可继承? |
---|---|---|
|
模型的数据库 |
是 |
|
存储数据的表名 |
否 |
|
动态生成表名的函数 |
是 |
|
要建立索引的字段列表 |
是 |
|
一个 |
是 |
|
表约束列表 |
是 |
|
模型的数据库架构 |
是 |
|
调用 model.save() 时,仅保存脏字段 |
是 |
|
用于创建表扩展的选项字典 |
是 |
|
关闭括号后要设置的设置字符串列表 |
是 |
|
指示临时表 |
是 |
|
使用旧表名生成(默认启用) |
是 |
|
指示此表依赖于另一个表才能创建 |
否 |
|
指示表不应具有 rowid(仅限 SQLite) |
否 |
|
指示严格的数据类型(仅限 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
。
此表显示了模型名称如何转换为 SQL 表名,具体取决于 legacy_table_names
的值
模型名称 |
legacy_table_names=True |
legacy_table_names=False(新) |
---|---|---|
用户 |
user |
user |
用户个人资料 |
userprofile |
user_profile |
API 响应 |
apiresponse |
api_response |
WebHTTP 请求 |
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 可以创建单列或多列索引,可以选择包括唯一约束。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'))")])
多列索引
多列索引可以定义为元属性,使用嵌套元组。每个数据库索引都是一个 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 支持使用 Model.add_index()
方法或直接使用 ModelIndex
帮助类来声明模型上的索引的更结构化的 API。
示例
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
尽管不诉诸于黑客手段,实现上述目的的更好方法是使用 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 对更改表的支持有限,因此在创建表后无法向表中添加外键约束。