RTTIを介したDelphiのアナログ.Netエンティティフレームワーク。 パート1、入門

デリーがEmbarcaderoで復活した後、私はC#での開発からより身近なツールに戻りました。 ほとんどの構文能力、クラス、およびさまざまな「ささいなこと」がシャープから魔法のように変わったことに非常に満足しています。 残念ながら、クラスのコレクションにあるデータベースからの選択を表示するような良い機会は、括弧で囲まれていませんでした。



私たちのプロジェクトでは、さまざまなサンプルのアルゴリズム処理が必要になることがよくありますが、SQLを使用した実装は不可能です。 各サンプルに対してクラスが作成され、新しいサンプルを作成する必要があるたびに、まったく同じ動作を実行する必要がありましたが、クラスフィールドをペンで埋める必要がありました。



私たちの頭を捨て、RTTIの可能性、人件費、タンバリンの利用可能性を評価すると、退屈な生活では不十分なデータベースを操作するためのウィッシュリストのリストが得られました。



  1. 開発されたデータベースのテーブル構造に応じたクラスの自動生成。
  2. クラスリストにテーブルのデータを入力する。
  3. クラスの作成を実装するために、データベーステーブルの構造を読み取ることは不要です。
  4. データベース構造を手元に持つことで、自動化できます。




そして、適切な実装と遠く離れた正確な作業により、さまざまなタイプのSQLサーバー間でのクロスプラットフォーム作業の可能性が見え始めています。



簡単なことから始めましょう



DataSetsからクラスにデータをマップする能力を確認しましょう。 更新された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の文字列をクラスのインスタンスにマップすることです。 読み取りコードはすべて別のモジュールに配置され、約150行かかります。



 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.
      
      





ジェネリッククラスを処理するために、仮想コンストラクタTBaseDataRecordを使用してベーステーブル行エンティティクラスを作成し、そこから実際のテーブル行エンティティを生成することが望ましいです(TUsersPersonsEntity宣言を参照)。 基本クラスに加えて、モジュールには汎用リーダークラスがあります。 そのタスクは、DataSetを調べ、行のインスタンスを作成し、現在の選択行からTBaseDataRecord後継の作成されたインスタンスにパームし、結果リストに保存することです。



サンプルからクラスにデータを表示する機能は、TBaseDataRecordでレンダリングされます。 クラスプロパティを列挙する場合、DataSetのフィールドは同じ名前で検索されます。 フィールドが見つかった場合、バリアント型とTValueを使用した簡単なシャーマニズムの後、必要な値がプロパティに表示されます。



残念ながら、「すべてがそれほど単純ではない」。 SetPropertyValueByFieldメソッドでは、現在のプロパティがTGUID型であることを確認する必要があります。 MSSQLはGUIDを文字列として提供し、直接割り当てを行うとエラーが発生します。 文字列をGUIDに明示的に変換する必要があります。 さらに、さらに使用すると、次のスクワットが必要になることがわかりました。





リストは、検出されると常に更新されます。 しかし、主なことはそれが機能することです。 そして、それは次のように機能します(実際のプログラムテキスト):



 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(クエリ); 。 それだけです。 ただし、コンパクト。 そして、これがアプリケーションの出力です:



画像



次は何ですか



これは単なる機会のテストです。 「フラットな」単純なクエリの場合、上記のメカニズムは非常に機能的です。



そして-データベースコントラクトとテーブルエンティティの自動作成、参照データベーススキーマの作成、エンティティリストのリンク、データの更新、リストのシリアル化、クロスプラットフォームの読み取り。



All Articles