Отражение объектов базы данных

Объекту Table можно поручить загрузить информацию о себе из соответствующего объекта схемы базы данных, уже существующего в базе данных. Этот процесс называется отражение. В самом простом случае вам нужно указать только имя таблицы, объект MetaData и аргумент autoload_with:

>>> messages = Table("messages", metadata_obj, autoload_with=engine)
>>> [c.name for c in messages.columns]
['message_id', 'message_name', 'date']

Приведенная выше операция будет использовать данный движок для запроса базы данных информации о таблице messages, а затем сгенерирует Column, ForeignKey и другие объекты, соответствующие этой информации, как если бы объект Table был создан вручную на Python.

При отражении таблиц, если данная таблица ссылается на другую таблицу через внешний ключ, создается второй объект Table внутри объекта MetaData, представляющего соединение. Ниже, предположим, что таблица shopping_cart_items ссылается на таблицу с именем shopping_carts. Отражение таблицы shopping_cart_items имеет такой эффект, что таблица shopping_carts также будет загружена:

>>> shopping_cart_items = Table("shopping_cart_items", metadata_obj, autoload_with=engine)
>>> 'shopping_carts' in metadata_obj.tables:
True

MetaData имеет интересное «синглтоноподобное» поведение: если вы запросили обе таблицы по отдельности, MetaData обеспечит создание ровно одного объекта Table для каждого отдельного имени таблицы. Конструктор Table фактически возвращает вам уже существующий объект Table, если он уже существует с заданным именем. Как показано ниже, мы можем получить доступ к уже созданной таблице shopping_carts, просто назвав ее:

shopping_carts = Table("shopping_carts", metadata_obj)

Конечно, хорошей идеей является использование autoload_with=engine с вышеупомянутой таблицей независимо от нее. Это делается для того, чтобы атрибуты таблицы были загружены, если они еще не были загружены. Операция автозагрузки происходит только для таблицы, если она еще не была загружена; после загрузки новые вызовы Table с тем же именем не будут повторно выдавать никаких запросов на отражение.

Переопределение отраженных столбцов

При отражении таблиц отдельные столбцы могут быть переопределены явными значениями; это удобно для указания пользовательских типов данных, ограничений, таких как первичные ключи, которые не могут быть настроены в базе данных, и т.д.:

>>> 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 с объектами Column, представляющими имена и типы каждого столбца в представлении «some_view».

Обычно при отражении представления желательно иметь как минимум ограничение первичного ключа, а то и внешних ключей. Отражение представления не экстраполирует эти ограничения.

Используйте для этого технику «переопределения», явно указывая те столбцы, которые являются частью первичного ключа или имеют ограничения внешнего ключа:

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)
for table in reversed(metadata_obj.sorted_tables):
    someengine.execute(table.delete())

Отражение таблиц из других схем

Раздел Указание имени схемы вводит понятие схем таблиц, которые представляют собой пространства имен в базе данных, содержащие таблицы и другие объекты, и которые могут быть заданы в явном виде. Схема» для объекта Table, а также для других объектов, таких как представления, индексы и последовательности, может быть задана с помощью параметра Table.schema, а также в качестве схемы по умолчанию для объекта MetaData с помощью параметра MetaData.schema.

Использование этого параметра схемы напрямую влияет на то, куда будет смотреть функция отражения таблиц, когда ее попросят отразить объекты. Например, если объект MetaData сконфигурирован с именем схемы по умолчанию «project» через параметр MetaData.schema:

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

Затем MetaData.reflect() будет использовать эту настроенную .schema для отражения:

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

Конечным результатом будет то, что объекты Table из схемы «project» будут отражены, и они будут заполнены как schema-qualified с этим именем:

>>> 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:

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

Мы можем вызывать MetaData.reflect() любое количество раз с разными аргументами MetaData.schema (или вообще без них), чтобы продолжить заполнение объекта MetaData новыми объектами:

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

Взаимодействие Schema-qualified Reflection со схемой по умолчанию

Обобщение лучших практик раздела

В этом разделе мы обсудим поведение SQLAlchemy при отражении таблиц, которые видны в «схеме по умолчанию» сессии базы данных, и как они взаимодействуют с директивами SQLAlchemy, которые включают схему в явном виде. В качестве лучшей практики убедитесь, что схема «по умолчанию» для базы данных - это просто одно имя, а не список имен; для таблиц, которые являются частью этой схемы «по умолчанию» и могут быть названы без указания схемы в DDL и SQL, оставьте соответствующие Table.schema и подобные параметры схемы установленными по умолчанию None.

Как описано в Указание имени схемы по умолчанию с помощью метаданных, базы данных, имеющие концепцию схем, обычно также включают концепцию схемы «по умолчанию». Причина этого, естественно, заключается в том, что если ссылаться на табличные объекты без схемы, как это часто бывает, база данных, поддерживающая схему, все равно будет считать, что эта таблица находится где-то в «схеме». Некоторые базы данных, такие как PostgreSQL, развивают эту концепцию до понятия schema search path, где множество имен схем могут рассматриваться в конкретной сессии базы данных как «неявные»; обращение к имени таблицы, что это любая из этих схем, не требует присутствия имени схемы (и в то же время совершенно нормально, если имя схемы присутствует).

Поскольку в большинстве реляционных баз данных существует концепция конкретного объекта таблицы, на который можно ссылаться как в соответствии со схемой, так и «неявно», когда схема отсутствует, это создает определенные сложности для функции отражения SQLAlchemy. Отражение таблицы в соответствии со схемой всегда будет заполнять ее атрибут Table.schema и дополнительно влиять на то, как эта Table организована в коллекцию MetaData.tables, то есть в соответствии со схемой. И наоборот, отражение одной и той же таблицы способом, не отвечающим требованиям схемы, организует ее в коллекцию MetaData.tables **без учета требований схемы. В итоге в одной коллекции Table будут два отдельных объекта MetaData, представляющих одну и ту же таблицу в реальной базе данных.

Чтобы проиллюстрировать последствия этого вопроса, рассмотрим таблицы из схемы «проект» в предыдущем примере, а также предположим, что схема «проект» является схемой по умолчанию нашего подключения к базе данных, или, если используется такая база данных, как PostgreSQL, предположим, что схема «проект» установлена в 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», то есть существует объект ForeignKeyConstraint, который является частью определения таблицы «messages».

Мы можем оказаться в ситуации, когда одна коллекция MetaData может содержать до четырех объектов Table, представляющих две таблицы базы данных, где одна или две из дополнительных таблиц были созданы процессом отражения; это происходит потому, что когда процесс отражения встречает ограничение внешнего ключа на отражаемой таблице, он разветвляется, чтобы отразить и ту таблицу, на которую ссылается. Принятие решения, которое он использует для назначения схемы этой ссылающейся таблице, заключается в том, что SQLAlchemy омитирует схему по умолчанию из отраженного объекта ForeignKeyConstraint, если в собственном Table также опущено имя схемы, а также в том, что эти два объекта находятся в одной схеме, но включит ее, если она не была опущена.

Распространенным сценарием является ситуация, когда отражение таблицы в соответствии со схемой загружает связанную таблицу, которая также будет выполнена в соответствии со схемой:

>>> # 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>)

если какая-то другая часть кода отражает «проекты» не в соответствии со схемой, то теперь есть две таблицы проектов, которые не являются одним и тем же:

>>> # 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.schema для любого Table, который ожидает расположения в схеме базы данных default.

Для PostgreSQL и других баз данных, которые поддерживают путь «поиска» для схем, добавьте следующую дополнительную практику:

  • Сужайте «путь поиска» только до одной схемы, которая является схемой по умолчанию.

См.также

postgresql_schema_reflection - дополнительные подробности этого поведения применительно к базе данных PostgreSQL.

Мелкозернистое отражение с инспектором

Также доступен низкоуровневый интерфейс, обеспечивающий независимую от бэкенда систему загрузки списков описаний схем, таблиц, столбцов и ограничений из данной базы данных. Он известен как «Инспектор»:

from sqlalchemy import create_engine
from sqlalchemy import inspect

engine = create_engine("...")
insp = inspect(engine)
print(insp.get_table_names())

Отражение с помощью типов, не зависящих от базы данных

При отражении столбцов таблицы с помощью параметра Table.autoload_with в Table или метода Inspector.get_columns() в Inspector, типы данных будут как можно более специфичными для целевой базы данных. Это означает, что если тип данных «integer» отражается от базы данных MySQL, то этот тип будет представлен классом sqlalchemy.dialects.mysql.INTEGER, который включает специфические для MySQL атрибуты, такие как «display_width». Или на PostgreSQL может быть возвращен специфический для PostgreSQL тип данных, такой как sqlalchemy.dialects.postgresql.INTERVAL или sqlalchemy.dialects.postgresql.ENUM.

Для рефлексии существует случай использования, когда данный Table должен быть перенесен в базу данных другого производителя. Для этого случая существует техника, с помощью которой эти специфические для поставщика типы данных могут быть преобразованы «на лету» в экземпляры бэкенд-агностических типов данных SQLAlchemy, например, такие типы, как Integer, Interval и Enum. Это может быть достигнуто путем перехвата отражения столбца с помощью события DDLEvents.column_reflect() в сочетании с методом TypeEngine.as_generic().

Дана таблица в 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)
)

Приведенная выше таблица включает целочисленные типы MEDIUMINT и TINYINT, предназначенные только для MySQL, а также VARCHAR, который включает опцию CHARACTER SET, предназначенную только для MySQL. Если мы отразим эту таблицу нормально, то получится объект Table, который будет содержать эти специфические для MySQL типы данных и опции:

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

Приведенный выше пример отражает схему таблицы в новом объекте Table. Затем, для демонстрации, мы можем вывести специфичный для MySQL оператор «CREATE TABLE», используя конструкцию CreateTable:

>>> 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, который мы могли бы перенести в другой поставщик баз данных, заменив специальные типы данных sqlalchemy.dialects.mysql.MEDIUMINT и sqlalchemy.dialects.mysql.TINYINT на Integer, мы можем вместо этого «обобщить» типы данных в этой таблице или изменить их любым другим способом, создав обработчик с помощью события 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 для этих типов данных. Теперь мы можем выдать оператор «CREATE TABLE», например, для базы данных PostgreSQL:

>>> pg_engine = create_engine("postgresql://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, используя только ту информацию, которая представлена в реляционной базе данных. Этот процесс по определению не может восстановить аспекты схемы, которые на самом деле не хранятся в базе данных. Состояние, которое недоступно при отражении, включает, но не ограничивается этим:

  • Умолчания на стороне клиента, либо функции Python, либо выражения SQL, определенные с помощью ключевого слова default в Column (обратите внимание, что это отдельно от server_default, что конкретно доступно через отражение).

  • Информация о столбцах, например, данные, которые могли быть помещены в словарь Column.info

  • Значение настройки .quote для Column или Table

  • Связь конкретного Sequence с конкретным Column

Реляционная база данных также во многих случаях сообщает метаданные таблицы в формате, отличном от того, который был указан в SQLAlchemy. Нельзя всегда полагаться на то, что объекты Table, возвращаемые в результате рефлексии, будут создавать идентичный DDL, как и исходные объекты Table, определяемые Python. К таким областям относятся серверные значения по умолчанию, последовательности, связанные с колонками, и различные идиосинкразии в отношении ограничений и типов данных. Умолчания на стороне сервера могут быть возвращены с помощью директив приведения (обычно PostgreSQL включает приведение ::<type>) или шаблонов цитирования, отличных от первоначально заданных.

Другая категория ограничений включает структуры схем, для которых отражение определено лишь частично или еще не определено. Недавние усовершенствования отражения позволяют отражать такие вещи, как представления, индексы и опции внешних ключей. На данный момент такие структуры, как ограничения CHECK, комментарии к таблицам и триггеры, не отражаются.

Вернуться на верх