Automap
定义一个扩展到sqlalchemy.ext.declarative
系统的系统,自动生成从数据库模式到映射类和关系,通常而不一定是一个反射的数据库模式。
希望AutomapBase
系统提供了一个快速和现代化的解决方案,解决了非常著名的SQLSoup也试图解决的问题,即从现有数据库动态生成快速和基本的对象模型。通过严格在映射器配置级别解决该问题,并与现有的声明类技术完全集成,AutomapBase
试图提供一个与问题紧密集成的方法,以迅速自动生成临时映射。
提示
Automap 扩展针对“零声明”方法,其中可以从数据库模式动态生成包括类和预命名关系在内的完整 ORM 模型。对于仍希望使用显式类声明以及与表反射结合使用的显式关系定义的应用程序,描述在使用 DeferredReflection 中的DeferredReflection
类是更好的选择。
基本用法
最简单的用法是将现有数据库反映到一个新模型中。我们创建一个新的AutomapBase
类,方式类似于我们创建声明性基类,使用automap_base()
。然后,我们调用AutomapBase.prepare()
在生成的基类上,要求它反映模式并生成映射:
from sqlalchemy.ext.automap import automap_base from sqlalchemy.orm import Session from sqlalchemy import create_engine Base = automap_base() # engine, suppose it has two tables 'user' and 'address' set up engine = create_engine("sqlite:///mydatabase.db") # reflect the tables Base.prepare(autoload_with=engine) # mapped classes are now created with names by default # matching that of the table name. User = Base.classes.user Address = Base.classes.address session = Session(engine) # rudimentary relationships are produced session.add(Address(email_address="foo@bar.com", user=User(name="foo"))) session.commit() # collection-based relationships are by default named # "<classname>_collection" u1 = session.query(User).first() print(u1.address_collection)
在上面,调用AutomapBase.prepare()
并传递AutomapBase.prepare.reflect
参数,表示将在此声明基类的MetaData
集合上调用MetaData.reflect()
方法; 然后,MetaData
中的每个** viable **Table
都将自动生成一个新的映射类。将连接各个表的ForeignKeyConstraint
对象将用于在类之间生成新的双向relationship()
对象。类和关系遵循一个默认命名方案,我们可以自定义。在这一点上,我们基本的映射包含了相关的User
和Address
类,可以以传统方式使用。
注意
通过** viable **,我们指的是表必须指定主键才能进行映射。此外,如果检测到表是两个其他表之间的纯关联表,则不会直接映射该表,而是将其配置为两个引用表的映射之间的多对多表。
从现有元数据生成映射
我们可以将预先声明的MetaData
对象传递给automap_base()
。该对象可以以任何方式构造,包括以编程方式、从序列化文件或从使用MetaData.reflect()
反映的自身构造。下面我们演示了反射和显式表声明的组合:
from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey from sqlalchemy.ext.automap import automap_base engine = create_engine("sqlite:///mydatabase.db") # produce our own MetaData object metadata = MetaData() # we can reflect it ourselves from a database, using options # such as 'only' to limit what tables we look at... metadata.reflect(engine, only=["user", "address"]) # ... or just define our own Table objects with it (or combine both) Table( "user_order", metadata, Column("id", Integer, primary_key=True), Column("user_id", ForeignKey("user.id")), ) # we can then produce a set of mappings from this MetaData. Base = automap_base(metadata=metadata) # calling prepare() just sets up mapped classes and relationships. Base.prepare() # mapped classes are ready User = Base.classes.user Address = Base.classes.address Order = Base.classes.user_order
从多个模式生成映射
当使用反射时,AutomapBase.prepare()
方法一次最多只能从一个模式中反射表,使用 AutomapBase.prepare.schema
参数来指示要从中反射的模式的名称。为了从多个模式中填充 AutomapBase
中的表,可以多次调用 AutomapBase.prepare()
,每次将不同的名称传递给 AutomapBase.prepare.schema
参数。AutomapBase.prepare()
方法会保持一个内部列表,其中包含已经映射过的 Table
对象,并且只会为自上次运行 AutomapBase.prepare()
以来新增的那些 Table
对象添加新的映射:
e = create_engine("postgresql://scott:tiger@localhost/test") Base.metadata.create_all(e) Base = automap_base() Base.prepare(e) Base.prepare(e, schema="test_schema") Base.prepare(e, schema="test_schema_2")
新版本 2.0 中新增了 AutomapBase.prepare()
方法,可以任意调用;每次运行时只会映射新增的表。在 1.4 版本及之前的版本中,多次调用会导致错误,因为它会尝试重新映射已经映射过的类。之前的解决方法是直接调用 MetaData.reflect()
,该方法仍然可用。
跨多个模式自动映射同名表
对于多个模式可能有同名表的常见情况,因此可能生成同名类,可以通过使用 AutomapBase.prepare.classname_for_table
钩子在每个模式基础上应用不同的类名来解决冲突,或者通过使用 AutomapBase.prepare.modulename_for_table
钩子来解决同名类的歧义,该钩子允许通过更改它们的有效 __module__
属性来区分同名类。在下面的示例中,此钩子用于为所有类创建一个 __module__
属性,其形式为 mymodule.
,如果没有模式,则使用模式名称 default
:
e = create_engine("postgresql://scott:tiger@localhost/test") Base.metadata.create_all(e) def module_name_for_table(cls, tablename, table): if table.schema is not None: return f"mymodule.{table.schema}" else: return f"mymodule.default" Base = automap_base() Base.prepare(e, modulename_for_table=module_name_for_table) Base.prepare(e, schema="test_schema", modulename_for_table=module_name_for_table) Base.prepare(e, schema="test_schema_2", modulename_for_table=module_name_for_table)
同名类被组织成可在 AutomapBase.by_module
中使用的分层集合。使用特定包/模块的点分隔名称向下遍历到所需的类名。
注意
当使用 AutomapBase.prepare.modulename_for_table
钩子来返回一个不是 None
的新 __module__
时,类不会被放入 AutomapBase.classes
集合中;只有那些没有给定显式模块名的类才会放在这里,因为该集合不能单独表示同名类。
在上面的示例中,如果数据库中包含了三个默认模式、test_schema
模式和 test_schema_2
模式中都命名为 accounts
的表,将会有三个单独的类可用,分别是:
Base.by_module.mymodule.default.accounts Base.by_module.mymodule.test_schema.accounts Base.by_module.mymodule.test_schema_2.accounts
对于所有 AutomapBase
类生成的默认模块命名空间是 sqlalchemy.ext.automap
。如果没有使用 AutomapBase.prepare.modulename_for_table
钩子,AutomapBase.by_module
的内容将完全在 sqlalchemy.ext.automap
命名空间内(例如 MyBase.by_module.sqlalchemy.ext.automap.
),其中将包含与 AutomapBase.classes
中看到的相同系列的类。因此,只有在存在显式 __module__
约定时才通常需要使用 AutomapBase.by_module
。
显式指定类
提示
如果在应用程序中期望显式类占据主要地位,请考虑改用 DeferredReflection
。
automap
扩展允许类被明确定义,类似于DeferredReflection
类的方式。从AutomapBase
继承的类表现得像常规的声明类,但在构建后不会立即映射,而是在调用AutomapBase.prepare()
时映射。AutomapBase.prepare()
方法将利用我们根据使用的表名建立的类。如果我们的模式包含表user
和address
,我们可以定义要使用的一个或两个类:
from sqlalchemy.ext.automap import automap_base from sqlalchemy import create_engine # automap base Base = automap_base() # pre-declare User for the 'user' table class User(Base): __tablename__ = "user" # override schema elements like Columns user_name = Column("name", String) # override relationships too, if desired. # we must use the same name that automap would use for the # relationship, and also must refer to the class name that automap will # generate for "address" address_collection = relationship("address", collection_class=set) # reflect engine = create_engine("sqlite:///mydatabase.db") Base.prepare(autoload_with=engine) # we still have Address generated from the tablename "address", # but User is the same as Base.classes.User now Address = Base.classes.address u1 = session.query(User).first() print(u1.address_collection) # the backref is still there: a1 = session.query(Address).first() print(a1.user)
在上面,更复杂的细节之一是,我们展示了覆盖relationship()
对象的过程,这是 automap 会创建的。为了做到这一点,我们需要确保名称与 automap 通常生成的名称匹配,即关系名称将是User.address_collection
,而从 automap 的角度来看,所指的类的名称被称为address
,尽管我们在使用这个类时将其称为Address
。
覆盖命名方案
automap
负责根据模式生成映射类和关系名称,这意味着它在确定这些名称时有决策点。这三个决策点是使用函数提供的,这些函数可以传递给AutomapBase.prepare()
方法,并被称为classname_for_table()
、name_for_scalar_relationship()
和name_for_collection_relationship()
。以下示例中提供了任意或所有这些函数,我们使用“驼峰命名法”为类名和使用 Inflect 包的“复数形式”为集合名:
import re import inflect def camelize_classname(base, tablename, table): "Produce a 'camelized' class name, e.g." "'words_and_underscores' -> 'WordsAndUnderscores'" return str( tablename[0].upper() + re.sub( r"_([a-z])", lambda m: m.group(1).upper(), tablename[1:], ) ) _pluralizer = inflect.engine() def pluralize_collection(base, local_cls, referred_cls, constraint): "Produce an 'uncamelized', 'pluralized' class name, e.g." "'SomeTerm' -> 'some_terms'" referred_name = referred_cls.__name__ uncamelized = re.sub( r"[A-Z]", lambda m: "_%s" % m.group(0).lower(), referred_name, )[1:] pluralized = _pluralizer.plural(uncamelized) return pluralized from sqlalchemy.ext.automap import automap_base Base = automap_base() engine = create_engine("sqlite:///mydatabase.db") Base.prepare( autoload_with=engine, classname_for_table=camelize_classname, name_for_collection_relationship=pluralize_collection, )
根据上述映射,我们现在将拥有User
和Address
两个类,其中从User
到Address
的集合被称为User.addresses
:
User, Address = Base.classes.User, Base.classes.Address u1 = User(addresses=[Address(email="foo@bar.com")])
关系检测
自动映射所实现的绝大部分是基于外键生成 relationship()
结构。其工作原理如下:
- 检查已知映射到特定类的给定
Table
是否存在ForeignKeyConstraint
对象。 - 对于每个
ForeignKeyConstraint
,将匹配到的远程Table
对象与其应映射到的类相匹配,如果有的话,否则将跳过。 - 由于我们正在检查的
ForeignKeyConstraint
对应于来自直接映射类的引用,因此关系将被设置为指向引用类的多对一关系;在引用类上将创建相应的一个对多反向引用,引用此类。 - 如果属于
ForeignKeyConstraint
的任何列不可为空(例如nullable=False
),则将在要传递给关系或反向引用的关键字参数中添加一个relationship.cascade
关键字参数,其值为all, delete-orphan
。如果ForeignKeyConstraint
报告对于一组非空列设置了ForeignKeyConstraint.ondelete
为CASCADE
,或者对于可为空列设置了SET NULL
,则在关系关键字参数集合中将选项relationship.passive_deletes
标志设置为True
。请注意,并非所有后端都支持对 ON DELETE 的反射。 - 关系的名称是使用
AutomapBase.prepare.name_for_scalar_relationship
和AutomapBase.prepare.name_for_collection_relationship
可调用函数确定的。重要的是要注意,默认关系命名是从实际类名派生的。如果您通过声明给出了特定类的显式名称,或者指定了备用类命名方案,那么关系名称将从该名称派生。 - 对于这些名称,类被检查是否存在匹配的已映射属性。如果在一侧检测到一个,但在另一侧没有,则
AutomapBase
尝试在缺失的一侧创建一个关系,然后使用relationship.back_populates
参数将新关系指向另一侧。 - 在通常情况下,如果任一侧都没有关系,则
AutomapBase.prepare()
会在“多对一”一侧生成一个relationship()
,并使用relationship.backref
参数将其与另一侧匹配。 relationship()
的生成以及可选地backref()
的生成由AutomapBase.prepare.generate_relationship
函数处理,该函数可以由最终用户提供,以增强传递给relationship()
或backref()
的参数或者使用这些函数的自定义实现。
自定义关系参数
AutomapBase.prepare.generate_relationship
钩子可用于向关系添加参数。对于大多数情况,我们可以利用现有的 generate_relationship()
函数,在使用我们自己的参数扩充给定的关键字字典后,返回对象。
下面是如何将 relationship.cascade
和 relationship.passive_deletes
选项传递给所有一对多关系的示例:
from sqlalchemy.ext.automap import generate_relationship from sqlalchemy.orm import interfaces def _gen_relationship( base, direction, return_fn, attrname, local_cls, referred_cls, **kw ): if direction is interfaces.ONETOMANY: kw["cascade"] = "all, delete-orphan" kw["passive_deletes"] = True # make use of the built-in function to actually return # the result. return generate_relationship( base, direction, return_fn, attrname, local_cls, referred_cls, **kw ) from sqlalchemy.ext.automap import automap_base from sqlalchemy import create_engine # automap base Base = automap_base() engine = create_engine("sqlite:///mydatabase.db") Base.prepare(autoload_with=engine, generate_relationship=_gen_relationship)
多对多关系
automap
将生成多对多关系,例如包含 secondary
参数的关系。生成这些关系的过程如下:
- 在任何映射类被分配给它之前,给定的
Table
将被检查是否包含ForeignKeyConstraint
对象。 - 如果表包含两个且仅两个
ForeignKeyConstraint
对象,并且此表中的所有列都是这两个ForeignKeyConstraint
对象的成员,则假定该表是“secondary”表,并且不会直接映射。 Table
所指向的两个(或一个,用于自引用)外部表将与它们将要映射到的类进行匹配,如果有的话。- 如果双方的映射类位于同一位置,则在两个类之间创建一个双向的多对多
relationship()
/backref()
对。 - 多对多的覆盖逻辑与一对多/多对一的相同;在调用
generate_relationship()
函数生成结构后,现有属性将被保留。
具有继承关系的关系
automap
不会在处于继承关系的两个类之间生成任何关系。也就是说,对于以下给定的两个类:
class Employee(Base): __tablename__ = "employee" id = Column(Integer, primary_key=True) type = Column(String(50)) __mapper_args__ = { "polymorphic_identity": "employee", "polymorphic_on": type, } class Engineer(Employee): __tablename__ = "engineer" id = Column(Integer, ForeignKey("employee.id"), primary_key=True) __mapper_args__ = { "polymorphic_identity": "engineer", }
Engineer
到 Employee
的外键不是用于建立关系,而是用于在两个类之间建立连接的继承关系。
请注意,这意味着自动映射将不会为从子类到父类的外键生成 任何 关系。如果一个映射还具有从子类到父类的实际关系,那么这些关系需要是显式的。在下面的例子中,由于 Engineer
到 Employee
有两个单独的外键,我们需要设置我们想要的关系以及 inherit_condition
,因为这些都不是 SQLAlchemy 可以猜测的:
class Employee(Base): __tablename__ = "employee" id = Column(Integer, primary_key=True) type = Column(String(50)) __mapper_args__ = { "polymorphic_identity": "employee", "polymorphic_on": type, } class Engineer(Employee): __tablename__ = "engineer" id = Column(Integer, ForeignKey("employee.id"), primary_key=True) favorite_employee_id = Column(Integer, ForeignKey("employee.id")) favorite_employee = relationship(Employee, foreign_keys=favorite_employee_id) __mapper_args__ = { "polymorphic_identity": "engineer", "inherit_condition": id == Employee.id, }
处理简单的命名冲突
在映射过程中如果出现命名冲突的情况,根据需要覆盖 classname_for_table()
、name_for_scalar_relationship()
和 name_for_collection_relationship()
中的任何一个。例如,如果自动映射尝试将一个多对一关系命名为一个现有列相同的名称,可以有条件地选择替代约定。给定一个模式:
CREATE TABLE table_a ( id INTEGER PRIMARY KEY ); CREATE TABLE table_b ( id INTEGER PRIMARY KEY, table_a INTEGER, FOREIGN KEY(table_a) REFERENCES table_a(id) );
上述模式将首先将 table_a
表自动映射为名为 table_a
的类;然后将在 table_b
的类上自动映射一个与此相关类相同名称的关系,例如 table_a
。这个关系名称与映射列 table_b.table_a
冲突,并且将在映射时发出错误。
我们可以通过以下方式使用下划线解决这个冲突:
def name_for_scalar_relationship(base, local_cls, referred_cls, constraint): name = referred_cls.__name__.lower() local_table = local_cls.__table__ if name in local_table.columns: newname = name + "_" warnings.warn("Already detected name %s present. using %s" % (name, newname)) return newname return name Base.prepare( autoload_with=engine, name_for_scalar_relationship=name_for_scalar_relationship, )
或者,我们可以在列的一侧更改名称。可以使用在 Naming Declarative Mapped Columns Explicitly 中描述的技术修改映射的列,通过将列显式地分配给一个新名称:
Base = automap_base() class TableB(Base): __tablename__ = "table_b" _table_a = Column("table_a", ForeignKey("table_a.id")) Base.prepare(autoload_with=engine)
SqlAlchemy 2.0 中文文档(三十)(2)https://developer.aliyun.com/article/1562436