命名约束的重要性#
值得一提的一个重要主题是约束命名约定。在本文中,我们讨论了如何添加表和列,并且我们还暗示了 操作参考 中列出的许多其他操作,例如支持添加或删除约束(如外键和唯一约束)。迁移脚本中引用这些约束的方式是通过名称,但在大多数情况下,这些名称在创建约束时默认由所使用的关系数据库生成。例如,如果你在 Postgresql 上发出了两个 CREATE TABLE 语句,如下所示:
test=> CREATE TABLE user_account (id INTEGER PRIMARY KEY);
CREATE TABLE
test=> CREATE TABLE user_order (
test(> id INTEGER PRIMARY KEY,
test(> user_account_id INTEGER REFERENCES user_account(id));
CREATE TABLE
假设我们要删除刚应用于 user_order.user_account_id
列的 REFERENCES,该怎么做?在提示符下,我们将使用 ALTER TABLE <tablename> DROP CONSTRAINT <constraint_name>
,或者如果使用 Alembic,我们将使用 Operations.drop_constraint()
。但这两个函数都需要一个名称 - 此约束的名称是什么?
它确实有一个名称,在这种情况下,我们可以通过查看 Postgresql 目录表来找出它
test=> SELECT r.conname FROM
test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid
test-> WHERE c.relname='user_order' AND r.contype = 'f'
test-> ;
conname
---------------------------------
user_order_user_account_id_fkey
(1 row)
上面的名称不是 Alembic 或 SQLAlchemy 创建的; user_order_user_account_id_fkey
是 Postgresql 内部用于命名未命名的约束的命名方案。
此方案似乎并不复杂,我们可能只想利用我们的知识,以便知道为我们的 Operations.drop_constraint()
调用使用什么名称。但这是一个好主意吗?例如,如果我们需要我们的代码也在 Oracle 上运行。好的,Oracle 肯定使用相同的方案,对吗?如果不是,那就类似。让我们检查一下
Oracle Database 10g Express Edition Release 10.2.0.1.0 - Production
SQL> CREATE TABLE user_account (id INTEGER PRIMARY KEY);
Table created.
SQL> CREATE TABLE user_order (
2 id INTEGER PRIMARY KEY,
3 user_account_id INTEGER REFERENCES user_account(id));
Table created.
SQL> SELECT constraint_name FROM all_constraints WHERE
2 table_name='USER_ORDER' AND constraint_type in ('R');
CONSTRAINT_NAME
-----------------------------------------------------
SYS_C0029334
哦,我们可以看到它……更糟。Oracle 的名称是完全不可预测的字母数字代码,这将使编写迁移变得非常繁琐,因为我们需要查找所有这些名称。
查找名称的解决方案是创建自己的名称。这是一个简单但手动操作起来很繁琐的事情。例如,要在 SQLAlchemy 中创建我们的模型,确保我们为外键约束使用名称,如下所示
from sqlalchemy import MetaData, Table, Column, Integer, ForeignKey
meta = MetaData()
user_account = Table('user_account', meta,
Column('id', Integer, primary_key=True)
)
user_order = Table('user_order', meta,
Column('id', Integer, primary_key=True),
Column('user_order_id', Integer,
ForeignKey('user_account.id', name='fk_user_order_id'))
)
很简单,尽管这有一些缺点。首先是它很繁琐;我们需要记住为每个 ForeignKey
对象使用一个名称,更不用说每个 UniqueConstraint
、CheckConstraint
、Index
,甚至可能是 PrimaryKeyConstraint
,如果我们希望能够更改这些约束,并且除此之外,所有名称都必须全局唯一。即使付出了所有这些努力,如果我们心中有一个命名方案,在每次手动执行时都很容易出错。
更糟糕的是,手动命名约束(和索引)变得更加繁琐,因为我们无法再使用 Column
上的 .unique=True
或 .index=True
标志等便利功能。
user_account = Table('user_account', meta,
Column('id', Integer, primary_key=True),
Column('name', String(50), unique=True)
)
在上面,unique=True
标志创建了一个 UniqueConstraint
,但同样,它没有名称。如果我们想给它命名,则必须手动放弃使用 unique=True
并键入整个约束
user_account = Table('user_account', meta,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
UniqueConstraint('name', name='uq_user_account_name')
)
解决所有这些命名工作的方案是使用自动命名约定。多年来,SQLAlchemy 一直鼓励使用 DDL 事件来创建命名方案。after_parent_attach()
事件尤其适合在 Constraint
和 Index
对象与父 Table
对象关联时进行拦截,并使用表和关联列的名称为约束分配 .name
。
但还有更好的方法,即利用 SQLAlchemy 0.9.2 中的新功能,该功能利用幕后称为 naming_convention
的事件。在这里,我们可以创建一个新的 MetaData
对象,同时传递一个引用命名方案的字典
convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
}
metadata = MetaData(naming_convention=convention)
如果我们使用 MetaData
定义我们的模型,如上所示,则给定的命名约定字典将用于为所有约束和索引提供名称。
另请参阅
配置约束命名约定 - SQLAlchemy 命名约定支持概述
将命名约定集成到操作中,自动生成#
从 Alembic 0.6.4 开始,命名约定功能已集成到 Operations
对象中,以便该约定对任何未命名的约束生效。使用 MigrationsContext.configure.target_metadata
参数将命名约定传递给 Operations
,该参数通常在使用自动生成时进行配置
# in your application's model:
class Base(DeclarativeBase):
metadata = MetaData(naming_convention={
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
})
# .. in your Alembic env.py:
# add your model's MetaData object here
# for 'autogenerate' support
from myapp import mymodel
target_metadata = mymodel.Base.metadata
# ...
def run_migrations_online():
# ...
context.configure(
connection=connection,
target_metadata=target_metadata
)
上面,当我们呈现如下指令时
op.add_column('sometable', Column('q', Boolean(name='q_bool')))
布尔类型将呈现一个名为 "ck_sometable_q_bool"
的 CHECK 约束,假设所使用的后端不支持本机布尔类型。
如果命名约定不需要,我们还可以对约束使用 op 指令,并且根本不给它们命名。None
的值将被转换为遵循适当命名约定的名称
def upgrade():
op.create_unique_constraint(None, 'some_table', 'x')
当自动生成在迁移脚本中呈现约束时,它通常会呈现它们及其已完成的名称。如果至少使用 Alembic 0.6.4 以及 SQLAlchemy 0.9.4,这些将使用特殊指令 Operations.f()
呈现,该指令表示该字符串已标记化
def upgrade():
op.create_unique_constraint(op.f('uq_const_x'), 'some_table', 'x')
此 Operations.f()
构造可以显式使用以绕过命名约定,如下一节所示。
绕过 CREATE 和 DROP 操作的命名约定#
在使用约束命名约定时,特别是如果 %(constraint_name)s
标记正在使用,则与 Operations.create_check_constraint()
和 Operations.drop_constraint()
等方法一起使用的约束名称将包括命名约定,除非使用其他指令。
在 env.py
中给定配置为
target_metadata = MetaData(naming_convention={
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
})
# ...
def run_migrations_online():
# ...
context.configure(
connection=connection,
target_metadata=target_metadata
)
以下操作将删除名为 ck_t1_some_check_const
的 CHECK 约束
>>> op.drop_constraint("some_check_const", "t1", type_="check")
ALTER TABLE t1 DROP CONSTRAINT ck_t1_some_check_const
为了在绕过已配置的命名约定的情况下应用该操作,请使用 Operations.f()
构造。这会生成一个不会被标记化的字符串表达式
>>> op.drop_constraint(op.f("some_check_const"), "t1", type_="check")
ALTER TABLE t1 DROP CONSTRAINT some_check_const