amaslyaev / noorm

NoORM (Not only ORM) - Python library that makes your database operations convenient and natural
MIT License
16 stars 0 forks source link

Lazy fetching #2

Closed LecronRu closed 2 months ago

LecronRu commented 3 months ago

Часто клиенту не нужен список. Например при аггрегировании на клиенте или другой обработке множества записей. В том числе в вашем же примере. Причем множество может доходить до миллионов. Разумно вместо списка возвращать генератор, а клиент, если ему понадобится, сам сделает all_users = list(get_all_users())

Заодно можно переименовать методы в более говорящие sql_objects и sql_scalars (убрав слово fetch). А для сохранения обратной совместимости, старым методам присвоить алиасы на новые.

amaslyaev commented 3 months ago

Да, тоже думал над этим. Не сделано только потому что в наших проектах пока что не было востребовано. А вообще, согласен, фича полезная. Будет называться sql_iterate для объектов. Полезность итерирования скаляров для меня сомнительна. Можете придумать юзкейс для этого?

На следующей неделе попытаюсь запилить.

LecronRu commented 3 months ago

Полезность ровно такая же как для объектов, когда "объект" из одного поля. Прямо сейчас идет обработка: for text, in execute('SELECT text FROM s WHERE id in (SELECT s_id FROM p WHERE word = ?)') Обратите внимание на запятую распаковки в скаляр после "for text'.

Фича не просто полезная, а основаная. Рекомендую в документации упор делать именно на нее. Дело не в востребоанности, а в best practic — функция выполняет одну задачу. Одна выполняет извлечение данных (в библиотеке), вторая преобразует в нужную последовательность — кортеж, список и даже словарь, — в клиентском коде.

amaslyaev commented 3 months ago

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

Список датаклассов - предельно простая и предсказуемая сущность. Он не преподнесёт никаких сюрпризов. Итератор, особенно такой, который там внутри что-то дотягивает из внешней системы - гораздо более капризная штука.

Допустим, у на есть такой кусок кода:

self.orders = get_user_orders(conn, self.user_id)

Если get_user_orders возвращает список, то:

Если же это итератор, то:

А если у нас async, это ещё добавит интересных нюансов.

Короче, работа через итератор - неплохой способ организовать себе минное поле на ровном месте. И первым же на нём подорваться. Главное, ради чего эта копеечная экономия?

Когда мне бывает нужно откуда-нибудь куда-нибудь переложить сотню-другую гигабайт, я обычно вместо стриминга данных просто вычитываю кусками по, например, 10000 строчек. Так оно получается надёжнее. Но я прекрасно понимаю, что если таких задач становится много, это покусочечное вычитывание начнёт напрягать. Поэтому таки да, стриминг надо сделать, но с припиской, что применять осторожно и думать дважды.

Кстати, вопрос. Может быть, сразу сделать стриминг сразу с нарезкой на чанки? Типа чтобы итератор отдавал объекты не по одному, а порциями? Или это будет лишнее усложнение?

LecronRu commented 2 months ago

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

self.orders = list(get_user_orders(conn, self.user_id))

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

Проходить до конца не надо. Если выходим из области видимости контекстного менеджера, соединение будет закрыто. Если же работаете без контекстного менеджера, естественно извольте закрывать вручную. Но это опять же, рядовая работа программиста. Скажу больше, в sqlite без wal-mode, можно даже наткнутся на блокировку записи в базу.

ради чего эта копеечная экономия?

Это уж пусть клиент решает, не надо лишать его выбора. Ваша библиотека лишь упорядочивает получение данных из базы данных. Не надо брать лишнего. Представьте, как бы клиент получал данные без нее. Либо итерировался, со всеми особенностями итераторов, либо делал execute().fetchall() или list(execute()).

Также и с чанками. Если память не изменяет, в more-itertools такая функция должна быть.

amaslyaev commented 2 months ago

Короче, приделываю декораторы sql_iterate и sql_iterate_scalars. На выходных надеюсь закончить.

Рекомендуемый подход остаётся без итераторов, то есть через соответственно sql_fetch_all и sql_fetch_scalars.

amaslyaev commented 2 months ago

Done in version 0.1.2