2025, Nov 17 15:02

Чем отличается scalar от scalar_one_or_none в SQLAlchemy

Разбираем разницу между scalar и scalar_one_or_none в SQLAlchemy: когда берется первая строка, когда выбрасывается MultipleResultsFound, примеры применения.

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

Постановка задачи

Рассмотрим две функции, которые загружают одну сущность по id. Обе, кажется, возвращают либо единственный экземпляр модели, либо None, если ничего не найдено.

def find_group_lenient(conn: Session, group_id: int) -> Group | None:
    q = select(Group).filter_by(id=group_id)
    return conn.scalar(q)


def find_group_strict(conn: Session, group_id: int) -> Group | None:
    q = select(Group).filter_by(id=group_id)
    return conn.execute(q).scalar_one_or_none()

Что происходит на самом деле

Разница проявляется, когда набор результатов содержит более одной строки. Вызов scalar возвращает первый элемент из первой строки набора. Напротив, scalar_one_or_none выбрасывает MultipleResultsFound, если присутствует больше одной строки, и возвращает None только когда строк нет вовсе.

Полезно представлять набор результатов как список кортежей, каждый из которых содержит один или несколько элементов. Метод scalars преобразует этот список, забирая первый элемент из каждого кортежа. Метод scalar затем берёт только первый элемент самого первого кортежа. Когда запрос возвращает экземпляры моделей, каждый элемент в кортеже — это экземпляр модели. Когда запрос возвращает «голые» значения столбцов, элемент — это простое значение, например int или str.

Если хотите наблюдать SQL, который выполняется для этих вызовов, создайте движок с echo=True и посмотрите, что именно отправляется.

Решение

Выбирайте API, соответствующий намерению. Если по контракту допускается не более одной строки, используйте scalar_one_or_none, чтобы явно получить MultipleResultsFound при наличии дубликатов. Если цель — взять первый элемент первой строки независимо от числа совпадений, scalar делает именно это.

def get_group_enforcing_uniqueness(conn: Session, group_id: int) -> Group | None:
    q = select(Group).filter_by(id=group_id)
    return conn.execute(q).scalar_one_or_none()

Почему это важно

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

Итоги

Используйте scalar_one_or_none, когда запрос должен вернуть ноль или одну строку, и вам нужно исключение, если найдено больше. Применяйте scalar, когда приемлемо вернуть первый элемент первой строки. Держите в голове модель набора результатов как списка кортежей: scalars вытягивает первый элемент из каждого кортежа, а scalar — только первый элемент самого первого кортежа. Если сомневаетесь, включите echo=True, чтобы посмотреть выполняемый SQL и лучше понять, как оценивается запрос.