Было, уже проходили такое.
Параллельно зародилось два подхода. И они использовались некоторое время.
1. Для серилизации в БД использовали RTTI, через published поля. Базовый класс, с методами Save/Load. Пользовались недолго, но было удобно. В итоге отказались(и перешли на третий вариант) из-за непортируемости самого RTTI и еще каких-то причин с ним же связанных.
2. Использовали кодогенератор. По требуемому описанию, генерили юниты для доступа в БД, генерили весь SQL для создания и работы с нужной таблицей (одной таблице соответствовал один юнит).
Использовали долго. Точнее терпели долго (напомнило афоризм про мышей и кактус).
Недостатки:
* Такой подход годится только для простейших таблиц и взаимосвязей
* Для более сложных таблц и запросов была возможность в шаблоне метаописания классов(таблиц-классов) добавлять кастомный код, колторый вставлялся в генерируемые юниты, но на практике это все превращалось в кошмар — чтоб добавить примитивную, но кастомную реализацию чего-то, приходилось писать кучу кода в шаблоне.
* Как следствие ограниченная работа с транзакциями. Фактически было два вида транзакции: readonly и default. А зачем больше, коли это все предназначалось только для чтения и записи целиком строк в одной таблице? Вот тут и поджидала неприятность с кастомным кодом: ему этих типов транзакций было мало, а отходить от стандартной практики означало делать чудовищный оверхед по количеству написанного кода.
* Просто огромное кол-во сгенеренных методов и кода, который по сути являлся примитивным (прочитай да запиши, толко в различных комбинациях)
* Для добавления новых колонок в таблицу или их изменения приходилось перегенеривать весь код, а таблицу ALTER'ить вручную
Сейчас перешли на 3-й вариант, который до того в течении 5-ти лет обкатывался на сайд-проекте.
Есть контейнер иерархической структуры данных, похожей на XML. Только в отличие от последнего заточенный не на разметку, а на хранения данных: минимум потребляемой памяти, все данные лежат компактно и обычно влазят в кеш, возможна иерархия, каждый элемент контейнера имеет свой тип — наподобии варианта только компактней и быстрее.
Под этот контейнер, написан интерфейс работы с БД -- вместо TDataSet используется вышеописанный TDataList.
Ну и вся работа с БД ведется через этот контейнер и эту прослойку к самой БД.
Как показала практика, работа с рекордами как с классами и их свойствами нам нафиг не нужна, хотя это было и совсем не очевидно.
Место класса заменил TDataList, а пропертя — элементы хранящиеся в этом контейнере.
Именно за счет "плавающего" количества "свойств" и достигается подобная гибкость: один TDataList может прочитать любую таблицу, включая составную(View, Join-ы) или служебную (метаинформация о БД; в частности список колонок и их тип для любой таблицы в БД — по этой инфе можно построить ALTER TABLE для добавления(апгрейда версии) новых колонок).
Насчет производительности, то в теории это конечно медленее чем доступ напрямую к пропертям класса, но практика показывает что даже самая топорная реализация "в лоб" оказывается на порядок(!!!) быстрее чем использование RTTI. С применением хеширования строк все становится еще быстрее.
В итоге все выглядит где-то так (это все server-side):
- Код: Выделить всё
var
db: TDBConnection;
data_src, data_dst: TDataList;
sql: string;
begin
....
db := nil;
try
// Лочим коннект к БД из пула (у нас многопоточный сервер, потому нужна гарантия что с этим соединением больше никто работать не будет)
db := DBPool.AcquireConnection(ilRepeatableRead); // ilRepeatableRead - будет транзакцией по-дефолту на все дальнейшие действия
sql := Build.SQL.Select('*') // А это мой уменьшитель кода ;)
.From(TABLE_ORDERS, TABLE_CUSTOMERS)
.JoinLeft(TABLE_CUSTOMERS, ORD_CUSTOMER_ID, CST_ID)
.Limit(100).toString;
// читаем все строки в data
db.SelectRecords(data_src, sql);
// перебераем все строки, что-нибуть по ним высчитываем
for i:=0 to data_src.Last do
begin
row := data_src.Sections[i];
// что-нибуть делаем с row (напр. какие-то расчеты)
end;
// заполняем поля будущего рекорда в БД
data_dst.Int['ID'] := db.GenerateID(GENERATOR_NAME); // Методика для FireBird/Interbase - ID получаем перед вставкой, а не после как в MySQL
data_dst.Int['field1'] := 123;
data_dst.WideStr['field2'] := 'Something other';
data_dst['field3'] := 'Value of field 1' ; // просто строка AnsiString
data_dst.DateTime['field4'] := Now;
// пишем результат в БД
db.InsertRecord(data_dst, TABLE_RESULTS);
// Коммит всего этого
db.Commit;
finally
// Если коммит не случился, то это значит что произошло исключение. Тогда перед освобождением соединения, там делается автоматический RollBack.
// А само исключение пускай всплывает выше — это не наша забота
DBPool.ReleaseConnection(db);
end;
end;