При использовании SQLAlchemy ORM взаимодействие с базой данных происходит через объект Session. Он также захватывает соединение с базой данных и транзакции. Транзакция неявно стартует как только Session начинает общаться с базой данных и остается открытой до тех пор, пока Session не коммитится, откатывается или закрывается.
Для создания объекта session можно использовать класс Session из sqlalchemy.orm.
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
engine = create_engine("postgresql+psycopg2://postgres:1111@localhost/sqlalchemy_tuts")
session = Session(bind=engine)
Создавать объект Session нужно будет каждый раз при взаимодействии с базой.
Конструктор Session принимает определенное количество аргументов, которые определяют режим его работы. Если создать сессию таким способом, то в дальнейшем конструктор Session нужно будет вызывать с одним и тем же набором параметров.
Чтобы упростить этот процесс, SQLAlchemy предоставляет класс sessionmaker, который создает класс Session с аргументами для конструктора по умолчанию.
Два объекта созданы. Получить доступ к их атрибутам можно с помощью оператора точки (.).
Дальше в сессию добавляются объекты.
session.add(c1)
session.add(c2)
Но добавление объектов не влияет на запись в базу, а лишь готовит объекты к сохранению в следующем коммите. Проверить это можно, получив первичные ключи объектов.
Значение атрибута idобоих объектов — None. Это значит, что они еще не сохранены в базе данных.
Вместо добавления одного объекта за раз можно использовать метод add_all(). Он принимает список объектов, которые будут добавлены в сессию.
session.add_all([c1, c2])
Добавление объекта в сессию несколько раз не приводит к ошибкам. В любой момент на имеющиеся объекты можно посмотреть с помощью session.new.
IdentitySet([<__main__.Customer object at 0x000001BD25928C40>, <__main__.Customer object at 0x000001BD25928C70>])
Наконец, для сохранения данных используется метод commit():
session.commit()
После сохранения транзакции ресурсы соединения, на которые ссылается объект Session, возвращаются в пул соединений. Последующие операции будут выполняться в новой транзакции.
Сейчас таблица Customer выглядит вот так:
Пока что покупатели ничего не приобрели. Поэтому c1.ordersи c2.orders вернут пустой список.
В данном случае в сессию добавляются только объекты Order(o1и o2). Order и OrderLine связаны отношением один-ко-многим. Добавление объекта Order в сессию неявно добавляет также и объекты OrderLine. Но даже если добавить последние вручную, ошибки не будет.
Вместо передачи объекта Order при создании экземпляра OrderLine можно сделать следующее:
Все это возможно благодаря отношениям relationship() моделей.
Получение данных
Чтобы сделать запрос в базу данных используется метод query() объекта session. Он возвращает объект типа sqlalchemy.orm.query.Query, который называется просто Query. Он представляет собой инструкцию SELECT, которая будет использована для запроса в базу данных. В следующей таблице перечислены распространенные методы класса Query.
Метод
Описание
all()
Возвращает результат запроса (объект Query) в виде списка
count()
Возвращает общее количество записей в запросе
first()
Возвращает первый результат из запроса или None, если записей нет
scalar()
Возвращает первую колонку первой записи или None, если результат пустой. Если записей несколько, то бросает исключение MultipleResultsFound
one
Возвращает одну запись. Если их несколько, бросает исключение MutlipleResultsFound. Если данных нет, бросает NoResultFound
get(pk)
Возвращает объект по первичному ключу (pk) или None, если объект не был найден
filter(*criterion)
Возвращает экземпляр Query после применения оператора WHERE
limit(limit)
Возвращает экземпляр Query после применения оператора LIMIT
offset(offset)
Возвращает экземпляр Query после применения оператора OFFSET
order_by(*criterion)
Возвращает экземпляр Query после применения оператора ORDER BY
join(*props, **kwargs)
Возвращает экземпляр Query после создания SQL INNER JOIN
outerjoin(*props, **kwargs)
Возвращает экземпляр Query после создания SQL LEFT OUTER JOIN
group_by(*criterion)
Возвращает экземпляр Query после добавления оператора GROUP BY к запросу
having(criterion)
Возвращает экземпляр Query после добавления оператора HAVING
Метод all()
В базовой форме метод query() принимает в качестве аргументов один или несколько классов модели или колонок. Следующий код вернет все записи из таблицы customers.
Так же можно получить записи из таблиц items и orders.
Чтобы получить сырой SQL, который используется для выполнения запроса в базу данных, примените sqlalchemy.orm.query.Query следующим образом: print(session.query(Customer)).
SELECT
customers. ID AS customers_id,
customers.first_name AS customers_first_name,
customers.last_name AS customers_last_name,
customers.username AS customers_username,
customers.email AS customers_email,
customers.address AS customers_address,
customers.town AS customers_town,
customers.created_on AS customers_created_on,
customers.updated_on AS customers_updated_on
FROM
customers
Вызов метода all() на большом объекте результата не очень эффективен. Вместо этого стоит использовать цикл for для перебора по объекту Query:
SELECT
customers.id AS customers_id,
customers.first_name AS customers_first_name,
customers.last_name AS customers_last_name,
customers.username AS customers_username,
customers.email AS customers_email,
customers.address AS customers_address,
customers.town AS customers_town,
customers.created_on AS customers_created_on,
customers.updated_on AS customers_updated_on
FROM
customers
WHERE
customers.first_name = %(first_name_1)s
Строка %(first_name_1)s в операторе WHERE — это заполнитель, который будет заменен на реальное значение (Vadim) при выполнении запроса.
Можно передать несколько фильтров в метод filter() и они будут объединены с помощью оператора AND. Например:
# все клиенты с именем Vadim и Tatyana
session.query(Customer).filter(or_(
Customer.first_name == 'Vadim',
Customer.first_name == 'Tatyana'
)).all()
# найти всех с именем и Pavel фамилией НЕ Yatsenko
session.query(Customer).filter(and_(
Customer.first_name == 'Pavel',
not_(
Customer.last_name == 'Yatsenko',
)
)).all()
Следующий перечень демонстрирует, как использовать распространенные операторы сравнения с методом filter().
SELECT
customers. id AS customers_id,
customers.first_name AS customers_first_name,
customers.last_name AS customers_last_name,
customers.username AS customers_username,
customers.email AS customers_email,
customers.address AS customers_address,
customers.town AS customers_town,
customers.created_on AS customers_created_on,
customers.updated_on AS customers_updated_on
FROM
customers
LIMIT %(param_1)s
Метод offset()
Метод offset() добавляет оператор OFFSET к запросу. Он принимает в качестве аргумента значение смещения. Часто используется с оператором limit().
SELECT
customers. ID AS customers_id,
customers.first_name AS customers_first_name,
customers.last_name AS customers_last_name,
customers.username AS customers_username,
customers.email AS customers_email,
customers.address AS customers_addrees,
customers.town AS customers_town,
customers.created_on AS customers_created_on,
customers.updated_on AS customers_updated_on
FROM
customers
LIMIT %(param_1)s OFFSET %(param_2)s
Метод order_by()
Метод order_by()используется для сортировки результата с помощью оператора ORDER BY. Он принимает названия колонок, по которым необходимо сортировать результат. По умолчанию сортирует по возрастанию.
SELECT
customers.id AS customers_id,
customers.first_name AS customers_first_name,
customers.last_name AS customers_last_name,
customers.username AS customers_username,
customers.email AS customers_email,
customers.address AS customers_address,
customers.town AS customers_town,
customers.created_on AS customers_created_on,
customers.updated_on AS customers_updated_on
FROM
customers
JOIN orders ON customers.id = orders.customer_id
Этот оператор часто используется для получения данных из одной или нескольких таблиц в одном запросе. Например:
В этом запросе левой таблицей является customers. Это значит, что он вернет все записи из customersи только те, которые соответствуют условию, из orders.
Создать FULL OUTER JOIN можно, передав в метод full=True. Например:
Результаты группируются с помощью group_by(). Этот метод принимает одну или несколько колонок и группирует записи в соответствии со значениями в колонке.
Следующий запрос использует join() и group_by() для подсчета количества заказов, сделанных Dmitriy Yatsenko.
Чтобы отфильтровать результаты на основе значений, которые возвращают агрегирующие функции, используется метод having(), добавляющий оператор HAVING к инструкции SELECT. По аналогии с where() он принимает условие.
Приведение (конвертация) данных от одного типа к другому — распространенная операция, которая выполняется с помощью функции cast() из библиотеки sqlalchemy.
i = session.query(Item).get(8)
i.selling_price = 25.91
session.add(i)
session.commit()
Таким образом можно обновлять только один объект за раз. Для обновления нескольких записей за раз используйте метод update() объекта Query. Он возвращает общее количество обновленных записей. Например:
from sqlalchemy import text
session.query(Customer).filter(text("first_name = 'Vladimir'")).all()
session.query(Customer).filter(text("username like 'Cal%'")).all()
session.query(Customer).filter(text("username like 'Cal%'")).order_by(text("first_name, id desc")).all()
Транзакции
Транзакция — это способ выполнения набора SQL-инструкций так, что выполняются или все вместе, или ни одна из них. Если хотя бы одна инструкция из транзакции была провалена, база данных возвращается к предыдущему состоянию.
В базе данных есть два заказа, в процессе отгрузки заказа есть такие этапы:
В колонке date_placed таблицы orders устанавливается дата отгрузки.
Количество заказанных товаров вычитается из items.
Оба действия должны быть выполнены как одно, чтобы убедиться, что данные в таблицах корректны.
В следующем коде определяем метод dispatch_order(), который принимает order_idв качестве аргумента и выполняет описанные выше задачи в одной транзакции.
def dispatch_order(order_id):
# проверка того, правильно ли указан order_id
order = session.query(Order).get(order_id)
if not order:
raise ValueError("Недействительный order_id: {}.".format(order_id))
try:
for i in order.line_items:
i.item.quantity = i.item.quantity - i.quantity
order.date_placed = datetime.now()
session.commit()
print("Транзакция завершена.")
except IntegrityError as e:
print(e)
print("Возврат назад...")
session.rollback()
print("Транзакция не удалась.")
dispatch_order(1)
В первом заказе 3 стула и 2 ручки. dispatch_order() с идентификатором заказа 1даст следующий вывод: