Pull to refresh

Comments 51

Транзакции уходят на откуп вашему драйверу.

А почему вы минусанули? Это не ORM. Ваш драйвер умеет справляться с транзакциями нативно: зачем оборачивать это сверху абстракцией поверх уже имеющейся?

Затем, что транзакция может быть для нескольких SQL операций, а не только для одного запроса. В этом её смысл.

Так в этом вся суть библиотеки, что ты только делаешь запросы. А транзакции делаешь из драйвера. Т.е. это conn.execute(...) или conn.fetch(...) под капотом просто с удобным интерфейсом поверх.

Об этом существует множество статей

При этом, в комментах как минимум одной статьи привели убедительные доводы за ОРМ. Просто нужно брать нормальный ОРМ.

  1. Вставлять аргументы в нужные места запроса будем напрямую при помощи f-string. При этом не должно быть никаких SQL инъекций: вставку аргументов отдаем на откуп выбранному драйверу базы данных.

Как-то самонадеянно. Они могут быть, см. пример тут. Так что хотелось бы подтверждение от вас, что их не будет.

Насчет "хороших-плохих" ORM не буду спорить. Я просто не люблю ОРМ, мне они кажутся жутко неудобными.

Насчет второго вашего пункта могу ответить твердо: библиотека не вставляет аргументы в строку, а только плейсхолдеры. Например, в случае asyncpg $1, $2, $3, и т.д. В статье есть пример чуть подальше, когда используется subquery. Также можете сами убедиться в этом, запустив в дебагере любой ваш запрос. В следующем комментарии приведу код.

@qrk.query('AccountBanReason', shape='one')
def get_account_ban_reason(game_id, maybe_banned_account_id):
    return f'''
        SELECT 
            {ban.reason} AS {-attr.reason}
        FROM   
            {game}
        
        INNER JOIN
            {player}
        ON
            {game.host_id} = {player.id}
        
        INNER JOIN
            {ban}
        ON
            {player.account_id} = {ban.account_id}
        
        WHERE  
            {game.id} = {+game_id}
            AND {ban.banned_account_id} = {+maybe_banned_account_id}
        '''

# Превращается в такой запрос:
"""

  SELECT 
      account_ban.reason AS reason
  FROM   
      durak_game
  
  INNER JOIN
      durak_game_player
  ON
      durak_game.host_id = durak_game_player.id
  
  INNER JOIN
      account_ban
  ON
      durak_game_player.account_id = account_ban.account_id
  
  WHERE  
      durak_game.id = $1
      AND account_ban.banned_account_id = $2
        
"""

Как видите, аргументы не подставляются в строку, а только плейсхолдеры. Таким образом, мы имеем абсолютно экранированный запрос, без инъекций.

Насколько я понял - получилась просто static compiled вариация ORM - так в чём преимущество то по сравнению с другими правильными ORM (а правильные тоже умеют код генерировать, правда не всегда это static compiled генерация)?

Лично мне больше вот такой подход симпатизирует (почитайте и другие комментарии там же) - там тоже в основном static compile подразумевается!

Ну, это не ORM. ORM дает абстракцию над базой данных. Моя библиотека дает абстракцию над запросами к базе данных. ORM объявляет, что модель -- это строка в таблице. Моя библиотека объявляет, что любая возвращенная из базы строка -- это модель. И поверх этого просто генерируются информации о типах и аннотации функции. Для каждого запроса -- отдельный тип данных. Вручную это долго и муторно (бойлерплейт-с), а вот автоматически -- раз плюнуть!

Еще раз хочу повторить: это просто очень удобный инструмент для отправки в базу данных запросов и получения результатов этих запросов в удобной форме.

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

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

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

Я ничего не имею против Вашего инструмента. Просто прям чего-то нового и превосходящего правильные ORM - я тут не вижу. Хотя и считаю подход кодогенерации очень правильным. Видимо для Python ещё не хватало таких библиотек, а вот, скажем, под .NET кодогенерация уже давно не в новинку. Но я за более сложную кодогенерацию и более абстрактное и кроссплатформенное (я про СУБД) программирование

Не знаю, может, я что-то упустил. Например, в Python основные ORMки могут общаться только модельками (т. е. таблицами). Если надо что-то посложнее, то подгружается SQL Builder (SQLAlchemy Core тот же) и из объектов типа Select(...).OrderBy(...) составляется запрос. А программа по сути не имеет понятия, что за тип данных вернет запрос, поэтому возвращаться будет условный dict, доступные ключи которого надо смотреть из запроса. А типы значений в ключах уже из схемы...

Я так понимаю, что в шарпах как-то по-другому это решается?

А какие проблемы Sqlalchemy Core решает этот проект?

Библиотека не решает проблем sqlalchemy core, потому что это не дополнение к sqlalchemy core... Вы читали статью вообще?

Каюсь, просмотрел по диалогонали. Увидел: написанный текстом SQL и тезис "Никаких SQL билдеров, никаких ORM, никаких моделей - просто запросы и их результаты в любой удобной для проекта форме". И вот непонимаю, елси это дополнение к SQLAlchemy Core, то почему запросы текстом. А ещё тезис, что линтер как-то поможет тут - какой линтер проверит что строковые тексты запросов соответсвуют структуре БД?

Ниже вы подставляете имена колонок из таблиц ORM, но в этом случае непонятно зачем вообще это писать в таком виде. Мы лишаемся возможности динамичеки формировать запросы, а получаем что? Очередную библиотеку маппинга на датаклассы, но почему-то привязанную к СУБД?

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

Прочитайте еще раз, может, заметите что-то еще.

Насчет линтера, проверяющего правильность SQL запросов: этого нет и не будет. Это возможно только на уровне отдельно взятого IDE, но никак не на уровне библиотеки.

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

Еще раз: ЭТО НЕ ORM, ЭТО НЕ SQL BUILDER.

Это взгляд на проблему с другого ракурса.

ORM используют, потому что в ООП языках нужны объекты. Пусть инстансом класса будет строка таблицы. Сделайте INNER JOIN с еще одной таблицей, что это за объект теперь? Я уж не говорю о нормальных запросах с несколькими CTE и агрегатами.

Такая же проблема с Query Builderами. Что возвратит собранный запрос? Да хз, надо читать запрос, а потом в схему смотреть.

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

Вы пишете обычный SQL запрос в виде python функции, вызываете обычную python функцию, получаете взамен обычные python объекты типа данных на ваш выбор.

И я считаю, что это решает 90% проблем доступа в базу данных. Если вам нужны динамические запросы, напишите эти 2-3 запроса через SQL Builder или ORM.

Спсибо большое, я понял о какой проблеме речь. То есть мы теперь мы

  • потеряли:

    • возможность из линтера (без живой БД) частично проверять, что поля в запросе соответствуют некоторой определенной в коде модели

    • возможность динамически собирать запрос

    • переиспользовать типы, определенные вне слоя работы с БД

  • получили:

    • генерацию типов, соответствующих реальной БД и реальным запросам

так?

  • Нет. Как вы потеряли, если все модели определены? Почитайте до конца. Там пример, как можно использовать ORM модели, чтобы иметь подсказки по полям таблицы. Опять же, подсказки по колонкам - это единственное удобство ORM, разве нет? Если даже взять условный filter в любой питоновской ORM, он вам не подскажет, какие есть поля, т. к. там все строго на **kwargs, а иначе никак не реализуешь такой механизм. А эти "удобства" типа salary__gte=max_salary, уф.

  • Частично да. Запрос обязан быть статическим: динамические только аргументы. Если надо, можно собрать разные варианты запросов как разные функции. Но если прикинуть, то поистине динамических запросов в проекте процентов 5%. По крайней мере у меня из ~200 запросов около 10 динамических (например, INSERT INTO с вариативным кол-вом кортежей). Считаю, что это нестрашно: при необходимости всегда можно подключить любой другой удобный инструмент.

  • Нет. Взгляните в код. Тип сгенерирован и реален: делайте с ним, что душе угодно.

Ну а по факту давайте так: в SQL что вам еще нужно, кроме реальных типов для реальных запросов?

Надуманные приколы вроде: "Мое приложение на завтрак работает на Oracle, в обед Mysql, а на ужин на Postgres." или "Я пишу приложение, но у меня нет под рукой инстанса базы - ваша либа такое потянет??" - не считаются!

Тип сгенерирован и реален: делайте с ним, что душе угодно.

а если у меня есть уже тип, который я определил при проектировании бизнес логики, как мне заставить эти функции его возвращать?

Лично мне от слоя работы с БД мне нужно две вещи: возможность легко собирать в нем разные запросы и маппить результат в какие-то классы. И тут я вижу два варианта: эти классы определены в слое работы с БД как ORM и у вас и тогда не понимаю чем плох ORM, или они определены в доменном слое и тогда я не понял как будет маппинг на них

У вас каждый SELECT возвращает другой тип данных, если вы делаете запрос с хотя бы одним JOINом. Чем плох ORM, я уже 3 раза написал и в статье, и в комментариях, и лично вам расписал.

Если у вас есть функция, которая возвращает что-то, пусть и дальше возвращает, вы вообще о чем?

Вы либо не понимаете, либо не хотите понимать. Зачем тогда спорите? Сделайте JOIN через SQL, без ORM. Гляньте, какой там тип данных. Идите для каждого запроса в программе руками напишите к каждому запросу свой тип данных. А потом при рефакторинге проследите, чтобы типы данных остались консистентными со схемой. Вам удачи, встретимся через пару лет.

Иметь типы для каждого запроса тупо удобнее. Это как использовать type hints или нет.

Либа в свободном доступе. Чтобы ее запустить, нужна БД postgres и 20 минут свободного времени. Вот пример проекта. Сделайте `git clone` и запустите, если реально интересно. Может, тогда будет понятен замысел, и в чем удобство заключается.

А иначе ни о чем разговор, вы просто отказываетесь уложить это в голове.

Если у меня есть запросы вида get by id, list с различной фильтрацией, то я хочу чтобы полученная сущность была одного типа. Желательно того, который я спроектировал до того как начал детализировать логику запроса.

Джойн не меняет сам по себе состав полей результата, он может использоваться например для фильтрации по связанным данным, а список полей при этом быть такой же как без джойна.

Я правильно понял, что вы предполагаете сгенерировть кучу одинаковых классов и если мне дальше надо результат таких запросов одинаково преобразовывать - то делать union?

В статье есть ссылка на этот пункт в документации (пункт начинается словами "Внимательный читатель может отметить...").

Если кратко, в декоратор нужно передать "родительский" запрос, из которого хотите переиспользовать тип данных. Тогда новый запрос просто импортирует тип данных и будет возвращать его.

Например, я вообще не понимаю, что вы имеете в виду под "доменным" слоем. Если у вас есть какой-то тип при проектировании, то вы его из базы запросом достаете, наверное. Вот, запрос если правильно написали, то тип сам сгенерируется с указанным вами названием...

Зачем вы что-то слоите! Вот запрос, вот ответ - все дела. Маппинг обычнейший: кортежи из базы маппяться в инстанс сгенерированного скриптом класса. Разве еще что-то от базы нужно? Остальная логика в приложении ведь! При желании написать генератор pydantic моделей и можно сразу API строить прямо из запросов!

Мне изначально кажется, что общение с базой переусложнено в край. Что может быть проще и выразительнее обычного SQL? К чему десятки слоев абстракции? В итоге все равно приходим обратно к SQL, просто в кривой leaky abstraction обертке...

А вашего примера я совсем не понял, не обессудьте... Там совсем с виду какая-то криптография. Вот этот пример, как я понял?

def dsHDAResFM : #dbfieldgroup //1
{
 HDA1DAT -> DAT
 HDAUSID -> USID 
 HDAUNAM -> UNAM 
 HDABRNM -> BRNM
 HDABRN -> BRN
 HDACRD -> CRD
 HDAMBN -> MBN 
 GPVDSC -> MBND  = left(it ?? "",35) //2 
 HDA1SQN -> SQN
}

def HDAFM : #dbfieldgroup
require {CUS,CLC,TYP,DAT,SQN} 

def HDA1PFFM : HDAFM 
with #dbfieldmap rename = 
  () -> ("HDA${it}" -> "{it}") //3

def HDAPFFM : HDAFM 
with #dbfieldmap rename = 
  () -> ("HDA1${it}" -> "{it}") //3

def HDA1PFWM : #dbfieldgroup
require {
 HDA1CUS -> &dbparameter CUS, //4
 HDA1CLC -> &dbparameter CLC,
 HDA1TYP -> &dbparameter TYP,
} 

#dbsource(path.to.database) //5
{
#dbtablekey(path.to.table.HDAPF) HDAPFFM  //6
#dbtablekey(path.to.table.HDA1PF) HDA1PFFM 

from HDA1PF<-HDAPF<-GPVPF.where(GPVITM=="DAMBN").on(GPVVLE==HDAMBN)  //7
where HDA1PFWM.ToCondition() //8 
var dsHDARes = get dsHDAResFM //9
}

 (e<-dsHDARes(CUS=1, CLC=2, Typ=2)) //10
  -> { // какая-то обработка}

Там же ниже даны пояснения для каждой нумерованной строки - или я плохо пояснил (указал же - что над почить и другие комментарии).

Если кратко - то там выше, не мной, был приведён пример на SQL (очень тяжело читаемый - в основном из-за диких идентификаторов) - я его переписал на квази-языке программирования в декларативной модели (условно на коленке всё придумал - так что не претендую на красоту и точность изложения - это просто пример концепции) - тоже подразумевающей дальнейшую кодогенерацию по результату анализа данной схемы - с генерации кода, как выполняемого на стороне СУБД (не только запросы, но и хранимые процедуры (могут быть и другие операционные виды СУБД), так и код для объектной модели сервера приложений (клиента, если нет сервера). И конечные ЯП тут непринципиальны - из единой исходной формы должен генерироваться код для любой (поддерживаемой) целевой платформы.

Цель - писать унифицировано, и как можно меньше уделяя внимания деталям (любого толка: как деталям бизнес модели, так и деталям целевой платформы, или деталям разных стратегий кодирования, например применять и как многопоточность или нет, и в каком месте реализовывать какую-то обработку данных - на стороне СУБД, или где-то ещё), т.е. повышая уровень абстракции и отложенных вычислений. При этом за результат генерации отвечает AI компилятор, и несколько этапов компиляции. А так же сбор статистики и динамическая перекомпиляция при необходимости (а непрерывный фидбэк программисту о профайлинге и проблемных местах, с рекомендациями по улучшению - если AI не может сделать это сам в силу каких-либо заданных ограничений).

При этом, по умолчанию весь код стремится быть вынесен на сторону СУБД (насколько она позволяет на целевой платформе) - если надо - алгоритм может быть автоматически разбит на несколько этапов взаимодействия сервера приложений и СУБД (хотя это не приветствуется - и скорее всего будет являться ошибкой в алгоритме или не ходимости пересмотра архитектуры модели; об этом всё программист так же должен быть проинформирован). Но это лишь базовая стратегия - разные настройки и хинты в коде могут направлять кодогенерацию в иное русло!

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

При этом, ниже в комментариях, я этот код ещё больше упростил - повышая уровень абстракции. В таком подходе - чем меньше деталей реализации в единице кода - тем лучше (но без потери общего смысла) - для больших программ это очень хорошо - код становится проще, а детали можно размазывать по отдельным модулям, не боясь за проблемы оптимизации и расхождений в деталях - везде стараются сохраняться неопределённости, которыми и идёт оперирование - так что, условно, ошибка в идентификаторах тут не очень страшна - когда нет повторения этих идентификаторов в куче мест.

В остальном пониманию кода и помощи в программировании его абстракций должна активно помогать IDE c AI помощником - проводящим постоянный анализ кода, построение его полной модели, и осуществляющий выдачу детальных, а не только абстрактных подсказок!

Это разве не "database-first" подход? Ну типа база уже есть, и по ней мы кодогенерим типы.

Да, вы правы, это именно так задумывалось. Что база - это единственный источник правды.

Недавно я создавал похожую вещь для Дефли, за исключением того, что строить запрос можно тоже кодом.

  var Params: TSQLParams;  //Набор параметров, которые будут в итоге получены
  var Sel :=               //Тут мы получим структуру запроса
    Select([User, UserRole.Desc.Table('ur').&As('description')]).
    From(User).
    LeftJoin(
      Select('*').
      From(UserRole).Where(UserRole.RoleType = 1), 'ur').
      On(User.RoleId = UserRole.Id.Table('ur')).
    Where(not (User.Id = TGUID.NewGuid) or (User.Status in [1, 2, 3])).
    Where(User.Status and 1 = 0).
    Where(User.Name = 'Dan').
    Where(User.Status in
      Select(User.Status).From(User).Where(User.RoleId in [TGUID.NewGuid, TGUID.NewGuid])).
    OrderBy([User.Name, DESC(User.Status)]).
    GroupBy([User.Id]);

  writeln(Sel.Build(Params));
Результат подобного кода
SELECT user.*, ur.desc description
 FROM user
 LEFT JOIN (
    SELECT *
     FROM user_role
     WHERE user_role.type = :p0) ur ON user.role_id = ur.id
 WHERE (NOT (user.id = :p1)) OR (user.status in (:p2, :p3, :p4)) AND user.status & 1 = :p5 AND user.name = :p6 AND user.status in (
    SELECT user.status
     FROM user
     WHERE user.role_id in (:p7, :p8))
 ORDER BY user.name, user.status DESC
 GROUP BY user.id

Список параметров

p0: Integer = 1
p1: TGUID = {8EAB037E-598C-4D6E-82E0-957126E80810}
p2: Integer = 1
p3: Integer = 2
p4: Integer = 3
p5: Integer = 0
p6: string = Dan
p7: TGUID = {5BA30C4A-FD12-49BD-9BF5-0710A915D4DC}
p8: TGUID = {7F309C2C-8CE5-483B-A198-14914C49C5D8}

Структура таблиц
  [TableName('user')]
  TUser = class(TSQLTable)
    [FieldName('id')]
    Id: TFGUID;
    [FieldName('role_id')]
    RoleId: TFGUID;
    [FieldName('status')]
    Status: TFInteger;
    [FieldName('name')]
    Name: TFString;
  end;

  [TableName('user_role')]
  TUserRole = class(TSQLTable)
    [FieldName('id')]
    Id: TFGUID;
    [FieldName('type')]
    RoleType: TFInteger;
    [FieldName('desc')]
    Desc: TFString;
  end;

Осталось только передать текст запроса и набор параметров в Query.

Как видно, набор столбцов и таблиц мы не указываем текстом, если это не псевдоним столбца или подзапроса.

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

Это у вас получился Query Builder. Вы его не выкладывали на GitHub? Возможно, делфи программистов бы это очень заинтересовало, если еще нет на делфи популярных Query Builderов. Потому как они очень популярны в других языках.

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

Не понял немного, а куда Params вставляется? Вроде объявлен пустым, и выписывается в конце, а с виду нигде больше не дергается. А TGUID - это ваш плейсхолдер для параметра?

Params передается в билдер запроса. Билдер наполняет его данными из запроса. Т.е. все значения будут именно параметрами, а не просто конкатенацией.

Параметры просто передаются Query, вместе с самим запросом, т.е. например в FDQuery или ADOQuery.
Выглядеть будет примерно так:

FDQuery.Open(Sel.Build(Params), Params);

Кажется, я вас понял.

Хочу отметить, у меня тоже нет конкатенации параметров, а также сначала вставляются плейсхолдеры, только потом база данных сама вставляет параметры. Как у вас :p1, :p2, etc. (я так понимаю, это mssql), так и у меня будет $1, $2, etc. (у меня драйвер для postgres).

Я так понял, TGUID - это плейсхолдер для параметра? Я в том числе долго размышлял, как сделать так, чтобы параметры из функции 1 к 1 мапились в параметры запроса, т. к. часто испытывал неудобства при рефакторинге какого-то запроса, и параметры между собой мешались в нумерации -- и все части программы, использующие этот запрос, в итоге страдали из-за неправильной последовательности.

У меня тоже postgres.

TGUID - это тип данных "GUID" (уникальный идентификатор). Плейсхолдеров у меня нет. И анализа текста - тоже (ни снаружи, ни под капотом языка). Здесь все работает нативно.

Проблема с нумерацией решается тем, что всем генераторам внутри билдера передается одна и та же ссылка на список параметров, который они пополняют, а номер параметра - это просто "кол-во параметров" + 1.

а также будет автокомплит ко всем полям в таблице - очепяток не будет

насколько понимаю, в ультимейт версии это работает без хаков. Подключаете к проекту БД, и plain text запросы начинают подсвечиваться, работает автокомплит, в соответствии со схемой.

Ну да, тут наверняка можно так сделать. У меня, к сожалению, нет ультимейта :)
Тем не менее, это скорее дополнительное замечание вне библиотеки, чем основа функционала, поэтому и пометил как хак и фишку.

Что-то подобное для продакшен использовании я на C# сделал.
Идёт генерация мапперов с полным CRUD на ADO.NET.
Поддерживается SQL Server и PostgreSQL.
Демонстрационный сервер тут https://github.com/KlestovAlexej/Wattle3.DemoServer

К примеру метод маппера GetAsync :

https://github.com/KlestovAlexej/Wattle3.DemoServer/blob/31bd12fe5e68af449b3e88f8e9c6e3b2597815b2/src/DemoServer.Processing.DataAccess.Postgresql/Generated/ShtrihM.Wattle3.CodeGeneration.Generator.Implements/ShtrihM.Wattle3.CodeGeneration.Generator.Implements.SourceGenerator/DbMappers.Implements.PostgreSql.Generated.cs#L1304

    /// <summary>
    /// Получить запись с указаным идентити.
    /// </summary>
    /// <param name="mappersSession">Сессия БД.</param>
    /// <param name="id">Идентити записи.</param>
    /// <param name="cancellationToken">Токен отмены.</param>
    /// <returns>Возвращает значение если запись существует иначе возвращает <see langword="null" /> если запись не существует.</returns>
    [SuppressMessage("ReSharper", "UseObjectOrCollectionInitializer")]
    [SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
    [SuppressMessage("ReSharper", "RedundantCast")]
    public virtual async ValueTask<IMapperDto> GetAsync(IMappersSession mappersSession, long id, CancellationToken cancellationToken = default)
    {
        if (mappersSession == null)
        {
            throw new ArgumentNullException(nameof(mappersSession));
        }

        try
        {
            var typedSession = (IPostgreSqlMappersSession)mappersSession;

            // ReSharper disable once ConvertToUsingDeclaration
            var command = await typedSession.CreateCommandAsync(false, cancellationToken).ConfigureAwait(false);
            await using (command.ConfigureAwait(false))
            {
                command.CommandType = CommandType.Text;
                command.CommandText = @"SELECT

Id
FROM ChangeTracker WHERE
(Id = @Id)";

                {
                    var parameter = new NpgsqlParameter<long>("@Id", NpgsqlDbType.Bigint) { TypedValue = id };
                    command.Parameters.Add(parameter);
                }

                var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancellationToken).ConfigureAwait(false);
                await using (reader.ConfigureAwait(false))
                {
                    if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
                    {
                        GetColumnIndexes(
                            reader,
                            out var indexId);

                        var result = Read(
                            reader,
                            indexId);

                        return ((IMapperDto)result);
                    }
                }

                return ((IMapperDto)null);
            }
        }
        catch (Exception exception)
        {
            CatchExceptionOnGet(mappersSession, exception, id);
            CatchException(mappersSession, exception);

            var targetException = await m_exceptionPolicy.ApplyAsync(exception, cancellationToken).ConfigureAwait(false);
            if (ReferenceEquals(targetException, exception))
            {
                ExceptionDispatchInfo.Capture(exception).Throw();
            }

            throw targetException;
        }


https://cashapp.github.io/sqldelight/2.0.0/

SQLDelight generates typesafe Kotlin APIs from your SQL statements. It verifies your schema, statements, and migrations at compile-time and provides IDE features like autocomplete and refactoring which make writing and maintaining SQL simple.

Это очень похоже, удобная штука! Спасибо, что поделились!

Много лет тому назад я подсмотрел очень интересную коцепцию SQL шаблонизатора с условными блоками у Дмитрия Котерова (dklab).

Потом сделал sql-template-engine, который уже несколько лет работает на одном крупном веб-сайте РФ.

У вас интересный подход. Позволяет бОльшую свободу действий, чем у меня (например, динамическое кол-во вставляемых кортежей). Но честно скажу, с первого взгляда тяжело понять!

ну ок. победа над орм. Победили.
Какая она, победа?
Только для PG? для других БД переписывать все 129 запросов на другой диалект?
Не помню такого с орм...

Дать на фронт get_params в качестве фильтров в запросе, с той же джангой или алхимией - стандартная практика. В Вашем случае - пишем новый кодогенератор? Тестами покрываем...
Ну т.е. опять не про продакшен.

"Хитрые" способы наследования моделей из коробки - моделирование нужного полиморфного поведения - опять ОРМ. Наследование датаклассов - я как то пробовал...Предлагаете все изобрести заново?

Мне кажется это(ваша библиотечка) не про победу над ОРМ. Заголовок в корне не соответствует. М.б "альтернатива орм, в отдельных, необременительных случаях" ?

В целом мне нравится. Есть определенная "ниша" - прототипы, инди-проекты, и другие кейсы когда 120тн вентилятор устанавливать не нужно, а хочется стартануть "также быстро".

  1. Абсолютно согласен, что для другой БД часть запросов придется переписать. Но я и не пытался решить проблему несовместимости диалектов SQL. Конечно, это решение не подойдет для проектов, где используется несколько баз данных от разных вендоров одновременно, но у меня лично к таким проектам есть вопросы относительно целесообразности и осознанности такого решения.

  2. Согласен с вашей критикой, тесты нужны. Но спешу вас обрадовать: код на выходе там довольно тривиальный, и служит скорее автоматически генерируемой документацией, чем тяжело работающим механизмом. Если же вы про запросы с несколькими условиями, то можно просто эти условия сразу учесть в запросе. Базе данных ничего не стоит сравнить десяток дополнительных полей в рекорде. А вам даже спокойнее, что запрос всего один, и вы видите какой.

  3. Не уверен, как ORM может быть мощнее SQL. Моей идеей было предоставить максимально удобный интерфейс для выполнения чистого SQL, потому как я искренне считаю, что нет никакой лучшей абстракции над SQL, чем он сам. Проверено создателями бесконечной кучи ORM, каждый из которых в конце концов начинает строить костыль на свой вкус и цвет.

Благодарен за критику, но я не нашел в ваших тезисах ничего нового, чего еще не видел. Я уже видел миллион раз на форумах с холиварами "ORM vs raw SQL" тезис: "А что вы будете делать, если надо на другой БД запустить проект?". Ну не знаю, подумаю, если надо будет. YAGNI.

account.last_name по схеме null=True, а в датаклассе не Optional и без дефолта...
вложенность ифов конечно повергает...
какой смысл писать в одной строке str | None, а в следующей typing.Optional[... ?
mypy бы запустить на этом коде и pylint...
строк документации у функций/классов не хватает.

чойсы(и все остальные констрейнты) бы тоже хотелось от базы получать в модельку. И...валидацию тоже хочется иметь у модели , на случай если в инсерт и апдейт эти модельки потом отдавать.
Может лучше Pydantic классы генерить и чойсы Literal аннотацией?

Прочитайте пункт, начинающийся словами "Внимательный читатель заметит...". Как раз там ссылки на документацию, которые ответят на ваши вопросы.

Если вам интересно запустить pylint, mypy или просто попробовать библиотеку в действии, то приглашаю в тестовый проект, который использовался для написания документации и этой статьи: https://github.com/racinette/querky_showcase

Не понял только, о каких if-ах вы говорите, потому как в примерах вообще ни одного нет...

Насчет обновления коммента: можно настроить на любой удобный вам возвращаемый класс, в этом фишка. Можно и pydantic, нет проблем. Необходимо сделать подкласс TypeConstructor, который вам будет код писать для любого класса. Туда поступает набор полей с их типами, а вы возвращаете код для этого типа - и все.

Насчет чойсов: если я правильно понял, речь идет об ENUM. Тут несколько вариантов в зависимости от БД, но именно в постгресе - это новый тип данных. Его нужно промапить по названию, т. е. сообщить кодогенератору, какой тип в Python отвечает типу в базе. В доках про это немного написано. Я для этого использую небольшую обертку над обычными Enum и SQLAlchemy моделями. Могу поделиться, если интересно.

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

Констреинты лучше не трогать, т. к. они на самом деле не выражают ничего вне контекста таблицы. Суть в том, что все поля по сути NULLABLE всегда, а констреинт просто ограничивает этот момент. Поэтому по факту все возвращаемые типы всегда Optional, и только вы как разработчик можете сказать, может ли какое-то значение в запросе быть NULL или нет. Поэтому библиотека позволяет дополнять, где необходимо, но никогда не делает выбор за вас.

ОРМ так же под под разные диалекты дописаны. А в общих случаях, когда речь именно о запросах, то они подойдут к любой БД. Потому что SQL - стандарт, который поддерживается базами. Только если более узкие вещи делать, то придется под разные диалекты писать. Только в статье пишутся запросы под каждый функционал. Следовательно, в другом проекте, даже если будет другая база, писать запросы нужно будет так же с нуля.

Ну да, конечно, придется писать. Замысел дальше в том, чтобы для полного кайфа сделать кодогенератор базовых CRUD запросов на основе схемы БД как у @AleksejMsk (если я правильно понял его код). И тогда только для каких-то сложных и необычных моментов надо самому будет дописывать.

Но пока не уверен, что стоит это делать, т. к. все-таки задача непростая, а профита пока никакого: даже звезды на гитхабе не ставят.

А мне так вообще несложно простенькие крудики пописать на SQL, даже приятно иногда, мозг отдыхает.

Да моя библиотека (C# https://github.com/KlestovAlexej/Wattle3.Examples) генерирует CRUD но и много сопутствующих фичей полезных на практике.

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

Помимо CRUD создаётся код с типичными запросами:

  • Выборка по альтернативному ключу (ключам).

  • Выборка по полю (полям) группировки.

  • Создание WHERE специфичного для БД (SQL Server и PostgreSQL) по запросу собраному в C# или из текста.

  • Постраничная выборка (с и без WHERE).

Все генерируемые мапперы поддерживают кэширования при выборке объектов по первичному ключу.
По мониторингу боевых серверов - попадания к кэш стремится с 99%, что сильно разгружает сеть и БД.
Рассинхрон кэша и БД решается автоматически.

Реализовано создания изначений первичных ключей на основе последовательностей БД с минимальной нагрузкой на саму БД.

Звучит круто.

У меня тоже можно кэш прикрутить, но только для единичных запросов.

А у вас прямо система получается, которая следит за инсертами, делитами и апдейтами по PK и локально модифицирует результаты всех запросов, которые также имеют доступ к таким рекордам, я правильно понимаю?

Если о кэше - есть ключевые точки - обновление, удаление, выборка.
Они причины движений в кэше.
Это помимо протухания элемента кэша по TTL и есть еще принудительное автоматическое удаление элемента из кэша при подозрении на рассинхрон с БД.

Моя библиотека только в комплексе даёт профит.
Просто голый кэш мапера это недостаточно для вкусной жизни ПО.

Помимо мапера в доменной области есть стратегия решения конфликтной ситуации при комите в БД - система автоматически даёт программисту уведомление что комит успешен (данные в БД) или не успешен (данные не в БД) - даже если данные в БД попали, а их потом физически удалил.
Причём где бы не упал код до комита или даже в момент самого комита transaction.Commit().
В демо сервер есть примеры https://github.com/KlestovAlexej/Wattle3.DemoServer/blob/c9074eb2604efae8c8f8926ec843806193547651/src/DemoServer.Processing.Model/DomainObjects/DemoObjectX/DomainObjectDemoObjectX.cs#L226.
Система автоматически понимает - следит на транзакциями БД отслеживает их делает сверки с БД (у каждой БД свои маперы которые знают специфику).
Всё из коробки.

Sign up to leave a comment.

Articles