Введение в SQLAlchemy

править

SQLAlchemy — это программное обеспечение с открытым исходным кодом для работы с базами данных при помощи языка SQL. Оно реализует технологию программирования ORM (Object-Relational Mapping), которая связывает базы данных с концепциями объектно-ориентированных языков программирования. SQLAlchemy позволяет описывать структуры баз данных и способы взаимодействия с ними прямо на языке Python. SQLAlchemy реализована в виде пакета для Python под лицензией MIT, а значит возможно ее использование в проприетарном ПО.

SQLAlchemy была выпущена в феврале 2006 и быстро стала одним из самых распространенных инструментов ORM среди разработчиков на Python. SQLAlchemy обладает несколькими областями применения, которые могут использоваться как вместе, так и по отдельности. Его основные компоненты приведены ниже.

 
Основные компоненты SQLAlchemy и их зависимости

Здесь будут представлены несколько уроков по использованию этого замечательного фреймворка.

Установка SQLAlchemy

править

Вы можете установить SQLAlchemy с нуля, используя setuptools. Если они установлены, то вы можете просто набрать в консоли:

# easy_install SQLAlchemy

Эта команда скачает последнюю версию SQLAlchemy из Python Cheese Shop и установит ее на вашу машину.

Также можно просто и без затей скачать архив с SQLAlchemy c официального сайта и выполнить установочный скрипт setup.py:

# python setup.py install

Установка с помощью pip:

# pip install sqlalchemy

Для проверки правильности установки следует проверить версию библиотеки:

import sqlalchemy#начните работу с этой библиотеки
print("Версия SQLAlchemy:", sqlalchemy.__version__)# посмотреть версию SQLALchemy

Объектно-реляционная модель SQLAlchemy

править
Соединение с базой данных
править

В этом уроке мы будем использовать только БД SQLite, хранящуюся в памяти. Чтобы соединиться с СУБД, мы используем функцию create_engine():

from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=True)

Флаг echo включает ведение лога через стандартный модуль logging Питона.

Когда он включен, мы увидим все созданные нами SQL-запросы. Если вы хотите просто пробежать этот урок и убрать отладочный вывод, то просто уберите его, поставив

echo=False
Создание таблицы в базе данных
править

Далее мы хотим рассказать SQLAlchemy о наших таблицах. Мы начнем с одиночной таблицы users, В которой будем хранить записи о наших конечных пользователях, которые посещают некий сайт N. Мы определим нашу таблицу внутри каталога MetaData, используя конструктор Table(), который похож на SQLный CREATE TABLE:

from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
metadata = MetaData()
users_table = Table('users', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String),
    Column('fullname', String),
    Column('password', String)
)

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

Далее же мы пошлем базе команду CREATE TABLE, параметры которой будут взяты из метаданных нашей таблицы. Мы вызовем метод create_all() и передадим ему наш объект engine, который и указывает на базу. Там сначала будет проверено присутствие такой таблицы перед ее созданием, так что можно выполнять это много раз — ничего страшного не случится.

metadata.create_all(engine)

Те, кто знаком с синтаксисом SQL и в частности CREATE TABLE, могут заметить, что колонки VARCHAR создаются без указания их длины. В SQLite и PostgreSQL это вполне допустимый тип данных, но во многих других СУБД так делать нельзя. Для того, чтобы выполнить этот урок в MySQL, длина должна быть передана строкам, как здесь:

Column('name', String(50))

Поле «длина» в строках String, как и простая разрядность/точность в Integer, Numeric и т. п. не используются более нигде, кроме как при создании таблиц.

Определение класса Python для отображения в таблицу
править

В то время, как класс Table хранит информацию о нашей БД, он ничего не говорит о логике объектов, что используются нашим приложением. SQLAlchemy считает это отдельным вопросом. Для соответствия нашей таблице users создадим элементарный класс User. Нужно только унаследоваться от базового питоньего класса Object (то есть у нас совершенно новый класс).

class User(object):
    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password

    def __repr__(self):
        return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)

__init__ — это конструктор, __repr__ же вызывается при операторе print. Они определены здесь для удобства. Они не обязательны и могут иметь любую форму. SQLAlchemy не вызывает __init__ напрямую

Настройка отображения
править

Теперь мы хотим слить в едином порыве нашу таблицу user_table и класс User. Здесь нам пригодится пакет SQLAlchemy ORM. Мы применим функцию mapper, чтобы создать отображение между users_table и User.

from sqlalchemy.orm import mapper
mapper(User, users_table) 
# <Mapper at 0x...; User>

Функция mapper() создаст новый Mapper-объект и сохранит его для дальнейшего применения, ассоциирующегося с нашим классом. Теперь создадим и проверим объект типа User:

from sqlalchemy.orm import mapper  #достать "Отобразитель" из пакета с объектно-реляционной моделью
print(mapper(User, users_table))  # и отобразить. Передает класс User и нашу таблицу
user = User("Вася", "Василий", "qweasdzxc")
print(user)  #Напечатает <User('Вася', 'Василий', 'qweasdzxc'>
print(user.id)  #Напечатает None

Атрибут id, который не определен в __init__, все равно существует из-за того, что колонка id существует в объекте таблицы users_table. Стандартно mapper() создает атрибуты класса для всех колонок, что есть в Table. Эти атрибуты существуют как Python дескрипторы и определяют его функциональность. Она может быть очень богатой и включать в себе возможность отслеживать изменения и АВТОМАТИЧЕСКИ подгружать данные в базу, когда это необходимо. Так как мы не сказали SQLAlchemy сохранить Василия в базу, его id выставлено на None. Когда позже мы сохраним его, в этом атрибуте будет храниться некое автоматически сформированное значение.

Декларативное создание таблицы, класса и отображения за один раз
править

Предыдущый подход к конфигурированию, включающий таблицу Table, пользовательский класс и вызов mapper() иллюстрируют классический пример использования SQLAlchemy, в которой очень ценится разделение задач. Большое число приложений, однако, не требуют такого разделения, и для них SQLAlchemy предоставляет альтернативный, более лаконичный стиль: декларативный.

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:///:memory:', echo=True)

Base = declarative_base()
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)
    
    def __init__(self, name, fullname, password):
        self.name = name
        self.fullname = fullname
        self.password = password
    def __repr__(self):
        return "<User('%s','%s', '%s')>" % (self.name, self.fullname, self.password)

# Создание таблицы
Base.metadata.create_all(engine)

Выше — функция declarative_base(), что определяет новый класс, который мы назвали Base, от которого будет унаследованы все наши ORM-классы. Обратите внимание: мы определили объекты Column безо всякой строки имени, так как она будет выведена из имени своего атрибута. Низлежащий объект Table, что создан нашей declarative_base() версией User, доступен через атрибут __table__

users_table = User.__table__

Имющиеся метаданные MetaData также доступны:

metadata = Base.metadata

Еще один «декларативный» метод для SQLAlchemy доступен в сторонней библиотеке Elixir. Это полнофункциональный продукт, который включает много встроенных конфигураций высокоуровневого отображения. Как и деклaративы, как только классы и отображения определены, использование ORM будет таким же, как и в классическом SQLAlchemy.

Создание сессии
править

Теперь мы готовы начать наше общение с базой данных. «Ручка» базы в нашем случае — это сессия Session. Когда сначала мы запускаем приложение, на том же уровне нашего create_engine() мы определяем класс Session, что служит фабрикой объектов сессий (Session).

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)

В случае же, если наше приложение не имеет Engine-объекта базы данных, можно просто сделать так:

Session = sessionmaker()

А потом, когда вы создадите подключение к базе с помощью create_engine(), соедините его с сессией, используя configure():

Session.configure(bind=engine)  # Как только у вас появится engine

Такой класс Session будет создавать Session-объекты, которые привязаны к нашей базе.

Другие транзакционные параметры тоже можно определить вызовом sessionmaker()’а, но они будут описаны в следующей главе. Так, когда вам необходимо общение с базой, вы создаете объект класса Session:

session = Session()

Сессия здесь ассоциирована с SQLite, но у нее еще нет открытых соединений с этой базой. При первом использовании она получает соединение из набора соединений, который поддерживается engine и удерживает его до тех пор, пока мы не применим все изменения и/или не закроем объект сессии.

Добавление новых объектов
править

Чтобы сохранить наш User-объект, нужно добавить его к нашей сессии, вызвав add():

vasiaUser = User("vasia", "Vasiliy Pypkin", "vasia2000")
session.add(vasiaUser)

Этот объект будет находиться в ожидании сохранения, никакого SQL-запроса пока послано не будет. Сессия пошлет SQL-запрос, чтобы сохранить Васю, как только это понадобится, используя процесс сброса на диск(flush). Если мы запросим Васю из базы, то сначала вся ожидающая информация будет сброшена в базу, а запрос последует потом.

Для примера, ниже мы создадим новый объект запроса (Query), который загружает User-объекты. Мы «отфильтровываем» по атрибуту «имя=Вася» и говорим, что нам нужен только первый результат из всего списка строк. Возвращается тот User, который равен добавленному:

ourUser = session.query(User).filter_by(name="vasia").first()
# <User('vasia','Vasiliy Pypkin', 'vasia2000')>

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

Та концепция ORM, что работает здесь, известная как карта идентичности, обеспечивает, что все операции над конкретной записью внутри сессии оперируют одним и тем же набором данных. Как только объект с неким первичным ключом появится в сессии, все SQL-запросы на этой сессии вернут те же самые объекты для этого самого первичного ключа. Будет выдана ошибка в случае попытки поместить в эту сессию другой, уже сохраненный объект с тем же первичным ключом.

Мы можем добавить больше User-объектов, использовав add_all()

session.add_all([User("kolia", "Cool Kolian[S.A.]","kolia$$$"), User("zina", "Zina Korzina", "zk18")])   #добавить сразу пачку записей

А вот тут Вася решил, что его старый пароль слишком простой, так что давайте его сменим:

vasiaUser.password = "-=VP2001=-"   #старый пароль был таки ненадежен

Сессия внимательно следит за нами. Она, для примера, знает, что Вася был модифицирован:

session.dirty
IdentitySet([<User('vasia','Vasiliy Pypkin', '-=VP2001=-')>])

И что еще пара User’ов ожидают сброса в базу:

session.new
IdentitySet([<User('kolia','Cool Kolian[S.A.]', 'kolia$$$')>, <User('zina','Zina Korzina', 'zk18')>])

А теперь мы скажем нашей сессии, что мы хотим отправить все оставшиеся изменения в базу и применить все изменения, зафиксировав транзакцию, которая до того была в процессе. Это делается с помощью commit():

session.commit()

commit() сбрасывает все оставшиеся изменения в базу и фиксирует транзакции. Ресурсы подключений, что использовались в сессии, снова освобождаются и возвращаются в набор. Последовательные операции с сессией произойдут в новой транзакции, которая снова запросит себе ресурсов по первому требованию. Если посмотреть Васин атрибут id, что раньше был None, то мы увидим, что ему присвоено значение:

vasiaUser.id
# 1

После того, как сессия вставит новые записи в базу, все только что созданные идентификаторы … будут доступны в объекте, немедленно или по первому требованию. В нашем случае, целая запись была перезагружена при доступе, так как после вызова commit() началась новая транзакция. SQLAlchemy стандартно обновляет данные от предыдущей транзакции при первом обращении с новой транзакцией, так что нам доступно самое последнее ее состояние. Уровень подгрузки настраивается, как описано в главе «Сессии».

Откат изменений
править

Учитывая то, что сессия работает с транзакциями, мы можем откатить сделанные изменения. Давайте внесем два изменения, которые затем мы откатим; поменяем имя пользователя vasiaUser на Vaska:

vasiaUser.name = 'Vaska'

и добавим ошибочного пользователя, fake_user:

fake_user = User('fakeuser', 'Invalid', '12345')
session.add(fake_user)

Посылая запросы в сессию, мы можем увидеть, что они записаны в текущую транзакцию:

session.query(User).filter(User.name.in_(['Vasko', 'fakeuser'])).all() 
# [<User('Vaska','Vasiliy Pypkin', '-=VP2001=-')>, <User('fakeuser','Invalid', '12345')>]

Откатывая, мы видим, что имя vasiaUser опять стало vasia, и fake_user был удален из транзакции:

session.rollback()

vasiaUser.name 
# u'vasia'
fake_user in session
# False

посылая запрос SELECT, видно какие изменения произошли в базе данных:

session.query(User).filter(User.name.in_(['vasia', 'fakeuser'])).all() 
# [<User('vasia','Vasiliy Pupkin', '-=VP2001=-')>]
Запросы
править

Запрос создается посредством использования функции query() для сессии. Эта функция берет переменное число аргументов, которыми может быть любая комбинация классов и дескрипторов, созданных с помощью классов. Ниже, мы демонстрируем запрос, который загружает экземпляры User. В итеративном цикле возвращается список объектов User:

for instance in session.query(User).order_by(User.id): 
    print instance.name, instance.fullname
# vasia Vasiliy Pupkin
# kolia Cool Kolian[S.A.]
# zina Zina Korzina

Запрос также поддерживает в качестве аргументов дескрипторы, созданные с помощью ORM. Каждый раз, когда запрашиваются разнообразные объекты классов или многоколоночные объекты в качестве аргументов функции query(), результаты возвращаются в виде кортежей:

for name, fullname in session.query(User.name, User.fullname): 
    print name, fullname
# vasia Vasiliy Pupkin
# kolia Cool Kolian[S.A.]
# zina Zina Korzina

Кортежи, которые возвращаются в запросе, являются именованными, и могут обрабатываться, как обычные объекты Python’а. Имена такие же, как и имена атрибутов для атрибутов, и имена классов для классов:

for row in session.query(User, User.name).all(): 
   print row.User, row.name
# <User('vasia','Vasiliy Pupkin', '-=VP2001=-')> vasia
# <User('kolia','Cool Kolian[S.A.]', 'kolia$$$')> kolia
# <User('zina','Zina Korzina', 'zk18')> zina

Вы можете определять свои имена, используя конструкцию label() для скалярных атрибутов и aliased() для классов:

from sqlalchemy.orm import aliased
user_alias = aliased(User, name='user_alias')
for row in session.query(user_alias, user_alias.name.label('name_label')).all(): 
   print row.user_alias, row.name_label
# <User('vasia','Vasiliy Pupkin', '-=VP2001=-')> vasia
# <User('kolia','Cool Kolian[S.A.]', 'kolia$$$')> kolia
# <User('zina','Zina Korzina', 'zk18')> zina

Базовые операции с запросами включают в себя LIMIT и OFFSET, которые удобно использовать со срезами массивов из Python, и обычно в сочетании с ORDER BY:

for u in session.query(User).order_by(User.id)[1:2]: 
   print u
# <User('kolia','Cool Kolian[S.A.]', 'kolia$$$')>
# <User('zina','Zina Korzina', 'zk18')>

и фильтруя результаты, которые выполнены либо с помощью filter_by(), которая использует ключевые аргументы:

for name in session.query(User.name).filter_by(fullname='Vasiliy Pupkin'): 
   print name
# vasia

…либо filter(), которая использует более подходящие для SQL конструкции языка. Это позволяет использовать более привычные операторы Python’а с атрибутами класса вашего отображения класса:

for name in session.query(User.name).filter(User.fullname=='Vasiliy Pupkin'):
   print name
# vasia

Объект запроса является полностью генеративным(порождающим), а это означает, что каждый вызов метода возвращает объект запроса, для которого можно добавить дополнительные ограничения. Например, чтобы получить пользователей с именем \"vasia\" и полным именем \"Vasiliy Pupkin\", вы можете повторно использовать функцию filter(), которая объединит критерии отбора, используя AND

for user in session.query(User).filter(User.name=='vasia').filter(User.fullname=='Vasiliy Pupkin'): 
   print user
# <User('vasia','Vasiliy Pupkin', '-=VP2001=-')>

См. также

править