Fintech Python #6
ВиФи
63281-53547
Реляционные базы данных
import sqlite3
Встраиваемая база данных
База данных - 1 файл
Нет сервера
только файл
Полноценная база данных
import sqlite3
conn = sqlite3.connect('example.db')
Минимальный пример
c = conn.cursor()
c.execute("""
CREATE TABLE stocks
(date text, trans text, symbol text,
qty real, price real)
""")
c.execute("""
INSERT INTO stocks VALUES
('2006-01-05','BUY','RHAT',100,35.14)
""")
conn.commit()
conn.close()t = ('RHAT',)
c.execute('SELECT * FROM stocks WHERE symbol=?', t)
print(c.fetchone())
for row in c.execute('SELECT * FROM stocks ORDER BY price'):
print(row)
('2006-01-05', 'BUY', 'RHAT', 100, 35.14)
('2006-03-28', 'BUY', 'IBM', 1000, 45.0)
('2006-04-06', 'SELL', 'IBM', 500, 53.0)
('2006-04-05', 'BUY', 'MSFT', 1000, 72.0)Не работает по сети
NFSSAMBA- и т.д
Один писатель
Отсюда остальные проблемы особенности
Docker
Что такое docker?
Зачем docker?
Простота
- Не парится об установке баз данных
- Быстро
- Безопасность
- Воспроизводимость
С чего можно начать
Зачем нужны базы данных?
Что такое транзакция?
ACID
Атомарность
невозможность разбиения на меньшие части
Согласованность
База находится с точки зрения приложения в "хорошем состоянии"
Изоляция
Конкурентные транзакции изолированы друг от друга
Линеаризуемость?
Долговечность
База не теряет записанных
(успешно зафиксированных)
транзакций данных
Даже если аппаратный сбой
Атомарность
Если по середине транзакции произошла ошибка
то транзакция надо прервать и откатить все сделанные изменения
"все или ничего"
Изоляция
Конкурентно выполняемые транзакции не должны мешать друг другу
Одна транзакция выполняем несколько записей, то другая видит либо все их результаты, либо никакие!
Уровни изоляций
Сильные 💪
Serializable — 💰
Сделаем вид что все последовательно
Слабые 🙈
Сложные для разработчика
Пример: биржа биткоинов на mongodb
Грязные
чтения
read uncommitted

Read committed
Repeatable read
a = file.read()
time.sleep(100)
assert a == file.read()Потери обновлений
1. if money > price:
2.
3. money = money - price
money > 01. if money > price:
2. money = money - price
money > 0
Два телевизора 👍
Потери обновлений
1. if money > price:
2.
3. money -= price
money < 01. if money > price:
2. money -= price
money < 0Два телевизора 👍
Выход?
SERIALIZABLE
- блокировки
- атомарные операции
Обнаружение потери обновлений
- PostgreSQL
- Oracle
- SQL Server
-
MySQL💩
Асимметрия записи Фантомы
Для их автоматического определения требуется настоящая линеаризуемость!
Больше блокировок
select ... for update
SQL
дата создания: 1974 год

Что он дает?
[ WITH [ RECURSIVE ] with_query [, ...] ]
SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]
[ * | expression [ [ AS ] output_name ] [, ...] ]
[ FROM from_item [, ...] ]
[ WHERE condition ]
[ GROUP BY grouping_element [, ...] ]
[ HAVING condition [, ...] ]
[ WINDOW window_name AS ( window_definition ) [, ...] ]
[ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ]
[ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
[ LIMIT { count | ALL } ]
[ OFFSET start [ ROW | ROWS ] ]
[ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ]
[ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF table_name [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ]
where from_item can be one of:
[ ONLY ] table_name [ * ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
[ TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ] ]
[ LATERAL ] ( select ) [ AS ] alias [ ( column_alias [, ...] ) ]
with_query_name [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
[ LATERAL ] function_name ( [ argument [, ...] ] )
[ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
[ LATERAL ] function_name ( [ argument [, ...] ] ) [ AS ] alias ( column_definition [, ...] )
[ LATERAL ] function_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] )
[ LATERAL ] ROWS FROM( function_name ( [ argument [, ...] ] ) [ AS ( column_definition [, ...] ) ] [, ...] )
[ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
from_item [ NATURAL ] join_type from_item [ ON join_condition | USING ( join_column [, ...] ) ]
and grouping_element can be one of:
( )
expression
( expression [, ...] )
ROLLUP ( { expression | ( expression [, ...] ) } [, ...] )
CUBE ( { expression | ( expression [, ...] ) } [, ...] )
GROUPING SETS ( grouping_element [, ...] )
and with_query is:
with_query_name [ ( column_name [, ...] ) ] AS ( select | values | insert | update | delete )
TABLE [ ONLY ] table_name [ * ]Relational algebra
Отношения важны
Как можно связать две сущности между собой?
JOIN
- INNER JOIN
- OUTER JOIN (left, right, full)
- CROSS JOIN
- NATURAL JOIN
Constraints
Ограничения целостности
- foreign key
- unique
- check
- other
| Сотрудник | Отдел | Телефон |
|---|---|---|
| Гришин | Бухгалтерия | 11-22-33 |
| Васильев | Бухгалтерия | 11-22-33 |
| Петров | Снабжение | 44-55-66 |
| Отдел | Телефон |
|---|---|
| Бухгалтерия | 11-22-33 |
| Снабжение | 44-55-66 |
| Сотрудник | Отдел |
|---|---|
| Гришин | Бухгалтерия |
| Васильев | Бухгалтерия |
| Петров | Снабжение |

Денормализация
Отступления от 3 нормальной
ради скорости или ограничений
Raw SQL
sql = """select * from table where field = 1"""
def some_code():
...
cursor = connection.execute(sql)
result = cursor.fetchall()
...Эффективность важнее
читаемости ☝️
Сложно читать
select distinct
cast(null as integer) NZAP,
d_ext_uslug_napr_zag.id_doc_public NNAPR,
d_ext_uslug_napr_zag.date_doc DNAPR,
d_ext_uslug_napr_zag.form_med_pom FOMP,
tab_lpu.lpu_ext_code MONAPR,
tab_struct_obos_unit.code_ffoms PMONAPR,
cast (:lpu_ext_code as varchar(20)) MO, TSOU1.code_ffoms PMO,
tab_other_strings.code_external VPOLIS,patients.polis_s SPOLIS,patients.polis_n NPOLIS,
tab_msk.smocod SMO, tab_msk.TF_OKATO TER,
patients.TELEFON TEL,
d_ext_uslug_napr_zag.ds_osn_mkb DSNAPR,
TAB_PLACE_PARAM.profil_koek_for_foms PROFK,
tab_profile.CODE_PROFILE_EGISZ_PRFO PROFO,
TAB_PLACE_PARAM.profil_koek_for_foms PROFKNAPR,
tab_profile.CODE_PROFILE_EGISZ_PRFO PROFONAPR,
users.snils MEDRAB,
med_kart.id_kart_public NKART, med_kart.ds_priem_mkb DS,
d_ext_uslug_napr_zag.date_plan_gosp DPGOSP,
MED_KART.date_vipis_planed DPOGOSP
from d_ext_uslug_napr_zag
left outer join patients on patients.id_pac=d_ext_uslug_napr_zag.pac_id and patients.dae>='31.12.9999'
left outer join tab_other_strings on tab_other_strings.id_string=patients.polis_type
left outer join tab_profile on tab_profile.id_profile=d_ext_uslug_napr_zag.TARGET_PODR_PROFILE
left outer join R_USERS_DOLGN on R_USERS_DOLGN.ID_KD=d_ext_uslug_napr_zag.NAPR_DOCTOR_KD_ID
left outer join users on users.id_user=d_ext_uslug_napr_zag.napr_doctor_user_id
left outer join tab_msk on tab_msk.id_msk=patients.msk
left outer join tab_place on tab_place.id_place=d_ext_uslug_napr_zag.napr_place_id
left outer join tab_struct_obos_unit on tab_struct_obos_unit.lpu_id=d_ext_uslug_napr_zag.from_lpu_id
left outer join tab_struct_obos_unit TSOU1 on TSOU1.id_obos_unit=tab_place.obos_unit_id
left outer join d_diagn on d_diagn.id_doc = d_ext_uslug_napr_zag.id_doc
left outer join med_kart on med_kart.pac_id = patients.id_pac
and med_kart.dt_coming>=:O_BEG and med_kart.dt_coming<=:O_END
left outer join d_dvig on d_dvig.med_kart=med_kart.id_kart and d_dvig.type_dvig=31
left outer join tab_place_param on tab_place_param.id_place=d_dvig.target_place
and tab_place_param.dab<=:O_BEG and tab_place_param.dae>:O_BEG
left outer join tab_struct_place_har on tab_struct_place_har.place_id=tab_place.id_place
left outer join tab_lpu on tab_lpu.lpu_code= med_kart.napr_lpu
where d_ext_uslug_napr_zag.IS_INCOMING=0 and d_ext_uslug_napr_zag.pay_id=1 --IS_INCOMING=0 только исходящие,
and d_ext_uslug_napr_zag.date_doc>=:O_BEG and d_ext_uslug_napr_zag.date_doc<=:O_ENDСложно менять
if, f'string и шаблоны с %s
def hello(key, some_iter):
columns = [f'{column} IN %({key})s'
for column in some_iter]
return [f'({" OR ".join(columns)})']
def hello2(some_iter):
sql = ','.join(some_iter)
return f"kind IN ({sql})"
SELECT * FROM table WHERE field in ()SQL Injections

query = "SELECT * FROM users WHERE name = '%s'"
cur.execute(query % name)SQL optimization
Индексы
Explain
ORM
Объектно-реляционное отображение
Связь БД с концепциями ООП
ORM - Object-Relational Mapping
Перерыв
SQLAlchemy
Работает с базой данных как с движком реляционной алгебры, а не просто с набором таблиц.

Объектно-реляционное несовпадение потенциалов
- Рассогласованность объектного и реляционного подхода
- Разница в типах данных
- Транзакционность и ACID
Статья «Вьетнам компьютерной науки»
q1 = [('associated_with', search_for),
('profile_type__slug__exact', profile_type),
('gender__in', gender),
('rank__in', rank),
('styles__style__in', styles),
('age__gte', age_from),
('age__lte', age_to)]
q1_list = [Q(x) for x in q1 if x[1]]
q2 = [('user__first_name__icontains', search_term),
('user__last_name__icontains', search_term),
('profile_type__name__icontains', search_term),
('styles__style__icontains', search_term),
('rank__icontains', search_term)]
q2_list = [Q(x) for x in q2 if x[1]]
if q1_list:
objects = Profile.objects.filter(
reduce(operator.and_, q1_list))
if q2_list:
if objects:
objects = objects.filter(
reduce(operator.or_, q2_list))
else:
objects = Profile.objects.filter(
reduce(operator.or_, q2_list))
if order_by_ranking_level == 'desc':
objects = objects.order_by('-ranking_level').distinct()
else:
objects = objects.order_by('ranking_level').distinct()SQLAlchemy CORE
- Подключение к базе данных
- Пулы подключений
- Описание таблиц
- Составление запросов
Описание таблиц
import sqlalchemy as sa
metadata = sa.MetaData()
users = sa.Table(
'users', metadata,
sa.Column('user_id', sa.Integer, primary_key=True),
sa.Column('name', sa.String),
sa.Column('fullname', sa.String),
)users = sa.table(
"users",
sa.column("user_id"),
sa.column("name"),
sa.column("description"),
)import sqlalchemy as sa
metadata = sa.MetaData()
users = sa.Table(
'users', metadata,
sa.Column('user_id', sa.Integer, primary_key=True),
sa.Column('name', sa.String),
sa.Column('fullname', sa.String),
sa.ForeignKeyConstraint(['user_id'], ['t1.user_id']),
sa.CheckConstraint('name != "kek"', name='check1')
)Целостность данных
Index('idx1', func.lower(t1.c.somecol))
Index('idx2', t1.c.data,postgresql_with={"fillfactor":50})
Index('idx3', t1.c.data,postgresql_using='gin')Запросы
ins = sa.insert(users)
str(ins)
'INSERT INTO users (id, name, fullname) '
'VALUES (:id, :name, :fullname)'
s = sa.select([users])
str(s)
'SELECT users.id, users.name, users.fullname '
'FROM users'
d = sa.delete(users)
str(d)
'DELETE FROM users'Запросы
sa.insert(users).execute([(1,2,3), (2,3,4)])
sa.select([users]).execute()
sa.delete(users).execute()
Запросы сложнее
import sqlalchemy as sa
tmp_users = sa.table(
'tmp_users',
sa.column('name'),
sa.column('fullname')
)
select = sa.select([tmp_users])
names = [users.c.name, users.c.fullname]
insert = sa.insert(users).from_select(names, select)
str(insert)
'INSERT INTO users (name, fullname) '
'SELECT tmp_users.name, tmp_users.fullname\n'
'FROM tmp_users'Диалекты
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite, postgresql
f = sa.func.now()
str(f.compile(dialect=sqlite.dialect()))
'CURRENT_TIMESTAMP'
str(f.compile(dialect=postgresql.dialect()))
'now()'Поддержка на Python
enum, json, fts, array types и т.д
import enum
import sqlalchemy as sa
from sqlalchemy.schema import CreateTable
class MyEnum(enum.Enum):
one = 1
two = 2
t = sa.Table('data', sa.MetaData(),
sa.Column('value', sa.Enum(MyEnum)))
str(CreateTable(t))
"CREATE TABLE data (\n"
" value VARCHAR(5),\n"
" CONSTRAINT myenum CHECK (value IN ('one', 'two')))"- Генерируем запросы SQL код с помощью Python
- Описываем структуру базу с помощью Python
- Нет RAW-запросов
- Результаты запросов — это простые типы
Итог
SQLAlchemy ORM
- Declarative
- Session
- Query
- Relationships
Declarative
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'some_table'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(50))
c = User()Описываем таблицу как класс
Declarative
instance.pk
instance.remove()
instance.save()
Model._meta
...- Не добавляет публичных методов
- Из коробки нет популярного паттерна active record
- Близнец транзакции БД на стороне Python
- Явный объект, создаваемый по необходимости, а не на каждый HTTP-запрос
- flush()
session = Session()
session.add(user1) # insert
session.delete(user2) # delete
user3.name = 'new name'
session.add(user3) # update
session.commit()
# или
session.rollback()Session
session.query(User).filter(User.name == 'test')
session.query(User).filter(
User.name.in_(['Edwardo', 'fakeuser'])
)- Позволяет делать запросы в базу
- Результаты запросов — классы
- Работает и с запросами из CORE
Query
Query
У колонок переопределены многие операторы:
where = (User.name == 'foo') | (User.name == 'bar')
str(where.compile())
'some_table.name = :name_1 OR some_table.name = :name_2'where = User.id.op('#')(0xff) # bitwise xor postgresql
str(where.compile())
'some_table.id # :id_1'Можно использовать любой доступный в базе оператор:
Relationships
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Parent(Base):
__tablename__ = 'parent'
id = sa.Column(sa.Integer, primary_key=True)
children = orm.relationship("Child")
class Child(Base):
__tablename__ = 'child'
id = sa.Column(sa.Integer, primary_key=True)
parent_id = sa.Column(sa.Integer,
sa.ForeignKey(Parent.id))
parent = orm.relationship("Parent")- Описываем таблицы
- Настраиваем отношения
- Интеграция c CORE
- Управляем транзакциями с помощью Session
Итог
Connection pool

Connection invalidation
Миграции
pip install alembic
Зачем надо?
alembic revision -m "create account table"
Generating /yourproject/alembic/1975ea83b712_create_account_table.py...doneПросто набор операций
"""create account table
Revision ID: 1975ea83b712
"""
# revision identifiers, used by Alembic.
revision = '1975ea83b712'
down_revision = None
branch_labels = None
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
passdef upgrade():
op.create_table(
'account',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('name', sa.String(50), nullable=False),
sa.Column('description', sa.Unicode(200)),
)
def downgrade():
op.drop_table('account')Накатываем 🍻 и откатываем
Автогенерация
alembic revision --autogenerateRedBook.io
5-ое издание

Комментарии к академическим статьям по базам данных
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
@app.route('/user/<username>')
def show_user(username):
user = User.query.filter_by(username=username).first_or_404()
return render_template('show_user.html', user=user)Тестирование
from project.db import metadata
@pytest.fixture(autouse=True)
def _init_db():
metadata.create_all()
yield
metadata.drop_all()
from project import db
def test_user_create():
user = db.User('test')
db.session.add(user)
db.session.commit()
assert user.id
pytest-flask-sqlalchemy
def test_a_transaction(db_session):
row = db_session.query(Table).get(1)
row.name = 'testing'
db_session.add(row)
db_session.commit()
def test_transaction_doesnt_persist(db_session):
row = db_session.query(Table).get(1)
assert row.name != 'testing'ДЗ
Fintech Python #6
By Denis Kataev
Fintech Python #6
- 608