5 Kasım 2018 Pazartesi

Generics (soysallar) ve TCustomAttribute kullanımı ve RTTI

Genericleri anlatmaya tersten başladım aslında, yani listelerden. Oysa öncelikle soysal tip bildirimi kullanılan sınıflardan bahsetmem gerekiyordu. Belki de bu sistemi yeni kullanmaya başladığım içindir.

Ancak şu unutulmamalıdır ki generic kullanımı normalde fazla gerekmez, ta ki RTTI kullanıncaya kadar. Ve RTTI kullanımı da sizi modern tasarımlara Design Pattern'lere götürecektir, Design Pattern kullanmaya başladıktan sonra ise tüm tasarımlarınız generic içermeye başlayacaktır. Yani aslında şunu söyleyebilirim, eğer Design Pattern kullanmaya niyetiniz yoksa bu sayfayı okumanıza da hiç gerek yok bence. Direkt olarak atlamanızı tavsiye ederim.

Benim Delphi yazılımcılarında tanık olduğum en ciddi sorun, moderniteye sırtlarını dönmüş olmaları. Çoğunlukla Delphi 7 onlara yetiyor, web yazılımı yapacakları zaman DotNet ya da PHP ile işlerini halletmeye çalışıyorlar, bitiyor gidiyor. İşin enteresanı, bu dillere geçtikleri zaman modern yazılımı, Design Pattern'leri öğrenmeye çalışıyorlar. Bunları Delphi'de uygulamak akıllarına gelmiyor. Varsa yoksa komponentler, hem de VCL. Neyse bu ayrı bir yazı konusu.

Bu yazıda bir procedure'de generic nasıl kullanılır, onu inceleyeceğiz.
Generic'i tanımlarsak : Bir Class'a, bir procedure ya da function'a <T : Base type> şeklinde verdiğimiz bir tip bildirimidir. Bu tip bildirimini daha sonra Class'ın (ya da neye verdiysek onun) içinde kullanabiliriz. Bu run time esnasında sanki aralara a.inc dosyasını include etmek gibidir. Yani buradaki T tipinin geçtiği her yere bu tip kopyalanır. Burada istersek Base type da kullanabiliriz. Bunun anlamı, verdiğimiz tip bildirimi bu base type ya da bundan türemiş bir type olması gerektiğidir. Bu sistemin en güzel yanı, aralarında bir uyumsuzluk olduğunda hemen derleme hatası vermesidir. Görüldüğü gibi bize ciddi bir type safety (tip güvenliği) sağlamaktadır.

Bunun için öncelikle bir DB tablosunun entity'sini oluşturacağız,
Bunun için MySQL üzerinde gelen örnek veritabanı olan SAKILA'yı kullanacağım.

  TActor = class(TBaseModel)
  private
    Factor_id: Integer;
    Ffirst_name: string;
    Flast_name: string;
    Flast_update: TDateTime;
    Fis_active: Boolean;
  published
    property ActorId: Integer read Factor_id write Factor_id;
    property FirstName: string read Ffirst_name write Ffirst_name;
    property LastName: string read Flast_name write Flast_name;
    property IsActive: Boolean read Fis_active write Fis_active;
    property LastUpdate: TDateTime read Flast_update write Flast_update;
  end;


Bu tabloya fazladan IsActive kolonunu, tablo yaratmak için Sql komutunu oluştururken  bazı  kısımlara da girmesi için ekledim.

Sql komutunu yazdıracağımız entity bu ancak bu sınıfı sql komutuna çevirebilmek için bu bilgilerden fazlasına ihtiyacımız var. Tablonun adı, tablo kolonlarının adı, uzunluğu, ondalık kısım gibi bilgileri de bilmemiz gerekiyor. İşte bu noktada imdadımıza TCustomAttribute yetişiyor.
Custom attribute'ler bir class, class'ın bir değişkeni (field), bir property'si veya metodu için tanımlanabilirler. Custom attribute, bu class'ın şu özelliği de var demek anlamına gelir. Daha sonra bu özellikleri alarak onlar üzerinde işlemler yapabiliriz.  Kullanımı şu şekildedir;

  [Entity('actor')]                // EntityAttribute
  TActor = class(TBaseModel)
    ...
    [Column('first_name', [], 45)]

    property FirstName: string read Ffirst_name write Ffirst_name;
    ...
  end;

Custom Attribute için ayrı bir unit oluşturup içine Attribute tanımlarımızı yapıyoruz. EntityAttribute tablonun ismini Entity class'ına iliştirmek ve daha sonra almak için kullanılır. ColumnAttribute ise her bir kolonun özelliklerini o kolonlara iliştirmek için kullanılır.

  EntityAttribute = class(TCustomAttribute)
  private
    FTableName : string;
    FSchemaName : string;
  public
    constructor Create(const ATableName: string; const ASchemaName: string = '');

    property TableName: string read FTableName;
    property Schema: string read FSchemaName;
  end;

  TColProp = (
    cpPrimaryKey,
    cpRequired,
    cpUnique,
    cpNotNull,
    cpAutoGenerated
  );
  TColProps = set of TColProp;
  ColumnAttribute = class(TCustomAttribute)
  private
    FColumnName : string;
    FLength: Integer;
    FPrecision: Integer;
    FScale: Integer;
    FColProps : TColProps;
    function GetIsPrimaryKey : Boolean;
    function GetIsAutoGenerated : Boolean;
  public
    constructor Create(const AColumnName : string) overload;
    constructor Create(const AColumnName : string; AColProps : TColProps;
                       ALength : Integer=0;
                       APrecision : Integer=0;
                       AScale : Integer=0); overload;
    property ColumnName: string read FColumnName write FColumnName;
    property Length: Integer read FLength write FLength;
    property Precision: Integer read FPrecision write FPrecision;
    property Scale: Integer read FScale write FScale;
    property ColProps: TColProps read FColProps write FColProps;
    property IsPrimaryKey: Boolean read GetIsPrimaryKey;
    property IsAutoGenerated: Boolean read GetIsAutoGenerated;
  end;

Ve bunların metodlarını yazıyoruz (sadece atamalar var).

Bunun arkasından Entity'lerimizi Attribute'lerle donatıyoruz.

  [Entity('actor')]                //] EntityAttribute
  TActor = class(TBaseModel)
  private
    Factor_id: Integer;
    Ffirst_name: string;
    Flast_name: string;
    Flast_update: TDateTime;
    Fis_active: Boolean;
  published
    [Column('actor_id', [cpPrimaryKey, cpRequired, cpUnique, cpNotNull])]
    property ActorId: Integer read Factor_id write Factor_id;
    [Column('first_name', [], 45)]
    property FirstName: string read Ffirst_name write Ffirst_name;
    [Column('last_name', [], 45)]
    property LastName: string read Flast_name write Flast_name;
    [Column('is_active')]
    property IsActive: Boolean read Fis_active write Fis_active;
    [Column('last_update')]
    property LastUpdate: TDateTime read Flast_update write Flast_update;
  end;

Bu biraz ORM tanımlamalarına benziyor değil mi? Zaten amacımız bu Entity'yi kullanarak Table Create SQL cümleciği oluşturmak.

Şimdi sıra bu Sql'i oluşturacak sınıfı yazmaya geldi.
  TSqlCreateBuilder<T : TBaseModel> = class
  private
    function GetColumnDefinition(const AColName: string; AColProps: TColProps;
      AColType: TTypeKind; ALen, APrec, AScl: Integer; ATypeInfo : Pointer;
      AAutoGen : boolean): string;
    function BuildSQLDataType(AColType : TTypeKind; ALen, APrec, AScl : integer; ATypeInfo : Pointer) : string;
  public
    function Execute : string;
  end;

Burada gördüğümüz gibi T yi tip belirteci olarak gösterdik. Daha sonra yazdığımız TBaseModel sözcüğü T için göndereceğimiz tipin en azından TBaseModel olması ya da ondan türetilmiş bir sınıf olması gerektiğini belirtir. Bu yanlış class geçmeyi engellemek için iyi bir yöntemdir. Programdan daha sonra geçtiğimiz entity tipi bu class içinde doğrudan o tanımlıymış gibi işlem görecektir.

Yazacağımız yordamda ilk önce EntityAttribute'den tablo adını elde ediyoruz. 
1-RttiContext yarat,
  FRttiContext := TRttiContext.Create;
2-Bundan GetType ile RttiType bilgisini al,
    LRttiType := FRttiContext.GetType(TypeInfo(T));
3-Bu entity'ye iliştirilmiş tüm Attribute'leri tarayıp EntityAttribute'yi bulmak için bir for döngüsü oluştur ve EntityAttribute'yi bulunda tableName'i al, döngüden çık.

    tblName := '';
    for attr in LRttiType.GetAttributes do
      if attr is EntityAttribute then
      begin
        tblName := (attr as EntityAttribute).FullName;
        Break;
      end;

4-Tablonun kolon bilgilerini almak için Entity'nin property'leri üzerinde bir döngü oluşturuyoruz.

    for prop in LRttiType.GetProperties do
      if prop.Visibility=mvPublished  then    //!!!
      begin
        aautogen := False;
        for attr in prop.GetAttributes do
          if ((attr as ColumnAttribute).IsAutoGenerated) then
          begin
            aautogen := True;
            Break;
          end;

        fldName := prop.Name;
        alength := 0;
        aprecision := 0;
        ascale := 0;
        colProps := [];

        for attr in prop.GetAttributes do
          if (attr is ColumnAttribute) then
          begin
            fldName := (attr as ColumnAttribute).ColumnName;
            alength := (attr as ColumnAttribute).Length;
            aprecision := (attr as ColumnAttribute).Precision;
            ascale := (attr as ColumnAttribute).Scale;
            colProps := (attr as ColumnAttribute).ColProps;
            Break;
          end;

        s := GetColumnDefinition(fldName, colProps, prop.PropertyType.TypeKind, alength, aprecision, ascale, prop.PropertyType.Handle, aautogen);
        if Result='' then
          Result := Result + s + #13#10
        else
          Result := Result + ','+s + #13#10;
      end;

Görüldüğü gibi 2.satırda sadece Published yapılmış property'leri almış oluyoruz. 
Tüm kolon özelliklerini alıyoruz ve GetColumnDefinition adlı metoda gönderip gelen sonucu Sql komutuna ekliyoruz.

5-GetColumnDefinition'ı yazıyoruz;

function TSqlCreateBuilder<T>.GetColumnDefinition(const AColName: string;
  AColProps: TColProps; AColType: TTypeKind; ALen, APrec, AScl: Integer;
  ATypeInfo: Pointer; AAutoGen: boolean): string;
begin
  Result := Format('%0:s %1:s %2:s %3:s', [
    AColName,
    BuildSQLDataType(AColType, ALen, APrec, AScl, ATypeInfo),
    IfThen(cpNotNull in AColProps, 'NOT NULL', 'NULL'),
    IfThen(cpPrimaryKey in AColProps, IfThen(AAutoGen, 'IDENTITY(1,1) ')+'PRIMARY KEY')]);
end;

6-Burada adı geçen BuildSQLDataType adlı metodu yazıyoruz. 

function TSqlCreateBuilder<T>.BuildSQLDataType(AColType: TTypeKind; ALen, APrec,
  AScl: integer; ATypeInfo: Pointer): string;
begin
  Result := 'INTEGER';
  case AColType of
    tkUnknown: ;
    tkInteger, tkSet:
      if APrec > 0 then
        Result := Format('NUMERIC(%0:d, %1:d)', [APrec, AScl]);
    tkEnumeration:
      if ATypeInfo = System.TypeInfo(Boolean) then
        Result := 'BIT';
    tkInt64:
      if APrec > 0 then
        Result := Format('NUMERIC(%0:d, %1:d)', [APrec, AScl])
      else
        Result := 'BIGINT';
    tkChar: Result := Format('CHAR(%d)', [ALen]);
    tkFloat:
      if ATypeInfo = System.TypeInfo(TDate) then
        Result := 'DATE'
      else
      if ATypeInfo = System.TypeInfo(TDateTime) then
       Result := 'DATETIME'
       //Result := 'TIMESTAMP'
      else
      if ATypeInfo = System.TypeInfo(TTime) then
        Result := 'TIME'
      else
        if APrec > 0 then
          Result := Format('NUMERIC(%0:d, %1:d)', [APrec, AScl])
        else
          Result := 'FLOAT';
    tkString, tkLString: Result := Format('VARCHAR(%d)', [ALen]);
    tkClass, tkArray, tkDynArray, tkVariant: Result := 'BLOB';
    tkMethod: ;
    tkWChar: Result := Format('NCHAR(%d)', [ALen]);
    tkWString, tkUString: Result := Format('NVARCHAR(%d)', [ALen]);
    tkRecord: ;
    tkInterface: ;
    tkClassRef: ;
    tkPointer: ;
    tkProcedure: ;
  end;
end;

Görüldüğü gibi burada verilen Entity class'ından kolon tipini bulup onun karşılığını oluşturuyoruz (buradaki tanımlamalar MSSQL içindir).
Programı çalıştırıp Build SQL tuşuna basıldığı zaman;




Bu da son oluyor zaten.

Bununla ilgili kaynak kodları şu linkten indirebilirsiniz.