定义外键
SQL中的外键是表级结构,限制该表中的一个或多个列,使得值能存在不同列中,通常但不总是位于不同的表。我们调用了约束外键列的列和它们被约束到引用的列的列。被引用的列几乎总是为他们拥有的表定义主键,但也有例外。外键是将具有相互关系的行对连接在一起的“联合”,SQLAlchemy几乎在其操作的每个区域都非常重视这个概念。
在SQLAlchemy以及DDL中,可以将外键约束定义为table子句中的附加属性,或者对于单列外键,可以在单列的定义内指定外键约束。单列外键更常见,并且在列级别通过构建ForeignKey
对象作为Column
对象的参数来指定:
user_preference = Table('user_preference', metadata,
Column('pref_id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey("user.user_id"), nullable=False),
Column('pref_name', String(40), nullable=False),
Column('pref_value', String(100))
)
在上面,我们定义了一个新的表user_preference
,其中每行必须在user_id
列中包含一个值,该值也存在于user
表的user_id
列。
ForeignKey
的参数通常是< tablename>。< columnname>< / t3>的形式的字符串,或者对于远程模式中的表或“所有者” form < schemaname>。< tablename>。< columnname>。它也可能是一个实际的Column
对象,我们稍后会看到它是通过它的c
集合从现有的Table
对象进行访问的:
ForeignKey(user.c.user_id)
使用字符串的好处在于,仅当第一次需要时才解析user
和user_preference
之间的in-python链接,以便可以将表对象轻松地分布到多个模块中,按任何顺序定义。
外键也可以使用ForeignKeyConstraint
对象在表级定义。该对象可以描述单列或多列外键。多列外键称为复合外键,并且几乎总是引用具有复合主键的表。下面我们定义一个表invoice
,它有一个复合主键:
invoice = Table('invoice', metadata,
Column('invoice_id', Integer, primary_key=True),
Column('ref_num', Integer, primary_key=True),
Column('description', String(60), nullable=False)
)
然后使用引用invoice
的复合外键的表invoice_item
:
invoice_item = Table('invoice_item', metadata,
Column('item_id', Integer, primary_key=True),
Column('item_name', String(60), nullable=False),
Column('invoice_id', Integer, nullable=False),
Column('ref_num', Integer, nullable=False),
ForeignKeyConstraint(['invoice_id', 'ref_num'], ['invoice.invoice_id', 'invoice.ref_num'])
)
值得注意的是,ForeignKeyConstraint
是定义组合外键的唯一方法。虽然我们也可以在invoice_item.invoice_id
和invoice_item.ref_num
列上放置单个ForeignKey
对象,但SQLAlchemy不会意识到这两个值应该配对在一起 - 这将是两个单独的外键约束,而不是引用两列的单个组合外键。
通过ALTER创建/删除外键约束
我们在教程和其他地方看到的涉及DDL外键的行为表明,约束通常在CREATE TABLE语句内呈现为“内联”,如:
CREATE TABLE addresses (
id INTEGER NOT NULL,
user_id INTEGER,
email_address VARCHAR NOT NULL,
PRIMARY KEY (id),
CONSTRAINT user_id_fk FOREIGN KEY(user_id) REFERENCES users (id)
)
使用CONSTRAINT .. FOREIGN KEY在CREATE TABLE定义中“内联”的方式。
The MetaData.create_all()
and MetaData.drop_all()
methods do this by default, using a topological sort of all the Table
objects involved such that tables are created and dropped in order of their foreign key dependency (this sort is also available via the MetaData.sorted_tables
accessor).
这种方法在两个或更多外键约束涉及“依赖周期”的情况下无法工作,其中一组表彼此相互依赖,假设后端强制执行外键(除SQLite,MySQL / MyISAM数据)。因此,这些方法将在这种循环中将约束分解为单独的ALTER语句,而不是支持大多数ALTER形式的SQLite以外的所有后端。给定一个模式如下:
node = Table(
'node', metadata,
Column('node_id', Integer, primary_key=True),
Column(
'primary_element', Integer,
ForeignKey('element.element_id')
)
)
element = Table(
'element', metadata,
Column('element_id', Integer, primary_key=True),
Column('parent_node_id', Integer),
ForeignKeyConstraint(
['parent_node_id'], ['node.node_id'],
name='fk_element_parent_node_id'
)
)
当我们在后端(如Postgresql后端)上调用MetaData.create_all()
时,解析这两个表之间的循环并分别创建约束:
>>> with engine.connect() as conn:
... metadata.create_all(conn, checkfirst=False)
CREATE TABLE element (
element_id SERIAL NOT NULL,
parent_node_id INTEGER,
PRIMARY KEY (element_id)
)
CREATE TABLE node (
node_id SERIAL NOT NULL,
primary_element INTEGER,
PRIMARY KEY (node_id)
)
ALTER TABLE element ADD CONSTRAINT fk_element_parent_node_id
FOREIGN KEY(parent_node_id) REFERENCES node (node_id)
ALTER TABLE node ADD FOREIGN KEY(primary_element)
REFERENCES element (element_id)
为了为这些表发出DROP,应用相同的逻辑,但请注意,在SQL中,要发出DROP CONSTRAINT,需要该约束具有名称。在上面的'node'
表中,我们没有命名这个约束;系统将因此尝试仅为那些被命名的约束发出DROP:
>>> with engine.connect() as conn:
... metadata.drop_all(conn, checkfirst=False)
ALTER TABLE element DROP CONSTRAINT fk_element_parent_node_id
DROP TABLE node
DROP TABLE element
在循环无法解析的情况下,例如,如果我们没有在这里为任一约束应用名称,我们将收到以下错误:
sqlalchemy.exc.CircularDependencyError: Can't sort tables for DROP;
an unresolvable foreign key dependency exists between tables:
element, node. Please ensure that the ForeignKey and ForeignKeyConstraint
objects involved in the cycle have names so that they can be dropped
using DROP CONSTRAINT.
这个错误只适用于DROP情况,因为我们可以在没有名字的CREATE情况下发出“ADD CONSTRAINT”;数据库通常自动分配一个。
可以使用ForeignKeyConstraint.use_alter
和ForeignKey.use_alter
关键字参数来手动解决依赖关系周期。我们只能将这个标志添加到'element'
表中,如下所示:
element = Table(
'element', metadata,
Column('element_id', Integer, primary_key=True),
Column('parent_node_id', Integer),
ForeignKeyConstraint(
['parent_node_id'], ['node.node_id'],
use_alter=True, name='fk_element_parent_node_id'
)
)
在我们的CREATE DDL中,我们将只看到这个约束的ALTER语句,而不是另一个:
>>> with engine.connect() as conn:
... metadata.create_all(conn, checkfirst=False)
CREATE TABLE element (
element_id SERIAL NOT NULL,
parent_node_id INTEGER,
PRIMARY KEY (element_id)
)
CREATE TABLE node (
node_id SERIAL NOT NULL,
primary_element INTEGER,
PRIMARY KEY (node_id),
FOREIGN KEY(primary_element) REFERENCES element (element_id)
)
ALTER TABLE element ADD CONSTRAINT fk_element_parent_node_id
FOREIGN KEY(parent_node_id) REFERENCES node (node_id)
ForeignKeyConstraint.use_alter
and ForeignKey.use_alter
, when used in conjunction with a drop operation, will require that the constraint is named, else an error like the following is generated:
sqlalchemy.exc.CompileError: Can't emit DROP CONSTRAINT for constraint
ForeignKeyConstraint(...); it has no name
ON UPDATE和ON DELETE
Most databases support cascading of foreign key values, that is the when a parent row is updated the new value is placed in child rows, or when the parent row is deleted all corresponding child rows are set to null or deleted. 在数据定义语言中,这些使用诸如“ON UPDATE CASCADE”,“ON DELETE CASCADE”和“ON DELETE SET NULL”之类的短语来指定,这与外键约束对应。“ON UPDATE”或“ON DELETE”之后的短语也可能允许其他特定于正在使用的数据库的短语。ForeignKey
和ForeignKeyConstraint
对象支持通过onupdate
和ondelete
关键字参数生成此子句。该值是在适当的“ON UPDATE”或“ON DELETE”短语后输出的任何字符串:
child = Table('child', meta,
Column('id', Integer,
ForeignKey('parent.id', onupdate="CASCADE", ondelete="CASCADE"),
primary_key=True
)
)
composite = Table('composite', meta,
Column('id', Integer, primary_key=True),
Column('rev_id', Integer),
Column('note_id', Integer),
ForeignKeyConstraint(
['rev_id', 'note_id'],
['revisions.id', 'revisions.note_id'],
onupdate="CASCADE", ondelete="SET NULL"
)
)
请注意,这些子句在SQLite上不受支持,并且在与MySQL一起使用时需要InnoDB
表。他们也可能不支持其他数据库。
配置约束命名约定
关系数据库通常为所有约束和索引分配显式名称。在通常情况下,使用CREATE TABLE
创建表,其中CHECK,UNIQUE和PRIMARY KEY约束等约束与表定义时,数据库通常会有一个系统,在这个系统中,如果没有另外指定名称,名称会自动分配给这些约束。当使用诸如ALTER TABLE
的命令在数据库中更改现有数据库表时,此命令通常需要为新约束指定明确的名称以及能够指定要删除或修改的现有约束的名称。
可以使用Constraint.name
参数明确命名约束,并为索引Index.name
参数指定约束。但是,在约束条件下,该参数是可选的。还有一些使用Column.unique
和Column.index
参数来创建UniqueConstraint
和Index
未指定明确名称的对象。
更改现有表和约束的用例可以通过模式迁移工具(如Alembic)来处理。然而,Alembic和SQLAlchemy目前都没有为其中未指定名称的约束对象创建名称,导致能够更改现有约束的情况意味着必须对关系数据库使用的命名系统进行反向工程以自动分配名称或者必须小心确保所有约束都被命名。
与必须为所有Constraint
和Index
对象指定显式名称相反,可以使用事件构建自动命名方案。This approach has the advantage that constraints will get a consistent naming scheme without the need for explicit name parameters throughout the code, and also that the convention takes place just as well for those constraints and indexes produced by the Column.unique
and Column.index
parameters. 从SQLAlchemy 0.9.2开始,这个基于事件的方法被包含在内,并且可以使用参数MetaData.naming_convention
进行配置。
MetaData.naming_convention
refers to a dictionary which accepts the Index
class or individual Constraint
classes as keys, and Python string templates as values. 它也接受一系列的字符码作为替代键,即"fk"
,"pk"
,"ix"
,"ck"
,"uq"
分别为外键,主键,索引,检查和唯一约束。无论何时约束或索引与此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)
The above convention will establish names for all constraints within the target MetaData
collection. 例如,我们可以观察创建未命名的UniqueConstraint
时产生的名称:
>>> user_table = Table('user', metadata,
... Column('id', Integer, primary_key=True),
... Column('name', String(30), nullable=False),
... UniqueConstraint('name')
... )
>>> list(user_table.constraints)[1].name
'uq_user_name'
即使我们只使用Column.unique
标志,该功能也会生效:
>>> user_table = Table('user', metadata,
... Column('id', Integer, primary_key=True),
... Column('name', String(30), nullable=False, unique=True)
... )
>>> list(user_table.constraints)[1].name
'uq_user_name'
命名约定方法的一个关键优势是名称是在Python构建时建立的,而不是在DDL发布时。当使用Alembic的--autogenerate
特性时,这种效果是,当生成新的迁移脚本时,命名约定将是显式的:
def upgrade():
op.create_unique_constraint("uq_user_name", "user", ["name"])
上面的"uq_user_name"
字符串是从位于我们的元数据中的UniqueConstraint
对象中的--autogenerate
复制的。
The default value for MetaData.naming_convention
handles the long-standing SQLAlchemy behavior of assigning a name to a Index
object that is created using the Column.index
parameter:
>>> from sqlalchemy.sql.schema import DEFAULT_NAMING_CONVENTION
>>> DEFAULT_NAMING_CONVENTION
immutabledict({'ix': 'ix_%(column_0_label)s'})
The tokens available include %(table_name)s
, %(referred_table_name)s
, %(column_0_name)s
, %(column_0_label)s
, %(column_0_key)s
, %(referred_column_0_name)s
, and %(constraint_name)s
; the documentation for MetaData.naming_convention
describes each individually. 通过在naming_convention字典中指定一个额外的标记和一个可调用标记,还可以添加新的标记。例如,如果我们想使用GUID方案来命名我们的外键约束,我们可以这样做:
import uuid
def fk_guid(constraint, table):
str_tokens = [
table.name,
] + [
element.parent.name for element in constraint.elements
] + [
element.target_fullname for element in constraint.elements
]
guid = uuid.uuid5(uuid.NAMESPACE_OID, "_".join(str_tokens).encode('ascii'))
return str(guid)
convention = {
"fk_guid": fk_guid,
"ix": 'ix_%(column_0_label)s',
"fk": "fk_%(fk_guid)s",
}
上面,当我们创建一个新的ForeignKeyConstraint
时,我们会得到一个名字如下:
>>> metadata = MetaData(naming_convention=convention)
>>> user_table = Table('user', metadata,
... Column('id', Integer, primary_key=True),
... Column('version', Integer, primary_key=True),
... Column('data', String(30))
... )
>>> address_table = Table('address', metadata,
... Column('id', Integer, primary_key=True),
... Column('user_id', Integer),
... Column('user_version_id', Integer)
... )
>>> fk = ForeignKeyConstraint(['user_id', 'user_version_id'],
... ['user.id', 'user.version'])
>>> address_table.append_constraint(fk)
>>> fk.name
fk_0cd51ab5-8d70-56e8-a83c-86661737766d
命名CHECK约束
CheckConstraint
对象是针对任意SQL表达式配置的,该表达式可以包含任意数量的列,另外通常使用原始SQL字符串进行配置。因此,与CheckConstraint
一起使用的通用约定是我们期望该对象已经拥有一个名称的公约,然后我们使用其他约定元素对其进行了增强。A typical convention is "ck_%(table_name)s_%(constraint_name)s"
:
metadata = MetaData(
naming_convention={"ck": "ck_%(table_name)s_%(constraint_name)s"}
)
Table('foo', metadata,
Column('value', Integer),
CheckConstraint('value > 5', name='value_gt_5')
)
上表将生成名称ck_foo_value_gt_5
:
CREATE TABLE foo (
value INTEGER,
CONSTRAINT ck_foo_value_gt_5 CHECK (value > 5)
)
CheckConstraint
还支持%(columns_0_name)s
标记;我们可以通过确保我们在约束的表达式中使用Column
或sql.expression.column()
元素来使用它,或者通过声明与表格分开的约束:
metadata = MetaData(
naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"}
)
foo = Table('foo', metadata,
Column('value', Integer)
)
CheckConstraint(foo.c.value > 5)
或通过内联使用sql.expression.column()
:
from sqlalchemy import column
metadata = MetaData(
naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"}
)
foo = Table('foo', metadata,
Column('value', Integer),
CheckConstraint(column('value') > 5)
)
两者都会产生名称ck_foo_value
:
CREATE TABLE foo (
value INTEGER,
CONSTRAINT ck_foo_value CHECK (value > 5)
)
通过扫描列对象的给定表达式来确定“列零”的名称。如果表达式存在多个列,则扫描确实使用确定性搜索,但表达式的结构将确定将哪个列标记为“列零”。
为布尔型,枚举型和其他架构类型配置命名
SchemaType
类引用类型对象,例如Boolean
和Enum
,它们会生成伴随该类型的CHECK约束。通过发送“name”参数来最直接地设置约束的名字,例如, Boolean.name
Table('foo', metadata,
Column('flag', Boolean(name='ck_foo_flag'))
)
命名约定功能也可以与这些类型结合使用,通常通过使用包含%(constraint_name)s
的约定,然后将名称应用于类型:
metadata = MetaData(
naming_convention={"ck": "ck_%(table_name)s_%(constraint_name)s"}
)
Table('foo', metadata,
Column('flag', Boolean(name='flag_bool'))
)
上表将生成约束名称ck_foo_flag_bool
:
CREATE TABLE foo (
flag BOOL,
CONSTRAINT ck_foo_flag_bool CHECK (flag IN (0, 1))
)
SchemaType
类使用特殊的内部符号,因此只能在DDL编译时确定命名约定。在Postgresql上,有一个本地的BOOLEAN类型,所以不需要Boolean
的CHECK约束;我们可以安全地设置一个没有名字的Boolean
类型,即使检查约束有一个命名约定。如果我们针对没有像SQLite或MySQL这样的本机BOOLEAN类型的数据库运行,只会为CHECK约束查阅此约定。
CHECK约束还可以使用column_0_name
标记,该标记与SchemaType
很好地配合使用,因为这些约束只有一列:
metadata = MetaData(
naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"}
)
Table('foo', metadata,
Column('flag', Boolean())
)
上述模式将产生:
CREATE TABLE foo (
flag BOOL,
CONSTRAINT ck_foo_flag CHECK (flag IN (0, 1))
)
版本1.0更改:不包含%(constraint_name)s
的约束命名约定再次与SchemaType
约束一起使用。
索引
Indexes can be created anonymously (using an auto-generated name ix_<column label>
) for a single column using the inline index
keyword on Column
, which also modifies the usage of unique
to apply the uniqueness to the index itself, instead of adding a separate UNIQUE constraint. 对于具有特定名称或包含多个列的索引,请使用需要名称的Index
结构。
下面我们举例说明一个Table
,其中有几个Index
对象关联。“CREATE INDEX”的DDL在表的create语句之后立即发布:
meta = MetaData()
mytable = Table('mytable', meta,
# an indexed column, with index "ix_mytable_col1"
Column('col1', Integer, index=True),
# a uniquely indexed column with index "ix_mytable_col2"
Column('col2', Integer, index=True, unique=True),
Column('col3', Integer),
Column('col4', Integer),
Column('col5', Integer),
Column('col6', Integer),
)
# place an index on col3, col4
Index('idx_col34', mytable.c.col3, mytable.c.col4)
# place a unique index on col5, col6
Index('myindex', mytable.c.col5, mytable.c.col6, unique=True)
sqlmytable.create(engine)
注意在上面的例子中,Index
构造是直接使用Column
对象在它所对应的表的外部创建的。Index
还支持Table
中的“内联”定义,使用字符串名称来标识列:
meta = MetaData()
mytable = Table('mytable', meta,
Column('col1', Integer),
Column('col2', Integer),
Column('col3', Integer),
Column('col4', Integer),
# place an index on col1, col2
Index('idx_col12', 'col1', 'col2'),
# place a unique index on col3, col4
Index('idx_col34', 'col3', 'col4', unique=True)
)
Index
对象也支持它自己的create()
方法:
i = Index('someindex', mytable.c.col5)
sqli.create(engine)
功能索引
Index
支持目标后端支持的SQL和函数表达式。要使用降序值对列创建索引,可以使用ColumnElement.desc()
修饰符:
from sqlalchemy import Index
Index('someindex', mytable.c.somecol.desc())
或者使用支持Postgresql等函数索引的后端,可以使用lower()
函数创建“不区分大小写”的索引:
from sqlalchemy import func, Index
Index('someindex', func.lower(mytable.c.somecol))
0.8版新增功能: Index
支持SQL表达式和函数以及普通列。