В данной заметке будут предложены 3 примера работы с БД из Python:
- Работа через адаптер sqlite3.
- Взаимодействие через sqlalchemy.Table.
- Использование sqlalchemy.orm для тех же задач.
- Бонус - PonyORM!
Хочется сконцентрироваться на различиях и преимуществах, которые дают те или иные подходы.
Сразу обозначу условную задачу, которую будем решать: нужно создать программу / набор функций, позволяющих хранить и администрировать ссылки на фотографии и метки (теги) на эти фотографии.
 
    
    Взаимодействие через sqlalchemy.Table
Итак, мы уже имеем реализацию через sqlite3. Она имела ряд преимуществ:
- полный контроль над тем, какой же SQL код в итоге выполнится,
- явное написание SQL кода упростит использование тем, кто больше работал с SQL базами данных, чем с Python.
Однако, эти же преимущества могут быть и проблемами:
- при написании кода приходится постоянно перестраиваться с одного языка программирования на другой,
- среды разработки (IDE) обычно подсвечивают и автодополняют только код на одном языке.
В целом, если в коде одновременно встречаются вставки на разных языках программирования, это затрудняет работу над проектом.
Так что давайте немного отойдём от запросов на чистом SQL, и перейдём к использованию генераторов SQL запросов. Например, sqlalchemy. Для начала опишем таблицы и создадим их:
    from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey, create_engine, insert
    metadata = MetaData()
    engine = create_engine('sqlite:///example.db', echo=True)
    PhotoTable = Table(
        'photo', metadata,
        Column('id', Integer, primary_key=True),
        Column('url', String),
    )
    TagTable = Table(
        'tag', metadata,
        Column('id', Integer, primary_key=True),
        Column('name', String, unique=True),
    )
    PhotoTagTable = Table(
        'photo_tag', metadata,
        Column('photo_id', Integer, ForeignKey("photo.id"), nullable=False),
        Column('tag_id', Integer, ForeignKey("tag.id"), nullable=False),
    )
    metadata.create_all(engine)Создаём metadata для хранения информации о схеме базы данных. А также engine - как
адаптер к базе данных.
Далее - описание таблиц. Очень похоже на описание SQL-таблиц, но на Python. Сравните с описанием таблиц из предыдущего примера.
И в итоге - metadata.create_all(engine) - создаём описанные таблицы в базе данных.
Для добавления записей в таблицы (INSERT-ов) заведём функцию и создадим в ней нужные строки БД
для демонстрации запросов:
    def insert_data(conn):
        # Create photo
        stmt = insert(PhotoTable).values(url='https://900913.ru/static/img/logo-192x192.png')
        result = conn.execute(stmt)
        photo_id = result.lastrowid
        # Create tag
        stmt = insert(TagTable).values(name='python')
        tag_id = conn.execute(stmt).lastrowid
        # Create photo-tag link
        stmt = insert(PhotoTagTable).values(photo_id=photo_id, tag_id=tag_id)
        conn.execute(stmt)
        # Create one more tag and photo-tag link
        stmt = insert(TagTable).values(name='website')
        tag_id = conn.execute(stmt).lastrowid
        stmt = insert(PhotoTagTable).values(photo_id=photo_id, tag_id=tag_id)
        conn.execute(stmt)В insert(PhotoTable).values(url=...) создаётся запрос. Вывести его мы можем, добавив
print(stmt) строчкой ниже. В частности, данная конструкция преобразуется в запрос:
INSERT INTO photo (url) VALUES (:url).
Далее выполняем запрос conn.execute(stmt) и получаем объект результата запроса.
Как это было с курсором из примера запросов на sqlite3, result имеет атрибут lastrowid -
из него мы можем получить идентификатор добавленной строки. Также в SELECT запросах
можно применять fetchone() и fetchall() для получения результатов запроса.
Теперь перейдём к непосредственно запросу:
    def main():
        conn = engine.connect()
        insert_data(conn)
        stmt = (
            PhotoTable.select()
            .join(PhotoTagTable, PhotoTagTable.c.photo_id == PhotoTable.c.id)
            .join(TagTable, TagTable.c.id == PhotoTagTable.c.tag_id)
            .where(TagTable.c.name == 'website')
        )
        result = conn.execute(stmt)
        for id, url in result:
            print(url)Ровно такое же запрос, как и был в примере sqlite3: получаем фотографии, отмеченные указанным тегом.
Заметим, что вместо result.fetchall() мы используем for для итеративного прохода по данным.
Отмечу, что для указания полей в запросах используется атрибут .c класса таблицы.
Также для примера - запросы обновления и удаления:
    stmt = TagTable.update().where(TagTable.c.name == 'website').values(name='web')
    conn.execute(stmt)
    stmt = TagTable.delete().where(TagTable.c.name == 'web')
    conn.execute(stmt)В итоге мы получили возможность обращаться к базе данных на Python, пусть запросы и очень похожи на SQL.
- Работа через адаптер sqlite3.
- Взаимодействие через sqlalchemy.Table.
- Использование sqlalchemy.orm для тех же задач.
- Бонус - PonyORM!