SqlAlchemy 2.0 中文文档(三十九)(3)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: SqlAlchemy 2.0 中文文档(三十九)

SqlAlchemy 2.0 中文文档(三十九)(2)https://developer.aliyun.com/article/1562608


成员

text

类签名

sqlalchemy.engine.interfaces.ReflectedTableComment (builtins.dict)

attribute text: str | None

注释的文本 ## 使用与数据库无关的类型反射

当表的列被反映时,可以使用 Table.autoload_with 参数或 Inspector.get_columns() 方法,通过 TableInspector,数据类型将尽可能与目标数据库特定。这意味着,如果从 MySQL 数据库反映出一个“integer”数据类型,则该类型将由 sqlalchemy.dialects.mysql.INTEGER 类表示,其中包括 MySQL 特定属性,如“display_width”。或者在 PostgreSQL 上,可能返回 PostgreSQL 特定的数据类型,如 sqlalchemy.dialects.postgresql.INTERVALsqlalchemy.dialects.postgresql.ENUM

反映的一个使用案例是将给定的 Table 转移到不同的供应商数据库。为了适应这种使用情况,有一种技术,可以将这些供应商特定的数据类型即时转换为 SQLAlchemy 后端不可知数据类型的实例,例如上面的类型,如 IntegerIntervalEnum。这可以通过拦截列反映并使用 DDLEvents.column_reflect() 事件与 TypeEngine.as_generic() 方法来实现。

给定 MySQL 中的一个表(选择 MySQL 是因为 MySQL 有很多特定于供应商的数据类型和选项):

CREATE  TABLE  IF  NOT  EXISTS  my_table  (
  id  INTEGER  PRIMARY  KEY  AUTO_INCREMENT,
  data1  VARCHAR(50)  CHARACTER  SET  latin1,
  data2  MEDIUMINT(4),
  data3  TINYINT(2)
)

上述表包括仅限于 MySQL 的整数类型 MEDIUMINTTINYINT,以及一个包含 MySQL 专有 CHARACTER SET 选项的 VARCHAR。如果我们正常反映这个表,它将生成一个包含那些 MySQL 特定数据类型和选项的 Table 对象。

>>> from sqlalchemy import MetaData, Table, create_engine
>>> mysql_engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test")
>>> metadata_obj = MetaData()
>>> my_mysql_table = Table("my_table", metadata_obj, autoload_with=mysql_engine)

上述示例将上述表模式反映到一个新的 Table 对象中。然后,我们可以出于演示目的,使用 CreateTable 构造打印出特定于 MySQL 的“CREATE TABLE”语句:

>>> from sqlalchemy.schema import CreateTable
>>> print(CreateTable(my_mysql_table).compile(mysql_engine))
CREATE  TABLE  my_table  (
id  INTEGER(11)  NOT  NULL  AUTO_INCREMENT,
data1  VARCHAR(50)  CHARACTER  SET  latin1,
data2  MEDIUMINT(4),
data3  TINYINT(2),
PRIMARY  KEY  (id)
)ENGINE=InnoDB  DEFAULT  CHARSET=utf8mb4 

在上面的例子中,保留了特定于 MySQL 的数据类型和选项。如果我们想要一个能够干净地转移到另一个数据库供应商的 Table,并且用 Integer 替换特殊数据类型 sqlalchemy.dialects.mysql.MEDIUMINTsqlalchemy.dialects.mysql.TINYINT,我们可以选择在此表上“泛型化”数据类型,或以任何我们喜欢的方式进行更改,通过使用 DDLEvents.column_reflect() 事件建立一个处理程序。自定义处理程序将使用 TypeEngine.as_generic() 方法将上述 MySQL 特定类型对象转换为通用类型,方法是通过将传递给事件处理程序的列字典条目中的 "type" 条目替换为泛型。此字典的格式在 Inspector.get_columns() 中描述:

>>> from sqlalchemy import event
>>> metadata_obj = MetaData()
>>> @event.listens_for(metadata_obj, "column_reflect")
... def genericize_datatypes(inspector, tablename, column_dict):
...     column_dict["type"] = column_dict["type"].as_generic()
>>> my_generic_table = Table("my_table", metadata_obj, autoload_with=mysql_engine)

现在我们得到了一个新的通用 Table 并使用 Integer 作为那些数据类型。我们现在可以在 PostgreSQL 数据库上发出一个“CREATE TABLE”语句,例如:

>>> pg_engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/test", echo=True)
>>> my_generic_table.create(pg_engine)
CREATE  TABLE  my_table  (
  id  SERIAL  NOT  NULL,
  data1  VARCHAR(50),
  data2  INTEGER,
  data3  INTEGER,
  PRIMARY  KEY  (id)
) 

还需要注意的是,SQLAlchemy 通常会对其他行为做出合理的猜测,例如,MySQL 的 AUTO_INCREMENT 指令在 PostgreSQL 中最接近地使用 SERIAL 自增数据类型表示。

版本 1.4 新增了 TypeEngine.as_generic() 方法,并进一步改进了 DDLEvents.column_reflect() 事件的使用,以便方便地应用于 MetaData 对象。

反射的局限性

需要注意的是,反射过程仅使用在关系数据库中表示的信息重新创建 Table 元数据。根据定义,这个过程无法恢复数据库中实际未存储的模式方面。反射无法获取的状态包括但不限于:

  • 客户端默认值,即使用 Columndefault 关键字定义的 Python 函数或 SQL 表达式(请注意,这与通过反射获得的 server_default 是分开的)。
  • 列信息,例如可能放入 Column.info 字典中的数据
  • .quote 设置对于 ColumnTable 的价值。
  • 特定 Sequence 与给定 Column 的关联

在许多情况下,关系数据库报告的表元数据格式与 SQLAlchemy 中指定的格式不同。从反射返回的 Table 对象不能始终依赖于生成与原始 Python 定义的 Table 对象相同的 DDL。发生这种情况的地方包括服务器默认值、与列关联的序列以及有关约束和数据类型的各种特殊情况。服务器端默认值可能会带有转换指令(通常 PostgreSQL 将包括一个 :: 转换)或不同于最初指定的引号模式。

另一类限制包括反射仅部分或尚未定义的模式结构。最近对反射的改进允许反映视图、索引和外键选项等内容。截至本文撰写时,像 CHECK 约束、表注释和触发器等结构并未反映。

覆盖反射列

在反射表时,可以使用显式值覆盖单个列;这对于指定自定义数据类型、在数据库中未配置的主键等约束非常方便:

>>> mytable = Table(
...     "mytable",
...     metadata_obj,
...     Column(
...         "id", Integer, primary_key=True
...     ),  # override reflected 'id' to have primary key
...     Column("mydata", Unicode(50)),  # override reflected 'mydata' to be Unicode
...     # additional Column objects which require no change are reflected normally
...     autoload_with=some_engine,
... )

另请参阅

使用自定义类型和反射 - 演示了上述列覆盖技术如何应用于使用自定义数据类型进行表反射。

反射视图

反射系统也可以反映视图。基本用法与表相同:

my_view = Table("some_view", metadata, autoload_with=engine)

在上面,my_view是一个Table对象,其中包含代表视图“some_view”中每个列的名称和类型的Column对象。

通常,在反射视图时,至少希望有一个主键约束,如果可能的话还有外键。视图反射不会推断这些约束。

使用“override”技术,明确指定那些是主键或具有外键约束的列:

my_view = Table(
    "some_view",
    metadata,
    Column("view_id", Integer, primary_key=True),
    Column("related_thing", Integer, ForeignKey("othertable.thing_id")),
    autoload_with=engine,
)

一次性反射所有表

MetaData对象还可以获取表列表并反射完整集合。通过使用reflect()方法实现。调用后,所有定位的表都存在于MetaData对象的表字典中:

metadata_obj = MetaData()
metadata_obj.reflect(bind=someengine)
users_table = metadata_obj.tables["users"]
addresses_table = metadata_obj.tables["addresses"]

metadata.reflect()还提供了一种方便的方法来清除或删除数据库中的所有行:

metadata_obj = MetaData()
metadata_obj.reflect(bind=someengine)
with someengine.begin() as conn:
    for table in reversed(metadata_obj.sorted_tables):
        conn.execute(table.delete())

从其他模式反射表

章节指定模式名称介绍了表模式的概念,这是数据库中包含表和其他对象的命名空间,并且可以明确指定。可以使用Table.schema参数为Table对象以及其他对象如视图、索引和序列设置“模式”,还可以使用MetaData.schema参数为MetaData对象设置默认模式。

此模式参数的使用直接影响表反射功能在被要求反射对象时查找的位置。例如,给定一个通过其MetaData.schema参数配置了默认模式名称“project”的MetaData对象:

>>> metadata_obj = MetaData(schema="project")

MetaData.reflect()然后将利用配置的.schema进行反射:

>>> # uses `schema` configured in metadata_obj
>>> metadata_obj.reflect(someengine)

最终结果是,“project”模式中的Table对象将被反射,并且它们将以该名称的模式限定形式填充:

>>> metadata_obj.tables["project.messages"]
Table('messages', MetaData(), Column('message_id', INTEGER(), table=<messages>), schema='project')

同样,如果 Table 对象中包含了 Table.schema 参数,那么该表也将从该数据库模式中反映出来,覆盖了可能已在拥有的 MetaData 集合上配置的任何默认模式:

>>> messages = Table("messages", metadata_obj, schema="project", autoload_with=someengine)
>>> messages
Table('messages', MetaData(), Column('message_id', INTEGER(), table=<messages>), schema='project')

最后,MetaData.reflect() 方法本身也允许传递一个 MetaData.reflect.schema 参数,因此我们也可以为默认配置的 MetaData 对象从“project”模式加载表:

>>> metadata_obj = MetaData()
>>> metadata_obj.reflect(someengine, schema="project")

我们可以使用不同的 MetaData.schema 参数(或者不使用任何参数)多次调用 MetaData.reflect() 方法,以便继续向 MetaData 对象中添加更多对象:

>>> # add tables from the "customer" schema
>>> metadata_obj.reflect(someengine, schema="customer")
>>> # add tables from the default schema
>>> metadata_obj.reflect(someengine)

带有默认模式的模式限定反射的交互

最佳实践总结部分

在本节中,我们讨论了 SQLAlchemy 关于数据库会话中“默认模式”中可见表的反射行为,以及这些与显式包含模式的 SQLAlchemy  指令的交互方式。 作为最佳实践,请确保数据库的“默认”模式只是一个单一名称,而不是名称列表; 对于属于此“默认”模式并且可以在 DDL 和  SQL 中无需模式限定名称的表,将相应的 Table.schema 和类似的模式参数设置为其默认值 None

如 使用 MetaData 指定默认模式名称 中所述,具有模式概念的数据库通常也包括“默认”模式的概念。  这自然是因为,当一个通常的表对象没有模式时,具有模式的数据库仍然会认为该表在某处的“模式”中。 一些数据库(如  PostgreSQL)进一步将此概念扩展为“模式搜索路径”的概念,其中可以在特定数据库会话中将 多个 模式名称视为“隐式”; 指的是任何这些模式中的表名称将不需要模式名称存在(同时,如果模式名称存在,也是完全可以的)。

由于大多数关系数据库都有一个特定的表对象的概念,可以以模式限定的方式引用它,以及一个“隐式”的方式,其中没有模式存在,这为 SQLAlchemy 的反射特性带来了复杂性。以模式限定的方式反映表将始终填充其Table.schema属性,并且还会影响如何将此Table组织到MetaData.tables集合中,即以模式限定的方式。相反,以非模式限定的方式反映相同的表将在不模式限定的情况下将其组织到MetaData.tables集合中。最终的结果是,在实际数据库中,单一的MetaData集合中会有两个单独的Table对象,表示相同的表。

为了说明这个问题的影响,考虑上一个示例中来自“project”模式的表,并假设“project”模式是我们数据库连接的默认模式,或者如果使用诸如 PostgreSQL 之类的数据库,则假设“project”模式在 PostgreSQL 中设置了search_path。这意味着数据库接受以下两个 SQL 语句是等价的:

-- schema qualified
SELECT  message_id  FROM  project.messages
-- non-schema qualified
SELECT  message_id  FROM  messages

这不是一个问题,因为可以双向找到表。但是在 SQLAlchemy 中,是Table对象的标识决定了它在 SQL 语句中的语义角色。根据 SQLAlchemy 当前的决定,这意味着如果我们以模式限定和非模式限定的方式同时反映同一个“messages”表,我们会得到两个Table对象,它们不会被视为语义上等价:

>>> # reflect in non-schema qualified fashion
>>> messages_table_1 = Table("messages", metadata_obj, autoload_with=someengine)
>>> # reflect in schema qualified fashion
>>> messages_table_2 = Table(
...     "messages", metadata_obj, schema="project", autoload_with=someengine
... )
>>> # two different objects
>>> messages_table_1 is messages_table_2
False
>>> # stored in two different ways
>>> metadata.tables["messages"] is messages_table_1
True
>>> metadata.tables["project.messages"] is messages_table_2
True

上述问题在反映的表包含对其他表的外键引用时变得更加复杂。假设“messages”有一个“project_id”列,它引用另一个模式本地表“projects”的行,这意味着“messages”表定义的一部分是一个ForeignKeyConstraint对象。

我们可能会发现自己处于这样一种情况:一个MetaData集合可能包含多达四个Table对象,代表这两个数据库表,其中一个或两个附加表是由反射过程生成的;这是因为当反射过程遇到一个正在被反射的表上的外键约束时,它会分支出去反射那个被引用的表。它用于为这个被引用的表分配模式的决策是,如果拥有的Table也省略了其模式名称,并且这两个对象位于同一模式中,那么 SQLAlchemy 将省略默认模式的反射ForeignKeyConstraint对象,但如果没有省略,则包括它。

常见情况是以模式合格的方式反映表,然后以同样的方式加载相关表:

>>> # reflect "messages" in a schema qualified fashion
>>> messages_table_1 = Table(
...     "messages", metadata_obj, schema="project", autoload_with=someengine
... )

上述的 messages_table_1 也会以模式合格的方式引用 projects。这个 projects 表会自动反射,因为 “messages” 引用了它:

>>> messages_table_1.c.project_id
Column('project_id', INTEGER(), ForeignKey('project.projects.project_id'), table=<messages>)

如果代码的其他部分以非模式合格的方式反映“projects”,现在就有了两个不同的 projects 表:

>>> # reflect "projects" in a non-schema qualified fashion
>>> projects_table_1 = Table("projects", metadata_obj, autoload_with=someengine)
>>> # messages does not refer to projects_table_1 above
>>> messages_table_1.c.project_id.references(projects_table_1.c.project_id)
False
>>> # it refers to this one
>>> projects_table_2 = metadata_obj.tables["project.projects"]
>>> messages_table_1.c.project_id.references(projects_table_2.c.project_id)
True
>>> # they're different, as one non-schema qualified and the other one is
>>> projects_table_1 is projects_table_2
False

上述混淆可能会在使用表反射加载应用程序级别Table对象的应用程序中造成问题,以及在迁移场景中,特别是在使用 Alembic 迁移检测新表和外键约束时。

可以通过坚持一个简单的做法来纠正上述行为:

  • 对于任何期望位于数据库的默认模式中的Table,不要包含Table.schema参数。

对于支持模式的“搜索”路径的 PostgreSQL 和其他数据库,请添加以下附加做法:

  • 将“搜索路径”限制为一个模式,即默认模式

另请参阅

远程模式表反射和 PostgreSQL search_path - 关于 PostgreSQL 数据库的此行为的附加详细信息。### 模式合格反射与默认模式的交互

最佳实践概述部分

在本节中,我们将讨论 SQLAlchemy 在数据库会话的“默认模式”中可见的表的反射行为,以及这些表如何与显式包含模式的  SQLAlchemy 指令进行交互。作为最佳实践,请确保数据库的“默认”模式只是一个单一的名称,而不是名称列表;对于属于此“默认”模式且可以在  DDL 和 SQL 中不带模式限定命名的表,将相应的 Table.schema 和类似的模式参数设置为它们的默认值 None

如在使用 MetaData  指定默认模式名称中描述的那样,具有模式概念的数据库通常还包括“默认”模式的概念。这自然是因为当人们引用常见的无模式表对象时,具有模式功能的数据库仍会认为该表位于某个“模式”中。一些数据库,如  PostgreSQL,将这个概念进一步发展成为模式搜索路径的概念,其中一个特定数据库会话中可以考虑多个模式名称为“隐式”;引用任何这些模式中的表名都不需要模式名(同时如果模式名存在也完全可以)。

因此,由于大多数关系数据库都有一种特定的表对象的概念,既可以以模式限定的方式引用,也可以以“隐式”方式引用,其中不需要模式,这给 SQLAlchemy 的反射特性带来了复杂性。以模式限定的方式反映表将始终填充其 Table.schema 属性,并且另外影响到这个 Table 如何以模式限定的方式组织到 MetaData.tables 集合中。相反,以非模式限定的方式反映相同的表将以不带模式的方式组织到 MetaData.tables 集合中。最终结果是,在实际数据库中表示同一张表的单个 MetaData 集合中将有两个单独的 Table 对象。

为了说明这个问题的后果,考虑前面示例中“project”模式中的表,并假设“project”模式是我们数据库连接的默认模式,或者如果使用像 PostgreSQL 这样的数据库,假设“project”模式设置在 PostgreSQL 的search_path中。这意味着数据库接受以下两个 SQL 语句作为等价:

-- schema qualified
SELECT  message_id  FROM  project.messages
-- non-schema qualified
SELECT  message_id  FROM  messages

这并不是一个问题,因为表可以以两种方式找到。然而,在 SQLAlchemy 中,是Table对象的标识决定了它在 SQL 语句中的语义角色。根据 SQLAlchemy 当前的决策,这意味着如果我们以模式限定和非模式限定的方式反射相同的“messages”表,我们会得到两个不会被视为语义等价的Table对象:

>>> # reflect in non-schema qualified fashion
>>> messages_table_1 = Table("messages", metadata_obj, autoload_with=someengine)
>>> # reflect in schema qualified fashion
>>> messages_table_2 = Table(
...     "messages", metadata_obj, schema="project", autoload_with=someengine
... )
>>> # two different objects
>>> messages_table_1 is messages_table_2
False
>>> # stored in two different ways
>>> metadata.tables["messages"] is messages_table_1
True
>>> metadata.tables["project.messages"] is messages_table_2
True

当被反射的表包含对其他表的外键引用时,上述问题变得更加复杂。假设“messages”有一个“project_id”列,它引用另一个模式本地表“projects”,这意味着“messages”表的定义中包含一个ForeignKeyConstraint对象。

我们可能会发现自己处于这样一种情况,一个MetaData集合可能包含代表这两个数据库表的四个Table对象,其中一个或两个额外的表是由反射过程生成的;这是因为当反射过程遇到被反射表上的外键约束时,它会分支出去反射该引用表。它用于为这个引用表分配模式的决策是,如果拥有的Table也省略了它的模式名称,那么 SQLAlchemy 将省略默认模式从反射的ForeignKeyConstraint对象中,如果这两个对象在同一个模式中,则包括它,但如果没有被省略的话。

常见情况是以模式限定方式反射表,然后以模式限定方式加载相关表:

>>> # reflect "messages" in a schema qualified fashion
>>> messages_table_1 = Table(
...     "messages", metadata_obj, schema="project", autoload_with=someengine
... )

上述messages_table_1也将以模式限定方式引用projects。这个projects表将被自动反射,因为“messages”引用了它:

>>> messages_table_1.c.project_id
Column('project_id', INTEGER(), ForeignKey('project.projects.project_id'), table=<messages>)

如果代码的其他部分以非模式限定方式反射“projects”,那么现在有两个不同的 projects 表:

>>> # reflect "projects" in a non-schema qualified fashion
>>> projects_table_1 = Table("projects", metadata_obj, autoload_with=someengine)
>>> # messages does not refer to projects_table_1 above
>>> messages_table_1.c.project_id.references(projects_table_1.c.project_id)
False
>>> # it refers to this one
>>> projects_table_2 = metadata_obj.tables["project.projects"]
>>> messages_table_1.c.project_id.references(projects_table_2.c.project_id)
True
>>> # they're different, as one non-schema qualified and the other one is
>>> projects_table_1 is projects_table_2
False

上述混淆可能会在使用表反射加载应用级别Table对象的应用程序内以及在迁移方案中引起问题,特别是在使用 Alembic Migrations 检测新表和外键约束时。

以上行为可以通过坚持一个简单的做法来纠正:

  • 不要为任何期望位于数据库默认模式中的Table包括Table.schema参数。

对于支持“搜索”模式的 PostgreSQL 和其他数据库,添加以下额外的做法:

  • 将“搜索路径”限制为仅一个模式,即默认模式

另请参阅

远程模式表内省和 PostgreSQL search_path - 关于 PostgreSQL 数据库的此行为的附加细节。


SqlAlchemy 2.0 中文文档(三十九)(4)https://developer.aliyun.com/article/1562617

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
关系型数据库 数据库连接 数据库
SqlAlchemy 2.0 中文文档(三十九)(1)
SqlAlchemy 2.0 中文文档(三十九)
28 0
|
2月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(三十一)(3)
SqlAlchemy 2.0 中文文档(三十一)
19 1
|
2月前
|
JSON 测试技术 数据格式
SqlAlchemy 2.0 中文文档(三十一)(4)
SqlAlchemy 2.0 中文文档(三十一)
24 1
|
2月前
|
存储 SQL 测试技术
SqlAlchemy 2.0 中文文档(三十一)(1)
SqlAlchemy 2.0 中文文档(三十一)
22 1
|
2月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(三十一)(2)
SqlAlchemy 2.0 中文文档(三十一)
20 1
|
2月前
|
关系型数据库 MySQL 数据库
SqlAlchemy 2.0 中文文档(三十九)(5)
SqlAlchemy 2.0 中文文档(三十九)
18 0
|
2月前
|
关系型数据库 数据库连接 数据库
SqlAlchemy 2.0 中文文档(三十九)(2)
SqlAlchemy 2.0 中文文档(三十九)
19 0
|
2月前
|
缓存 数据库连接 数据库
SqlAlchemy 2.0 中文文档(三十九)(4)
SqlAlchemy 2.0 中文文档(三十九)
16 0
|
2月前
|
SQL 缓存 数据库
SqlAlchemy 2.0 中文文档(四十二)(2)
SqlAlchemy 2.0 中文文档(四十二)
25 0
|
2月前
|
存储 JSON 数据库
SqlAlchemy 2.0 中文文档(四十二)(4)
SqlAlchemy 2.0 中文文档(四十二)
27 0