После того, как в Embarcadero оживили Delhi, я вернулся с разработки на C# к более привычному инструменту. Серьезно порадовало, что большинство синтаксических возможностей, классов и различных «рюшечек» волшебным образом переехало из шарпа. К сожалению, такая приятная возможность, как отображение выборки из базы данных на коллекции классов осталась за скобками.

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

Раскинув мозгом и оценив возможности RTTI, трудозатраты и наличный запас бубнов, у нас получился список «хотелок» для работы с БД, которых не хватает в нашей скучной жизни:

  1. Автоматическая генерация классов по структуре таблиц разрабатываемой БД.
  2. Заполнение списков классов данными из таблиц.
  3. Для реализации создания классов будет не лишним считывать структуру таблиц БД.
  4. Имея на руках структуру БД можно автоматизировать:

  • Сравнение структуры существующей БД с эталонной для предупреждения ошибок при обновлении разрабатываемого ПО у конечного пользователя;
  • Формирование «контракта БД», содержащего в себе константы названий таблиц, полей, хранимых процедур и функций;
  • Создание классов из пп. 1. с учетом связей между таблицами.
  • Создание «оберток» для вызова хранимых процедур и функций.

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

Начнем с простого


Проверим саму возможность отображения данных из DataSet-ов на классы. Обновленный RTTI позволяет перечислять имена свойств класса, а также, получать и устанавливать значения свойств.

Создадим пример выборки из простой таблицы и заполнения списка классов, содержащих публичные свойства, совпадающие по названию с полями таблицы. Работать будем MS SQL сервером.

Создадим БД, в ней таблицу с физ. лицами и парой записей:

USE [master]
GO

CREATE DATABASE [TestRtti]
GO

USE [TestRtti]
GO

CREATE TABLE [dbo].[Users_Persons](
	[Guid] [uniqueidentifier] ROWGUIDCOL  NOT NULL CONSTRAINT [DF_Users_Persons_Guid]  DEFAULT (newid()),
	[Created] [datetime2](7) NOT NULL CONSTRAINT [DF_Users_Persons_Created]  DEFAULT (getutcdate()),
	[Written] [datetime2](7) NOT NULL CONSTRAINT [DF_Users_Persons_Written]  DEFAULT (getutcdate()),
	[First_Name] [nvarchar](30) NOT NULL,
	[Middle_Name] [nvarchar](30) NOT NULL,
	[Last_Name] [nvarchar](30) NOT NULL,
	[Sex] [bit] NOT NULL,
	[Born] [date] NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Users_Persons] ADD  CONSTRAINT [PK_Users_Persons] PRIMARY KEY NONCLUSTERED 
(
	[Guid] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

INSERT [dbo].[Users_Persons] ([Guid], [Created], [Written], [First_Name], [Middle_Name], [Last_Name], [Sex], [Born]) 
VALUES (N'291fefb5-2d4e-4ccf-8ca0-25e97fabefff', CAST(N'2016-07-21 10:56:16.6630000' AS DateTime2), CAST(N'2016-12-09 16:22:01.8670000' AS DateTime2), 
N'Петр', N'Николаевич', N'Иванов', 1, CAST(N'1970-01-01' AS Date))
GO
INSERT [dbo].[Users_Persons] ([Guid], [Created], [Written], [First_Name], [Middle_Name], [Last_Name], [Sex], [Born]) 
VALUES (N'11ad8670-158c-4777-a099-172acd61cbd3', CAST(N'2016-07-21 10:59:02.2030000' AS DateTime2), CAST(N'2016-12-09 16:22:10.4730000' AS DateTime2), 
N'Андрей', N'Юрьевич', N'Смирнов', 1, CAST(N'1970-01-01' AS Date))
GO

Ручками в модуле UsersPersonsEntity.pas создадим класс TUsersPersonsEntity и, забегая вперед, объявим его список и создадим для него тип класса-читателя:

unit UsersPersonsEntity;

interface

uses
  Generics.Collections, DataSetReader;

type
  TUsersPersonsEntity = class(TBaseDataRecord)
  private
    FGuid: TGUID;
    FCreated: TDateTime;
    FWritten: TDateTime;
    FFirstName: String;
    FMiddleName: String;
    FLastName: String;
    FSex: Boolean;
    FBorn: TDate;
  public
    property Guid: TGUID read FGuid write FGuid;
    property Created: TDateTime read FCreated write FCreated;
    property Written: TDateTime read FWritten write FWritten;
    property First_Name: String read FFirstName write FFirstName;
    property Middle_Name: String read FMiddleName write FMiddleName;
    property Last_Name: String read FLastName write FLastName;
    property Sex: Boolean read FSex write FSex;
    property Born: TDate read FBorn write FBorn;
  end;

  TUsersPersonsList = TDataRecordsList<TUsersPersonsEntity>;
  TUsersPersonsReader = TDataReader<TUsersPersonsEntity>;

implementation

end.

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

unit DataSetReader;

interface

uses
  System.TypInfo, System.Rtti, SysUtils, DB, Generics.Collections, Generics.Defaults;

type
  TBaseDataRecord = class
  public
    constructor Create; overload; virtual;
    procedure SetPropertyValueByField(ClassProperty: TRttiProperty;
      Field: TField; FieldValue: Variant);
    procedure SetRowValuesByFieldName(DataSet: TDataSet);
    procedure AfterRead; virtual;
  end;

  TBaseDataRecordClass = class of TBaseDataRecord;

  TDataRecordsList<T: TBaseDataRecord> = class(TObjectList<T>);

  TDataReader<T: TBaseDataRecord, constructor> = class
  public
    function Read(DataSet: TDataSet; ListInstance: TDataRecordsList<T> = nil;
      EntityClass: TBaseDataRecordClass = nil): TDataRecordsList<T>;
  end;

implementation

var
  Context: TRttiContext;

  { TBaseDataRecord }

constructor TBaseDataRecord.Create;
begin
end;

procedure TBaseDataRecord.AfterRead;
begin
end;

procedure TBaseDataRecord.SetPropertyValueByField(ClassProperty: TRttiProperty; Field: TField;
  FieldValue: Variant);

  function GetValueGuidFromMsSql: TValue;
  var
    Guid: TGUID;
  begin
    if Field.IsNull then
      Guid := TGUID.Empty
    else
      Guid := StringToGUID(Field.AsString);
    Result := TValue.From(Guid);
  end;

var
  Value: TValue;
  GuidTypeInfo: PTypeInfo;
begin
  if Field = nil then
    Exit;
  GuidTypeInfo := TypeInfo(TGUID);
  Value := ClassProperty.GetValue(Self);
  case Field.DataType of
    ftGuid: begin
        if Value.TypeInfo = GuidTypeInfo then
          ClassProperty.SetValue(Self, GetValueGuidFromMsSql)
        else
          ClassProperty.SetValue(Self, TValue.FromVariant(FieldValue));
      end;
  else
    ClassProperty.SetValue(Self, TValue.FromVariant(FieldValue));
  end;

end;

procedure TBaseDataRecord.SetRowValuesByFieldName(DataSet: TDataSet);
var
  Field: TField;
  FieldName: String;
  FieldValue: Variant;
  ClassName: String;
  ClassType: TRttiType;
  ClassProperty: TRttiProperty;
begin
  ClassName := Self.ClassName;
  ClassType := Context.GetType(Self.ClassType.ClassInfo);
  for ClassProperty in ClassType.GetProperties do
  begin
    Field := DataSet.FindField(ClassProperty.Name);
    if Field <> nil then
    begin
      FieldName := Field.FieldName;
      FieldValue := Field.Value;
      SetPropertyValueByField(ClassProperty, Field, FieldValue);
    end;
  end;
end;

{ TDataReader<T> }

function TDataReader<T>.Read(DataSet: TDataSet; ListInstance: TDataRecordsList<T>;
  EntityClass: TBaseDataRecordClass): TDataRecordsList<T>;
var
  Row: T;
begin
  if ListInstance = nil then
    Result := TDataRecordsList<T>.Create
  else begin
    Result := ListInstance;
    Result.OwnsObjects := True;
    Result.Clear;
  end;

  DataSet.DisableControls;
  Result.Capacity := DataSet.RecordCount;
  while not DataSet.Eof do
  begin
    if EntityClass = nil then
      Row := T.Create()
    else
      Row := EntityClass.Create() as T;

    Row.SetRowValuesByFieldName(DataSet);

    Row.AfterRead;
    Result.Add(Row);
    DataSet.Next;
  end;
end;

initialization

Context := TRttiContext.Create;

end.

Для удобства оперирования generic классами желательно создать базовый класс сущности строки таблицы с виртуальным конструктором TBaseDataRecord и порождать от него реальные сущности строк таблиц (см. объявление TUsersPersonsEntity). Помимо базового класса, в модуле присутствует generic класс «читатель». Его задача пробегаться по DataSet-у, создавать экземпляры строк и подсовывать текущую строку выборки созданному экземпляру наследника TBaseDataRecord и складировать его в результирующий список.

Функционал отображения данных из выборки на класс вынесен в TBaseDataRecord. При переборе свойств класса производится поиск в DataSet полей с таким же именем. Если поле найдено, то после легкого шаманства с вариантными типами и TValue, в свойстве оказывается требуемое значение.

К сожалению, «не всё так однозначно». В методе SetPropertyValueByField приходится проверять, что текущее свойство имеет тип TGUID. MSSQL отдает GUID в виде строки и прямое присвоение даст ошибку. Приходится явно преобразовывать строку к GUID. Более того, дальнейшее применение показало необходимость дополнительных приседаний для:

  • MSSQL, OLEDB и DATE, DATETIME
  • Обработка BLOB-ов
  • Firebird и GUID при хранении в CHAR(16) CHARACTER SET OCTETS
  • Firebird и TIMESTAMP

Список постоянно пополняется по мере обнаружения. Но главное — оно работает. И работает следующим образом (собственно текст программы):

program TestRtti;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  DB, ADODB, System.SysUtils, ActiveX,
  DataSetReader in 'DataSetReader.pas',
  UsersPersonsEntity in 'UsersPersonsEntity.pas';
var
  Connection: TADOConnection;
  Query: TADOQuery;
  UsersPersons: TUsersPersonsList;
  UserPerson: TUsersPersonsEntity;
  Reader: TUsersPersonsReader;
  i: Integer;

begin
  ReportMemoryLeaksOnShutdown := True;
  UsersPersons := nil;
  try
    CoInitialize(nil);
    Connection := TADOConnection.Create(nil);
    try
      Connection.ConnectionString :=
        'Provider=SQLNCLI11.1;Integrated Security=SSPI;Persist Security Info=False;User ID="";' +
        'Initial Catalog="TestRtti";Data Source=localhost;Initial File Name="";Server SPN=""';
      Connection.Connected := True;

      Query := TADOQuery.Create(nil);
      Reader := TUsersPersonsReader.Create;
      try
        Query.Connection := Connection;
        Query.SQL.Text := 'SELECT * FROM Users_Persons';
        Query.Open;

        UsersPersons := Reader.Read(Query);

        Writeln('Прочитано записей: ', UsersPersons.Count);
        for i := 0 to UsersPersons.Count - 1 do begin
          UserPerson := UsersPersons[i];
          Writeln(Format('%d. %s %s %s %s', [i + 1, UserPerson.First_Name, UserPerson.Middle_Name,
            UserPerson.Last_Name, FormatDateTime('dd.mm.yyyy', UserPerson.Born)]));
        end;
        Writeln('Нажмите Enter для завершения...');
        Readln;
      finally
        Query.Free;
        Reader.Free;
      end;
    finally
      Connection.Free;
      if UsersPersons <> nil then
        FreeAndNil(UsersPersons);
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Главное в коде это строка UsersPersons := Reader.Read(Query);. И всё. Компактненько, однако. А вот и вывод приложения:

image

Что дальше


Это только проверка возможностей. Хотя для «плоских» простых запросов приведенный механизм вполне работоспособен.

А дальше -­ автоматическое создание контракта БД и сущностей таблиц, создание эталонной схемы БД, связывание списков сущностей, обновление данных, сериализация списков и кроссплатформенное чтение.
Поделиться с друзьями
-->

Комментарии (29)


  1. Varim
    23.12.2016 15:52

    В Delphi есть Класс Expression?
    RTTI в Delphi это аналог Reflection в .net или аналог RTTI в C++?
    Я не настоящий сварщик, вроде бы в RTTI C++ нет доступа к полям и наследованию?
    EF code-first migrations собираетесь делать?


    1. maxx0
      23.12.2016 17:31

      В Delphi есть Класс Expression?

      Если 10-ке есть LINQ, то возможно и есть
      RTTI в Delphi это аналог Reflection в .net или аналог RTTI в C++?
      Я не настоящий сварщик, вроде бы в RTTI C++ нет доступа к полям и наследованию?

      Есть старая и новая модели RTTI в Delphi. Но рефлексия в чистом виде — сомневаюсь. А старая модель должна быть близка к плюсАм. Вот только к каким?
      EF code-first migrations собираетесь делать?

      Нашими силами не справимся, тут помощник нужен


    1. Error1024
      23.12.2016 18:49
      -2

      Скорее Reflection в .net это аналог Delphi RTTI.


      1. dmitryredkin
        25.12.2016 14:36

        Зря заминусовали.
        Delphi 1.0 (RTTI) — 1995.
        Java 1.1 (java.lang.reflect) — 1997
        Про .Net вообще молчу.


        1. Varim
          25.12.2016 15:00

          так RTTI вроде гораздо скромнее версия чем .net reflection


          1. Error1024
            29.12.2016 01:54

            Сейчас RTTI в Delphi очень богат


  1. indestructable
    23.12.2016 16:20

    А в новом Delphi разве не .net?


    1. maxx0
      23.12.2016 17:16

      Нет, не .net

      Процесс среды разработки bds.exe 2-й и 3-й XE распознавался Sysinternals Process Explorer как .Net.
      Embarcadero объясняли «тяжелым наследием Delphi.Net». В XE6 такого не наблюдается, всё таки это нативный код


    1. dmitryredkin
      25.12.2016 14:36

      Не, этот поезд ушел лет 10 назад…


  1. impwx
    23.12.2016 16:24
    +1

    «Аналог EF» — очень громко сказано. Пока что у вас получился даже не ORM, а просто маппер данных из БД в сущности, наподобие Dapper. Однако желаю удачи в вашем проекте. Будет интересно посмотреть, есть ли в Delphi возможность реализовать нечто аналогичное LINQ.


    1. maxx0
      23.12.2016 17:10

      Статья вводная, см. сабж. И проходила песочницу, не понятно было, пройдет ли.
      Заявленная цель — проверка возможности мапинга.
      LINQ в Абракадабре обещали в 10-ке. Мы пока пользуем 6-ку.
      А так — реализация всего заявленного в окончании статьи на руках есть и работает


      1. eddHunter
        23.12.2016 17:33
        -1

        На delphi я программировал на младших курсах ВУЗА, поэтому интересно: лямбы появились в delphi?
        Если да — аналог linq можно и самому написать по приколу)


        1. Error1024
          23.12.2016 18:52
          -1

          Лямдбы — да, правда с «жирным» синтаксисом.


        1. indestructable
          23.12.2016 19:31
          +2

          Для аналога LINQ нужны не лямбды, а Expressions, которые компилятором преобразуются в дерево выражений.


      1. Error1024
        23.12.2016 19:43

        LINQ в Абракадабре обещали в 10-ке.

        Хм, не видел, можно линк.
        Впрочем в 10-ке их LINQ нет.


      1. Varim
        24.12.2016 06:01

        LINQ в Абракадабре обещали в 10-ке
        Что такое Абракадабра?


        1. yarosroman
          24.12.2016 07:12

          Embarcadero


    1. Impet
      24.12.2016 16:00

      (не рекламы ради а для информации)
      есть продукт TMS Aurelius. Реализует ORM и нечто подобное на LINQ.


  1. eddHunter
    23.12.2016 17:29

    Было бы не плохо найти способ закешировать маппер классы, дабы каждый раз не обращаться к рефлексии.


  1. icex
    23.12.2016 21:31

    Embarcadero добавило "сахару" — возможность была ранее даже в версии 6 (ниже не знаю). published, METHODINFO ON для класса или подключение интерфейса IInvokable


  1. Luptik
    24.12.2016 16:01
    +1

    Для Delphi есть DevArt EntityDAC — ORM with LINQ support
    https://www.devart.com/entitydac/


  1. kryvichh
    24.12.2016 16:01

    А почему не взяли за основу mORMot к примеру?


  1. DARKShadow
    24.12.2016 16:01

    Посмотрите в сторону spring4d. Особенно на ветку develop.


  1. maxx0
    24.12.2016 16:14

    1. Зачем брать что то, если в состоянии своими силами и небольшими трудозатратами закрыть функционал, в котором потом будешь уверен на 146%? Вопрос чисто риторический — сколько, например, в Fast Report, Pascal Script etc. предупреждений при сборке? Как часто красиво валится Chart в design-time? Кто нибудь группу проектов в Delphi числом больше 20-ти собирал без EOutOfMemory? Мы стараемся в нашу «систему с ответственностью 24x7» не вносить чужого кода в критичные компоненты.
    2. Каждому члену команды интересно развиваться. У меня очередь выстраивалась на «дай RTTI ковырнуть», очень полезно для саморазвития, позволяет отвлечься и поднять ЧСВ. Здесь смайл.


    1. Luptik
      24.12.2016 20:29

      Брать и не нужно. Это скорее примеры с ответами на Ваши вопросы. Можно что-то подсмотреть, почитать отзывы и переступить их грабли! :)


    1. ElectroGuard
      26.12.2016 11:27

      Вроде бы как в десятке OutOfMemory поправили, увеличив в два раза доступное место для проектов в памяти в 2 раза — с 2х до 4х. Мы пока сидим на 6ке, тоже с ответственными проектами 24х7х365.


  1. Begunini
    26.12.2016 11:28

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


    ИМХО, но модели баз данных надо делать самому, чтобы в БД данные писались по образу модели, а не наоборот


    1. maxx0
      26.12.2016 13:49

      Т.е. строго по изложенному «ИМХО» и приведенной цитате — нужно автоматически генерировать структуру БД из классов? Выражайтесь яснее


      1. Luptik
        28.12.2016 03:48

        Это наверное об MDA (model driven architecture). Лучше сконцентрироваться на реальном, а не плюшках. MDA может быть надстройкой… или совсем отдельным проектом…