命名约束的重要性#

值得一提的一个重要主题是约束命名约定。在本文中,我们讨论了如何添加表和列,并且我们还暗示了 操作参考 中列出的许多其他操作,例如支持添加或删除约束(如外键和唯一约束)。迁移脚本中引用这些约束的方式是通过名称,但在大多数情况下,这些名称在创建约束时默认由所使用的关系数据库生成。例如,如果你在 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 对象使用一个名称,更不用说每个 UniqueConstraintCheckConstraintIndex,甚至可能是 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() 事件尤其适合在 ConstraintIndex 对象与父 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