Не секрет, что 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 код Алхимии кажется слишком сложным, и это плата за гибкость.
В течение года я работал с Алхимией на сложном проекте, и понял, что для полного счастья хочется иметь:
- Удобный CRUD: для простого
create
в Алхимии надо создать объект, да добавить его в сессию, да сделать flush - Динамическое построение фильтров/сортировки на основе магических строк как в Django:
Post.objects.filter(user__name__startswith='John')
- Вложенный eager load, когда нужно с комментарием сразу загрузить пост, а к посту его юзера (ладно, он есть, но не очень удобен)
- Информативный
__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
.
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()
Вообще-то, на более низком слое 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) # не потребуется дополнительного запроса в БД, т.к. юзер был приджойнен
Подробное описание с примерами см. тут.
Если Вы не любите магию
Вот пример магической фильтрации и сортировки в реальном проекте.
Вопрос ко всем, особенно к нелюбителям магических строк:
- Сколько строк понадобится для подобных фильтров и сортировки на чистой Алхимии?
- Насколько получившийся чисто-алхимический правильный код будет понятным и поддерживаемым?
- Насколько легко будет его реюзать?
- Итого, что лучше: немного оттестированной, документированной магии или кастомный велосипед на чистой Алхимии по 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>]
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'>]
Подробное описание с примерами тут.
Итоги
Реализованы фичи, без которых лично мне очень тяжко работалось с Алхимией.
Теперь жить стало легче, ведь:
- C Active Record мы можем быстро делать CRUD, не заморачиваясь с flush-ем сессии.
- С мощной фильтрацией/сортировкой в стиле Django мы можем делать очень сложные, динамические джойны в одну строку, которую можно хоть с UI передать.
- Мы может задавать eager load декларативно.
- Отладка стала удобнее с информативным
__repr__
.
На боевом проекте это помогло сильно упростить поддержку и повысить читабельность кода.
Все фичи, опять же, реализованы тут.
Я очень плотно работаю с Алхимией уже год на сложном боевом проекте. Все задачи, которые я привёл — не прихоть, а реальная необходимость.
Я люблю Алхимию и её стандартный подход. Мне нравится её гибкость, контроль над SQL, явность и т.д. Я считаю Алхимию лучшей ORM, что я встречал. Никакой Active Record с ней не сравнится.
Но плата за гибкость — более длинный код. И в простых задачах вроде CRUD код Алхимии сильно сложней аналогичного в Active Record ORM.
Поэтому я и написал свой пакет, который иногда можно использовать как дополнение к чистой Алхимии.
Мой пакет — лишь надстройка над Алхимией. Я до сих пор люблю Алхимию такой, какая она есть и не предлагаю отказаться от её изначального использования.
Об Active Record:
я понимаю всю мощь паттерна unit of work для оптимизации производительности приложения. Но в простых задачах, когда нужно в контроллере просто создать объект, писать для этого одни и те же 3 строки (создать объект, добавить в сессию, flush сессии) не вдохновляет. Поэтому я и сделал Active Record поверх, а не вместо Data Mapper для простых задач.
Об фильтрации/сортировке в стиле 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)
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])
) и вообще что угодно.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%'}
VovanZ
26.03.2017 20:12+1Хмм, ок, теперь я понял вашу идею.
Всё же, эта идея кажется мне достаточно стрёмной: по сути, мы позволяем фронтенду делать любые запросы, без явной реализации этих запросов в контроллере.
Я вижу в этом некоторую опасность: например, какой-нибудь злодей может подобрать максимально "тяжёлую" комбинацию параметров (фильрация по параметрам, у которых нет индекса, использование таких параметров для "like", которые не используют индекс) и создать аномальную нагрузку на БД.
Я предпочитаю такие вещи контролировать в явном виде.
litvinenko1706
26.03.2017 20:57Да, в таком подходе есть минусы. Ещё минус, что в таких "магических строках" легко ошибиться и, допустим, вместо
user___name
, написатьyser___name
, IDE такое не провалидирует, и получим ошибку в runtime.
Я тоже предпочитаю писать в явном виде, но бывают ситуации, когда надо строить запрос "на лету", когда фильтров не знаешь заранее. И вот такого как раз Алхимия не умеет.
А по поводу злодейской "тяжёлой комбинации" и вообще валидации: на боевом проекте мы валидируем такие магические строки, приходящие с UI (с помощью marshmallow)
Имею в планах сделать возможность валидации таких строк.
novoxudonoser
26.03.2017 21:35+1Вы можете хоть по условию, хоть в цикле, в зависимости от входных данных формировать динамическую фильтрацию через query.filter(...). Если вы можете написать алгоритм который по входным данным формирует магическую строку, то вы можете написать и алгоритм который применяет цепочку filter.
litvinenko1706
26.03.2017 21:54можно. но каждый раз придётся писать новый алгоритм.
я же, как и создатели Django ORM, решил иметь возможность описать нужные джоины декларативно и дать протестированному коду это джоины сделать.
Это — не серебряная пуля, здесь есть свои минусы. Однако это удобная дополнительная фишка, которая может сделать код короче.
ikovrigin
26.03.2017 21:57+1Вопрос даже не в быстродействии, что мешает такими фильтрами подбирать пароли userpasswordstartswith='a'.
litvinenko1706
26.03.2017 22:00это скорее вопрос к проектировке безопасности приложения.
конечно, не следует разрешать фронтенду передавать что угодно в качестве фильтра.
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.
В целом у меня сложилось впечатление что вы только начали работать с алхимией и теперь несётесь на всех парах всё в ней «исправлять». Но в целом я только за ваши начинания, ведь для того чтобы придти к хорошему решению сначала нужно много надопускать ошибок, это естественно.litvinenko1706
26.03.2017 21:501. Тут вопрос стиля и личных предпочтений. Вы привыкли к одному, я — к другому.
Я не против стандартного синтаксиса Алхимии, просто в некоторых местах мне он неудобен. А в других местах, напротив, Алхимия очень рулит.
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 наболело очень многое. Что и вылилось с описанный пакет.
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. Он более выразителен и логичен. Лишнее абстрагирование от хранилища ни к чему хорошему не приведёт.litvinenko1706
27.03.2017 12:341.
Как-то это… Не 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
Deepwalker
27.03.2017 12:15+1Вы пошли по стандартному сценарию — не открыли свое сердце для алхимии, а сделали привычный интерфейс, с которым познакомились первым. Я бы не сказал что что-то из этого круче и задачи бывают разные, но задетектить в этом плане свое когнитивное искажение стоит.
Интерфейс алхимии сильно гибче аналогичных из перечисленного. Алхимия не прячет от вас SQL, наоборот в алхимии гораздо проще понять что в итоге получишь. Какой запрос для вас сготовит джанга — та хз, надо смотреть логи и в кишках копаться. Когда джанга кинет запрос, где словишь N+1 и тому подобные тонкости.
С другой стороны к алхимии у меня тоже есть пачка претензий, но она гораздо тоньше чем к django orm и аналогам.
Неспроста в RoR есть arel, а activerecord поверх.
Вашим пакетом лично я вряд ли буду пользоваться, лишний слой абстракции, и новичкам сильно бы не советовал. Но писать опенсорс достойно, желаю успехов )litvinenko1706
27.03.2017 12:43Я люблю Алхимию именно за её гибкий интерфейс и явный SQL. В Алхимии можно гораздо больше, чем в любой Active Record ORM.
Но гибкость всегда выливается в более длинный код, и это нормально.
Хотелось иметь "ярлыки" к этому гибкому коду для самого простого CRUD-а или быстрых джоинов.
С ними можно простые задачи выполнять очень коротким кодом, имея возможность писать более гибко на обычной Алхимии.
Я рассматриваю свой пакет не как замену Алхимии, но как ярлыки для самых простых задач.
Также см. подобные мысли в ответе выше.
litvinenko1706
28.03.2017 17:13При чтении статьи могло сложиться неверное впечатление (см. комментарии вверху), что автор:
- не любит Алхимию такой, какая она есть / считает её подход злом
- не попытавшись понять философию Алхимии, полез её исправлять под привычный Active Record
- любит магию и не любит писать явный код
Это не так, поэтому чуть подправил некоторые формулировки и добавил спойлер в конце статьи с обоснованием актуальности проделанной работы.
Спасибо!
Zagrebelion
По поводу джанговских
filter(blog__name='John')
смотрите, как pycharm умеет (у модели поле FKgmfoto
к другой модели, у которой есть поле-строкаcover_hash
:https://habrastorage.org/files/869/bb9/1c9/869bb91c98d948e9942c9294f1352960.png
litvinenko1706
Спасибо, буду знать!
evocatus
Это только в Pro версии?