Было, уже проходили такое.
Параллельно зародилось два подхода. И они использовались некоторое время.
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;