SQLAlchemy доступ к реляционным данным в стиле Python Юревич Юрий, http://pyobject.ru Семинар Учебного центра Люксофт, 16 сентября 2008
May 31, 2015
SQLAlchemyдоступ к реляционным данным
в стиле Python
Юревич Юрий, http://pyobject.ru
Семинар Учебного центра Люксофт,16 сентября 2008
2
«SQLAlchemy — это Python SQL тулкит и ORM,
которые предоставляют разработчику
всю мощь и гибкость SQL»
http://sqlalchemy.org
3
Active Record (Django ORM, Storm)
4
Data Mapper (SQLAlchemy)
5
Архитектура SQLAlchemy
6
Про что будет
7
Про что (почти) не будет
8
Управление схемой данных
9
Схема == метаданные
10
Создание таблиц: SQL
CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR(255), PRIMARY KEY (id));
CREATE TABLE creds ( id INTEGER NOT NULL, user_id INTEGER, login VARCHAR(20), passwd VARCHAR(40), PRIMARY KEY (id), FOREIGN KEY(user_id) REFERENCES users (id));
11
Создание таблиц: SQLAlchemy
meta = MetaData()
users = Table('users', meta, Column('id', Integer, primary_key=True), Column('name', Unicode(255)),)
creds = Table('creds', meta, Column('id', Integer, primary_key=True), Column('user_id', Integer, ForeignKey('users.id')), Column('login', String(20)), Column('passwd', String(40)),)
12
MetaData: что умеет
● Создавать/удалять:
– Всё сразу: meta.create_all() / meta.drop_all()
– По отдельности: users.create() / users.drop()
● Рефлексия:
– Потаблично: users = Table('users', meta, autoload=True)
– Всё сразу: meta.reflect()
● Интроспекция:
– meta.tables
13
MetaData: пример из жизни
# ... описание схемы
def get_indexes(): return [ Index('ix_users_id', users.c.id, unique=True), Index('ix_creds_id', creds.c.id, unique=True), Index('ix_creds_user_id', creds.c.user_id), ]
def load(): # ... загрузка данных pass
def postload(): for ix in get_indexes(): ix.create()
14
Язык SQL-выражений
15
Для любителей трехбуквенных сокращений
● DML (Data Managament Language):– insert/update/delete
● DQL (Data Query Language):– select
16
Insert
● Данные указываются при создании ins = users.insert(values={'name': u'Jack'}) ins.execute()
● Данные указываются при выполнении ins = users.insert() ins.execute(name=u'Jack')
● Несколько записей сразу ins = users.insert() ins.execute([{'name': u'Jack'}, {'name': u'Ed'}])
● Явно указывая соединение engine = create_engine('sqlite:///') ins = insert(users) engine.connect().execute(ins, {'name': u'Jack'})
17
Delete
● Условие — SQLAlchemy SQL expressiondel_ = users.delete(users.c.name==u'Jack')del_.execute()
● Условие — строка с SQLdel_ = users.delete('users.name=:user')del_.params({'user': u'Jack'}).execute()
18
SelectSELECT * FROM users
q = users.select()
SELECT users.id, users.name FROM users
19
SelectSELECT id FROM users WHERE name='Jack'
q = users.select([users.c.id], users.c.name==u'Jack')
SELECT users.id FROM users WHERE users.name=:name
20
SelectSELECT * FROM usersJOIN creds ONcreds.user_id=users.id
q = users.join(creds).select()
SELECT users.id, users.name, creds.id, creds.user_id, creds.login, creds.passwdJOIN creds ONusers.id=creds.user_id
21
Почему SA SQL Expr?
● Потому что Python круче SQL
22
Почему SA SQL Expr?
● Потому что Python круче SQL● Генерация SQL на лету:
– q = select([users, creds.c.login], from_obj=users.join(creds), whereclause=users.c.name==u'Jack')
– q = users.select()q = q.where(users.c.name==u'Jack')q = q.column(creds.c.login)q.append_from(join(users, creds))
– SELECT users.id, users.name, creds.loginFROM usersJOIN creds ON creds.user_id = users.idWHERE users.name = 'Jack'
23
Object Relational Mapper (ORM)
24
Data Mapper, снова
25
Рабочий пример: схема данных
26
Рабочий пример: таблицы в SQL
CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR(255), PRIMARY KEY (id));
CREATE TABLE creds ( id INTEGER NOT NULL, user_id INTEGER, login VARCHAR(20), passwd VARCHAR(20), PRIMARY KEY (id), FOREIGN KEY(user_id) REFERENCES users (id)
);
CREATE TABLE messages ( id INTEGER NOT NULL, subject VARCHAR(255), body TEXT, author_id INTEGER, PRIMARY KEY (id), FOREIGN KEY(author_id) REFERENCES users (id));
CREATE TABLE message_recipients ( message_id INTEGER NOT NULL, recipient_id INTEGER NOT NULL, PRIMARY KEY ( message_id, recipient_id), FOREIGN KEY(message_id) REFERENCES messages (id), FOREIGN KEY(recipient_id) REFERENCES users (id));
27
Рабочий пример:Данные
users = { u'Jack': ['jack', 'jack-rabbit'], u'Edvard': ['ed'], u'Mary': ['mary'],}
messages = ( { 'author': u'Jack', 'recipients': [u'Edvard', u'Mary'], 'title': u'The first', 'body': u'Ha-ha, I\'m the first!, }, { 'author': u'Edvard', 'recipients': [u'Jack', u'Mary'], 'title': u'Hey all', 'body': u'Hi, I\'m here', }, { 'author': u'Edvard', 'recipients': [u'Mary'], 'title': u'The silence', 'body': u'Why are you ignoring me?', }, { 'author': u'Mary', 'recipients': [u'Jack'], 'title': u'Hi', 'body': u'Hi, Jack, how are you?', },)
28
Рабочий пример: таблицы в SA SQL Expr
users = Table('users', meta,Column('id', Integer, primary_key=True),Column('name', Unicode(255)),)
creds = Table('creds', meta,Column('id', Integer, primary_key=True),Column('user_id', Integer, ForeignKey('users.id')),Column('login', String(20)),Column('passwd', String(20)),)
messages = Table('messages', meta,Column('id', Integer, primary_key=True),Column('subject', Unicode(255)),Column('body', Text),Column('author_id', Integer, ForeignKey('users.id')),)
message_recipients = Table('message_recipients', meta,Column('message_id', Integer, ForeignKey('messages.id'), primary_key=True),Column('recipient_id', Integer, ForeignKey('users.id'), primary_key=True),)
29
Рабочий пример: Mappings
Session = scoped_session(sessionmaker(autoflush=False))
class Base(object): def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value)
class User(Base): passclass Cred(Base): passclass Message(Base): pass
Session.mapper(User, users) # (1)
Session.mapper(Cred, creds, properties={ # (2) 'user': relation(User, backref='credentials'),})
Session.mapper(Message, messages, properties={ # (3) 'recipients': relation(User, backref='inbox', secondary=message_recipients), 'author': relation(User, backref='outbox'),})
30
Рабочий пример: готовность №1
[ 1]>>> import schema as sch[ 2]>>> import mappings as m
[ 3]>>> engine = create_engine('sqlite:///example.sqlite')[ 4]>>> sch.meta.bind = engine[ 5]>>> sch.meta.create_all()[ 6]>>> m.Session.bind = engine
[ 7]>>> u1 = m.User.query.get(1)[ 8]>>> u1.id<<< 1[ 9]>>> u1.name<<< u'Jack'[10]>>> u1.credentials<<< [<Cred: jack>, <Cred: jack-rabbit>][11]>>> u1.outbox<<< [<Message: from Jack to Edvard, Mary (subj: The first)>]
31
Рабочий пример: выборки
[1]>>> q = m.User.query.filter(User.id>1)[2]>>> print str(q)SELECT users.id AS users_id, users.name AS users_nameFROM usersWHERE users.id > ?
[3]>>> q = q.filter(m.User.name!=None)[4]>>> print str(q)SELECT users.id AS users_id, users.name AS users_nameFROM usersWHERE users.id > ? AND users.name IS NOT NULL
[5]>>> list(q)<<< [<User u'Edvard'>, <User u'Mary'>]
[6]>>> q.first()<<< <User u'Edvard'>
32
Рабочий пример:выборки (продолжение)
[1]>>> rabbit = m.Cred.query.\ filter_by(login='jack-rabbit').one()
[2]>>> rabbit_user = m.User.query.\ filter(User.credentials.contains(rabbit)).\ one()
[3]>>> rabbit_messages = m.Message.query.\ filter(or_( Message.author==rabbit_user, Message.recipients.contains(rabbit_user) ))[4]>>> list(rabbit_messages)<<< [<Message: from Jack to Edvard, Mary (subj: The first)>, <Message: from Edvard to Jack, Mary (subj: Hey all)>, <Message: from Mary to Jack (subj: Hi)>]
33
Рабочий пример:выборки (хардкор)
# Выбрать пользователей, у которых# в исходящих больше одного сообщения[1]>>> sess = m.Session()[2]>>> q = sess.query(m.User, func.count('*')).\ join(m.Message).group_by(m.User).\ having(func.count('*')>1)[3]>>> list(q)<<< [(<User u'Edvard'>, 2)]
# Выбрать пользователей, у которых# во входящих больше одного сообщения[4]>>> sess = m.Session()[5]>>> q = sess.query(m.User, func.count('*')).\ join(sch.message_recipients).group_by(m.User).\ having(func.count('*')>1)[6]>>> list(q)<<< [(<User u'Jack'>, 2), (<User u'Mary'>, 3)]
34
Рабочий пример:сессия, unit of work
[ 1]>>> engine = create_engine('sqlite:///example.sqlite', echo=True)[ 2]>>> sch.meta.bind = engine[ 3]>>> m.Session.bind = engine[ 4]>>> sess = m.Session()[ 5]>>> jack = m.User.query.filter_by(name=u'Jack').one()2008-09-14 14:19:11,504 INFO sqlalchemy.engine.base.Engine.0x...ca2c SELECT...[ 6]>>> ed = m.User.query.filter_by(name=u'Edvard').one()2008-09-14 14:20:22,731 INFO sqlalchemy.engine.base.Engine.0x...ca2c SELECT...[ 7]>>> jack.name = u'Jack Daniels'[ 8]>>> sess.dirty<<< IdentitySet([<User u'Jack Daniels'>])[ 9]>>> ed.name = u'Edvard Noringthon'[10]>>> sess.dirty<<< IdentitySet([<User u'Edvard Noringthon'>, <User u'Jack Daniels'>])[11]>>> sess.flush()2008-09-14 14:21:00,535 INFO sqlalchemy.engine.base.Engine.0x...ca2c UPDATE users SET name=? WHERE users.id = ?2008-09-14 14:21:00,535 INFO sqlalchemy.engine.base.Engine.0x...ca2c ['Jack Daniels', 1]2008-09-14 14:21:00,604 INFO sqlalchemy.engine.base.Engine.0x...ca2c UPDATE users SET name=? WHERE users.id = ?2008-09-14 14:21:00,604 INFO sqlalchemy.engine.base.Engine.0x...ca2c ['Edvard Noringthon', 2]
35
users = Table( 'users', meta, ...)
class User: pass
mapper(users, User)
«user» 5 раз, скучно?
36
Elixir: ActiveRecord поверх SQLAlchemy● Elixir (http://elixir.ematia.de):
– Декларативное описание в стиле Django
– События:● До (update, insert, delete)● После (update, insert, delete)
– Плагины:● acts_as_versioned (автоматическое хранение истории)● acts_as_encrypted (шифрование колонок)● associable (аналог Django generic relations)● ...
37
Еще вкусности SQLAlchemy● «Хитрые» атрибуты:
– Отложенная/непосредственная подгрузка
– SQL-выражение как атрибут
● Партицирование:– Вертикальное (часть таблиц в одной БД, часть в другой)
– Горизонтальное (шардинг, содержимое одной таблицы «раскидано» по таблицам/БД)
● Нетривиальные маппинги:– Несколько маппингов к одному классу
– Маппинг классов к SQL-запросам
● ...
38
Диагноз: нужна ли вам SA
● Используете другой ORM и счастливы?– Нет, в SQLAlchemy
«много буков», нет смысла
● Используете DB API2 и «чистый» SQL?– Да, SQLAlchemy
даст вам это и много что еще