ORM 配置
如何映射没有主键的表?
为了映射到一个特定的表,sqlAlchemy ORM需要至少有一列被表示为主键列;多列,即复合主键当然也是完全可行的。这些列有 not 数据库实际上需要知道它们是主键列,不过最好知道它们是主键列。只需要列 表现 就像主键一样,例如作为行的唯一且不可为空的标识符。
大多数ORM都要求对象定义某种主键,因为内存中的对象必须对应于数据库表中唯一可标识的行;至少,这允许对象以更新和删除语句为目标,这些语句只影响对象的行,而不影响其他行。然而,主键的重要性远远超出了这一点。在SQLAlchemy中,所有ORM映射对象始终在 Session
使用名为 identity map ,是sqlAlchemy使用的工作单元系统的中心模式,也是ORM使用最常见(而不是最常见)模式的关键。
注解
需要注意的是,我们只讨论sqlAlchemy ORM;一个建立在核心之上并且只处理 Table
物体, select()
结构等, 不 需要任何主键以任何方式出现在一个表上或与之关联(不过,在SQL中,所有表实际上都应该有某种主键,以免实际需要更新或删除特定的行)。
在几乎所有情况下,表都有一个所谓的 candidate key ,它是唯一标识一行的一列或一系列列。如果某个表确实没有此功能,并且具有实际完全重复的行,则该表不对应于 first normal form 并且无法映射。否则,组成最佳候选键的任何列都可以直接应用于映射器::
class SomeClass(Base): __table__ = some_table_with_no_pk __mapper_args__ = { 'primary_key':[some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar] }
更好的方法是在使用完全声明的表元数据时,使用 primary_key=True
这些列上的标志:
class SomeClass(Base): __tablename__ = "some_table_with_no_pk" uid = Column(Integer, primary_key=True) bar = Column(String, primary_key=True)
关系数据库中的所有表都应该有主键。即使是多对多关联表-主键也将是两个关联列的组合:
CREATE TABLE my_association ( user_id INTEGER REFERENCES user(id), account_id INTEGER REFERENCES account(id), PRIMARY KEY (user_id, account_id) )
如何配置属于python保留字或类似的列?
基于列的属性可以被赋予映射中所需的任何名称。见 从属性名称清楚地命名列 .
如何获得给定映射类的所有列、关系、映射属性等的列表?
此信息可从 Mapper
对象。
到达 Mapper
对于特定的映射类,请调用 inspect()
功能:
from sqlalchemy import inspect mapper = inspect(MyClass)
从那里,可以通过以下属性访问有关类的所有信息:
Mapper.attrs
-所有映射属性的命名空间。这些属性本身就是MapperProperty
,其中包含可导致映射的SQL表达式或列(如果适用)的其他属性。Mapper.column_attrs
-映射的属性命名空间仅限于列和SQL表达式属性。你可能想用Mapper.columns
到达Column
直接对象。Mapper.relationships
-全部的命名空间RelationshipProperty
属性。Mapper.all_orm_descriptors
-所有映射属性的命名空间,以及使用诸如hybrid_property
,AssociationProxy
以及其他。Mapper.columns
-的命名空间Column
对象和与映射关联的其他命名SQL表达式。Mapper.mapped_table
-Table
或其他可选择的映射器。Mapper.local_table
-Table
这是这个映射器的“本地”;这与Mapper.mapped_table
对于使用继承映射到组合可选项的映射器。
“在属性y下隐式组合列x”会收到警告或错误。
这个条件指的是当一个映射包含两个列,由于它们的名称而被映射到同一个属性名下,但是没有迹象表明这是有意的。映射类需要为每个要存储独立值的属性都有显式名称;当两个列具有相同的名称并且没有消除歧义时,它们属于同一属性,其效果是一列中的值是 已复制 另一个,根据先分配给属性的列。
这种行为通常是可取的,如果两列通过继承映射中的外键关系链接在一起,则允许这种行为而不发出警告。当出现警告或异常时,可以通过将列分配给不同的命名属性来解决问题,或者如果需要将它们组合在一起,则可以使用 column_property()
把这一点说清楚。
举例如下:
from sqlalchemy import Integer, Column, ForeignKey from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = 'a' id = Column(Integer, primary_key=True) class B(A): __tablename__ = 'b' id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey('a.id'))
从SQLAlchemy版本0.9.5开始,检测到上述情况,并警告 id
列 A
和 B
正在同一命名属性下组合 id
这是一个严重的问题,因为这意味着 B
对象的主键将始终与其 A
.
解决此问题的映射如下:
class A(Base): __tablename__ = 'a' id = Column(Integer, primary_key=True) class B(A): __tablename__ = 'b' b_id = Column('id', Integer, primary_key=True) a_id = Column(Integer, ForeignKey('a.id'))
假设我们确实想要 A.id
和 B.id
尽管事实上 B.a_id
是哪里 A.id
是相关的。我们可以用 column_property()
::
class A(Base): __tablename__ = 'a' id = Column(Integer, primary_key=True) class B(A): __tablename__ = 'b' # probably not what you want, but this is a demonstration id = column_property(Column(Integer, primary_key=True), A.id) a_id = Column(Integer, ForeignKey('a.id'))
我正在使用声明性并使用 and_()
或 or_()
,我收到一条有关外键的错误消息。
你在这样做吗?::
class MyClass(Base): # .... foo = relationship("Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar"))
那是一个 and_()
在两个字符串表达式中,SQLAlchemy无法对其应用任何映射。声明性允许 relationship()
要指定为字符串的参数,这些参数使用 eval()
. 但这不会发生在 and_()
表达式-它是一个特殊的操作,声明性只适用于 整体 作为字符串传递给primaryjoin或其他参数的内容:
class MyClass(Base): # .... foo = relationship("Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)")
或者,如果您需要的对象已经可用,则跳过字符串:
class MyClass(Base): # .... foo = relationship(Dest, primaryjoin=and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar))
同样的观点也适用于所有其他论点,例如 foreign_keys
::
# wrong ! foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"]) # correct ! foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]") # also correct ! foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id]) # if you're using columns from the class that you're inside of, just use the column objects ! class MyClass(Base): foo_id = Column(...) bar_id = Column(...) # ... foo = relationship(Dest, foreign_keys=[foo_id, bar_id])
为什么是 ORDER BY
要求与 LIMIT
(尤其是与 subqueryload()
)?
当没有设置显式排序时,关系数据库可以以任意顺序返回行。虽然这种排序通常与表中的行的自然顺序相对应,但并非所有数据库和查询都是如此。其结果是任何限制行使用的查询 LIMIT
或 OFFSET
应该 总是 指定一个 ORDER BY
. 否则,将实际返回哪些行是不确定的。
当我们使用像 Query.first()
我们实际上正在申请 LIMIT
对于查询,如果没有显式的排序,我们实际返回的行是不确定的。虽然对于通常按自然顺序返回行的数据库上的简单查询,我们可能不会注意到这一点,但如果我们也使用 subqueryload()
加载相关集合,我们可能无法按预期加载集合。
SqlAlchemy实现 subqueryload()
通过发出单独的查询,其结果与第一个查询的结果匹配。我们看到两个这样发出的查询:
>>> session.query(User).options(subqueryload(User.addresses)).all() -- the "main" query SELECT users.id AS users_id FROM users -- the "load" query issued by subqueryload SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, anon_1.users_id AS anon_1_users_id FROM (SELECT users.id AS users_id FROM users) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id
第二个查询将第一个查询嵌入为行的源。当内部查询使用 OFFSET
和/或 LIMIT
如果不排序,两个查询可能看不到相同的结果:
>>> user = session.query(User).options(subqueryload(User.addresses)).first() -- the "main" query SELECT users.id AS users_id FROM users LIMIT 1 -- the "load" query issued by subqueryload SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, anon_1.users_id AS anon_1_users_id FROM (SELECT users.id AS users_id FROM users LIMIT 1) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id
根据数据库的具体情况,我们可能会得到以下两个查询的结果:
-- query #1 +--------+ |users_id| +--------+ | 1| +--------+ -- query #2 +------------+-----------------+---------------+ |addresses_id|addresses_user_id|anon_1_users_id| +------------+-----------------+---------------+ | 3| 2| 2| +------------+-----------------+---------------+ | 4| 2| 2| +------------+-----------------+---------------+
上面,我们收到两个 addresses
行为 user.id
2个,1个没有。我们浪费了两行,未能实际加载集合。这是一个潜在的错误,因为如果不查看SQL和结果,ORM将不会显示出任何问题;如果我们访问 addresses
对于 User
我们有,它将为集合发出一个懒惰的负载,我们不会看到任何实际出错。
此问题的解决方案是始终指定确定性排序顺序,以便主查询始终返回相同的行集。这通常意味着你应该 Query.order_by()
在表的唯一列上。主键是一个很好的选择:
session.query(User).options(subqueryload(User.addresses)).order_by(User.id).first()
请注意 joinedload()
由于只发出了一个查询,所以装载器策略不会遇到相同的问题,因此装载查询不能与主查询不同。同样, selectinload()
热切的加载程序策略也没有这个问题,因为它将其集合加载直接链接到刚加载的主键值。
参见