Не секрет, что SQLAlchemy — самая популярная ORM на Python. Она позволяет писать куда более продвинутые вещи, чем большинство Active Record собратьев. Но плата за это — более сложный код, и в простых задачах вроде CRUD это напрягает.


О том, как я сделал Алхимию удобной, воспользовавшись опытом лучших Active Record ORM, читайте под катом.



Введение


Я начал использовать SQLAlchemy, предварительно работав с Active Record в Ruby on Rails, c Eloquent ORM в PHP и c Django ORM и Peewee в Python. Все эти Active Record ORM имеют лаконичный код, и мне они очень нравятся.


Алхимия очень хороша: может строить совсем сложные запросы, да и паттерн Data Mapper рулит. Но, увы, после Active Record код Алхимии кажется слишком сложным, и это плата за гибкость.


В течение года я работал с Алхимией на сложном проекте, и понял, что для полного счастья хочется иметь:


  1. Удобный CRUD: для простого create в Алхимии надо создать объект, да добавить его в сессию, да сделать flush
  2. Динамическое построение фильтров/сортировки на основе магических строк как в Django: Post.objects.filter(user__name__startswith='John')
  3. Вложенный eager load, когда нужно с комментарием сразу загрузить пост, а к посту его юзера (ладно, он есть, но не очень удобен)
  4. Информативный __repr__: print(post) выдаёт что-то вроде <myapp.models.Post at 0x04287A50>, для отладки это не годится

Я с грустью вспоминал Active Record ORM, где всё это было (конечно, в каждой ORM были не все фичи, но большинство). Время шло, проект становился всё сложнее, и в конце концов не мог стерпеть и решил описанные задачи.


Решение оформлено в хорошо оттестированный и документированный пакет. Работает на боевом проекте и чувствует себя очень хорошо.


Есть готовые решения, скажете вы!

Да, есть, но они либо тяжело внедряются, либо заточены под конкретный фреймворк, либо плохо документированы.
Хотелось иметь универсальное, легко подключаемое решение, чтобы, к примеру, написать


from прекрасный_модуль import ActiveRecordMixin

class User(Base, ActiveRecordMixin):
     pass

и иметь готовый Active Record.
Варианты "инициализируйте Алхимию только через меня" и дополнения к flask-sqlalchemy не годятся.


Чем не устраивают конкретные пакеты, см. тут.


Подробнее о каждой задаче — ниже в статье.


О примерах в статье


Я буду приводить примеры для простенького блога с типовыми сущностями User, Post, Comment.


Схема БД и код ORM

DB


class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    posts = relationship('Post')

class Post(Base):
    __tablename__ = 'post'
    id = Column(Integer, primary_key=True)
    body = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))

    user = relationship('User')
    comments = relationship('Comment')

class Comment(Base):
    __tablename__ = 'comment'
    id = Column(Integer, primary_key=True)
    body = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))
    post_id = Column(Integer, ForeignKey('post.id'))
    rating = Column(Integer)

    user = relationship('User')
    post = relationship('Post')

С кодом можно поиграться тут.
В нём ORM-классы, инициализация чистой Алхимии (без моего пакета) и наполнение начальными данными.


Как запустить

Установите Алхимию:


pip install sqlalchemy

Потом сохраните тестовое приложение в файл и запустите:


python файл.py

Active Record


После Active Record ORM, я не понимал, почему я должен для создания объекта писать 3 строчки


bob = User(name='Bobby', age=1)
session.add(bob)
session.flush()

вместо одной?


 bob = User.create(name='Bobby', age=1)

Я понимаю, что ручной flush сессии нужен, чтобы запросы в БД пошли одной пачкой, да и вообще паттерн unit of work даёт много преимуществ в плане производительности.


Но в реальных веб-приложениях большинство задач — тривиальный CRUD, и оттого, что в БД будет делаться не 3 запроса, а один, выигрыш невелик. Во всяком случае, он не стоит такого усложнения кода. Да и вообще, не зря же создатели Django, Ruby on Rails, Laravel, Yii выбрали Active Record ORM.


Что ж, ничто не мешает реализовать Active Record поверх Data Mapper! Для этого всего-то и надо, что при инициализации приложения сессию передать модели


BaseModel.set_session(session) # это базовый класс ОРМ
# теперь у нас есть доступ к BaseModel.session

Теперь ОРМ имеет доступ к сессии, и можно реализовывать методы save, create, update, delete и т.д.


bob = User.create(name='Bobby', age=1)
bob.update(name='Bob', age=21)
bob.delete()

Насчёт update

Вообще-то, на более низком слое SQLAlchemy апдейт полей из массива есть, но хочется его иметь на уровне ORM.


Ну и ещё хочется быстро создать запрос на модель


User.query # вместо session.query(User)

и быстро достать первую или все записи


User.first() # вместо session.query(User).first()
User.all() # вместо session.query(User).all()

или найти запись по id, обвалившись ошибкой если надо


User.find(1) # вместо session.query(User).get(1)
User.find_or_fail(123987) # выбросит исключение, если не найдено

В итоге у нас получается полноценный Active Record как в любимых мною Django, Laravel и Ruby on Rails, но под капотом у нас мощный Data Mapper. Таким образом, мы имеем лучшее из двух миров.


Я не против стандартного подхода Алхимии, но для простых задач хочется иметь более простой код (см. спойлер в конце статьи)

Подробное описание и примеры см. тут.


Eager Load


Для решения проблемы N+1 запросов каждая ORM имеет свои решения.


Допустим, мы отображаем на странице 10 юзеров и все посты каждого юзера. Чтобы не вышло 11 запросов (1 на юзеров и 10 на посты), в SQLAlchemy можно эти посты приджойнить


session.query(User).options(joinedload('posts'))

или загрузить отдельным запросом


session.query(User).options(subqueryload('posts'))

Что ж, прекрасно! Только вот если надо с постами отображать комментарии, а с комментариями их авторов? Алхимия это позволяет, но на практике это оказалось неудобно.


Хочется декларативно задавать отношения, которые мы хотим подгрузить:


User.with_({
    'posts': {
        'comments': {
            'user': JOINED
        }
    }
}).all()

можно и без магических строк:


User.with_({
    User.posts: {
        Post.comments: {
            Comment.user: JOINED
        }
    }
}).all()

Кроме того, можно задавать разную стратегию загрузки: joinedload или subqueryload.
Следующий код приведёт к 2 запросам: на посты (с приджойненными авторами постов) и на комментарии (с приджойненными авторами комментариев):


Post.with_({
    'user': JOINED, # джойним юзера
    'comments': (SUBQUERY, {  # но комментарии грузим отдельным запросом 
        'user': JOINED  #  и в этом отдельном запросе к каждому комментарию джойним автора 
    })
}).all()

Ну и для простых случаев, когда нужен joinedload или subqueryload пары-тройки отношений, есть простой синтаксис (метод with_subquery вышел совсем как в Eloquent):


Comment.with_joined('user', 'post', 'post.comments').all()
# то же самое на чистой Алхимии:
# session.query(Comment).options(joinedload('user'), joinedload('post'), 
#                                joinedload('post.comments')).all()

User.with_subquery('posts', 'posts.comments').all()
# то же самое на чистой Алхимии:
# session.query(User).options(subqueryload('posts'), subqueryload('posts.comments')).all()

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


Подробное описание с примерами можно найти тут.


P.S. Отдельное спасибо моим коллегам за код.


Магические операторы и join отношений, как в Django


Первое, что мне бросилось в глаза при изучении Django — это магические операторы в фильтрах:


Entry.objects.filter(headline__startswith="What")

и совсем поразила фильтрация по связям:


Entry.objects.filter(blog__name='Beatles Blog')

это проще, чем более "правильное" решение в Алхимии:


session.query(Entry).join(Entry.blog).filter(Blog.name=='Beatles Blog')

Хотя...
  • конечно, это менее гибко, сложных запросов тут не выйдет
  • магические строки и могут потенциально дать баг в Runtime, если сделать опечатку, например вместо blog__name написать blogg__name. Такие строки, в отличие от свойств класса вроде Entry.blog, IDE не будет инспектировать.

Помимо эстетики, магические строки позволяют строить запросы динамически (например, передавая фильтры с UI):


filters =  {'entry__headline__contains': 'Lennon', 'entry__pub_date__year': 2008} # это мог передать фронтенд
Blog.objects.filter(**filters)

Это особенно полезно в приложениях, где пользователь может строить произвольные фильтры.


Увы, в Алхимии нет возможности строить запросы столь динамично. Максимум, что она позволяет — простенькую фильтрацию типа "колонка=значение":


session.query(MyClass).filter_by(name = 'some name')

Взяв за образец готовое решение (которого всё же было недостаточно), я сделал свой аналог Джанги и теперь можно фильтровать декларативно:


Post.where(rating__in=[2, 3, 4], user___name__like='%Bi%').all()

Как это сделано

Строка user___name__like парсится и мы понимаем, что надо приджойнить отношение Post.user и применить фильтр User.name.like('...').
То есть


Post.where(user___name__like='%Bi%').all()

превращается в


session.query(Post).join(Post.user).filter(User.name.like('%Bi%')).all()

А на самом деле всё сложнее

Вообще-то может статься так, что в запросе какая-то таблица возникнет 2 раза.
Допустим, я хочу достать юзеров, посты которых комментировал Вася


User.where(posts___comments___user___name='Vasya').all()

Получается, есть юзер, которого я запрашиваю, а есть автор комментария.
Проблему решают через alias'ы, т.е. в итоговом запросе будут присутствовать 2 таблицы: user и user_1.


Конечно, мы не можем заранее знать, будут ли повторяться таблицы, поэтому делаем каждому отношению, которое джойним, свой alias:


post_alias = User.posts.property.argument() # так можно вытащить целевой класс из relationship
session.query(User).outerjoin(post_alias) # и т.д.

Вот упрощенный аналог реального кода:


from sqlalchemy.orm import aliased
from sqlalchemy.sql import operators

# Имеем на входе {'posts___comments___user___name__like': 'Vasya'}. Достанем:
relations = ['posts', 'comments', 'user'] # 1. отношения, они были разделены ___
attr_name = 'name' # 2. аттрибут, он был после последнего ___
op_name = 'like' # 3. оператор, он был после __
# получаем оператор Алхимии на основе op_name.
# в реале имеется фиксированное соответствие OPERATORS = {'like': operators.like_op},
# и из него оператор достаётся как OPERATORS[op_name]
operator = operators.like_op

value = 'Vasya'

cls = User # в жизни это статический метод и текущий класс хранится в cls
query = session.query(cls) # делаем начальный запрос

# джойним все связи в цикле
last_alias = cls
for relation in relations:
    relation = getattr(last_alias, relation) # берём relation, например User.posts
    next_alias = aliased(relation.property.argument()) # достаём целевой класс (Post для User.posts) 
                                                       # и делаем на него alias
    query = query.outerjoin(next_alias) # джойним класс/алиас
    last_alias = next_alias

# теперь применим SQL-оператор к последнему классу/алиасу в цепочке (у нас это User)
attr = getattr(last_alias, attr_name) # получаем реальный аттрибут User.name
query = query.filter(operator(attr, value)) # применим SQL-оператор, передав ему аттрибут User.name и Васю
print(query.all())

Вот готовый к запуску код.


и сортировать:


Post.sort('-rating', 'user___name').all() # sort by rating DESC, user name ASC

Автоматический eager load


Более того, раз уж мы автоматически делаем join связей, логично указать SQLAlchemy, что указанные связи уже приджойнены, при помощи contains_eager. Теперь, если отношение Post.user использовалось в фильтре/сортировке, то мы сможем достать юзера без дополнительного запроса:


post = Post.sort('user___name').first()
print(post.user) # не потребуется дополнительного запроса в БД, т.к. юзер был приджойнен

Подробное описание с примерами см. тут.


Если Вы не любите магию


Вот пример магической фильтрации и сортировки в реальном проекте.
Вопрос ко всем, особенно к нелюбителям магических строк:


  1. Сколько строк понадобится для подобных фильтров и сортировки на чистой Алхимии?
  2. Насколько получившийся чисто-алхимический правильный код будет понятным и поддерживаемым?
  3. Насколько легко будет его реюзать?
  4. Итого, что лучше: немного оттестированной, документированной магии или кастомный велосипед на чистой Алхимии по 100 строк каждый раз, когда нужны сложные фильтры?

Всё в кучу: фильтры, сортировка, eager load


В реальном мире приходится одновременно фильтровать, сортировать, да ещё и eager load'ить связи.


Допустим, мы фильтруем и сортируем посты по одному и тому же отношению Post.user. Может статься, что фильтрация и сортировка 2 раза приджойнят одно и то же отношение, что скажется на быстродействии.


Разве Алхимия сама не разберётся?

Если просто писать


session.query(Post).join(Post.user).join(Post.user)

то, действительно, Алхимия сделает только один join.
Штука в том, что мы для каждого отношения делаем свой alias (см. спойлер "как это сделано" выше), и поэтому Алхимия не знает, что 2 alias-а на Post.user — это по сути одно и то же, и надо следить за этим самостоятельно.


Поэтому фильтрацию, сортировку и eager load (да, его тоже) пришлось сделать в одной функции, чтобы иметь информацию о всех требуемых джоинах (точнее, иметь единый список alias-ов, см. спойлер "как это сделано") и делать их только один раз:


 Comment.smart_query(
     filters={
         'post___public': True,
         'user__isnull': False
     },
     sort_attrs=['user___name', '-created_at'],
     schema={
         'post': {
             'user': JOINED
         }
     }).all()

Подробное описание с примерами можно найти тут.


Удобная отладка с удобным __repr__


Мне, как разработчику, очень важно играться с приложением в консоли. И было очень неприятно после красоты в Laravel видеть вот такое:


>>> session.query(Post).all()
[<myapp.models.Post object at 0x04287A50>, <myapp.models.Post object at 0x04287A90>]

Это жутко неинформативно и очень бесит. Поэтому я решил выводить хотя бы ID


>>> session.query(Post).all()
[<Post #11>, <Post #12>]

Как универсально получить ID
from sqlalchemy import inspect
ids = inspect(post).identity # вернёт кортеж со значениями primary ключей

А вообще у каждой модели есть 1-2 аттрибута, которые отражают её суть. Например у юзера это имя, а у поста это юзер и тело поста. Поэтому я сделал возможность задать эти аттрибуты декларативно:


class User(BaseModel):
    __repr_attrs__ = ['name']
    # ...

class Post(BaseModel):
    __repr_attrs__ = ['user', 'body'] # body is just column, user is relationship
    # ...

Теперь имеем совсем удобный __repr__:


>>> session.query(Post).all()
[<Post #11 user:<User #1 'Bill'> body:'post 11'>,
 <Post #12 user:<User #2 'Bob'> body:'post 12'>]

Подробное описание с примерами тут.


Итоги


Реализованы фичи, без которых лично мне очень тяжко работалось с Алхимией.
Теперь жить стало легче, ведь:


  1. C Active Record мы можем быстро делать CRUD, не заморачиваясь с flush-ем сессии.
  2. С мощной фильтрацией/сортировкой в стиле Django мы можем делать очень сложные, динамические джойны в одну строку, которую можно хоть с UI передать.
  3. Мы может задавать eager load декларативно.
  4. Отладка стала удобнее с информативным __repr__.

На боевом проекте это помогло сильно упростить поддержку и повысить читабельность кода.
Все фичи, опять же, реализованы тут.


Если вам кажется, что: (1) всё это зло; (2) я против чистой Алхимии; (3) я, не разобравшись, полез её улучшать
  1. Я очень плотно работаю с Алхимией уже год на сложном боевом проекте. Все задачи, которые я привёл — не прихоть, а реальная необходимость.


  2. Я люблю Алхимию и её стандартный подход. Мне нравится её гибкость, контроль над SQL, явность и т.д. Я считаю Алхимию лучшей ORM, что я встречал. Никакой Active Record с ней не сравнится.


  3. Но плата за гибкость — более длинный код. И в простых задачах вроде CRUD код Алхимии сильно сложней аналогичного в Active Record ORM.
    Поэтому я и написал свой пакет, который иногда можно использовать как дополнение к чистой Алхимии.
    Мой пакет — лишь надстройка над Алхимией. Я до сих пор люблю Алхимию такой, какая она есть и не предлагаю отказаться от её изначального использования.


  4. Об Active Record:
    я понимаю всю мощь паттерна unit of work для оптимизации производительности приложения. Но в простых задачах, когда нужно в контроллере просто создать объект, писать для этого одни и те же 3 строки (создать объект, добавить в сессию, flush сессии) не вдохновляет. Поэтому я и сделал Active Record поверх, а не вместо Data Mapper для простых задач.


  5. Об фильтрации/сортировке в стиле Django:


    • я знаю, что магические строки вроде user___name__like — это неявно и чревато ошибками в runtime. Но иногда это может быть удобно.


    • я тоже предпочитаю писать явно: session.query(Post).join(Post.user).filter(User.name.like(...)), но бывает, что заранее не знаешь, по какому полю фильтровать, или фильтры бывают такие сложные, что явно писать — будет много кода и фиг в нём потом разберёшься. Попробуйте-ка написать вот такой фильтр и сортировку на чистой Алхимии. Вы-то напишите, но кода будет много.
      И самое главное, писать кучу сложного кода придётся каждый раз. А так можно декларативно задать нужные фильтры и сортировку


    • я понимаю, что нельзя пускать с фронтенда любые фильтры без валидации:
      • в боевом проекте мы задали список допустимых фильтров типа ['product___price', 'product___user___name] и отсекаем всё остальное
      • если магический фильтр неправильный, пакет выдаст удобочитаемую ошибку (cм. слова "test incorrect attribute")
      • есть задумка сделать удобный валидатор магических строк


Спасибо за внимание!

Поделиться с друзьями
-->

Комментарии (20)


  1. Zagrebelion
    26.03.2017 18:14

    По поводу джанговских filter(blog__name='John') смотрите, как pycharm умеет (у модели поле FK gmfoto к другой модели, у которой есть поле-строка cover_hash:
    https://habrastorage.org/files/869/bb9/1c9/869bb91c98d948e9942c9294f1352960.png


    1. litvinenko1706
      26.03.2017 18:40

      Спасибо, буду знать!


    1. evocatus
      26.03.2017 20:50

      Это только в Pro версии?


  1. VovanZ
    26.03.2017 18:55
    +3

    Увы, в Алхимии нет возможности строить запросы столь динамично. Максимум, что она позволяет — простенькую фильтрацию типа "колонка=значение":

    Странно, что вы не посмотрели на соседний метод (filter)[http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.filter], который позволяет фильтровать куда более "динамично", и главное — читабельно:


    session.query(MyClass).    filter(MyClass.name == 'some name', MyClass.id > 5)

    Помимо этого, с помощью других методов атибутов можно записать и in ( MyClass.some_value.in([1, 2, 3]) ) и вообще что угодно.


    1. litvinenko1706
      26.03.2017 20:02

      я имел в виду, что запросы нельзя строить на основе магических строк, т.е. нельзя получить с фронтенда нужные фильтры и применить их на лету:


      filters = {'rating__in': [2, 3, 4], 'body__like': '%search%'} # пришло с фронтенда
      Post.where(*filters).all()

      Придется писать ручками:


      session.query(Post).filter(Post.rating.in_([2,3,4]), Post.body.like('%search%')).all()

      тут дело именно в том, что нельзя задать фильтр/сортировку, передав строку


      filters = {'rating__in': [2, 3, 4], 'body__like': '%post%'}


      1. VovanZ
        26.03.2017 20:12
        +1

        Хмм, ок, теперь я понял вашу идею.


        Всё же, эта идея кажется мне достаточно стрёмной: по сути, мы позволяем фронтенду делать любые запросы, без явной реализации этих запросов в контроллере.
        Я вижу в этом некоторую опасность: например, какой-нибудь злодей может подобрать максимально "тяжёлую" комбинацию параметров (фильрация по параметрам, у которых нет индекса, использование таких параметров для "like", которые не используют индекс) и создать аномальную нагрузку на БД.


        Я предпочитаю такие вещи контролировать в явном виде.


        1. litvinenko1706
          26.03.2017 20:57

          Да, в таком подходе есть минусы. Ещё минус, что в таких "магических строках" легко ошибиться и, допустим, вместо user___name, написать yser___name, IDE такое не провалидирует, и получим ошибку в runtime.


          Я тоже предпочитаю писать в явном виде, но бывают ситуации, когда надо строить запрос "на лету", когда фильтров не знаешь заранее. И вот такого как раз Алхимия не умеет.


          А по поводу злодейской "тяжёлой комбинации" и вообще валидации: на боевом проекте мы валидируем такие магические строки, приходящие с UI (с помощью marshmallow)


          Имею в планах сделать возможность валидации таких строк.


          1. novoxudonoser
            26.03.2017 21:35
            +1

            Вы можете хоть по условию, хоть в цикле, в зависимости от входных данных формировать динамическую фильтрацию через query.filter(...). Если вы можете написать алгоритм который по входным данным формирует магическую строку, то вы можете написать и алгоритм который применяет цепочку filter.


            1. litvinenko1706
              26.03.2017 21:54

              можно. но каждый раз придётся писать новый алгоритм.
              я же, как и создатели Django ORM, решил иметь возможность описать нужные джоины декларативно и дать протестированному коду это джоины сделать.


              Это — не серебряная пуля, здесь есть свои минусы. Однако это удобная дополнительная фишка, которая может сделать код короче.


          1. ikovrigin
            26.03.2017 21:57
            +1

            Вопрос даже не в быстродействии, что мешает такими фильтрами подбирать пароли userpasswordstartswith='a'.


            1. litvinenko1706
              26.03.2017 22:00

              это скорее вопрос к проектировке безопасности приложения.


              конечно, не следует разрешать фронтенду передавать что угодно в качестве фильтра.


            1. VovanZ
              26.03.2017 23:16

              Подбирать хэши паролей, вы имели ввиду? :)


  1. evocatus
    26.03.2017 20:51

    Кстати, гляньте на Pony ORM: https://ponyorm.com/


  1. novoxudonoser
    26.03.2017 21:28
    +3

    Спасибо за статью, но блин простите, вы выбрали плохой путь, может вам так и удобней, но…

    Удобный CRUD: для простого create в Алхимии надо создать объект, да добавить его в сессию, да сделать flush

    Это вопрос скорее не удобства а стиля (и иногда производительности), лично для меня написать больше строк но сделать всё наглядным удобней, может конечно наверно и потому что в моих задачах обычно используется не простой CRUD.
    Да и вообще, не зря же создатели Django, Ruby on Rails, Laravel, Yii выбрали Active Record ORM.

    И всё из этого списка огромные раздутые монстры, которые очень не удобны и не логичны, не надо брать их в пример, люди не зря идут в сторону микрофреймворков.
    Динамическое построение фильтров/сортировки на основе магических строк как в Django: Post.objects.filter(user__name__startswith='John')

    Одно из списка, из за чего лично у меня возникает б**дская ненависть к Django, для меня это жутко не удобно и антипатерн. Ды и к тому же доверять клиенту нельзя, формировать магические строки на клиенте — плохая идея.
    Вложенный eager load, когда нужно с комментарием сразу загрузить пост, а к посту его юзера (ладно, он есть, но не очень удобен)

    Я не очень понимаю чем вам такой синтаксис не удобен?:
    .options(
        joinedload_all(Watchdog.recipe, Recipe.recipeset, RecipeSet.job),
        joinedload_all(Watchdog.recipe, Recipe.recipeset, RecipeSet.lab_controller),
        joinedload_all(Watchdog.recipetask, RecipeTask.task)
    )
    

    Ваше предложение:
    User.with_({
        User.posts: {
            Post.comments: {
                Comment.user: None
            }
        }
    }.all()
    

    Плохой синтаксис, пихать всё в словари, очень напоминает реализацию sequelize.js, только там так сделано от того что в js нет способа сделать это красиво как в python.

    В целом у меня сложилось впечатление что вы только начали работать с алхимией и теперь несётесь на всех парах всё в ней «исправлять». Но в целом я только за ваши начинания, ведь для того чтобы придти к хорошему решению сначала нужно много надопускать ошибок, это естественно.


    1. litvinenko1706
      26.03.2017 21:50

      1. Тут вопрос стиля и личных предпочтений. Вы привыкли к одному, я — к другому.
      Я не против стандартного синтаксиса Алхимии, просто в некоторых местах мне он неудобен. А в других местах, напротив, Алхимия очень рулит.


      2.


      Я не очень понимаю чем вам такой синтаксис не удобен?:
      .options(
      joinedload_all(Watchdog.recipe, Recipe.recipeset, RecipeSet.job),
      joinedload_all(Watchdog.recipe, Recipe.recipeset, RecipeSet.lab_controller),
      joinedload_all(Watchdog.recipetask, RecipeTask.task)
      )

      лично мне не видно иерархии.


      3. С Алхимией работаю год. И за этот год после Active Record наболело очень многое. Что и вылилось с описанный пакет.


  1. bosha
    27.03.2017 11:59
    +1

    Как-то это… Не python-way совсем. Давайте оставим джангу в стороне. Алхимия в том виде, в каком она Вам кажется неудобной на самом деле заставляет вас писать более очевидный и правильный код:

    bob = User(name='Bobby', age=1)
    session.add(bob)
    session.flush()
    


    Я тут сразу вижу, где я добавил конкретно этот объект, а где сказал алхимии «пиши в базу».

    bob = User.create(name='Bobby', age=1
    


    А что если мне id нужен вставляемой сущности? Мы ж ещё не сделали commit, почему нет? Кстати, а когда у Вас там commit будет? А flush?

    Про «динамические поля» для фильтров, и почему плохо с фронта фильтры текстом принимать я думаю Вы сами понимаете.

    User.with_({
        User.posts: {
            Post.comments: {
                Comment.user: None
            }
        }
    }.all()
    


    Это вообще какая-то адская, совсем не очевидная магия…

    Да и в целом советую забить на ORM, и использовать SQLA core. Он более выразителен и логичен. Лишнее абстрагирование от хранилища ни к чему хорошему не приведёт.


    1. litvinenko1706
      27.03.2017 12:34

      1.


      Как-то это… Не python-way совсем. Давайте оставим джангу в стороне. Алхимия в том виде, в каком она Вам
      кажется неудобной на самом деле заставляет вас писать более очевидный и правильный код:

      Я не говорю что подход Алхимии плохой. Я говорю, что мне простота кода была важнее, чем правильность. Для обычного создания объекта в контроллере в чистой Алхимии надо писать одни и те же 3 строки вместо одной, как в Active Record ORM. Алхимия напрягала в простых задачах, где не нужно 10 объектов добавить в сессию, а потом только один раз flush.


      Мне подход Алхимии нравится именно отсутствием магии и контролируемостью кода. Но в простых задачах это неудобно, и я решил сделать Active Record поверх, а не вместо Data Mapper, как дополнительную плюшку, а не как основной вариант работы.


      Я люблю SQLAlchemy такой, какая она есть. И именно от любви к ней я сделал её ещё удобнее (как минимум, для себя и коллег на работе).


      2.


      Кстати, а когда у Вас там commit будет? А flush?

      При create, update, delete будет сразу flush. При надобности можно сделать настройку, чтоб бы не flush, а commit (ну или autoflush выставить)


      3 .


      А что если мне id нужен вставляемой сущности?

      Вы его получите после flush-а, т.е. после


      user = User.create(...)

      вы имеете user.id


  1. Deepwalker
    27.03.2017 12:15
    +1

    Вы пошли по стандартному сценарию — не открыли свое сердце для алхимии, а сделали привычный интерфейс, с которым познакомились первым. Я бы не сказал что что-то из этого круче и задачи бывают разные, но задетектить в этом плане свое когнитивное искажение стоит.

    Интерфейс алхимии сильно гибче аналогичных из перечисленного. Алхимия не прячет от вас SQL, наоборот в алхимии гораздо проще понять что в итоге получишь. Какой запрос для вас сготовит джанга — та хз, надо смотреть логи и в кишках копаться. Когда джанга кинет запрос, где словишь N+1 и тому подобные тонкости.

    С другой стороны к алхимии у меня тоже есть пачка претензий, но она гораздо тоньше чем к django orm и аналогам.
    Неспроста в RoR есть arel, а activerecord поверх.

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


    1. litvinenko1706
      27.03.2017 12:43

      Я люблю Алхимию именно за её гибкий интерфейс и явный SQL. В Алхимии можно гораздо больше, чем в любой Active Record ORM.


      Но гибкость всегда выливается в более длинный код, и это нормально.
      Хотелось иметь "ярлыки" к этому гибкому коду для самого простого CRUD-а или быстрых джоинов.
      С ними можно простые задачи выполнять очень коротким кодом, имея возможность писать более гибко на обычной Алхимии.


      Я рассматриваю свой пакет не как замену Алхимии, но как ярлыки для самых простых задач.


      Также см. подобные мысли в ответе выше.


  1. litvinenko1706
    28.03.2017 17:13

    При чтении статьи могло сложиться неверное впечатление (см. комментарии вверху), что автор:


    • не любит Алхимию такой, какая она есть / считает её подход злом
    • не попытавшись понять философию Алхимии, полез её исправлять под привычный Active Record
    • любит магию и не любит писать явный код

    Это не так, поэтому чуть подправил некоторые формулировки и добавил спойлер в конце статьи с обоснованием актуальности проделанной работы.


    Спасибо!