Загрузите `отношение` с подмножеством элементов в SqlAlchemy (аналогично объекту Django` Prefetch`) - PullRequest
0 голосов
/ 21 апреля 2019

Есть ли способ извлечь подмножество элементов из отношения в SqlAlchemy, но сохранить порядок, который я определил в backref (отношение) при создании класса?

Допустим, у меня есть две таблицы / модели: A User и UserLog, которые содержат действия, выполняемые пользователем (Base - это просто Declarative_base () )

Каждый UserLog имеет ForeignKey для идентификатора User, который выполнил действие, и тип действия для каждого журнала можно выбрать из Enum. Каждый UserLog также имеет отметку времени created, которую я заказываю с: В обычном сценарии я хотел бы получить журналы от самого старого до самого нового (сначала самого старого).

Однако я также хотел бы иметь возможность получить конкретного пользователя с загруженным только подмножеством журналов (журналов определенного действия). Я понимаю, что это противоречит «сути» объединенной нагрузки, поскольку я получал бы нереалистичное состояние базы данных (есть больше журналов, связанных с пользователем, которые отражал бы мой извлеченный объект User), но это было бы полезно в некоторых случаях.

class UserLog(Base):
    __tablename__ = 'user_logs'
    id = Column(UUID(as_uuid=True), primary_key=True)
    timestamp_created = Column(
        DateTime, server_default=sqlalchemy.text("now()")
    )
    user_id = Column(
        UUID(as_uuid=True),
        ForeignKey("users.id", ondelete="CASCADE")
    )
    action = Column(SaEnum(UserLogsConstants), nullable=False)

    user = relationship(
        "User", 
        backref=backref(
          'logs', order_by="asc(UserLog.timestamp_created)"
        ) # The order_by is important here
    )

Спасибо за этот SO-ответ Мне удалось загрузить подмножество журналов, используя contains_eager, но затем я теряю порядок на timestamp_created, который я указал в backref.

Допустим, у меня есть два доступных действия для журналов: UPLOAD_START, UPLOAD_END (для столбца action)

Я настроил тест, в котором я создаю User с 5 UPLOAD_START действиями и 5 UPLOAD_END действиями, созданными из строя искусственно вставив сначала "новые" действия).

Итак, мне бы хотелось получить User с его отношением .logs, содержащим только журналов, события которых UPLOAD_START и сохраняют порядок по метке времени, указанной в backref объект.

Вот тестовый запрос

u = session.query(User) \
    .filter(User.id == test_user_id) \
    .join(User.logs) \
    .filter(UserLog.action == UserLogsConstants.UPLOAD_START) \
    .options(contains_eager(User.logs)) \
    .one()

Эти два утверждения работают нормально:

assert len(u.logs) == 5  
# I inserted 10 logs total, only 5 with action=UPLOAD_START

assert all(ul.action == UserLogsConstants.UPLOAD_START for ul in u.logs)
# Neat, only logs with `UPLOAD_START`

Но этот провал: порядок не соблюдается.

assert all(
    u.logs[i].timestamp_created < u.logs[i + 1].timestamp_created
    for i in range(len(u.logs) - 1)
)

Это имеет смысл. Если я понимаю поведение contains_eager, это как бы забыть, что вы думаете об отношениях, и использовать то, что я вам говорю в предыдущем запросе (и я ничего не говорю о порядок в этом запросе). И еще: я смотрю на SQL, сгенерированный SqlAlchemy, и здесь нет условия ORDER BY. Полагаю, я всегда мог бы сам добавить это в запрос, но если бы он был чище, если бы мне это не нужно.

Для тех, кто знаком с Django, я пытаюсь эмулировать объект Prefech, где я могу указать подзапрос для получения загруженных объектов:

User.objects.prefetch_related(
     Prefetch(
         'logs', 
         queryset=UserLogs.objects.filter(action=UserLogsConstants.UPLOAD_START)
     )
).get(pk='foo-uuid')

1 Ответ

1 голос
/ 21 апреля 2019

При использовании contains_eager вы указываете SQLAlchemy игнорировать настроенные отношения и вместо этого заполняете контейнер .logs из запроса, созданного вручную. Это означает, что любой набор порядка в отношении также игнорируется.

Так что, если вам нужно заказать данные журнала, вам нужно сделать это самостоятельно:

u = (
    session.query(User)
        .filter(User.id == test_user_id)
        .join(User.logs)
        .filter(UserLog.action == UserLogsConstants.UPLOAD_START)
        .order_by(User.id, asc(UserLog.timestamp_created))
        .options(contains_eager(User.logs))
        .one()
)

Если вы чаще фильтруете пользовательские журналы, то другой вариант - сделать logs динамическое отношение . Это заменит контейнер предварительно заполненным Query объектом, по которому вы будете фильтровать:

user = relationship(
    "User", 
    backref=backref(
      'logs', lazy='dynamic', order_by="asc(UserLog.timestamp_created)"
    )
)

и используйте

u = session.query(User).filter(User.id == test_user_id).one()
upload_start_logs = u.logs.filter(UserLog.action == UserLogsConstants.UPLOAD_START).all()

Конечно, это вызывает отдельный запрос.

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