# SQLAlchemy

# Sqlalchemy

<div class="align-center" id="bkmrk-%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0">**Установка**</div><div id="bkmrk-%D0%AF%D0%B4%D1%80%D0%BE%C2%A0">Ядро </div>```bash
pip install sqlalchemy
```

<div id="bkmrk-%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D1%8F%D0%B4%D1%80%D0%B0%3A-pip-"></div><div id="bkmrk-%D0%94%D1%80%D0%B0%D0%B9%D0%B2%D0%B5%D1%80-%D0%B4%D0%BB%D1%8F-postgres">Драйвер для postgres, mysql</div>```bash
pip install psycopg2
```

```bash
pip install psycopg2-binary
```

```bash
pip  install  pymysql
```

<div id="bkmrk-%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D1%80%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%B8%D0%B9"></div><div class="align-center" id="bkmrk-">**Подключение**</div>```python
from sqlalchemy import create_engine
engine = create_engine('postgresql+psycopg2://username:password@localhost:5432/mydb')
engine = create_engine('mysql+pymysql://cookiem:chip@mysql01.com/cookies', pool_recycle=3600)
engine = create_engine('sqlite:///cookies.db')
engine2 = create_engine('sqlite:///:memory:')
```

<div id="bkmrk-engine-%3D-create_engi"></div><div id="bkmrk-%D0%94%D0%BE%D0%BF.-%D0%BA%D0%BB%D1%8E%D1%87%D0%B8-%D1%87%D0%B5%D1%80%D0%B5%D0%B7-%D0%B7%D0%B0%D0%BF">Доп. ключи через запятую:</div><div id="bkmrk-echo-%D0%B1%D1%83%D0%BB%D0%B5%D0%B2%D0%BE.-%D0%9B%D0%BE%D0%B3-%D0%B7%D0%B0%D0%BF"><table border="1" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 17.7977%;"></col><col style="width: 82.3215%;"></col></colgroup><tbody><tr><td>echo</td><td>булево. Лог запросов. По-умолчанию False.</td></tr><tr><td>encoding</td><td>строка. По-умолчанию utf-8</td></tr><tr><td>isolation\_level</td><td>уровень изоляции</td></tr><tr><td>pool\_recycle</td><td>число секунд для переподключения, желательно выставлять 3600. При работе с mysql соединение может быть активным до 4 часов.</td></tr></tbody></table>

</div>Создание engine не создает фактического соединения с БД.

```python
connection = engine.connect()
```

Сырой запрос:

```python
result = connection.execute("select * from orders").fetchall()
```

<div id="bkmrk-2.-%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C-%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E"></div><div class="align-center" id="bkmrk-%D0%A1%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0-%D1%84%D1%80%D0%B5%D0%B9%D0%BC%D0%B2%D0%BE%D1%80%D0%BA%D0%B0">**Структура фреймворка**</div><div id="bkmrk-%D0%A2%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B--%3E-metadata-">Таблицы -&gt; MetaData -&gt; Engine -&gt; Dialect -&gt; DB</div><div id="bkmrk-metadata%3A-%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%2C-%D0%B2-"><div>**MetaData:** объект, в котором таблицы, индексы,...</div>- Может получать информацию о существующих сейчас сущностях в БД
- Может хранить шаблоны именования индексов и ограничений (constraint). Т е перед началом проекта добавляется настройка именования, затем при создании/удалении все ок. Это словарь ограничение:шаблон

</div><table border="1" id="bkmrk-%D0%9E%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B8%D0%BB%D0%B8-%D0%B8%D0%BD%D0%B4%D0%B5" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><thead><tr><td class="align-center">Ограничение или индекс</td><td class="align-center">Описание</td></tr></thead><tbody><tr><td class="align-center">ix</td><td>обычный индекс</td></tr><tr><td class="align-center">uq</td><td>уникальный индекс</td></tr><tr><td class="align-center">ck</td><td>ограничение проверки</td></tr><tr><td class="align-center">fk</td><td>foreign-ключ</td></tr><tr><td class="align-center">pk</td><td>primary-ключ</td></tr></tbody></table>

```python
from sqlalchemy import MetaData
convention = {
'all_column_names': lambda constraint, table: '_'.join([
column.name for column in constraint.columns.values()
]),
'ix': 'ix__%(table_name)s__%(all_column_names)s',
'uq': 'uq__%(table_name)s__%(all_column_names)s',
'ck': 'ck__%(table_name)s__%(all_column_names)s',
'fk': ('fk__%(table_name)s__%(all_column_names)s' '%(referred_table_name)s'),
'pk': 'pk__%(table_name)s'
}
metadata_obj = MetaData(naming_convention=convention)
```

<div id="bkmrk-%D0%94%D0%BE%D0%BB%D0%B6%D0%B5%D0%BD-%D0%B1%D1%8B%D1%82%D1%8C-%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8">- Должен быть инициализирован до обращения к нему в таблицах
- Изначально пустой объект, можно или вручную занести данные, или получить из базы.
- Получение информации об одной таблице по имени 
    - Нельзя получить одновременно две таблицы. Либо одна, либо вся база
    - Нельзя (и одна таблица, и база) получить ограничения (CONSTRAINT), комментарии, триггеры, значения по умолчанию. Но можно вручную добавить данные. Но похоже проще импортировать описание.

</div>```python
from sqlalchemy import ForeignKeyConstraint
album.append_constraint(ForeignKeyConstraint(['ArtistId'], ['artist.ArtistId']))
```

<div id="bkmrk-%D0%BF%D1%80%D0%B8-%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B8-%D0%B1%D0%B0%D0%B7%D1%8B%2C-"><div>- - при получении базы, имена таблиц с большой и маленькой буквы. Т е удваивается количество объектов.

</div></div>```python
engine = create_engine(...)
metadata = MetaData()
cookie_tbl = Table('cookies', metadata, autoload_with=engine)
s = select(cookie_tbl)
with engine.connect() as conn:
    m = conn.execute(s)
    print(m.keys())
```

<div id="bkmrk-%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D0%B8"><div>- Получение информации обо всей базе

</div></div>```python
from sqlalchemy import create_engine, MetaData

engine = create_engine(...)
metadata = MetaData()
metadata.reflect(bind=engine)
for table in metadata.sorted_tables:
    print(table.name)
#Потом получить объект таблицы: 
mytable = metadata.tables['mytable']
```

<div id="bkmrk-%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D0%B8-1"><div>- Получение информации обо всей базе через ORM + Automap

</div><div>  
</div></div>```python
from sqlalchemy.ext.automap import automap_base
from sqlalchemy import create_engine

Base = automap_base()
engine = create_engine('sqlite:///Chinook_Sqlite.sqlite')
Base.prepare(engine, reflect=True)
# данные о классах загружены. 
#Например для получения списка классов:
Base.classes.keys()
Artist = Base.classes.Artist # создание классов
#Внешние связи - в свойстве <related_object>_collection
artist = session.query(Artist).first()
for album in artist.album_collection:
    print('{} - {}'.format(artist.Name, album.Title))
```

<div id="bkmrk-engine%3A-%D0%A1%D0%BA%D1%80%D1%8B%D0%B2%D0%B0%D0%B5%D1%82-%D0%BF%D1%83%D0%BB"><div>  
</div><div>**Engine:** Скрывает пул подключений и диалект.   
**Dialect:** Скрывает детали реализации в конкретной базе  
**Core:** SQL в чистом виде  
**ORM:** абстракции</div></div><div id="bkmrk-%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D-%D0%9D%D0%B5%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8"></div><div id="bkmrk-foreignkeyconstraint"></div><div class="align-center" id="bkmrk-%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D-1">**Работа с данными**</div><div id="bkmrk-insert">**INSERT**</div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-1%3A">Вариант 1:</div>```python
ins = cookies.insert().values(
    cookie_name="chocolate chip",
    cookie_recipe_url="http://some.aweso.me/cookie/recipe.html",
    cookie_sku="CC01",
    quantity="12",
    unit_cost="0.50"
    )
    result = connection.execute(ins)
    print(result.inserted_primary_key)
```

<div id="bkmrk-%C2%A0-%C2%A0-print%28result.ins"></div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-2%3A">Вариант 2:</div>```python
from sqlalchemy import insert
ins = insert(cookies).values( 
    cookie_name="chocolate chip",
    cookie_recipe_url="http://some.aweso.me/cookie/recipe.html",
    cookie_sku="CC01",
    quantity="12",
    unit_cost="0.50"
)
```

<div id="bkmrk-%29-3"></div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-3">Вариант 3</div>```python
ins = cookies.insert()
result = connection.execute(
    ins, 
    cookie_name='dark chocolate chip', 
    cookie_recipe_url='http://some.aweso.me/cookie/recipe_dark.html',
    cookie_sku='CC02',
    quantity='1',
    unit_cost='0.75'
)
result.inserted_primary_key
```

<div id="bkmrk-result.inserted_prim"></div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-4">Вариант 4</div>```python
inventory_list = [ 
    {
        'cookie_name': 'peanut butter',
        'cookie_recipe_url': 'http://some.aweso.me/cookie/peanut.html',
        'cookie_sku': 'PB01',
        'quantity': '24',
        'unit_cost': '0.25'
    },
    {
        'cookie_name': 'oatmeal raisin',
        'cookie_recipe_url': 'http://some.okay.me/cookie/raisin.html',
        'cookie_sku': 'EWW01',
        'quantity': '100',
        'unit_cost': '1.00'
    }
]
result = connection.execute(ins, inventory_list)
```

<div id="bkmrk-result-%3D-connection.-2"></div><div id="bkmrk-select%C2%A0">**SELECT** </div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-1">Вариант 1</div>```python
from sqlalchemy.sql import select
s = select([cookies]) 
rp = connection.execute(s)
results = rp.fetchall()
```

<div id="bkmrk-results-%3D-rp.fetchal"></div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-2">Вариант 2</div>```python
s = cookies.select()
rp = connection.execute(s)
results = rp.fetchall()
```

<div id="bkmrk-results-%3D-rp.fetchal-1"></div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-3-1">Вариант 3</div>```python
s = cookies.select()
rp = connection.execute(s) 
for record in rp:
    print(record.cookie_name)
```

<div id="bkmrk-%C2%A0-%C2%A0-print%28record.coo"></div><div id="bkmrk-select-%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D1%85-">**SELECT определенных столбцов**</div>```python
s = select([cookies.c.cookie_name, cookies.c.quantity])
```

<div id="bkmrk-ordering">**ORDERING**</div>```python
s = select([cookies.c.cookie_name, cookies.c.quantity])
```

<div id="bkmrk-%D0%9F%D1%80%D1%8F%D0%BC%D0%B0%D1%8F-%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0">Прямая сортировка  
</div>```python
s = s.order_by(cookies.c.quantity)
```

<div id="bkmrk-%D0%9E%D0%B1%D1%80%D0%B0%D1%82%D0%BD%D0%B0%D1%8F-%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0">Обратная сортировка  
</div>```python
s = s.order_by(desc(cookies.c.quantity))
```

```python
rp = connection.execute(s)
```

<div id="bkmrk-%D0%B8%D0%BB%D0%B8-s-%3D-s.order_by%28d"></div><div id="bkmrk-limiting">**LIMITING**</div>```python
s = select([cookies.c.cookie_name, cookies.c.quantity])
s = s.order_by(cookies.c.quantity)
s = s.limit(2)
rp = connection.execute(s)
```

<div id="bkmrk-rp-%3D-connection.exec-4"></div><div id="bkmrk-%D0%92%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8-s">**Встроенные функции SQL**</div><div id="bkmrk-%D0%A1%D1%83%D0%BC%D0%BC%D0%B0%3A-func.sum">Сумма: func.sum</div>```python
from sqlalchemy.sql import func
s = select([func.sum(cookies.c.quantity)])
rp = connection.execute(s)
print(rp.scalar())
```

<div id="bkmrk-print%28rp.scalar%28%29%29%C2%A0">Количество: func.count</div>```python
s = select([func.count(cookies.c.cookie_name)])
rp = connection.execute(s)
record = rp.first()
print(record.keys()) # ключи могут быть разные.
print(record.count_1) 
```

<div id="bkmrk-print%28record.count_1"></div><div id="bkmrk-%D0%9F%D1%80%D0%B8-%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D0%B8-label-%D0%BC%D0%BE%D0%B6">Название свойства: label </div>```python
s = select([func.count(cookies.c.cookie_name).label('inventory_count')]) 
rp = connection.execute(s)
record = rp.first()
print(record.keys())
print(record.inventory_count)
```

<div id="bkmrk-print%28record.invento"></div><div id="bkmrk-where">**WHERE**</div><div id="bkmrk-%D0%98%D1%85-%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE-%D0%BA%D0%BE%D0%BC%D0%B1%D0%B8%D0%BD%D0%B8%D1%80%D0%BE%D0%B2%D0%B0">Их можно комбинировать, используется AND</div>```python
s = select([cookies]).where(cookies.c.cookie_name == 'chocolate chip')
rp = connection.execute(s)
```

<div id="bkmrk-rp-%3D-connection.exec-8"></div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%D1%8B-%D0%BC%D0%BE%D0%B4%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%82%D0%BE%D1%80">Варианты модификаторов:</div>- between(cleft, cright) Значение столбца между cleft и cright
- concat(column\_two) Объединение column и column\_two
- distinct() Находит только уникальные значения в столбце
- in\_(\[list\]) Только если значения столбца в списке
- is\_(None) Проверка на пустые значения
- contains(string) Значение столбца содержит строку
- endswith(string) Оканчивается строкой, зависит от больших букв
- like(string) зависит от больших букв
- startswith(string) зависит от больших букв
- ilike(string) не зависит от больших букв
- Есть отрицательные модификаторы not&lt;method&gt;, исключение - метод isnot()

<div id="bkmrk-%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B4%D0%BB%D1%8F-like">**Пример для LIKE**</div>```python
s = select([cookies]).where(cookies.c.cookie_name.like('%chocolate%'))
rp = connection.execute(s)
for record in rp.fetchall():
    print(record.cookie_name)
```

<div id="bkmrk-%C2%A0-%C2%A0-print%28record.coo-1"></div><div id="bkmrk-%D0%9C%D0%BE%D0%B6%D0%BD%D0%BE-%D0%BC%D0%BE%D0%B4%D0%B8%D1%84%D0%B8%D1%86%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C">Модификация полученных данных по шаблону</div><div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-1-1">Вариант 1: к каждому значению столбца</div>```python
s = select([cookies.c.cookie_name, 'SKU-' + cookies.c.cookie_sku]) - добавит строку 'SKU-'
```

<div id="bkmrk-%D0%92%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82-2%3A-%D1%87%D0%B5%D1%80%D0%B5%D0%B7-%D1%84%D1%83%D0%BD">Вариант 2: через функцию cast</div>```python
from sqlalchemy import cast 
s = select([cookies.c.cookie_name,
          cast((cookies.c.quantity * cookies.c.unit_cost),
               Numeric(12,2)).label('inv_cost')]) 
for row in connection.execute(s):
    print('{} - {}'.format(row.cookie_name, row.inv_cost))
```

<div id="bkmrk-%C2%A0-%C2%A0-print%28%27%7B%7D---%7B%7D%27."></div><div id="bkmrk-%D0%9B%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8%3A">**Логические функции**</div>```python
from sqlalchemy import and_, or_, not_
s = select([cookies]).where(
    and_(
        cookies.c.quantity > 23,
        cookies.c.unit_cost < 0.40
   )
)
```

<div id="bkmrk-%29-5"></div><div id="bkmrk-%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85">**Извлечение данных**</div>```python
first_row = results[0] 
first_row[1] 
first_row.cookie_name 
first_row[cookies.c.cookie_name]
```

<div id="bkmrk-first_row%5Bcookies.c."></div><div id="bkmrk-%D0%94%D0%BB%D1%8F-rp-%D0%B5%D1%81%D1%82%D1%8C-%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8">Для rp есть следующие варианты, однако без limit() извлекаются все данные, затем одна строка:</div>- first() - Первая запись и закрытие соединения
- fetchone() - Одна запись и оставляет открытый курсор для последующих запросов
- scalar() - Одно значение если запрос возвращает одно значение в одной строке
- rp.keys() - список столбцов

<div id="bkmrk-%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85">**Обновление данных**</div>```python
from sqlalchemy import update
u = update(cookies).where(cookies.c.cookie_name == "chocolate chip")
u = u.values(quantity=(cookies.c.quantity + 120)) 
result = connection.execute(u)
```

<div id="bkmrk-result-%3D-connection.-3"></div><div id="bkmrk-%D0%A3%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85">**Удаление данных**</div>```python
from sqlalchemy import delete
u = delete(cookies).where(cookies.c.cookie_name == "dark chocolate chip")
result = connection.execute(u)
```

<div id="bkmrk-result-%3D-connection.-4"></div><div id="bkmrk-join">**JOIN**</div>```python
columns = [orders.c.order_id, users.c.username, users.c.phone,
           cookies.c.cookie_name, line_items.c.quantity,
           line_items.c.extended_cost]
cookiemon_orders = select(columns)
cookiemon_orders = cookiemon_orders.select_from(orders.join(users).join( 
                        line_items).join(cookies)).where(users.c.username ==
                            'cookiemon')
result = connection.execute(cookiemon_orders).fetchall()
```

<div id="bkmrk-result-%3D-connection.-5"></div><div id="bkmrk-outer-join%3A">**OUTER JOIN**</div><div id="bkmrk-%D0%94%D0%BB%D1%8F-%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D1%8F-%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%BD">Для получения обратной статистики</div>```python
columns = [users.c.username, func.count(orders.c.order_id)]
all_orders = select(columns)
all_orders = all_orders.select_from(users.outerjoin(orders)) 
all_orders = all_orders.group_by(users.c.username)
result = connection.execute(all_orders).fetchall()
```

<div id="bkmrk-result-%3D-connection.-6"></div><div id="bkmrk-%D0%95%D1%81%D1%82%D1%8C-%D0%BF%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%BA%D0%B0-alias">Есть поддержка ALIAS</div><div id="bkmrk-%D0%93%D1%80%D1%83%D0%BF%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%3A">**Группировка данных**</div>```python
columns = [users.c.username, func.count(orders.c.order_id)] 
all_orders = select(columns)
all_orders = all_orders.select_from(users.outerjoin(orders))
all_orders = all_orders.group_by(users.c.username) 
result = connection.execute(all_orders).fetchall()
```

<div id="bkmrk-result-%3D-connection.-7"></div><div id="bkmrk-%D0%9E%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2">**Объединение запросов по условию**</div>```python
def get_orders_by_customer(cust_name, shipped=None, details=False):
    columns = [orders.c.order_id, users.c.username, users.c.phone]
    joins = users.join(orders)
    if details:
        columns.extend([cookies.c.cookie_name, line_items.c.quantity,
                       line_items.c.extended_cost])
        joins = joins.join(line_items).join(cookies)
    cust_orders = select(columns)
    cust_orders = cust_orders.select_from(joins)


    cust_orders = cust_orders.where(users.c.username == cust_name)
    if shipped is not None:
        cust_orders = cust_orders.where(orders.c.shipped == shipped)
    result = connection.execute(cust_orders).fetchall()
    return result
get_orders_by_customer('cakeeater') 
get_orders_by_customer('cakeeater', details=True) 
get_orders_by_customer('cakeeater', shipped=True) 
get_orders_by_customer('cakeeater', shipped=False) 
get_orders_by_customer('cakeeater', shipped=False, details=True) 
```

<div id="bkmrk-get_orders_by_custom-4"></div><div id="bkmrk-print%28result%29">  
</div>

# ORM режим

Таблица это класс с требованиями:

- Потомок объекта, возвращаемого функцией declarative\_base
- Включает \_\_tablename\_\_ с именем таблицы
- Включает 1+ атрибутов, являющихся объектом Column
- При определении не включает имя столбца в конструкторе Column, имя столбца = имя атрибута
- 1+ атрибутов определяют первичный ключ
- \_\_table\_args\_\_ свойства таблицы (ограничения,...)

```python
from sqlalchemy import Table, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
			
	Base = declarative_base() 
	
	class Cookie(Base): 
		__tablename__ = 'cookies' 
		__table_args__ = (CheckConstraint('quantity >= 0', name='quantity_positive'),)
		cookie_id = Column(Integer(), primary_key=True) 
		cookie_name = Column(String(50), index=True)
		quantity = Column(Integer())
```

**Создание таблиц**

```python
from sqlalchemy import create_engine
from dataclasses import Base

engine = create_engine(...)
Base.metadata.create_all(engine)
```

**Ограничения**

```python
__table_args__ = (ForeignKeyConstraint(['id'], ['other_table.id']), CheckConstraint(unit_cost >= 0.00', name='unit_cost_positive'))
```

**Внешние связи**

- Определяется столбец с ForeignKey
- Определяется дополнительный атрибут с relationship и необязательным backref
- При определении backref, relationship будет определен в указанном классе с указанным именем.

Один к одному:

```python
cookie = relationship("Cookie", uselist=False)
```

Один ко многим:

```python
user = relationship("User", backref=backref('orders'))
```

На себя (дерево) - неоднозначное решение.

```python
class Employee(Base):
	__tablename__ = 'employees'
	id = Column(Integer(), primary_key=True)
	manager_id = Column(Integer(), ForeignKey('employees.id'))
	name = Column(String(255), nullable=False)
	manager = relationship("Employee", backref=backref('reports'), remote_side=[id])
```

**Сессии**

В ORM режиме, сессия упаковывает

- соединение с БД через engine и предоставляет словарь объектов, загруженных через сессию или ассоциированных с сессией
- транзакции, которые открыты до коммита сессии

Это похожая на хэш-систему, состоящую из списка объектов, таблиц и ключей. Сессия создается через sessionmaker, один раз. Соединяется с базой в момент необходимости

```python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker 

engine = create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine) 
session = Session()
```

Состояния объекта в сессии:

<table border="1" id="bkmrk-transient-%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82-%D0%BD%D0%B5-" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 32.9167%;"></col><col style="width: 67.2025%;"></col></colgroup><tbody><tr><td>Transient</td><td>Объект не в сессии и не в БД</td></tr><tr><td>Pending</td><td>Объект добавлен в сессию add(), но не flush или commit</td></tr><tr><td>Persistent</td><td>Объект в сессии имеет связанную запись в БД</td></tr><tr><td>Detached</td><td>Объект больше не в сессии, но в БД есть соответствующая запись</td></tr><tr><td>Modified</td><td>Объект изменен</td></tr></tbody></table>

Просмотр состояния:

```python
from sqlalchemy import inspect
insp = inspect(cc_cookie)
```

Отключение объекта от сессии:

```python
session.expunge(cc_cookie)
```

Просмотр списка атрибутов и выяснение, что было модифицировано

```python
for attr, attr_state in insp.attrs.items():
	if attr_state.history.has_changes(): 
		print('{}: {}'.format(attr, attr_state.value))
		print('History: {}\n'.format(attr_state.history))
```

**Добавление данных**

Создаем объект класса, добавляем в сессию и коммитим. Множественное добавление данных:

```python
dcc = Cookie(...)
mcc = Cookie(...)
session.add(dcc)
session.add(mcc)
session.flush()
```

flush: не выполняет коммит и не завершает транзакцию, но получает id. Потом нужно в пределах сессии сделать commit. Commit в пределах чужой сессии не влияет. Дальше можно использовать объект.

Если дальше не нужно выполнять операции с объектами:

```
session.bulk_save_objects([dcc,mcc])
session.commit()
```

**Получение данных**

<table border="1" id="bkmrk-all%28%29-%D0%B2%D1%81%D0%B5-first%28%29-%D0%B2%D0%BE" style="border-collapse: collapse; width: 100%; height: 119.188px;"><colgroup><col style="width: 22.4025%;"></col><col style="width: 77.5975%;"></col></colgroup><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">all()</td><td style="height: 29.7969px;">все</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">first()</td><td style="height: 29.7969px;">возвращает одну запись если она единственная и закрывает соединение</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">one()</td><td style="height: 29.7969px;">возвращает одну запись и оставляет соединение. Аккуратно!</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">scalar()</td><td style="height: 29.7969px;">возвращает одно значение если результат запроса одна строка с одним столбцом</td></tr></tbody></table>

```
cookies = session.query(Cookie).all() 
print(cookies)
```

Если использовать через итератор, то без all:

```python
for cookie in session.query(Cookie): 
    print(cookie)
```

Получение определенных столбцов:

```
cookies = session.query(Cookie.cookie_id).all()
```

Сортировка:

```python
session.query(Cookie).order_by(Cookie.quantity).all()
.order_by(desc(Cookie.quantity))
```

Ограничения через срезы или .limit(2)

Встроенные функции:

```python
from sqlalchemy import func
inv_count = session.query(func.sum(Cookie.quantity)).scalar() 
print(inv_count)
rec_count = session.query(func.count(Cookie.cookie_name)).first()
```

Метки: можно добавить для дальнейшего обращения

```python
rec_count = session.query(func.count(Cookie.cookie_name).label('inventory_count')).first()
print(rec_count.inventory_count)
```

Фильтрация

```
record = session.query(Cookie).filter(cookie_name=='chocolate chip').first() 
record = session.query(Cookie).filter_by(cookie_name='chocolate chip').first()
query = session.query(Cookie).filter(Cookie.cookie_name.like('%chocolate%'))
query = session.query(Cookie).filter(Cookie.quantity > 23, Cookie.unit_cost < 0.40)
```

Изменение строк при выводе

```python
results = session.query(Cookie.cookie_name, 'SKU-' + Cookie.cookie_sku).all()
query = session.query(Cookie.cookie_name,
      cast((Cookie.quantity * Cookie.unit_cost),
           Numeric(12,2)).label('inv_cost'))
```

Join:

```python
query = session.query(Order.order_id, User.username)
results = query.join(User).all()

query = session.query(User.username, func.count(Order.order_id))
query = query.outerjoin(Order).group_by(User.username)
```

Group:

```python
query = session.query(User.username, func.count(Order.order_id))
query = query.outerjoin(Order).group_by(User.username)
```

Сырые запросы

```
from sqlalchemy import text
query = session.query(User).filter(text("username='cookiemon'"))
```

**Обновление данных**

Через объект

```python
query = session.query(Cookie) 
cc_cookie = query.filter(Cookie.cookie_name == "chocolate chip").first() 
cc_cookie.quantity = cc_cookie.quantity + 120
session.commit()
```

Через метод update

```python
query = session.query(Cookie)
query = query.filter(Cookie.cookie_name == "chocolate chip")
query.update({Cookie.quantity: Cookie.quantity - 20})
```

**Удаление**

```
session.delete(dcc_cookie)
session.commit()
```

**Исключения**

```python
from sqlalchemy.orm.exc import MultipleResultsFound 
try:
	results = session.query(Cookie).one()
except MultipleResultsFound as error: 
	print('We found too many cookies... is that even possible?')
```

**Транзакции**

В ORM транзакция создается автоматически до очередного коммита.

```python
session.add(order)
try:
	session.commit()
except IntegrityError as error: 
	session.rollback() 
```

**Пример структурирования**

db.py

```python
from datetime import datetime
from sqlalchemy import (MetaData, Table, Column, Integer, Numeric, String,
                        DateTime, ForeignKey, Boolean, create_engine)
class DataAccessLayer:
    connection = None
    engine = None
    conn_string = None
    metadata = MetaData()
    cookies = Table('cookies',
                    metadata,
                    Column('cookie_id', Integer(), primary_key=True),
                    Column('cookie_name', String(50), index=True),
                    Column('cookie_recipe_url', String(255)),
                    Column('cookie_sku', String(55)),
                    Column('quantity', Integer()),
                    Column('unit_cost', Numeric(12, 2))
              ) 
			  
    def db_init(self, conn_string): 
        self.engine = create_engine(conn_string or self.conn_string)
        self.metadata.create_all(self.engine)
        self.connection = self.engine.connect()
dal = DataAccessLayer()
```

app.py

```python
from db import dal 
from sqlalchemy.sql import select

def get_orders_by_customer(cust_name, shipped=None, details=False):
    columns = [dal.orders.c.order_id, dal.users.c.username, dal.users.c.phone]
    joins = dal.users.join(dal.orders) 
    if details:
        columns.extend([dal.cookies.c.cookie_name,
                        dal.line_items.c.quantity,
                        dal.line_items.c.extended_cost])
        joins = joins.join(dal.line_items).join(dal.cookies)
    cust_orders = select(columns)
    cust_orders = cust_orders.select_from(joins).where(
        dal.users.c.username == cust_name)
    if shipped is not None:
        cust_orders = cust_orders.where(dal.orders.c.shipped == shipped)
    return dal.connection.execute(cust_orders).fetchall()
```

test.py

```python
import unittest
class TestApp(unittest.TestCase): 
    
	@classmethod
    def setUpClass(cls): 
        dal.db_init('sqlite:///:memory:')
	
	def test_one(self):
		res = get_orders_by_customer('', False)
		self.assert_equal(res, [])
```

# Core режим

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

- Объект Table
- Декларативный класс
- Получение структуры из базы данных

**Сопоставление типов**

<table border="1" id="bkmrk-sqlalchemy-python-sq" style="border-collapse: collapse; width: 100%; height: 417.157px;"><colgroup><col style="width: 33.3333%;"></col><col style="width: 33.3333%;"></col><col style="width: 33.3333%;"></col></colgroup><thead><tr style="height: 29.7969px;"><td class="align-center" style="height: 29.7969px;">SQLAlchemy</td><td class="align-center" style="height: 29.7969px;">Python</td><td class="align-center" style="height: 29.7969px;">SQL</td></tr></thead><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">BigInteger</td><td style="height: 29.7969px;">int</td><td style="height: 29.7969px;">BIGINT</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Boolean</td><td style="height: 29.7969px;">bool</td><td style="height: 29.7969px;">BOOLEAN or SMALLINT</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Date</td><td style="height: 29.7969px;">datetime.date</td><td style="height: 29.7969px;">DATE (SQLite: STRING)</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">DateTime</td><td style="height: 29.7969px;">datetime.datetime</td><td style="height: 29.7969px;">DATETIME (SQLite: STRING)</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Time</td><td style="height: 29.7969px;">datetime.time</td><td style="height: 29.7969px;">DATETIME</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Enum</td><td style="height: 29.7969px;">str</td><td style="height: 29.7969px;">ENUM or VARCHAR</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Float</td><td style="height: 29.7969px;">float or Decimal</td><td style="height: 29.7969px;">FLOAT or REAL</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Integer</td><td style="height: 29.7969px;">int</td><td style="height: 29.7969px;">INTEGER</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Interval</td><td style="height: 29.7969px;">datetime.timedelta</td><td style="height: 29.7969px;">INTERVAL or DATE from epoch</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">LargeBinary</td><td style="height: 29.7969px;">byte</td><td style="height: 29.7969px;">BLOB or BYTEA</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Numeric</td><td style="height: 29.7969px;">decimal.Decimal</td><td style="height: 29.7969px;">NUMERIC or DECIMAL</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Unicode</td><td style="height: 29.7969px;">unicode</td><td style="height: 29.7969px;">UNICODE or VARCHAR</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">Text</td><td style="height: 29.7969px;">str</td><td style="height: 29.7969px;">CLOB or TEXT</td></tr></tbody></table>

**Metadata**

Каталог объектов Table с опциональной информацией о engine и соединении.

```python
from sqlalchemy import MetaData
metadata = MetaData()
```

**Создание таблицы**

```python
metadata.create_all(engine)
```

Метод ...create\_all не пересоздает таблицы.

Объект таблицы состоит из названия, переменной метаданных и столбцов.

```python
from sqlalchemy import Table, Column, Integer, Numeric, String, ForeignKey
from datetime import datetime
from sqlalchemy import DateTime

cookies = Table('cookies', metadata,
    Column('cookie_id', Integer(), primary_key=True), 
    Column('cookie_name', String(50), index=True), 
    Column('cookie_recipe_url', String(255)),
    Column('cookie_sku', String(55)),
    Column('quantity', Integer()),
    Column('unit_cost', Numeric(12, 2)) 
)

users = Table('users', metadata,
    Column('user_id', Integer(), primary_key=True),
    Column('username', String(15), nullable=False, unique=True), 
    Column('email_address', String(255), nullable=False),
    Column('phone', String(20), nullable=False),
    Column('password', String(25), nullable=False),
    Column('created_on', DateTime(), default=datetime.now), 
    Column('updated_on', DateTime(), default=datetime.now, onupdate=datetime.now) 
)
```

**Класс Column**

- название столбца 
    - тип данных 
        - в String обязательно указывать длину
        - Numeric(11,2) означает 11
    - доп. параметры

```python
primary_key=True
index=True
nullable=False
unique=True
default=datetime.now
onupdate=datetime.now
```

**Ключи, ограничения и индексы**

Могут быть определены в конструкторе столбца (primary\_key=True) или позже в конструкторе таблицы.

```python
from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint, CheckConstraint

users = Table(...
             PrimaryKeyConstraint('user_id', name='user_pk'),
             UniqueConstraint('username', name='uix_username'),
             CheckConstraint('unit_cost >= 0.00', name='unit_cost_positive'),
             ...)
```

Множественные ключи перечисляются через запятую.

```python
from sqlalchemy import Index
 
Index('ix_cookies_cookie_name', 'cookie_name')
Index('ix_test', mytable.c.cookie_sku, mytable.c.cookie_name)
```

Внешние связи

```python
from sqlalchemy import ForeignKey
orders = Table('orders', metadata,
    Column('order_id', Integer(), primary_key=True),
    Column('user_id', ForeignKey('users.user_id')),
    Column('shipped', Boolean(), default=False)
)
line_items = Table('line_items', metadata,
    Column('line_items_id', Integer(), primary_key=True),
    Column('order_id', ForeignKey('orders.order_id')),
    Column('cookie_id', ForeignKey('cookies.cookie_id')),
    Column('quantity', Integer()),
    Column('extended_cost', Numeric(12, 2))
)
```

Связь для поля order\_id:

```python
Column('user_id', ForeignKey('users.user_id'))
#При желании - ограничение
ForeignKeyConstraint(['order_id'], ['orders.order_id'])
```

**Добавление данных**

```python
from sqlalchemy import insert
перем = таблица.insert().values()
```

Лучше (?) вариант

```python
from sqlalchemy import insert
перем = insert(таблица).values()
```

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

```python
str(перем)
```

Компиляция запроса

```
перем.compile()
```

```
перем.compile().params
```

**Примеры**

```python
with engine.connect() as connection:
	metadata = ...
	cookies = Table...
	ins = insert(cookies).values(...)
	res = connection.execute(ins)
	#res.inserted_primary_key - какой в будущем будет ключ (сейчас фактически в БД нет данных)
	connection.commit()
```

```python
ins = cookies.insert()
inventory_list = [ 
	{
		'cookie_name': 'peanut butter',
		'cookie_recipe_url': 'http://some.aweso.me/cookie/peanut.html',
	},
	{
		'cookie_name': 'oatmeal raisin',
		'cookie_recipe_url': 'http://some.okay.me/cookie/raisin.html',
	}
]
result = connection.execute(ins, inventory_list)
```

```python
from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Numeric, String, ForeignKey

metadata = MetaData()
cookies = Table('cookies', metadata,
	Column('cookie_id', Integer(), primary_key=True), 
	Column('cookie_name', String(50), index=True), 
	Column('cookie_recipe_url', String(255))
)
engine = create_engine('sqlite:///:memory:')
connection = engine.connect()
metadata.create_all(engine)

from sqlalchemy import insert

ins = cookies.insert().values(
	cookie_name="chocolate chip",
	cookie_recipe_url="http://some.aweso.me/cookie/recipe.html"
	)
print(str(ins))
```

**Получение данных**

```python
from sqlalchemy.sql import select
s = select(cookies) 
rp = connection.execute(s)
```

Список столбцов

```
rp.keys()
```

Получение результата

```
results = rp.fetchall()
```

<table border="1" id="bkmrk-fetchall%28%29-%D0%92%D1%81%D0%B5-%D0%B7%D0%B0%D0%BF%D0%B8%D1%81" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 12.7977%;"></col><col style="width: 87.3215%;"></col></colgroup><tbody><tr><td>fetchall()</td><td>Все записи</td></tr><tr><td>first()</td><td>Возвращает одну запись если она единственная и закрывает соединение</td></tr><tr><td>fetchone()</td><td>Возвращает одну запись и оставляет соединение. Аккуратно!</td></tr><tr><td>scalar()</td><td>Возвращает одно значение если результат запроса одна строка с одним столбцом</td></tr></tbody></table>

Доступ возможен по:

<table border="1" id="bkmrk-first_row-%3D-results%5B" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 32.7679%;"></col><col style="width: 67.2321%;"></col></colgroup><tbody><tr><td>first\_row = results\[0\]</td><td>по индексу результата</td></tr><tr><td>first\_row\[1\]</td><td>по номеру столбца в результате</td></tr><tr><td>first\_row.cookie\_name</td><td>по имени столбца</td></tr><tr><td>first\_row\[cookies.c.cookie\_name\]</td><td>через объект таблицы</td></tr></tbody></table>

Сортировка

```python
s = select(cookies.c.cookie_name, cookies.c.quantity)
s = s.order_by(cookies.c.quantity)
s = s.order_by(desc(cookies.c.quantity))
```

Ограничения количества

```python
s = s.limit(2)
```

Встроенные функции

```python
from sqlalchemy.sql import func
s = select([func.sum(cookies.c.quantity)])
rp = connection.execute(s)
print(rp.scalar())
```

Фильтрация

```python
s = select([cookies]).where(cookies.c.cookie_name == 'chocolate chip')
```

<table border="1" id="bkmrk-%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80-%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-%3D%3D" style="border-collapse: collapse; width: 100%; height: 297.969px;"><colgroup><col style="width: 33.0358%;"></col><col style="width: 67.0834%;"></col></colgroup><thead><tr style="height: 29.7969px;"><td class="align-center" style="height: 29.7969px;">Оператор</td><td class="align-center" style="height: 29.7969px;">Описание</td></tr></thead><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">==</td><td style="height: 29.7969px;">Точное равенство</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">like('%chocolate%')</td><td style="height: 29.7969px;">Вхождение элемента (регистрозависимый)</td></tr><tr><td>ilike(string)</td><td>Вхождение элемента</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">between(cleft, cright)</td><td style="height: 29.7969px;">Элемент между значениями</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">concat(column\_two)</td><td style="height: 29.7969px;">Объединение столбцов</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">distinct()</td><td style="height: 29.7969px;">Только уникальные значения столбца</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">in\_(\[list\])</td><td style="height: 29.7969px;">Значения столбца в списке</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">is\_(None)</td><td style="height: 29.7969px;">Значение в столбце None</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">contains(string)</td><td style="height: 29.7969px;">Содержит в себе строку (регистрозависимый)</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">endswith(string)</td><td style="height: 29.7969px;">Заканчивается строкой (регистрозависимый)</td></tr><tr><td>startswith(string)</td><td>Начинается строкой (регистрозависимый)</td></tr><tr><td>notin\_()</td><td>Отрицание</td></tr><tr><td>isnot()</td><td>Исключение</td></tr></tbody></table>

Внутри where можно использовать and\_, or\_, not\_

```python
from sqlalchemy import and_, or_, not_
s = select([cookies]).where(
	and_(
		cookies.c.quantity > 23,
		cookies.c.unit_cost < 0.40
	)
)
```

Join

```python
columns = [orders.c.order_id, users.c.username, users.c.phone,
		cookies.c.cookie_name, line_items.c.quantity, line_items.c.extended_cost]
cookiemon_orders = select(*columns)
cookiemon_orders = cookiemon_orders.select_from(orders.join(users).join(line_items).join(cookies)).where(users.c.username == 'cookiemon')
result = connection.execute(cookiemon_orders).fetchall()
for row in result:
	print(row)
```

Для outerjoin: join -&gt; outerjoin

Алиасы

```python
manager = employee_table.alias('mgr')
```

Grouping:

```python
columns = [users.c.username, func.count(orders.c.order_id)] 
all_orders = select(columns)
all_orders = all_orders.select_from(users.outerjoin(orders))
all_orders = all_orders.group_by(users.c.username)
```

Обновление данных:

```python
from sqlalchemy import update
u = update(cookies).where(cookies.c.cookie_name == "chocolate chip")
u = u.values(quantity=(cookies.c.quantity + 120)) 
result = connection.execute(u)
```

Удаление данных:

```python
from sqlalchemy import delete
u = delete(cookies).where(cookies.c.cookie_name == "dark chocolate chip")
result = connection.execute(u)
```

Сырые запросы (raw)

```python
result = connection.execute("select * from orders").fetchall()
```

**Обработка исключений**

AttributeError - ошибка набора данных  
IntegrityError - ошибка ограничений  
Стандартная обработка подходит если исполняется один независимый запрос.

```python
try:
	result = connection.execute(ins)
except IntegrityError as error: 
	print(error.orig.message, error.params)
```

В случае нескольких взаимозависимых запросов необходимо использовать транзакции.

```python
transaction = connection.begin() 
try:
	...
	transaction.commit() 
except IntegrityError as error:
	transaction.rollback()
```

Пример:

```python
transaction = connection.begin() 
cookies_to_ship = connection.execute(s).fetchall() 
try:
	for cookie in cookies_to_ship:
		u = update(cookies).where(cookies.c.cookie_id == cookie.cookie_id)
		u = u.values(quantity = cookies.c.quantity-cookie.quantity)
		result = connection.execute(u)
	u = update(orders).where(orders.c.order_id == order_id)
	u = u.values(shipped=True)
	result = connection.execute(u)
	print("Shipped order ID: {}".format(order_id))
	transaction.commit() 
except IntegrityError as error:
	transaction.rollback() 
```

# Пример проекта

**Структура проекта**

<table border="1" id="bkmrk-%D0%94%D0%B8%D1%80%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%B8%D1%8F-%2F-%D1%84%D0%B0%D0%B9%D0%BB-%D0%9E%D0%BF" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 35.272%;"></col><col style="width: 64.728%;"></col></colgroup><thead><tr><td class="align-center">Директория / файл</td><td class="align-center">Описание</td></tr></thead><tbody><tr><td>alembic/  
</td><td>Настройки alembic  
</td></tr><tr><td>conf/</td><td>Настройки окружений. </td></tr><tr><td>conf/settings  
</td><td>Файлы основных настроек.

</td></tr><tr><td>db/</td><td>Описание структуры базы данных.

initializer.py - Инициализация базы данных, метаданных

</td></tr><tr><td>db/tablesdefinition  
</td><td>Файлы описания структур таблиц и методов взаимодействия с данными.

</td></tr><tr><td>docker/</td><td>Настройки контейнера</td></tr><tr><td>docker/data</td><td>Данные БД</td></tr><tr><td>docker/docker-entrypoint-initdb.d</td><td>Скрипты инициализации БД

main.sql - Файл скрипта иницализации

</td></tr><tr><td>docker/docker-compose.yml</td><td>Compose файл</td></tr><tr><td>src/</td><td>Дополнительные модули  
</td></tr><tr><td>main.py</td><td>Точка входа</td></tr><tr><td>error.log  
</td><td>Файл лога.  
</td></tr></tbody></table>

**Предварительная настройка**

Для работы примера необходимо установить docker.

Клонировать проекта с репозитория

```bash
git clone https://gitverse.ru/bobrobot/alembictemplate.git
```

Перейти в директорию проекта, создать виртуальное окружение и активировать

```
cd alembictemplate
python3 -m venv env
source env/bin/activate
```

Установить дополнительные модули

```bash
pip install -r requirements.txt
```

Перейти в директорию docker и в файле docker-compose.yml настроить пути, имя БД, логин и пароль к новой базе данных.

```yaml
services:
  postgres:
    image: postgres:latest
    environment:
      POSTGRES_DB: "learnsqlalchemy"
      POSTGRES_USER: "learner"
      POSTGRES_PASSWORD: "StrongPassword123"
      PGDATA: "/home/sergey/projects/alembictemplate/docker/data/pgdata"
    volumes:
      - .:/docker-entrypoint-initdb.d
      - mydata:/home/sergey/projects/alembictemplate/docker/data
    ports:
      - "5430:5432"
volumes:
  mydata:
```

В файле docker-entrypoint-initdb.d/main.sql изменить имя БД, логин и пароль.

```sql
CREATE DATABASE learnsqlalchemy;
CREATE USER learner WITH PASSWORD 'StrongPassword123';
ALTER ROLE learner WITH PASSWORD 'StrongPassword123';
GRANT ALL PRIVILEGES ON DATABASE learnsqlalchemy to learner;
```

В директории docker запустить контейнер БД в фоновом режиме.

```bash
docker compose up -d
```

Для остановки контейнера:

```bash
docker compose stop
```

Сейчас, запустив контейнер, при помощи консольного клиента psql можно проверить соединение с базой данных для пользователя.

```
psql -d learnsqlalchemy -U learner -W -h 127.0.0.1 -p 5430
```

Для работы с настройками в формате json используется библиотека src/libsettings.py [Описание библиотеки](http://bobrobotirk.ru/books/python/page/pip-opisanie-modulei "Описание модулей") Создать папку src, скопировать из проекта библиотеку libsettings.py

**Настройки системы**

Файлы основных настроек расположены в conf/settings/ Файл base.py

```python
'''Loading settings to project'''
import os
from src.libsettings import Jsettings

settingspath = os.path.join('conf', 'settings', 'settings.json')
schemapath = os.path.join('conf', 'settings', 'schema.json')
# dev settings
mysettings = Jsettings(settingsfname= settingspath,
                       schemafname=schemapath)
mysettings.load_settings()
```

Файл schema.json

```json
{
    "type": "object",
    "properties": {
        "db_username": {"type": "string"},
        "db_password": {"type": "string"},
        "db_host": {"type": "string"},
        "db_port": {"type": "string"},
        "db_name": {"type": "string"}
    }, 
    "required": ["db_username", "db_password", "db_host",
                "db_port", "db_name"]
}
```

Файл settings.json

```json
{
    "db_username": "learner",
    "db_password": "StrongPassword123",
    "db_host": "127.0.0.1",
    "db_port": "5430",
    "db_name": "learnsqlalchemy"
}
```

Файл base.py проверяет схему и создает объект настроек mysettings из файла settings.json. Для получения объекта настроек нужно импортировать объект mysettings в нужном модуле. В данный момент присутствуют настройки базы данных с префиксом db\_\*

Если такая усложненная система управления настройками покажется излишней, возможно использовать экспорт настроек напрямую из файла.

**Инициализация базы данных**

Создаем папку db, в ней создаем файл initializer.py

```python
'''Db classes and  initialization'''
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from conf.settings.base import mysettings

#load engine settings
engine = create_engine(
    "postgresql+psycopg2://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}".format(
        db_username=mysettings.db_username,
        db_password=mysettings.db_password,
        db_host=mysettings.db_host,
        db_port=mysettings.db_port,
        db_name=mysettings.db_name
    ),
    echo=True)
Base = declarative_base()
```

Здесь только создается engine для подключения к БД и класс Base. При настройке структуры таблиц данный файл будет обновлен, сейчас нужен только класс Base.

**Настройка системы версионирования базы данных alembic**

Инициализируем alembic в корне проекта.

```bash
alembic init alembic
```

В корне проекта будет создан файл alembic.ini, будет создана папка alembic с файлами инициализации. В большинстве инструкций параметры подключения задаются в файле alembic.ini однако, для доступа к настройкам из единой точки будет изпользоваться способ установки параметров в файле env.py Поэтому в файле alembic.ini переменная sqlalchemy.url должна быть закомментирована. Часть файла alembic.ini

```python
#sqlalchemy.url = driver://user:pass@localhost/dbname
```

В файле env.py

- импортируем путь

```python
import os
import sys

sys.path.append(os.getcwd())
```

Импортируем настройки, создаем строку соединения и создаем закоментированный ранее в файле alembic.ini параметр sqlalchemy.url

```python
from conf.settings.base import mysettings

connstring = "postgresql+psycopg2://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}".format(
        db_username=mysettings.db_username,
        db_password=mysettings.db_password,
        db_host=mysettings.db_host,
        db_port=mysettings.db_port,
        db_name=mysettings.db_name
    )
config.set_main_option(name="sqlalchemy.url", value=connstring)

```

Импортируем db.initializer и создаем метаданные

```python
import db.initializer

target_metadata = db.initializer.Base.metadata
```

Остальные параметры оставляем неизменными. Результирующий файл настроек окружения alembic env.py:

```python
from logging.config import fileConfig
import os
import sys

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

from conf.settings.base import mysettings

sys.path.append(os.getcwd())

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

connstring = "postgresql+psycopg2://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}".format(
        db_username=mysettings.db_username,
        db_password=mysettings.db_password,
        db_host=mysettings.db_host,
        db_port=mysettings.db_port,
        db_name=mysettings.db_name
    )
config.set_main_option(name="sqlalchemy.url", value=connstring)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
import db.initializer

target_metadata = db.initializer.Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline() -> None:
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online() -> None:
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

```

**Обновление конфигурации таблиц**

Для проверки создаем первую пустую миграцию. После ее выполнения создастся таблица alembic\_version

```bash
alembic revision -m "Empty Init"
```

В данный момент фактического соединения с БД не было. В папке versions сформируется файл вида &lt;id&gt;\_empty\_init.py

После выполнения команды

```bash
alembic upgrade head
```

в таблице alembic\_version появится одна запись - идентификатор текущей версии базы данных.

Сейчас в папке db создаем папку tablesdefinition. В ней будем хранить файлы описаний таблиц и методы для работы с таблицами. Создадим файл userprofile.py

```python
'''Definition tables of userprofile'''
import logging
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker
import db.initializer
from db.initializer import engine

def create_userprofile_class(Curbase):
    class Userprofile(Curbase):
        '''Class Userprofile definition'''
        __tablename__ = 'userprofile'

        user_id = Column(Integer(), primary_key=True)
        username = Column(String(15), nullable=False, unique=True)
        password = Column(String(255), nullable=False)
        email = Column(String(255))
        name = Column(String(100))
        second_name = Column(String(100))
        photo = Column(String(255))
        balance = Column(Integer())
    return Userprofile

def create_one_userprofile(username, password, email='', name='',
                           second_name='', photo='', balance=0):
    ''' Create one userprofile '''
    try:
        with engine.connect():
            Session = sessionmaker(bind=engine)
            with Session() as sess:
                upelem = db.initializer.userprofile(username=username, password=password,
                                     email=email, name=name, second_name=second_name,
                                     photo=photo, balance=balance)
                sess.add(upelem)
                sess.commit()
    except Exception as e:
        logging.error(e)
```

И в файле initializer.py после инициализации переменной Base добавим раздел инициализации описания таблицы

```python
#============================ Creation classes definitions =========================
# === Import Userprofile class ===
from db.tablesdefinition.userprofile import create_userprofile_class
userprofile = create_userprofile_class(Base)
# ==================================================================================
```

Теперь после выполнения команды

```bash
alembic revision --autogenerate -m "Added userprofile model"
```

будет автоматически сгененрирован файл миграции, и после

```bash
alembic upgrade head
```

создастся таблица userprofile.

P.s. В точке входа необходимо полностью импортировать initializer иначе будет ошибка, пример:

```python
'''Main learning module'''
import logging

from db import initializer
from db.tablesdefinition.userprofile import create_one_userprofile

logging.basicConfig(level=logging.INFO,
                    filename='error.log',
                    format="%(levelname)s %(message)s")

if __name__ == '__main__':
    create_one_userprofile(username = 'first6',
                           password = 'first',
                           balance = 1)
```