Работа с метаданными базы данных

Когда движки и выполнение SQL завершены, мы готовы приступить к алхимии. Центральным элементом как SQLAlchemy Core, так и ORM является язык SQL Expression Language, который позволяет бегло и композиционно строить SQL-запросы. Основой для этих запросов являются объекты Python, которые представляют концепции баз данных, такие как таблицы и столбцы. Эти объекты известны под общим названием database metadata.

Наиболее распространенные фундаментальные объекты для метаданных базы данных в SQLAlchemy известны как MetaData, Table и Column. В следующих разделах будет показано, как эти объекты используются как в Core-ориентированном стиле, так и в ORM-ориентированном стиле.

**Читатели **ORM, оставайтесь с нами!

Как и в других разделах, пользователи Core могут пропустить разделы ORM, но пользователям ORM лучше ознакомиться с этими объектами с обеих точек зрения.

Настройка метаданных с помощью объектов таблицы

Когда мы работаем с реляционной базой данных, основная структура, которую мы создаем и к которой делаем запросы, называется таблицей. В SQLAlchemy «таблица» представлена объектом Python с аналогичным названием Table.

Чтобы начать использовать язык выражений SQLAlchemy, нам необходимо создать объекты Table, которые представляют все таблицы базы данных, с которыми мы хотим работать. Каждый Table может быть декларированным, то есть мы явно прописываем в исходном коде, как выглядит таблица, или может быть отраженным, то есть мы генерируем объект на основе того, что уже присутствует в конкретной базе данных. Эти два подхода также могут быть смешаны различными способами.

Независимо от того, будем ли мы объявлять или отражать наши таблицы, мы начинаем с коллекции, в которой будут размещаться наши таблицы, известной как объект MetaData. Этот объект по сути является facade вокруг словаря Python, который хранит серию Table объектов, ключ к которым задан их строковым именем. Конструирование этого объекта выглядит следующим образом:

>>> from sqlalchemy import MetaData
>>> metadata_obj = MetaData()

Наличие одного объекта MetaData для всего приложения является наиболее распространенным случаем, он представлен как переменная на уровне модуля в одном месте приложения, часто в пакете типа «models» или «dbschema». Коллекций MetaData также может быть несколько, однако обычно наиболее полезно, если ряд Table объектов, связанных друг с другом, принадлежат одной коллекции MetaData.

Как только у нас есть объект MetaData, мы можем объявить несколько объектов Table. Этот учебник начнется с классической модели учебника SQLAlchemy - таблицы user, которая, например, представляет пользователей веб-сайта, и таблицы address, представляющей список адресов электронной почты, связанных со строками в таблице user. Обычно мы присваиваем каждому объекту Table переменную, которая будет тем, как мы будем ссылаться на таблицу в коде приложения:

>>> from sqlalchemy import Table, Column, Integer, String
>>> user_table = Table(
...     "user_account",
...     metadata_obj,
...     Column("id", Integer, primary_key=True),
...     Column("name", String(30)),
...     Column("fullname", String),
... )

Мы можем заметить, что приведенная выше конструкция Table очень похожа на оператор SQL CREATE TABLE; начинается с имени таблицы, затем перечисляются все столбцы, где каждый столбец имеет имя и тип данных. Объектами, которые мы используем выше, являются:

  • Table - представляет таблицу базы данных и присваивается коллекции MetaData.

  • Column - представляет столбец в таблице базы данных и присваивается объекту Table. Объект Column обычно включает строковое имя и объект типа. Доступ к коллекции объектов Column в терминах родительского Table обычно осуществляется через ассоциативный массив, расположенный по адресу Table.c:

    >>> user_table.c.name
    Column('name', String(length=30), table=<user_account>)
    
    >>> user_table.c.keys()
    ['id', 'name', 'fullname']
  • Integer, String - эти классы представляют типы данных SQL и могут быть переданы в Column с обязательным инстанцированием или без него. Выше мы хотели задать длину «30» для столбца «name», поэтому мы инстанцировали String(30). Но для «id» и «fullname» мы их не указывали, поэтому можем передать сам класс.

См.также

Справочная и API документация для MetaData, Table и Column находится по адресу Описание баз данных с помощью метаданных. Справочная документация по типам данных находится по адресу Объекты типов данных SQL.

В одном из следующих разделов мы проиллюстрируем одну из фундаментальных функций Table, которая заключается в генерации DDL на определенном соединении с базой данных. Но сначала мы объявим второй Table.

Объявление простых ограничений

Первый Column в приведенном выше user_table включает параметр Column.primary_key, который является сокращенным способом указания того, что этот Column должен быть частью первичного ключа для этой таблицы. Сам первичный ключ обычно объявляется неявно и представлен конструкцией PrimaryKeyConstraint, которую мы можем видеть на атрибуте Table.primary_key объекта Table:

>>> user_table.primary_key
PrimaryKeyConstraint(Column('id', Integer(), table=<user_account>, primary_key=True, nullable=False))

Ограничение, которое чаще всего объявляется явно, - это объект ForeignKeyConstraint, который соответствует базе данных foreign key constraint. Когда мы объявляем таблицы, которые связаны друг с другом, SQLAlchemy использует наличие этих объявлений ограничений внешнего ключа не только для того, чтобы они передавались в операциях CREATE базе данных, но и для помощи в построении SQL-выражений.

Ограничение ForeignKeyConstraint, которое включает только один столбец в целевой таблице, обычно объявляется с помощью сокращенной нотации на уровне столбцов через объект ForeignKey. Ниже мы объявляем вторую таблицу address, которая будет иметь ограничение внешнего ключа, ссылающееся на таблицу user:

>>> from sqlalchemy import ForeignKey
>>> address_table = Table(
...     "address",
...     metadata_obj,
...     Column("id", Integer, primary_key=True),
...     Column("user_id", ForeignKey("user_account.id"), nullable=False),
...     Column("email_address", String, nullable=False),
... )

В приведенной выше таблице также присутствует третий вид ограничений, который в SQL является ограничением «NOT NULL», указанным выше с помощью параметра Column.nullable.

Совет

При использовании объекта ForeignKey внутри определения Column можно не указывать тип данных для этого Column; он автоматически выводится из типа данных связанного столбца, в приведенном выше примере Integer тип данных столбца user_account.id.

В следующем разделе мы выпустим завершенный DDL для таблиц user и address, чтобы увидеть готовый результат.

Передача DDL в базу данных

Мы построили довольно сложную иерархию объектов для представления двух таблиц базы данных, начиная с корневого объекта MetaData, затем двух объектов Table, каждый из которых хранит коллекцию объектов Column и Constraint. Эта объектная структура будет в центре большинства операций, которые мы будем выполнять как с Core, так и с ORM в дальнейшем.

Первая полезная вещь, которую мы можем сделать с этой структурой, - это передать операторы CREATE TABLE, или DDL, в нашу базу данных SQLite, чтобы мы могли вставлять и запрашивать данные из них. У нас уже есть все необходимые для этого инструменты: мы вызываем метод MetaData.create_all() на нашем MetaData, посылая ему Engine, который ссылается на целевую базу данных:

>>> metadata_obj.create_all(engine)
BEGIN (implicit) PRAGMA main.table_...info("user_account") ... PRAGMA main.table_...info("address") ... CREATE TABLE user_account ( id INTEGER NOT NULL, name VARCHAR(30), fullname VARCHAR, PRIMARY KEY (id) ) ... CREATE TABLE address ( id INTEGER NOT NULL, user_id INTEGER NOT NULL, email_address VARCHAR NOT NULL, PRIMARY KEY (id), FOREIGN KEY(user_id) REFERENCES user_account (id) ) ... COMMIT

Процесс создания DDL по умолчанию включает некоторые специфические для SQLite операторы PRAGMA, которые проверяют существование каждой таблицы перед созданием CREATE. Вся серия шагов также включается в пару BEGIN/COMMIT, чтобы обеспечить возможность транзакционного DDL (SQLite действительно поддерживает транзакционный DDL, однако драйвер базы данных sqlite3 исторически запускает DDL в режиме «autocommit»).

Процесс create также заботится о создании операторов CREATE в правильном порядке; выше, ограничение FOREIGN KEY зависит от существования таблицы user, поэтому таблица address создается второй. В более сложных сценариях зависимости ограничения FOREIGN KEY также могут быть применены к таблицам постфактум с помощью ALTER.

Объект MetaData также имеет метод MetaData.drop_all(), который будет испускать утверждения DROP в обратном порядке, чем испускал бы CREATE для удаления элементов схемы.

Определение метаданных таблицы с помощью ORM

В этом разделе, посвященном только ORM, будет приведен пример объявления той же структуры базы данных, которая была показана в предыдущем разделе, с использованием парадигмы конфигурации, более ориентированной на ORM. При использовании ORM процесс объявления метаданных Table обычно сочетается с процессом объявления классов mapped. Сопоставленный класс - это любой класс Python, который мы хотим создать, и который затем будет иметь атрибуты, связанные с колонками в таблице базы данных. Хотя существует несколько разновидностей того, как это достигается, наиболее распространенный стиль известен как declarative и позволяет нам объявлять пользовательские классы и метаданные Table одновременно.

Настройка реестра

При использовании ORM коллекция MetaData остается в наличии, однако сама она содержится в объекте, предназначенном только для ORM, известном как registry. Мы создаем registry, конструируя его:

>>> from sqlalchemy.orm import registry
>>> mapper_registry = registry()

Приведенный выше registry при построении автоматически включает объект MetaData, который будет хранить коллекцию объектов Table:

>>> mapper_registry.metadata
MetaData()

Вместо того чтобы объявлять объекты Table напрямую, мы будем объявлять их косвенно через директивы, применяемые к нашим сопоставленным классам. В наиболее распространенном подходе каждый отображаемый класс происходит от общего базового класса, известного как декларативная база. Мы получаем новую декларативную базу из registry с помощью метода registry.generate_base():

>>> Base = mapper_registry.generate_base()

Совет

Шаги создания классов registry и «декларативной базы» можно объединить в один шаг с помощью исторически знакомой функции declarative_base():

from sqlalchemy.orm import declarative_base

Base = declarative_base()

Объявление сопоставленных классов

Приведенный выше объект Base является классом Python, который будет служить базовым классом для объявленных нами классов ORM mapped. Теперь мы можем определить ORM mapped классы для таблицы user и address в терминах новых классов User и Address:

>>> from sqlalchemy.orm import relationship
>>> class User(Base):
...     __tablename__ = "user_account"
...
...     id = Column(Integer, primary_key=True)
...     name = Column(String(30))
...     fullname = Column(String)
...
...     addresses = relationship("Address", back_populates="user")
...
...     def __repr__(self):
...         return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

>>> class Address(Base):
...     __tablename__ = "address"
...
...     id = Column(Integer, primary_key=True)
...     email_address = Column(String, nullable=False)
...     user_id = Column(Integer, ForeignKey("user_account.id"))
...
...     user = relationship("User", back_populates="addresses")
...
...     def __repr__(self):
...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"

Два вышеупомянутых класса теперь являются нашими отображенными классами и доступны для использования в операциях сохранения и запросов ORM, которые будут описаны позже. Но они также включают объекты Table, которые были созданы в процессе декларативного отображения и эквивалентны тем, которые мы объявили непосредственно в предыдущем разделе Core. Мы можем увидеть эти Table объекты из декларативно отображенного класса с помощью .__table__ атрибута:

>>> User.__table__
Table('user_account', MetaData(),
    Column('id', Integer(), table=<user_account>, primary_key=True, nullable=False),
    Column('name', String(length=30), table=<user_account>),
    Column('fullname', String(), table=<user_account>), schema=None)

Этот объект Table был создан в процессе декларативного процесса на основе атрибута .__tablename__, определенного для каждого из наших классов, а также с помощью объектов Column, назначенных атрибутам уровня класса внутри классов. Эти объекты Column обычно могут быть объявлены без явного поля «name» в конструкторе, так как процесс декларативного описания присваивает им имя автоматически на основе имени атрибута, который был использован.

См.также

Декларативное отображение - обзор декларативного отображения классов

Другие сведения о сопоставленных классах

Для краткого объяснения вышеперечисленных классов обратите внимание на следующие атрибуты:

  • классы имеют автоматически генерируемый метод __init__() - оба класса по умолчанию получают метод __init__(), который позволяет параметризованное построение объектов. Мы можем свободно предоставить и свой собственный метод __init__(). Метод __init__() позволяет нам создавать экземпляры User и Address, передавая имена атрибутов, большинство из которых выше связаны непосредственно с объектами Column, в качестве имен параметров:

    >>> sandy = User(name="sandy", fullname="Sandy Cheeks")

    Более подробно этот метод описан в Конструктор по умолчанию.

  • мы предоставили метод __repr__() - он совершенно необязателен, и предназначен исключительно для того, чтобы наши пользовательские классы имели описательное строковое представление, и не является обязательным:

    >>> sandy
    User(id=None, name='sandy', fullname='Sandy Cheeks')

    Интересно отметить, что атрибут id автоматически возвращает None при обращении к нему, а не поднимает AttributeError, как это обычно происходит в Python для отсутствующих атрибутов.

  • мы также включили двунаправленные отношения - это еще одна совершенно необязательная конструкция, где мы использовали конструкцию ORM под названием relationship() для обоих классов, которая указывает ORM, что эти классы User и Address ссылаются друг на друга в отношениях one to many / many to one. Использование relationship() выше для того, чтобы мы могли продемонстрировать его поведение позже в этом учебнике; оно не требуется для того, чтобы определить структуру Table.

Передача DDL в базу данных

Этот раздел называется так же, как и раздел Передача DDL в базу данных, рассмотренный в разделе Core. Это связано с тем, что эмиссия DDL с нашими сопоставленными классами ORM ничем не отличается. Если мы захотим создать DDL для объектов Table, которые мы создали как часть наших декларативно отображенных классов, мы можем использовать MetaData.create_all(), как и раньше.

В нашем случае мы уже создали таблицы user и address в нашей базе данных SQLite. Если бы мы этого еще не сделали, мы могли бы воспользоваться MetaData, связанным с нашим registry и декларативным базовым классом ORM, чтобы сделать это, используя MetaData.create_all():

# emit CREATE statements given ORM registry
mapper_registry.metadata.create_all(engine)

# the identical MetaData object is also present on the
# declarative base
Base.metadata.create_all(engine)

Объединение деклараций таблиц ядра с декларативностью ORM

В качестве альтернативного подхода к процессу отображения, показанному ранее в Объявление сопоставленных классов, мы также можем использовать объекты Table, которые мы создали непосредственно в разделе Настройка метаданных с помощью объектов таблицы в сочетании с декларативными отображенными классами из сгенерированного базового класса declarative_base().

Эта форма называется hybrid table, и она заключается в присвоении атрибута .__table__ непосредственно, а не в том, что декларативный процесс генерирует его:

mapper_registry = registry()
Base = mapper_registry.generate_base()


class User(Base):
    __table__ = user_table

    addresses = relationship("Address", back_populates="user")

    def __repr__(self):
        return f"User({self.name!r}, {self.fullname!r})"


class Address(Base):
    __table__ = address_table

    user = relationship("User", back_populates="addresses")

    def __repr__(self):
        return f"Address({self.email_address!r})"

Примечание

Приведенный выше пример является альтернативной формой отображения, впервые показанного ранее в Объявление сопоставленных классов. Этот пример приведен исключительно в иллюстративных целях, он не является частью шагов «doctest» данного руководства, и поэтому его не нужно запускать для читателей, выполняющих примеры кода. Приведенное здесь отображение и отображение в Объявление сопоставленных классов дают эквивалентные отображения, но в общем случае для конкретного отображаемого класса можно использовать только одну из этих двух форм.

Эти два класса эквивалентны тем, которые мы объявили в предыдущем примере отображения.

Традиционный подход «декларативной базы» с использованием __tablename__ для автоматической генерации объектов Table остается самым популярным методом объявления метаданных таблицы. Однако, если не принимать во внимание функциональность отображения ORM, которую он обеспечивает, с точки зрения объявления таблиц это просто синтаксическое удобство поверх конструктора Table.

В дальнейшем мы будем ссылаться на наши классы, сопоставленные с ORM, когда будем говорить о манипулировании данными в терминах ORM, в разделе Вставка строк с помощью ORM.

Отражение в таблице

В завершение раздела о работе с метаданными таблиц мы проиллюстрируем еще одну операцию, которая упоминалась в начале раздела, - отражение таблицы. Отражение таблицы относится к процессу создания Table и связанных с ним объектов путем чтения текущего состояния базы данных. Если в предыдущих разделах мы объявляли объекты Table в Python, а затем отправляли DDL в базу данных, то в процессе отражения все происходит наоборот.

В качестве примера отражения мы создадим новый объект Table, представляющий объект some_table, который мы создали вручную в предыдущих разделах этого документа. Существует несколько вариантов того, как это делается, однако наиболее простой заключается в создании объекта Table, заданного именем таблицы и коллекцией MetaData, к которой он будет принадлежать, затем вместо указания отдельных объектов Column и Constraint передайте ему целевой объект Engine с помощью параметра Table.autoload_with:

>>> some_table = Table("some_table", metadata_obj, autoload_with=engine)
BEGIN (implicit) PRAGMA main.table_...info("some_table") [raw sql] () SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = ? AND type = 'table' [raw sql] ('some_table',) PRAGMA main.foreign_key_list("some_table") ... PRAGMA main.index_list("some_table") ... ROLLBACK

В конце процесса объект some_table теперь содержит информацию об объектах Column, присутствующих в таблице, и этот объект можно использовать точно так же, как и объект Table, который мы объявили явно:

>>> some_table
Table('some_table', MetaData(),
    Column('x', INTEGER(), table=<some_table>),
    Column('y', INTEGER(), table=<some_table>),
    schema=None)

См.также

Подробнее об отражении таблиц и схем читайте в Отражение объектов базы данных.

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

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