Пример того, что может сделать SQLAlchemy, и Django ORM не может

В последнее время я много занимаюсь исследованиями использования Pyramid с SQLAlchemy и хранением текущего приложения в Django. Это само по себе является целым дебатом, но я не здесь, чтобы обсудить это.

Я хочу знать, почему SQLAlchemy универсально считается лучшим, чем Django ORM? Почти каждое, если не каждое, сравнение, которое я нашел между двумя вариантами SQLAlchemy. Я предполагаю, что производительность является большой, так как структура SQLAlchemy позволяет ему переводить на SQL гораздо более плавно.

Но я также слышал, что с более сложными задачами Django ORM практически невозможно использовать. Я хочу рассказать о том, насколько огромна проблема. Я читал одну из причин, чтобы переключиться на SQLAlchemy, когда Django ORM больше не подходит вашим потребностям.

Итак, вкратце, может ли кто-нибудь предоставить запрос (не обязательно быть синтаксисом SQL), который может выполнять SQLAlchemy, но Django ORM не может обойтись без добавления дополнительного raw SQL?

Обновление

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

В итоге мы закончили использование SQLAlchemy, и я должен сказать, что я доволен решением.

Я пересматриваю этот вопрос, чтобы предоставить дополнительную функцию SQLAlchemy, которая до сих пор не смогла реплицироваться в Django ORM. Если кто-то может дать пример того, как это сделать, я с удовольствием поеду с моими словами.

Предположим, вы хотите использовать некоторую функцию postgresql, такую ​​как сходство(), которая обеспечивает нечеткое сравнение (см. Быстрый поиск похожих строк с PostgreSQL - tl; dr вводят две строки, возвращают процентное сходство).

Я сделал несколько поисков о том, как это сделать с помощью Django ORM, и не нашел ничего, кроме использования raw sql, как это видно из их документации: https://docs.djangoproject.com/en/dev/topics/db/sql/.

то есть.

Model.objects.raw('SELECT * FROM app_model ORDER BY \
similarity(name, %s) DESC;', [input_name])

SQLalchemy, однако, имеет func(), как описано здесь: http://docs.sqlalchemy.org/en/latest/core/sqlelement.html#sqlalchemy.sql.expression.func

from sqlalchemy import desc, func
session.query(Model).order_by(func.similarity(Model.name, input_name))

Это позволяет вам сгенерировать sql для любой определенной функции sql/postgresql/etc и не требовать сырой sql.

Ответ 1

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

Предположим, нам нужно поддерживать инвентаризацию определенных предметов для нескольких разных, скажем, учетных записей. DDL следует:

CREATE TABLE account (
    id serial PRIMARY KEY,
    ...
);

CREATE TABLE item (
    id serial PRIMARY KEY,
    name text NOT NULL,
    ...
);

CREATE TABLE inventory (
    account_id integer NOT NULL REFERENCES account(id),
    item_id integer NOT NULL REFERENCES item(id),
    amount integer NOT NULL DEFAULT 0 CHECK (amount >= 0),
    PRIMARY KEY (account_id, item_id)
);

Прежде всего, Django ORM не может работать с составными первичными ключами. Да, вы всегда можете добавить суррогатный ключ и уникальное ограничение, но это еще один столбец и еще один индекс, чем вам действительно нужно. Для большой таблицы с небольшим количеством столбцов это добавит заметный размер и производительность. Кроме того, ORM обычно имеют проблемы с отображением идентичности с использованием чего-либо, кроме первичного ключа.

Теперь скажем, мы хотим запросить каждый элемент в инвентаре данной учетной записи, сопровождаемый ее количеством, но также включать все элементы, которые не присутствуют там с количеством, установленным в 0. И затем сортируйте это в порядке убывания по количеству. Соответствующий SQL:

SELECT item.id, item.name, ..., coalesce(inventory.amount, 0) AS amount
    FROM item LEFT OUTER JOIN inventory
        ON item.id = inventory.item_id AND inventory.team_id = ?
    ORDER BY amount DESC;

Невозможно выразить внешнее соединение с пользовательским условием в Django ORM. Да, вы можете сделать два простых отдельных запроса и выполнить объединение вручную в цикле Python. И производительность, вероятно, не пострадает в этом конкретном случае. Но это не так, потому что результаты каждого запроса могут быть воспроизведены на стороне приложения, используя только базовые SELECT s.

С SQLAlchemy:

class Account(Base):
    __tablename__ = 'account'
    id = Column(Integer, primary_key=True)
    ...

class Item(Base):
    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    ...

class Inventory(Base):
    __tablename__ = 'inventory'
    account_id = Column(Integer, ForeignKey('account.id'), primary_key=True,
            nullable=False)
    account = relationship(Account)
    item_id = Column(Integer, ForeignKey('item.id'), primary_key=True,
            nullable=False)
    item = relationship(Item)
    amount = Column(Integer, CheckConstraint('amount >= 0'), nullable=False,
            default=0)

account = session.query(Account).get(some_id)
result = (session
    .query(Item, func.coalesce(Inventory.amount, 0).label('amount'))
    .outerjoin(Inventory,
        and_(Item.id==Inventory.item_id, Inventory.account==account))
    .order_by(desc('amount'))
    .all())

В качестве побочного примечания SQLAlchemy упрощает сбор на основе словарей. С добавлением следующего кода в модель Account вы создаете связь с Inventory как бы то ни было: сопоставление элементов с их количеством.

items = relationship('Inventory',
    collection_class=attribute_mapped_collection('item_id'))
inventory = association_proxy('items', 'amount',
    creator=lambda k, v: Inventory(item_id=k, amount=v))

Это позволяет вам писать код, например:

account.inventory[item_id] += added_value

который прозрачно вставляет или обновляет записи в таблице Inventory.

Сложные объединения, подзапросы, агрегаты окон - Django ORM не справляется ни с чем из этого, не обращаясь к необработанному SQL.

Ответ 2

Это должно работать в Django 1.11:

inventory_amount = Subquery(account.inventory_set.filter(item=OuterRef('pk')).values('amount'))
Item.objects.annotate(inventory_amount=Coalesce(inventory_amount, Value(0)))