Быстрый старт ORM

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

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

Изменено в версии 2.0: ORM Quickstart обновлен для новейших возможностей PEP 484-aware с использованием новых конструкций, включая mapped_column(). Информацию о миграции см. в разделе Декларативные модели ORM.

Объявить модели

Здесь мы определяем конструкции на уровне модуля, которые формируют структуры, которые мы будем запрашивать из базы данных. Эта структура, известная как Declarative Mapping, определяет одновременно и объектную модель Python, и database metadata, которая описывает реальные таблицы SQL, которые существуют или будут существовать в конкретной базе данных:

>>> from typing import List
>>> from typing import Optional
>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import String
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship

>>> class Base(DeclarativeBase):
...     pass

>>> class User(Base):
...     __tablename__ = "user_account"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     name: Mapped[str] = mapped_column(String(30))
...     fullname: Mapped[Optional[str]]
...
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", cascade="all, delete-orphan"
...     )
...
...     def __repr__(self) -> str:
...         return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

>>> class Address(Base):
...     __tablename__ = "address"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     email_address: Mapped[str]
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...
...     user: Mapped["User"] = relationship(back_populates="addresses")
...
...     def __repr__(self) -> str:
...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"

Отображение начинается с базового класса, который выше называется Base, и создается путем создания простого подкласса на основе класса DeclarativeBase.

Затем создаются отдельные сопоставленные классы путем создания подклассов Base. Сопоставленный класс обычно ссылается на одну конкретную таблицу базы данных, имя которой указывается с помощью атрибута уровня класса __tablename__.

Далее объявляются столбцы, которые являются частью таблицы, путем добавления атрибутов, включающих специальную аннотацию типизации Mapped. Имя каждого атрибута соответствует столбцу, который должен быть частью таблицы базы данных. Тип данных каждого столбца берется сначала из типа данных Python, который связан с каждой аннотацией Mapped; int для INTEGER, str для VARCHAR и т.д. Недействительность зависит от того, используется или нет модификатор типа Optional[]. Более конкретная информация о типе может быть указана с помощью объектов типа SQLAlchemy в правой части директивы mapped_column(), например, тип данных String, использованный выше в колонке User.name. Связь между типами Python и типами SQL может быть настроена с помощью директивы type annotation map.

Директива mapped_column() используется для всех атрибутов на основе столбцов, которые требуют более конкретной настройки. Помимо информации о типе, эта директива принимает широкий спектр аргументов, которые указывают конкретные детали о столбце базы данных, включая значения по умолчанию сервера и информацию об ограничениях, таких как принадлежность к первичному ключу и внешним ключам. Директива mapped_column() принимает супермножество аргументов, которые принимаются классом SQLAlchemy Column, который используется SQLAlchemy Core для представления столбцов базы данных.

Все сопоставленные классы ORM требуют объявления хотя бы одного столбца как части первичного ключа, обычно с помощью параметра Column.primary_key на тех объектах mapped_column(), которые должны быть частью ключа. В приведенном выше примере столбцы User.id и Address.id обозначены как первичный ключ.

Вместе взятые, комбинация строкового имени таблицы и списка объявлений столбцов называется в SQLAlchemy table metadata. Настройка метаданных таблицы с использованием как Core, так и ORM подходов представлена в Унифицированный учебник по SQLAlchemy в Работа с метаданными базы данных. Приведенное выше отображение является примером того, что называется конфигурацией Annotated Declarative Table.

Существуют и другие варианты Mapped, чаще всего это конструкция relationship(), указанная выше. В отличие от атрибутов на основе столбцов, relationship() обозначает связь между двумя классами ORM. В приведенном выше примере User.addresses связывает User с Address, а Address.user связывает Address с User. Конструкция relationship() вводится в Унифицированный учебник по SQLAlchemy в Работа с объектами, связанными с ORM.

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

Создать двигатель

Engine - это фабрика, которая может создавать для нас новые соединения с базой данных, а также удерживать соединения внутри Connection Pool для быстрого повторного использования. Для целей обучения мы обычно используем базу данных SQLite только с памятью для удобства:

>>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://", echo=True)

Совет

Параметр echo=True указывает, что SQL, выдаваемый соединениями, будет записываться в стандартный журнал.

Полное введение в Engine начинается в Установление связи - двигатель.

Emit CREATE TABLE DDL

Используя метаданные нашей таблицы и наш движок, мы можем сгенерировать нашу схему сразу в нашей целевой базе данных SQLite, используя метод под названием MetaData.create_all():

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

Из того кусочка кода на Python, который мы написали, произошло очень многое. Для полного обзора того, что происходит с метаданными таблицы, перейдите в учебник по ссылке Работа с метаданными базы данных.

Создание объектов и постоянство

Теперь мы готовы к вставке данных в базу данных. Для этого мы создаем экземпляры классов User и Address, у которых уже есть метод __init__(), автоматически созданный в процессе декларативного отображения. Затем мы передаем их в базу данных с помощью объекта Session, который использует метод Engine для взаимодействия с базой данных. Метод Session.add_all() используется здесь для добавления нескольких объектов одновременно, а метод Session.commit() будет использоваться для flush любых ожидающих изменений в базе данных и затем commit текущей транзакции базы данных, которая всегда находится в процессе, когда используется Session:

>>> from sqlalchemy.orm import Session

>>> with Session(engine) as session:
...     spongebob = User(
...         name="spongebob",
...         fullname="Spongebob Squarepants",
...         addresses=[Address(email_address="spongebob@sqlalchemy.org")],
...     )
...     sandy = User(
...         name="sandy",
...         fullname="Sandy Cheeks",
...         addresses=[
...             Address(email_address="sandy@sqlalchemy.org"),
...             Address(email_address="sandy@squirrelpower.org"),
...         ],
...     )
...     patrick = User(name="patrick", fullname="Patrick Star")
...
...     session.add_all([spongebob, sandy, patrick])
...
...     session.commit()
{execsql}BEGIN (implicit)
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[...] ('spongebob', 'Spongebob Squarepants')
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[...] ('sandy', 'Sandy Cheeks')
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[...] ('patrick', 'Patrick Star')
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[...] ('spongebob@sqlalchemy.org', 1)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[...] ('sandy@sqlalchemy.org', 2)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[...] ('sandy@squirrelpower.org', 2)
COMMIT

Совет

Рекомендуется использовать Session в стиле контекстного менеджера, как описано выше, то есть с помощью оператора Python with:. Объект Session представляет активные ресурсы базы данных, поэтому полезно убедиться, что он закрывается после завершения серии операций. В следующем разделе мы будем держать объект Session открытым только для наглядности.

Основы создания Session приведены в Выполнение с помощью сеанса ORM, а более подробная информация - в Основы использования сеанса.

Затем в Вставка строк с использованием шаблона ORM Unit of Work представлены некоторые разновидности базовых операций персистентности.

Простой ВЫБОР

Имея несколько строк в базе данных, вот простейшая форма создания оператора SELECT для загрузки некоторых объектов. Для создания операторов SELECT мы используем функцию select() для создания нового объекта Select, который затем вызываем с помощью Session. Метод, который часто бывает полезен при запросе объектов ORM, - это метод Session.scalars(), который возвращает объект ScalarResult, перебирающий выбранные нами объекты ORM:

>>> from sqlalchemy import select

>>> session = Session(engine)

>>> stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))

>>> for user in session.scalars(stmt):
...     print(user)
{execsql}BEGIN (implicit)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name IN (?, ?)
[...] ('spongebob', 'sandy'){stop}
User(id=1, name='spongebob', fullname='Spongebob Squarepants')
User(id=2, name='sandy', fullname='Sandy Cheeks')

В приведенном выше запросе также использовался метод Select.where() для добавления критериев WHERE, а также метод ColumnOperators.in_(), который является частью всех конструкций SQLAlchemy, подобных столбцам, для использования оператора SQL IN.

Более подробно о том, как выбирать объекты и отдельные столбцы, рассказано в Выбор сущностей и столбцов ORM.

SELECT с JOIN

Очень часто приходится делать запросы сразу к нескольким таблицам, и в SQL ключевое слово JOIN является основным способом, с помощью которого это происходит. Конструкция Select создает соединения, используя метод Select.join():

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "sandy")
...     .where(Address.email_address == "sandy@sqlalchemy.org")
... )
>>> sandy_address = session.scalars(stmt).one()
{execsql}SELECT address.id, address.email_address, address.user_id
FROM address JOIN user_account ON user_account.id = address.user_id
WHERE user_account.name = ? AND address.email_address = ?
[...] ('sandy', 'sandy@sqlalchemy.org')
{stop}
>>> sandy_address
Address(id=2, email_address='sandy@sqlalchemy.org')

Приведенный выше запрос иллюстрирует несколько критериев WHERE, которые автоматически связываются вместе с помощью AND, а также то, как использовать столбцеподобные объекты SQLAlchemy для создания сравнений «равенства», для чего используется переопределенный метод Python ColumnOperators.__eq__() для создания объекта SQL-критерия.

Некоторые дополнительные сведения о вышеупомянутых концепциях приведены в Предложение WHERE и Явные предложения FROM и JOIN.

Внести изменения

Объект Session в сочетании с нашими ORM-сопоставленными классами User и Address автоматически отслеживает изменения в объектах по мере их внесения, что приводит к SQL-запросам, которые будут выданы при следующей очистке Session. Ниже мы изменим один адрес электронной почты, связанный с «sandy», а также добавим новый адрес электронной почты к «patrick», после того как выполним SELECT для получения строки для «patrick»:

>>> stmt = select(User).where(User.name == "patrick")
>>> patrick = session.scalars(stmt).one()
{execsql}SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)
{stop}

>>> patrick.addresses.append(Address(email_address="patrickstar@sqlalchemy.org"))
{execsql}SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (3,){stop}

>>> sandy_address.email_address = "sandy_cheeks@sqlalchemy.org"

>>> session.commit()
{execsql}UPDATE address SET email_address=? WHERE address.id = ?
[...] ('sandy_cheeks@sqlalchemy.org', 2)
INSERT INTO address (email_address, user_id) VALUES (?, ?)
[...] ('patrickstar@sqlalchemy.org', 3)
COMMIT
{stop}

Обратите внимание, когда мы обратились к patrick.addresses, был выдан SELECT. Это называется lazy load. В Стратегии работы с погрузчиками приведена информация о различных способах доступа к связанным элементам с использованием более или менее SQL.

Подробное описание манипулирования данными в ORM начинается с Манипулирование данными с помощью ORM.

Некоторые удаления

Все должно заканчиваться, как и в случае с некоторыми строками нашей базы данных - вот краткая демонстрация двух различных форм удаления, обе из которых важны в зависимости от конкретного случая использования.

Сначала мы удалим один из объектов Address у пользователя «sandy». Когда Session в следующий раз будет промыт, это приведет к удалению строки. Такое поведение мы настроили в нашей связке под названием delete cascade. Мы можем получить обращение к объекту sandy по первичному ключу с помощью Session.get(), а затем работать с объектом:

>>> sandy = session.get(User, 2)
{execsql}BEGIN (implicit)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,){stop}

>>> sandy.addresses.remove(sandy_address)
{execsql}SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (2,)

Последний SELECT выше был операцией lazy load, выполняемой для того, чтобы коллекция sandy.addresses могла быть загружена, чтобы мы могли удалить член sandy_address. Есть и другие способы выполнить эту серию операций, которые не будут содержать так много SQL.

Мы можем выдать SQL DELETE для того, что было изменено на данный момент, без фиксации транзакции, используя метод Session.flush():

>>> session.flush()
{execsql}DELETE FROM address WHERE address.id = ?
[...] (2,)

Далее мы полностью удалим пользователя «patrick». Для удаления верхнего уровня объекта самого по себе мы используем метод Session.delete(); этот метод фактически не выполняет удаление, но устанавливает объект для удаления при следующем удалении. Операция также будет cascade на связанные объекты на основе настроенных нами опций каскада, в данном случае на связанные объекты Address:

>>> session.delete(patrick)
{execsql}SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (3,)
SELECT address.id AS address_id, address.email_address AS address_email_address, address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (3,)

Метод Session.delete() в данном конкретном случае выдал два оператора SELECT, хотя не выдал DELETE, что может показаться удивительным. Это потому, что когда метод перешел к осмотру объекта, оказалось, что объект patrick был expired, что произошло, когда мы в последний раз вызывали Session.commit(), и SQL, который был испущен, был для повторной загрузки строк из новой транзакции. Это истечение является необязательным, и при обычном использовании мы часто будем отключать его в ситуациях, где оно не очень применимо.

Чтобы проиллюстрировать удаляемые строки, вот фиксация:

>>> session.commit()
{execsql}DELETE FROM address WHERE address.id = ?
[...] (4,)
DELETE FROM user_account WHERE user_account.id = ?
[...] (3,)
COMMIT
{stop}

В учебнике удаление ORM рассматривается в Удаление объектов ORM с помощью шаблона Unit of Work. Общие сведения об истечении срока действия объектов приведены в Истечение срока действия / Обновление; каскады подробно обсуждаются в Каскады.

Глубоко изучите вышеупомянутые концепции

Для нового пользователя вышеприведенные разделы, скорее всего, были вихревым туром. В каждом из вышеперечисленных шагов есть много важных понятий, которые не были освещены. После краткого обзора того, как все выглядит, рекомендуется проработать Унифицированный учебник по SQLAlchemy, чтобы получить твердые рабочие знания о том, что на самом деле происходит выше. Удачи!

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