unit Aurelius.Commands.Loader;

{$I Aurelius.Inc}

interface

uses
  Generics.Collections,
  Aurelius.Commands.Selecter,
  Aurelius.Criteria.Base,
  Aurelius.Drivers.Interfaces,
  Aurelius.Engine.ObjectManager,
  Aurelius.Mapping.Explorer,
  Aurelius.Mapping.Metadata,
  Aurelius.Sql.Commands,
  Aurelius.Types.Proxy;

type
  TProxyTempInfo = class
  public
    ProxyType: TProxyType;
    Association: TAssociation;
    FieldValue: Variant;
    constructor Create(AType: TProxyType; AAssociation: TAssociation; const AFieldValue: Variant);
  end;

  TEntityLoader = class
  strict private
    FSelecter: TSelecter;
    FResultSet: IDBResultSet;
    FCommand: TSelectCommand;
    FCriteria: TCriteria;
    FIdLoaded: Boolean;
    FUseCache: Boolean;
    function GetExplorer: TMappingExplorer;
    function GetManager: TObjectManager;
    function LoadId: Variant;
    property Selecter: TSelecter read FSelecter;
    property ResultSet: IDBResultSet read FResultSet;
    property Command: TSelectCommand read FCommand;
    property Criteria: TCriteria read FCriteria;
    property Explorer: TMappingExplorer read GetExplorer;
    property Manager: TObjectManager read GetManager;
    property UseCache: Boolean read FUseCache;
  strict private
    function HasNullId: Boolean;
    function CreateInstance(Depth: Integer): TObject;
    procedure LoadSimpleProperties(Entity: TObject);
    procedure LoadAssociations(Entity: TObject; Depth: Integer; ProxyTempList: TList<TProxyTempInfo>);
    procedure LoadSingleAssociationLazy(Entity: TObject; AssociationJoin: TAssociationJoin; ProxyTempList: TList<TProxyTempInfo>);
    procedure LoadSingleAssociationEager(Entity: TObject; AssociationJoin: TAssociationJoin; Depth: Integer);
    procedure LoadManyValuedAssociationLazy(Entity: TObject; AssociationJoin: TAssociationJoin; ProxyTempList: TList<TProxyTempInfo>);
    procedure LoadManyValuedAssociationEager(Entity: TObject; AssociationJoin: TAssociationJoin; Depth: Integer);
  strict protected
    function ColumnFieldName(Col: TColumn): string;
    function ReadFieldFromResultset(ResultSet: IDBResultSet; Col: TColumn): Variant; overload;
    function ReadFieldFromResultset(ResultSet: IDBResultSet; SelCol: TSelectColumn): Variant; overload;
  public
    function LoadObject(ResultObj: TObject; Depth: Integer; DontGoUpInheritance: Boolean): TObject;
    constructor Create(ASelecter: TSelecter; AResultSet: IDBResultSet;
      ACommand: TSelectCommand; ACriteria: TCriteria; AUseCache: Boolean);
  end;

function ReadObject(ASelecter: TSelecter; ResultObj: TObject; ACommand: TSelectCommand; AResultSet: IDBResultSet;
  ACriteria: TCriteria; Depth: Integer; DontGoUpInheritance: Boolean; UseCache: Boolean): TObject;

implementation

uses
  Variants, Rtti,
  Aurelius.Engine.BlobController,
  Aurelius.Engine.ProxyController,
  Aurelius.Global.Config,
  Aurelius.Sql.BaseTypes;

type
  TInternalCriteria = class(TCriteria)
  end;
  TInternalObjectManager = class(TObjectManager)
  end;

{ TProxyTempInfo }

constructor TProxyTempInfo.Create(AType: TProxyType; AAssociation: TAssociation;
  const AFieldValue: Variant);
begin
  ProxyType := AType;
  Association := AAssociation;
  FieldValue := AFieldValue;
end;

function ReadObject(ASelecter: TSelecter; ResultObj: TObject; ACommand: TSelectCommand; AResultSet: IDBResultSet;
  ACriteria: TCriteria; Depth: Integer; DontGoUpInheritance: Boolean; UseCache: Boolean): TObject;
var
  Loader: TEntityLoader;
begin
  Loader := TEntityLoader.Create(ASelecter, AResultSet, ACommand, ACriteria, UseCache);
  try
    Result := Loader.LoadObject(ResultObj, Depth, DontGoUpInheritance);
  finally
    Loader.Free;
  end;
end;

{ TEntityLoader }

function TEntityLoader.ColumnFieldName(Col: TColumn): string;
var
  Field: TSQLSelectField;
begin
  Field := TSQLSelectField.Create(Selecter.CreateSQLTable, Col.Name);
  try
    Result := Selecter.SQLGenerator.GetDefaultColumnName(Field);
  finally
    Field.Free;
  end;
end;

constructor TEntityLoader.Create(ASelecter: TSelecter; AResultSet: IDBResultSet;
  ACommand: TSelectCommand; ACriteria: TCriteria; AUseCache: Boolean);
begin
  FSelecter := ASelecter;
  FResultSet := AResultSet;
  FCommand := ACommand;
  FCriteria := ACriteria;
  FUseCache := AUseCache;
end;

function TEntityLoader.CreateInstance(Depth: Integer): TObject;
var
  ConcreteClass: TClass;
  FieldValue: Variant;
  descendantJoin: TAssociationJoin;
begin
  { Entity is not instantiated yet. We need to find the correct class to be instantiated.
    When using inheritance, it's a little more complex, and it depends on the inheritance
    strategy. }

  // First check if the columns belonging to primary key have null values. This can happen
  // in joined table strategy where we make joins to all possible subclasses.
  if HasNullId then
    Exit(nil);

  Result := nil;
  for descendantJoin in Selecter.DescendantJoins do
  begin
    Result := ReadObject(descendantJoin.Selecter, nil, Command, ResultSet, Criteria, Depth, True, UseCache);
    if Result <> nil then
      Break;
  end;

  if Result = nil then
  begin
    if (Selecter.DiscriminatorColumn <> nil) then
    begin
      FieldValue := Self.ReadFieldFromResultset(ResultSet, Selecter.DiscriminatorColumn);
      ConcreteClass := Explorer.GetClassByDiscriminator(Selecter.Clazz, FieldValue);
    end
    else
      ConcreteClass := Selecter.Clazz;

    Result := Manager.ObjectFactory.CreateInstance(ConcreteClass);
  end;
end;

function TEntityLoader.GetExplorer: TMappingExplorer;
begin
  Result := Selecter.Explorer;
end;

function TEntityLoader.GetManager: TObjectManager;
begin
  Result := TInternalObjectManager(Selecter.EntityManager);
end;

function TEntityLoader.HasNullId: Boolean;
begin
  // We just need to check if any of the id columns is null, because the purpose
  // is to see if there is a record for the specified table. If problems arise, review this later
  Result := Self.ReadFieldFromResultset(ResultSet, Selecter.PKSelectColumns[0]) = Variants.Null
end;

procedure TEntityLoader.LoadAssociations(Entity: TObject; Depth: Integer;
  ProxyTempList: TList<TProxyTempInfo>);
var
  associationJoin: TAssociationJoin;
  A: TAssociation;
begin
  for associationJoin in Selecter.AssociationJoins do
  begin
    A := associationJoin.Association;

    // We must test this, because when selecting, AssociationJoins might include
    // data from inherited classes. When using single-table inheritance, for example,
    // user asked to Find<TBaseClass> but we must include AssociationJoin from classes
    // inheriting from TBaseClass. But here in Loader we load each class specifically
    // so we don't need (should not) handle associations from inherited class
    if not Entity.ClassType.InheritsFrom(A.Optimization.ParentClass) then
      Continue;

    if A.Kind = TAssociationKind.SingleValued then
    begin
      // Association should be loaded in eager mode if any of these conditions is true:
      // 1. Association is not mapped as lazy (no proxy)
      // 2. User explicitly set FetchMode to Eager when building Criteria
      // 3. ProxyLoadDepth was not reached yet (ProxyLoadDepth indicates up until which level
      //    all proxies should be loaded as eager, no matter what).
      if not A.Lazy
        or ((associationJoin.Criteria <> nil) and (associationJoin.Criteria.FetchMode = TFetchMode.Eager))
        or (Manager.ProxyLoadDepth >= Depth) then
      begin
        LoadSingleAssociationEager(Entity, associationJoin, Depth);
      end
      else
        LoadSingleAssociationLazy(Entity, associationJoin, ProxyTempList);
    end
    else
    begin
      if not A.Lazy or (Manager.ProxyListLoadDepth >= Depth) then
      begin
        LoadManyValuedAssociationEager(Entity, associationJoin, Depth);
      end
      else
      begin
        LoadManyValuedAssociationLazy(Entity, associationJoin, ProxyTempList);
      end;
    end;
  end;
end;

function TEntityLoader.LoadId: Variant;
var
  FieldValue: Variant;
  I: Integer;
begin
  if Selecter.PKSelectColumns.Count = 1 then
    FieldValue := Self.ReadFieldFromResultset(ResultSet, Selecter.PKSelectColumns[0])
  else
  begin
    FieldValue := VarArrayCreate([0, Selecter.PKSelectColumns.Count - 1], varVariant);
    for I := 0 to Selecter.PKSelectColumns.Count - 1 do
      FieldValue[I] := Self.ReadFieldFromResultSet(ResultSet, Selecter.PKSelectColumns[I]);
  end;
  Result := FieldValue;
end;

procedure TEntityLoader.LoadManyValuedAssociationEager(Entity: TObject;
  AssociationJoin: TAssociationJoin; Depth: Integer);
var
  I: Integer;
  FieldValue: Variant;
  ListMemberClass: TClass;
  DetailObjects: TList<TObject>;
  A: TAssociation;
begin
  A := AssociationJoin.Association;
  ListMemberClass := A.Optimization.MemberClass;
  if A.JoinColumns.Count = 1 then
    FieldValue := Self.ReadFieldFromResultset(ResultSet, A.JoinColumns[0].ReferencedColumn)
  else
  begin
    FieldValue := VarArrayCreate([0, A.JoinColumns.Count - 1], varVariant);
    for I := 0 to A.JoinColumns.Count - 1 do
      FieldValue[I] := Self.ReadFieldFromResultSet(ResultSet, A.JoinColumns[I].ReferencedColumn);
  end;

  DetailObjects := TList<TObject>(Explorer.GetMemberValue(Entity, A.Optimization).AsObject);
  if DetailObjects = nil then
    DetailObjects := TList<TObject>(Manager.ObjectFactory.CreateInstance(ListMemberClass));
  DetailObjects.Clear;
  AssociationJoin.Selecter.SelectDetails(A.Optimization, FieldValue, DetailObjects, Depth + 1, not UseCache);
  Explorer.SetMemberValue(Entity, A.Optimization, DetailObjects);
end;

procedure TEntityLoader.LoadManyValuedAssociationLazy(Entity: TObject;
  AssociationJoin: TAssociationJoin; ProxyTempList: TList<TProxyTempInfo>);
var
  FieldValue: Variant;
  I: Integer;
  A: TAssociation;
begin
  A := AssociationJoin.Association;
  if A.JoinColumns.Count = 1 then
    FieldValue := Self.ReadFieldFromResultset(ResultSet, A.JoinColumns[0].ReferencedColumn)
  else
  begin
    FieldValue := VarArrayCreate([0, A.JoinColumns.Count - 1], varVariant);
    for I := 0 to A.JoinColumns.Count - 1 do
      FieldValue[I] := Self.ReadFieldFromResultSet(ResultSet, A.JoinColumns[I].ReferencedColumn);
  end;

  // Set proxy temporary info to create the proxy later
  // we have changed this code, before the proxy was being set here. Now we just set temporary info
  // and set the proxy only after this object is fully loaded.
  // This is because we need this object id and it's only available after full load
  // (because id might depend on associated objects)
  ProxyTempList.Add(TProxyTempInfo.Create(TProxyType.List, A, FieldValue));
end;

function TEntityLoader.LoadObject(ResultObj: TObject; Depth: Integer; DontGoUpInheritance: Boolean): TObject;
var
  C: TColumn;
  ProxyTempList: TList<TProxyTempInfo>;
  ProxyTempInfo: TProxyTempInfo;
  ResultId: Variant;
begin
  // Create the instance
  if ResultObj = nil then
  begin
    if UseCache and not FIdLoaded then
    begin
      FIdLoaded := True;
      ResultObj := Manager.FindCached(Selecter.Clazz, LoadId);
      if ResultObj <> nil then
        Exit(ResultObj)
    end;
    ResultObj := CreateInstance(Depth);
  end;
  if ResultObj = nil then Exit(nil);

  Result := ResultObj;

  // Read the simple properties for the object. All primitive columns declared in the class
  // that belongs to the same table/alias. That means that properties of ancestor class will
  // also be read here, if single table hierarchy. If joined table, ancestor/descendant properties will
  // be read in a different way
  LoadSimpleProperties(ResultObj);

  // Read data from ancestor class when using joined tables strategy.
  if not DontGoUpInheritance and Assigned(Selecter.AncestorJoin) then
    ReadObject(Selecter.AncestorJoin.Selecter, ResultObj, Command, ResultSet, Criteria, Depth, False, UseCache);

  ProxyTempList := TObjectList<TProxyTempInfo>.Create;
  try
    if Depth <= TGlobalConfigs.GetInstance.MaxEagerFetchDepth then
      LoadAssociations(ResultObj, Depth, ProxyTempList);

    // Get object Id
    ResultId := Explorer.GetIdValue(Result);

    // Set proxy controllers.
    // This must be AFTER all associations are loaded, because we might have a composite id that includes
    // an association, and thus we should have the id fields for association already loaded
    for ProxyTempInfo in ProxyTempList do
      Explorer.SetProxyController(Result, ProxyTempInfo.Association.Optimization,
        CreateProxyController(
          Manager,
          TOptimizedProxyInfo.Create(
            ProxyTempInfo.ProxyType,
            ProxyTempInfo.Association.Optimization,
            ProxyTempInfo.FieldValue,
            Result.ClassType,
            ResultId
          )));
  finally
    ProxyTempList.Free;
  end;

  // Set lazy attributes for blob
  // This must be AFTER all associations are loaded, because we might have a composite id that includes
  // an association, and thus we should have the id fields for association already loaded
  for C in Selecter.Columns do
  begin
    if not C.IsLazy then
      Continue;
    if not Result.ClassType.InheritsFrom(C.DeclaringClass) then
      Continue;
    Explorer.SetBlobController(Result, C.Optimization,
      TBlobController.Create(Manager, TBlobInfo.Create(C.Optimization, ResultId))
    );
  end;
end;

procedure TEntityLoader.LoadSimpleProperties(Entity: TObject);
var
  selectColumn: TSelectColumn;
  C: TColumn;
  FieldValue: Variant;
begin
  // Load regular properties
  for selectColumn in Selecter.SelectColumns do
  begin
    C := selectColumn.Column;
    if C.IsDiscriminator then
      Continue;
    if C.ReferencedClass <> nil then
      Continue;
    if not Entity.ClassType.InheritsFrom(C.DeclaringClass) then
      Continue;
    if C.IsForeign then
      Continue;
    if C.IsLazy then
      Continue;
//    if (Entity <> nil) and C.IsId then // TODO: Review this for composited id
//      Continue;

    FieldValue := Self.ReadFieldFromResultset(ResultSet, selectColumn);
    Explorer.SetColumnDbValue(Entity, C, FieldValue);
  end;
end;

procedure TEntityLoader.LoadSingleAssociationEager(Entity: TObject;
  AssociationJoin: TAssociationJoin; Depth: Integer);
var
  AssociatedObject: TObject;
begin
  AssociatedObject := ReadObject(AssociationJoin.Selecter, nil, Command, ResultSet, associationJoin.Criteria, Depth + 1, False, UseCache);
  Explorer.SetMemberValue(Entity, AssociationJoin.Association.Optimization, AssociatedObject);
end;

procedure TEntityLoader.LoadSingleAssociationLazy(Entity: TObject;
  AssociationJoin: TAssociationJoin; ProxyTempList: TList<TProxyTempInfo>);
var
  AssociationIsNull: boolean;
  I: Integer;
  FieldValue: Variant;
begin
  if AssociationJoin.SelectColumns.Count = 1 then
  begin
    FieldValue := Self.ReadFieldFromResultset(ResultSet, AssociationJoin.SelectColumns[0]);
    AssociationIsNull := VarIsNull(FieldValue);
  end
  else
  begin
    FieldValue := VarArrayCreate([0, AssociationJoin.SelectColumns.Count - 1], varVariant);
    AssociationIsNull := true;
    for I := 0 to AssociationJoin.SelectColumns.Count - 1 do
    begin
      FieldValue[I] := Self.ReadFieldFromResultSet(ResultSet, AssociationJoin.SelectColumns[I]);
      if not VarIsNull(FieldValue[I]) then
        AssociationIsNull := false;
    end;
  end;

  if AssociationIsNull then
    Explorer.SetMemberValue(Entity, AssociationJoin.Association.Optimization, TValue.Empty)
  else
  begin
    // Set proxy temporary info to create the proxy later
    // we have changed this code: before the proxy was being set here. Now we just set temporary info
    // and set the proxy only after this object is fully loaded.
    // This is because we need this object id and it's only available after full load
    // (because id might depend on associated objects)
    ProxyTempList.Add(TProxyTempInfo.Create(TProxyType.Single, AssociationJoin.Association, FieldValue));
  end;
end;

function TEntityLoader.ReadFieldFromResultset(ResultSet: IDBResultSet;
  SelCol: TSelectColumn): Variant;
var
  ResultSet2: IDBResultSet2;
  FieldName: string;
begin
  if SelCol.FieldIndex >= 0 then
    Result := Selecter.ReadFieldValue(ResultSet, SelCol.FieldIndex, SelCol.Column.FieldType)
  else
  begin
    FieldName := ColumnFieldName(SelCol.Column);
    if ResultSet.QueryInterface(IDBResultSet2, ResultSet2) = 0 then
    begin
      SelCol.FieldIndex := ResultSet2.GetFieldIndex(FieldName);
      Result := Selecter.ReadFieldValue(ResultSet, SelCol.FieldIndex, SelCol.Column.FieldType);
    end
    else
      Result := Selecter.ReadFieldValue(ResultSet, FieldName, SelCol.Column.FieldType);
  end;
end;

function TEntityLoader.ReadFieldFromResultset(ResultSet: IDBResultSet;
  Col: TColumn): Variant;
begin
  Result := Selecter.ReadFieldValue(ResultSet, ColumnFieldName(Col), Col.FieldType);
end;

end.
