1226 lines
32 KiB
ObjectPascal
1226 lines
32 KiB
ObjectPascal
{
|
|
Dainty
|
|
Simple object mapper for Delphi
|
|
|
|
Copyright (c) 2020 M. van Renswoude
|
|
https://github.com/MvRens/Dainty
|
|
|
|
|
|
This is free and unencumbered software released into the public domain.
|
|
|
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
distribute this software, either in source code form or as a compiled
|
|
binary, for any purpose, commercial or non-commercial, and by any
|
|
means.
|
|
|
|
In jurisdictions that recognize copyright laws, the author or authors
|
|
of this software dedicate any and all copyright interest in the
|
|
software to the public domain. We make this dedication for the benefit
|
|
of the public at large and to the detriment of our heirs and
|
|
successors. We intend this dedication to be an overt act of
|
|
relinquishment in perpetuity of all present and future rights to this
|
|
software under copyright law.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
For more information, please refer to <http://unlicense.org/>
|
|
}
|
|
unit Dainty;
|
|
|
|
interface
|
|
uses
|
|
Data.DB,
|
|
System.Generics.Collections,
|
|
System.Rtti,
|
|
System.SysUtils,
|
|
|
|
Dainty.Converter.Default;
|
|
|
|
|
|
type
|
|
TDaintyReader<T: class> = class;
|
|
|
|
|
|
/// <summary>
|
|
/// Annotates a field or property to change it's field name in the DataSet
|
|
/// as used for the Dainty methods.
|
|
/// </summary>
|
|
FieldName = class(TCustomAttribute)
|
|
private
|
|
FFieldName: string;
|
|
public
|
|
constructor Create(const AFieldName: string);
|
|
|
|
property FieldName: string read FFieldName;
|
|
end;
|
|
|
|
|
|
/// <summary>
|
|
/// Alias for the FieldName attribute, if you prefer it for clarity.
|
|
/// They are interchangable, so the same object can be used for both
|
|
/// row retrieval and insert/update queries.
|
|
/// </summary>
|
|
ParamName = FieldName;
|
|
|
|
|
|
TDaintyOrigin = (doFirst, doCurrent);
|
|
|
|
|
|
/// <summary>
|
|
/// Allows for direct calls to TDainty methods from any DataSet instance, for example
|
|
/// DataSet.Rows<> or DataSet.GetFirstOrDefault<>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Because class helpers are not exactly extension methods and only one can apply,
|
|
/// if you're having conflicts you can call TDainty.Rows<>(DataSet) instead.
|
|
/// </remarks>
|
|
TDaintyDataSetHelper = class helper for TDataSet
|
|
public
|
|
/// <summary>
|
|
/// Returns a typed enumerable which iterates the DataSet and returns the mapped
|
|
/// object for each row.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The objects returns are owned by the enumerable, and destroyed during and after the loop.
|
|
/// </remarks>
|
|
function Rows<T: class>(AOrigin: TDaintyOrigin = doFirst): IEnumerable<T>;
|
|
|
|
/// <summary>
|
|
/// Returns the DataSet rows as a list of objects.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller is responsible for freeing the list.
|
|
/// Note that the DataSet is not reset to First and will instead start at the current record.
|
|
/// </remarks>
|
|
function List<T: class>(AOrigin: TDaintyOrigin = doFirst): TList<T>;
|
|
|
|
|
|
/// <summary>
|
|
/// Provides access to the reader which allows control over the DataSet loop.
|
|
/// </summary>
|
|
function GetRowReader<T: class>: TDaintyReader<T>;
|
|
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Throws an exception if no
|
|
/// row is active.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
function GetFirst<T: class>(AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Returns nil if no row is active.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
function GetFirstOrDefault<T: class>(AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Throws an exception if no
|
|
/// row is active or if more than one row is remaining.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
function GetSingle<T: class>(AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Returns nil if no row is active
|
|
/// or if more than one row is remaining.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
function GetSingleOrDefault<T: class>(AOrigin: TDaintyOrigin = doFirst): T;
|
|
end;
|
|
|
|
|
|
TDaintyParamsMatching = (
|
|
/// <summary>
|
|
/// All parameters must have a matching member and all members must have a matching parameter.
|
|
/// This option is the most defensive, and ensures there are no mismatches either way.
|
|
/// </summary>
|
|
dpmExact,
|
|
|
|
/// <summary>
|
|
/// All parameters must have a matching member. Extra members are allowed and ignored.
|
|
/// This option will ensure the query has no missing parameters.
|
|
/// </summary>
|
|
dpmAllParams,
|
|
|
|
/// <summary>
|
|
/// All members must have a matching parameter. Extra parameters are allowed and will not be set.
|
|
/// This option can be used if some parameters are set elsewhere in code.
|
|
/// </summary>
|
|
dpmAllMembers,
|
|
|
|
/// <summary>
|
|
/// Match members and parameters, allow missing or extra for either.
|
|
/// I'm sure you have your reasons.
|
|
/// </summary>
|
|
dpmAny
|
|
);
|
|
|
|
/// <summary>
|
|
/// Allows for direct calls to TDainty methods from any TParams instance, for example
|
|
/// DataSet.Params.Apply().
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Because class helpers are not exactly extension methods and only one can apply,
|
|
/// if you're having conflicts you can call TDainty.ApplyParams(DataSet.Params, ...) instead.
|
|
/// </remarks>
|
|
TDaintyParamsHelpers = class helper for TParams
|
|
public
|
|
/// <summary>
|
|
/// Sets the params using the members of AValue.
|
|
/// </summary>
|
|
procedure Apply<T: class>(AValue: T; AMatching: TDaintyParamsMatching = dpmExact);
|
|
end;
|
|
|
|
|
|
/// <summary>
|
|
/// Provides row to object mapping functionality. Usually accessed using the TDaintyDataSetHelper.
|
|
/// </summary>
|
|
TDainty = class
|
|
protected
|
|
class procedure GotoOrigin(ADataSet: TDataSet; AOrigin: TDaintyOrigin);
|
|
public
|
|
/// <summary>
|
|
/// Returns a typed enumerable which iterates the DataSet and returns the mapped
|
|
/// object for each row.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The objects returns are owned by the enumerable, and destroyed during and after the loop.
|
|
/// Note that the DataSet is not reset to First and will instead start at the current record.
|
|
/// </remarks>
|
|
class function Rows<T: class>(ADataSet: TDataSet; AOrigin: TDaintyOrigin = doFirst): IEnumerable<T>;
|
|
|
|
/// <summary>
|
|
/// Returns the DataSet rows as a list of objects.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller is responsible for freeing the list.
|
|
/// Note that the DataSet is not reset to First and will instead start at the current record.
|
|
/// </remarks>
|
|
class function List<T: class>(ADataSet: TDataSet; AOrigin: TDaintyOrigin = doFirst): TList<T>;
|
|
|
|
|
|
/// <summary>
|
|
/// Provides access to the mapper which allows control over the DataSet loop.
|
|
/// </summary>
|
|
class function GetRowReader<T: class>(ADataSet: TDataSet): TDaintyReader<T>;
|
|
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Throws an exception if no
|
|
/// row is active.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
class function GetFirst<T: class>(ADataSet: TDataSet; AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Returns nil if no row is active.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
class function GetFirstOrDefault<T: class>(ADataSet: TDataSet; AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Throws an exception if no
|
|
/// row is active or if more than one row is remaining.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
class function GetSingle<T: class>(ADataSet: TDataSet; AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
/// <summary>
|
|
/// Returns the current row mapped to the specified class. Returns nil if no row is active
|
|
/// or if more than one row is remaining.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
class function GetSingleOrDefault<T: class>(ADataSet: TDataSet; AOrigin: TDaintyOrigin = doFirst): T;
|
|
|
|
|
|
/// <summary>
|
|
/// Sets the params using the members of AValue.
|
|
/// </summary>
|
|
class procedure ApplyParams<T: class>(AParams: TParams; AValue: T; AMatching: TDaintyParamsMatching = dpmExact);
|
|
end;
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Performs the mapping of the current row to the specified type.
|
|
/// </summary>
|
|
TDaintyReader<T: class> = class
|
|
public
|
|
/// <summary>
|
|
/// Maps the current row to a new object.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller must Free the returned object.
|
|
/// </remarks>
|
|
function MapRow: T; virtual; abstract;
|
|
end;
|
|
|
|
|
|
/// <summary>
|
|
/// Enumerates over the rows of a DataSet and returns the mapped objects.
|
|
/// Returned by the Rows<> method.
|
|
/// </summary>
|
|
TDaintyEnumerable<T: class> = class(TInterfacedObject, IEnumerable<T>)
|
|
private
|
|
FReader: TDaintyReader<T>;
|
|
FDataSet: TDataSet;
|
|
public
|
|
constructor Create(AReader: TDaintyReader<T>; ADataSet: TDataSet);
|
|
destructor Destroy; override;
|
|
|
|
function GetEnumerator: IEnumerator;
|
|
function GetEnumeratorGeneric: IEnumerator<T>;
|
|
|
|
function IEnumerable<T>.GetEnumerator = GetEnumeratorGeneric;
|
|
end;
|
|
|
|
|
|
/// <summary>
|
|
/// Internal enumerator returned by TDaintyEnumerable.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For internal use only. It's only in the interface section to prevent "Method of parameterized type declared in interface section must not use local symbol" error.
|
|
/// </remarks>
|
|
TDaintyEnumerator<T: class> = class(TInterfacedObject, IEnumerator<T>)
|
|
private
|
|
FReader: TDaintyReader<T>;
|
|
FDataSet: TDataSet;
|
|
FCurrent: T;
|
|
FOwnsCurrent: Boolean;
|
|
FResetPoint: TArray<Byte>;
|
|
public
|
|
constructor Create(AReader: TDaintyReader<T>; ADataSet: TDataSet);
|
|
destructor Destroy; override;
|
|
|
|
function GetCurrent: TObject;
|
|
function GetCurrentGeneric: T;
|
|
function MoveNext: Boolean;
|
|
procedure Reset;
|
|
|
|
function IEnumerator<T>.GetCurrent = GetCurrentGeneric;
|
|
|
|
function Acquire: T;
|
|
end;
|
|
|
|
|
|
/// <summary>
|
|
/// Performs the mapping of the object to parameters.
|
|
/// </summary>
|
|
TDaintyWriter<T: class> = class
|
|
public
|
|
procedure ApplyParams(AValue: T); virtual; abstract;
|
|
end;
|
|
|
|
|
|
|
|
TDaintyFieldReaderProc = reference to procedure(AInstance: TObject; AField: TField);
|
|
TDaintyParamWriterProc = reference to procedure(AInstance: TObject; AParam: TParam);
|
|
|
|
TDaintyConverter = record
|
|
FieldReader: TDaintyFieldReaderProc;
|
|
ParamWriter: TDaintyParamWriterProc;
|
|
end;
|
|
|
|
|
|
TDaintyRttiFieldNameMapping = record
|
|
FieldName: string;
|
|
Converter: TDaintyConverter;
|
|
|
|
constructor Create(const AFieldName: string; const AConverter: TDaintyConverter);
|
|
end;
|
|
|
|
TDaintyRttiClassMapping = class
|
|
private
|
|
FConstruct: TFunc<TObject>;
|
|
FFieldNameMapping: TList<TDaintyRttiFieldNameMapping>;
|
|
public
|
|
constructor Create(AConstruct: TFunc<TObject>);
|
|
destructor Destroy; override;
|
|
|
|
property Construct: TFunc<TObject> read FConstruct;
|
|
property FieldNameMapping: TList<TDaintyRttiFieldNameMapping> read FFieldNameMapping;
|
|
end;
|
|
|
|
TDaintyRttiFieldMapping = record
|
|
Field: TField;
|
|
Converter: TDaintyConverter;
|
|
|
|
constructor Create(const AField: TField; const AConverter: TDaintyConverter);
|
|
end;
|
|
|
|
|
|
TDaintyRttiReader<T: class> = class(TDaintyReader<T>)
|
|
private
|
|
FConstruct: TFunc<TObject>;
|
|
FFieldMapping: TList<TDaintyRttiFieldMapping>;
|
|
public
|
|
constructor Create(AClassMapping: TDaintyRttiClassMapping; ADataSet: TDataSet);
|
|
destructor Destroy; override;
|
|
|
|
function MapRow: T; override;
|
|
end;
|
|
|
|
|
|
TDaintyRttiParamMapping = record
|
|
Param: TParam;
|
|
Converter: TDaintyConverter;
|
|
|
|
constructor Create(const AParam: TParam; const AConverter: TDaintyConverter);
|
|
end;
|
|
|
|
|
|
TDaintyRttiWriter<T: class> = class(TDaintyWriter<T>)
|
|
private
|
|
FParamMapping: TList<TDaintyRttiParamMapping>;
|
|
public
|
|
constructor Create(AClassMapping: TDaintyRttiClassMapping; AParams: TParams; AMatching: TDaintyParamsMatching);
|
|
destructor Destroy; override;
|
|
|
|
procedure ApplyParams(AValue: T); override;
|
|
end;
|
|
|
|
|
|
|
|
TDaintyRttiMember = class
|
|
private
|
|
FRttiMember: TRttiMember;
|
|
protected
|
|
function GetRttiType: TRttiType; virtual; abstract;
|
|
public
|
|
constructor Create(ARttiMember: TRttiMember);
|
|
|
|
function GetValue(AInstance: TObject): TValue; virtual; abstract;
|
|
procedure SetValue(AInstance: TObject; const AValue: TValue); virtual; abstract;
|
|
|
|
property RttiMember: TRttiMember read FRttiMember;
|
|
property RttiType: TRttiType read GetRttiType;
|
|
end;
|
|
|
|
|
|
TDaintyRttiFieldMember = class(TDaintyRttiMember)
|
|
private
|
|
FField: TRttiField;
|
|
protected
|
|
function GetRttiType: TRttiType; override;
|
|
public
|
|
constructor Create(AField: TRttiField);
|
|
|
|
function GetValue(AInstance: TObject): TValue; override;
|
|
procedure SetValue(AInstance: TObject; const AValue: TValue); override;
|
|
end;
|
|
|
|
TDaintyRttiPropertyMember = class(TDaintyRttiMember)
|
|
private
|
|
FProperty: TRttiProperty;
|
|
protected
|
|
function GetRttiType: TRttiType; override;
|
|
public
|
|
constructor Create(AProperty: TRttiProperty);
|
|
|
|
function GetValue(AInstance: TObject): TValue; override;
|
|
procedure SetValue(AInstance: TObject; const AValue: TValue); override;
|
|
end;
|
|
|
|
|
|
TDaintyRttiConverterFactoryClass = class of TDaintyRttiConverterFactory;
|
|
|
|
TDaintyRttiConverterFactory = class
|
|
public
|
|
class function Construct(AMember: TDaintyRttiMember; out AConverter: TDaintyConverter): Boolean; virtual; abstract;
|
|
end;
|
|
|
|
|
|
TDaintyRttiMapperFactory = class
|
|
private type
|
|
TConverterFactoryRegistration = record
|
|
Factory: TDaintyRttiConverterFactoryClass;
|
|
Priority: Integer;
|
|
|
|
constructor Create(AFactory: TDaintyRttiConverterFactoryClass; APriority: Integer);
|
|
end;
|
|
private class var
|
|
SContext: TRttiContext;
|
|
SClassMappingCacheLock: TMultiReadExclusiveWriteSynchronizer;
|
|
SClassMappingCache: TDictionary<Pointer, TDaintyRttiClassMapping>;
|
|
SConverterFactoriesLock: TMultiReadExclusiveWriteSynchronizer;
|
|
SConverterFactories: TList<TConverterFactoryRegistration>;
|
|
SMembers: TObjectList<TDaintyRttiMember>;
|
|
private
|
|
class function GetClassMapping<T: class>: TDaintyRttiClassMapping;
|
|
class function GetFieldName(AMember: TRttiNamedObject): string;
|
|
class function GetConverter(AMember: TRttiMember): TDaintyConverter;
|
|
protected
|
|
class procedure Initialize;
|
|
class procedure Finalize;
|
|
public
|
|
class function ConstructReader<T: class>(ADataSet: TDataSet): TDaintyRttiReader<T>;
|
|
class function ConstructWriter<T: class>(AParams: TParams; AMatching: TDaintyParamsMatching): TDaintyRttiWriter<T>;
|
|
|
|
class procedure RegisterConverterFactory(AFactory: TDaintyRttiConverterFactoryClass; APriority: Integer = 0);
|
|
class procedure UnregisterConverterFactory(AFactory: TDaintyRttiConverterFactoryClass);
|
|
end;
|
|
|
|
|
|
|
|
implementation
|
|
uses
|
|
System.StrUtils,
|
|
System.TypInfo;
|
|
|
|
|
|
{ TDaintyDataSetHelper }
|
|
function TDaintyDataSetHelper.Rows<T>(AOrigin: TDaintyOrigin): IEnumerable<T>;
|
|
begin
|
|
Result := TDainty.Rows<T>(Self);
|
|
end;
|
|
|
|
|
|
function TDaintyDataSetHelper.List<T>(AOrigin: TDaintyOrigin): TList<T>;
|
|
begin
|
|
Result := TDainty.List<T>(Self, AOrigin);
|
|
end;
|
|
|
|
|
|
function TDaintyDataSetHelper.GetRowReader<T>: TDaintyReader<T>;
|
|
begin
|
|
Result := TDainty.GetRowReader<T>(Self);
|
|
end;
|
|
|
|
|
|
function TDaintyDataSetHelper.GetFirst<T>(AOrigin: TDaintyOrigin): T;
|
|
begin
|
|
Result := TDainty.GetFirst<T>(Self);
|
|
end;
|
|
|
|
|
|
function TDaintyDataSetHelper.GetFirstOrDefault<T>(AOrigin: TDaintyOrigin): T;
|
|
begin
|
|
Result := TDainty.GetFirstOrDefault<T>(Self);
|
|
end;
|
|
|
|
|
|
function TDaintyDataSetHelper.GetSingle<T>(AOrigin: TDaintyOrigin): T;
|
|
begin
|
|
Result := TDainty.GetSingle<T>(Self);
|
|
end;
|
|
|
|
|
|
function TDaintyDataSetHelper.GetSingleOrDefault<T>(AOrigin: TDaintyOrigin): T;
|
|
begin
|
|
Result := TDainty.GetSingleOrDefault<T>(Self);
|
|
end;
|
|
|
|
|
|
{ TDaintyParamsHelpers }
|
|
procedure TDaintyParamsHelpers.Apply<T>(AValue: T; AMatching: TDaintyParamsMatching);
|
|
begin
|
|
TDainty.ApplyParams<T>(Self, AValue, AMatching);
|
|
end;
|
|
|
|
|
|
|
|
{ TDainty }
|
|
class function TDainty.Rows<T>(ADataSet: TDataSet; AOrigin: TDaintyOrigin): IEnumerable<T>;
|
|
var
|
|
reader: TDaintyReader<T>;
|
|
|
|
begin
|
|
GotoOrigin(ADataSet, AOrigin);
|
|
reader := GetRowReader<T>(ADataSet);
|
|
Result := TDaintyEnumerable<T>.Create(reader, ADataSet);
|
|
end;
|
|
|
|
|
|
class function TDainty.List<T>(ADataSet: TDataSet; AOrigin: TDaintyOrigin): TList<T>;
|
|
var
|
|
reader: TDaintyReader<T>;
|
|
|
|
begin
|
|
GotoOrigin(ADataSet, AOrigin);
|
|
reader := GetRowReader<T>(ADataSet);
|
|
|
|
Result := TObjectList<T>.Create;
|
|
try
|
|
while not ADataSet.Eof do
|
|
begin
|
|
Result.Add(reader.MapRow);
|
|
ADataSet.Next;
|
|
end;
|
|
except
|
|
FreeAndNil(Result);
|
|
raise;
|
|
end;
|
|
end;
|
|
|
|
|
|
class function TDainty.GetRowReader<T>(ADataSet: TDataSet): TDaintyReader<T>;
|
|
begin
|
|
Result := TDaintyRttiMapperFactory.ConstructReader<T>(ADataSet);
|
|
end;
|
|
|
|
|
|
class function TDainty.GetFirst<T>(ADataSet: TDataSet; AOrigin: TDaintyOrigin): T;
|
|
var
|
|
enumerator: IEnumerator<T>;
|
|
|
|
begin
|
|
GotoOrigin(ADataSet, AOrigin);
|
|
enumerator := Rows<T>(ADataSet).GetEnumerator;
|
|
|
|
if not enumerator.MoveNext then
|
|
raise EDatabaseError.Create('Expected at least 1 record but none found');
|
|
|
|
Result := (enumerator as TDaintyEnumerator<T>).Acquire;
|
|
end;
|
|
|
|
|
|
class function TDainty.GetFirstOrDefault<T>(ADataSet: TDataSet; AOrigin: TDaintyOrigin): T;
|
|
var
|
|
enumerator: IEnumerator<T>;
|
|
|
|
begin
|
|
enumerator := Rows<T>(ADataSet).GetEnumerator;
|
|
|
|
if enumerator.MoveNext then
|
|
Result := (enumerator as TDaintyEnumerator<T>).Acquire
|
|
else
|
|
Result := nil;
|
|
end;
|
|
|
|
|
|
class function TDainty.GetSingle<T>(ADataSet: TDataSet; AOrigin: TDaintyOrigin): T;
|
|
var
|
|
enumerator: IEnumerator<T>;
|
|
|
|
begin
|
|
GotoOrigin(ADataSet, AOrigin);
|
|
enumerator := Rows<T>(ADataSet).GetEnumerator;
|
|
|
|
if not enumerator.MoveNext then
|
|
raise EDatabaseError.Create('Expected 1 record but none found');
|
|
|
|
Result := (enumerator as TDaintyEnumerator<T>).Acquire;
|
|
|
|
if enumerator.MoveNext then
|
|
begin
|
|
FreeAndNil(Result);
|
|
raise EDatabaseError.Create('Expected 1 record but more found');
|
|
end;
|
|
end;
|
|
|
|
|
|
class function TDainty.GetSingleOrDefault<T>(ADataSet: TDataSet; AOrigin: TDaintyOrigin): T;
|
|
var
|
|
enumerator: IEnumerator<T>;
|
|
|
|
begin
|
|
GotoOrigin(ADataSet, AOrigin);
|
|
enumerator := Rows<T>(ADataSet).GetEnumerator;
|
|
|
|
if not enumerator.MoveNext then
|
|
Exit(nil);
|
|
|
|
Result := (enumerator as TDaintyEnumerator<T>).Acquire;
|
|
|
|
if enumerator.MoveNext then
|
|
FreeAndNil(Result);
|
|
end;
|
|
|
|
|
|
class procedure TDainty.ApplyParams<T>(AParams: TParams; AValue: T; AMatching: TDaintyParamsMatching);
|
|
var
|
|
writer: TDaintyWriter<T>;
|
|
|
|
begin
|
|
writer := TDaintyRttiMapperFactory.ConstructWriter<T>(AParams, AMatching);
|
|
try
|
|
writer.ApplyParams(AValue);
|
|
finally
|
|
FreeAndNil(writer);
|
|
end;
|
|
end;
|
|
|
|
|
|
class procedure TDainty.GotoOrigin(ADataSet: TDataSet; AOrigin: TDaintyOrigin);
|
|
begin
|
|
if AOrigin = doFirst then
|
|
ADataSet.First;
|
|
end;
|
|
|
|
|
|
|
|
{ TDaintyEnumerable<T> }
|
|
constructor TDaintyEnumerable<T>.Create(AReader: TDaintyReader<T>; ADataSet: TDataSet);
|
|
begin
|
|
inherited Create;
|
|
|
|
FReader := AReader;
|
|
FDataSet := ADataSet;
|
|
end;
|
|
|
|
|
|
destructor TDaintyEnumerable<T>.Destroy;
|
|
begin
|
|
FreeAndNil(FReader);
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
function TDaintyEnumerable<T>.GetEnumerator: IEnumerator;
|
|
begin
|
|
Result := GetEnumeratorGeneric;
|
|
end;
|
|
|
|
function TDaintyEnumerable<T>.GetEnumeratorGeneric: IEnumerator<T>;
|
|
begin
|
|
Result := TDaintyEnumerator<T>.Create(FReader, FDataSet);
|
|
end;
|
|
|
|
|
|
{ TDaintyEnumerator<T> }
|
|
constructor TDaintyEnumerator<T>.Create(AReader: TDaintyReader<T>; ADataSet: TDataSet);
|
|
begin
|
|
inherited Create;
|
|
|
|
FReader := AReader;
|
|
FDataSet := ADataSet;
|
|
FResetPoint := ADataSet.GetBookmark;
|
|
|
|
FOwnsCurrent := False;
|
|
end;
|
|
|
|
|
|
destructor TDaintyEnumerator<T>.Destroy;
|
|
begin
|
|
if FOwnsCurrent then
|
|
FreeAndNil(FCurrent);
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
function TDaintyEnumerator<T>.Acquire: T;
|
|
begin
|
|
FOwnsCurrent := False;
|
|
Result := FCurrent;
|
|
end;
|
|
|
|
|
|
function TDaintyEnumerator<T>.GetCurrent: TObject;
|
|
begin
|
|
Result := GetCurrentGeneric;
|
|
end;
|
|
|
|
|
|
function TDaintyEnumerator<T>.GetCurrentGeneric: T;
|
|
begin
|
|
Result := FCurrent;
|
|
end;
|
|
|
|
|
|
function TDaintyEnumerator<T>.MoveNext: Boolean;
|
|
begin
|
|
if FDataSet.Eof then
|
|
Exit(False);
|
|
|
|
if FOwnsCurrent then
|
|
FreeAndNil(FCurrent);
|
|
|
|
FCurrent := FReader.MapRow;
|
|
FOwnsCurrent := True;
|
|
Result := True;
|
|
|
|
FDataSet.Next;
|
|
end;
|
|
|
|
|
|
procedure TDaintyEnumerator<T>.Reset;
|
|
begin
|
|
if FOwnsCurrent then
|
|
FreeAndNil(FCurrent);
|
|
|
|
FDataSet.GotoBookmark(FResetPoint);
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiMember }
|
|
constructor TDaintyRttiMember.Create(ARttiMember: TRttiMember);
|
|
begin
|
|
inherited Create;
|
|
|
|
FRttiMember := ARttiMember;
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiFieldMember }
|
|
constructor TDaintyRttiFieldMember.Create(AField: TRttiField);
|
|
begin
|
|
inherited Create(AField);
|
|
|
|
FField := AField;
|
|
end;
|
|
|
|
|
|
function TDaintyRttiFieldMember.GetRttiType: TRttiType;
|
|
begin
|
|
Result := FField.FieldType;
|
|
end;
|
|
|
|
|
|
function TDaintyRttiFieldMember.GetValue(AInstance: TObject): TValue;
|
|
begin
|
|
Result := FField.GetValue(AInstance);
|
|
end;
|
|
|
|
|
|
procedure TDaintyRttiFieldMember.SetValue(AInstance: TObject; const AValue: TValue);
|
|
begin
|
|
FField.SetValue(AInstance, AValue);
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiPropertyMember }
|
|
constructor TDaintyRttiPropertyMember.Create(AProperty: TRttiProperty);
|
|
begin
|
|
inherited Create(AProperty);
|
|
|
|
FProperty := AProperty;
|
|
end;
|
|
|
|
|
|
function TDaintyRttiPropertyMember.GetRttiType: TRttiType;
|
|
begin
|
|
Result := FProperty.PropertyType;
|
|
end;
|
|
|
|
|
|
function TDaintyRttiPropertyMember.GetValue(AInstance: TObject): TValue;
|
|
begin
|
|
Result := FProperty.GetValue(AInstance);
|
|
end;
|
|
|
|
|
|
procedure TDaintyRttiPropertyMember.SetValue(AInstance: TObject; const AValue: TValue);
|
|
begin
|
|
FProperty.SetValue(AInstance, AValue);
|
|
end;
|
|
|
|
|
|
|
|
{ TDaintyRttiMapperFactory }
|
|
class procedure TDaintyRttiMapperFactory.Initialize;
|
|
begin
|
|
if Assigned(SClassMappingCache) then
|
|
Exit;
|
|
|
|
SContext := TRttiContext.Create;
|
|
SClassMappingCacheLock := TMultiReadExclusiveWriteSynchronizer.Create;
|
|
SClassMappingCache := TObjectDictionary<Pointer, TDaintyRttiClassMapping>.Create([doOwnsValues]);
|
|
SConverterFactoriesLock := TMultiReadExclusiveWriteSynchronizer.Create;
|
|
SConverterFactories := TList<TConverterFactoryRegistration>.Create;
|
|
SMembers := TObjectList<TDaintyRttiMember>.Create;
|
|
end;
|
|
|
|
|
|
class procedure TDaintyRttiMapperFactory.RegisterConverterFactory(AFactory: TDaintyRttiConverterFactoryClass; APriority: Integer);
|
|
var
|
|
registration: TConverterFactoryRegistration;
|
|
registrationIndex: Integer;
|
|
|
|
begin
|
|
{ Initialization for Dainty.Converter.Default runs before ours, make sure we are initialized }
|
|
Initialize;
|
|
|
|
SConverterFactoriesLock.BeginWrite;
|
|
try
|
|
registration := TConverterFactoryRegistration.Create(AFactory, APriority);
|
|
|
|
for registrationIndex := 0 to Pred(SConverterFactories.Count) do
|
|
if SConverterFactories[registrationIndex].Priority <= APriority then
|
|
begin
|
|
SConverterFactories.Insert(registrationIndex, registration);
|
|
Exit;
|
|
end;
|
|
|
|
SConverterFactories.Add(registration);
|
|
finally
|
|
SConverterFactoriesLock.EndWrite;
|
|
end;
|
|
end;
|
|
|
|
|
|
class procedure TDaintyRttiMapperFactory.UnregisterConverterFactory(AFactory: TDaintyRttiConverterFactoryClass);
|
|
var
|
|
registrationIndex: Integer;
|
|
|
|
begin
|
|
SConverterFactoriesLock.BeginWrite;
|
|
try
|
|
for registrationIndex := Pred(SConverterFactories.Count) downto 0 do
|
|
if SConverterFactories[registrationIndex].Factory = AFactory then
|
|
SConverterFactories.Delete(registrationIndex);
|
|
finally
|
|
SConverterFactoriesLock.EndWrite;
|
|
end;
|
|
end;
|
|
|
|
|
|
class procedure TDaintyRttiMapperFactory.Finalize;
|
|
begin
|
|
FreeAndNil(SMembers);
|
|
FreeAndNil(SConverterFactories);
|
|
FreeAndNil(SConverterFactoriesLock);
|
|
FreeAndNil(SClassMappingCache);
|
|
FreeAndNil(SClassMappingCacheLock);
|
|
SContext.Free;
|
|
end;
|
|
|
|
|
|
class function TDaintyRttiMapperFactory.ConstructReader<T>(ADataSet: TDataSet): TDaintyRttiReader<T>;
|
|
var
|
|
mapping: TDaintyRttiClassMapping;
|
|
|
|
begin
|
|
mapping := GetClassMapping<T>;
|
|
Result := TDaintyRttiReader<T>.Create(mapping, ADataSet);
|
|
end;
|
|
|
|
|
|
class function TDaintyRttiMapperFactory.ConstructWriter<T>(AParams: TParams; AMatching: TDaintyParamsMatching): TDaintyRttiWriter<T>;
|
|
var
|
|
mapping: TDaintyRttiClassMapping;
|
|
|
|
begin
|
|
mapping := GetClassMapping<T>;
|
|
Result := TDaintyRttiWriter<T>.Create(mapping, AParams, AMatching);
|
|
end;
|
|
|
|
|
|
class function TDaintyRttiMapperFactory.GetClassMapping<T>: TDaintyRttiClassMapping;
|
|
var
|
|
typeInfoHandle: Pointer;
|
|
classInfo: TRttiType;
|
|
method: TRttiMethod;
|
|
instanceClassType: TClass;
|
|
fieldInfo: TRttiField;
|
|
propertyInfo: TRttiProperty;
|
|
converter: TDaintyConverter;
|
|
|
|
begin
|
|
SClassMappingCacheLock.BeginRead;
|
|
try
|
|
typeInfoHandle := TypeInfo(T);
|
|
if SClassMappingCache.TryGetValue(typeInfoHandle, Result) then
|
|
Exit;
|
|
|
|
SClassMappingCacheLock.BeginWrite;
|
|
try
|
|
{ Between the call to BeginWrite and actually acquiring the lock the state
|
|
may have changed. Check again to be sure. }
|
|
if SClassMappingCache.TryGetValue(typeInfoHandle, Result) then
|
|
Exit;
|
|
|
|
classInfo := SContext.GetType(typeInfoHandle);
|
|
Result := nil;
|
|
|
|
for method in classInfo.GetMethods do
|
|
begin
|
|
if method.IsConstructor and (Length(method.GetParameters) = 0) then
|
|
begin
|
|
instanceClassType := classInfo.AsInstance.MetaclassType;
|
|
Result := TDaintyRttiClassMapping.Create(
|
|
function: TObject
|
|
begin
|
|
Result := method.Invoke(instanceClassType, []).AsObject;
|
|
end);
|
|
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
if not Assigned(Result) then
|
|
raise ENoConstructException.Create('A constructor with no parameter is required for Dainty');
|
|
|
|
|
|
for fieldInfo in classInfo.GetFields do
|
|
begin
|
|
if not (fieldInfo.Visibility in [mvPublic, mvPublished]) then
|
|
Continue;
|
|
|
|
Result.FieldNameMapping.Add(TDaintyRttiFieldNameMapping.Create(GetFieldName(fieldInfo), GetConverter(fieldInfo)));
|
|
end;
|
|
|
|
|
|
for propertyInfo in classInfo.GetProperties do
|
|
begin
|
|
if not (propertyInfo.Visibility in [mvPublic, mvPublished]) then
|
|
Continue;
|
|
|
|
if not propertyInfo.IsWritable then
|
|
Continue;
|
|
|
|
Result.FieldNameMapping.Add(TDaintyRttiFieldNameMapping.Create(GetFieldName(propertyInfo), GetConverter(propertyInfo)));
|
|
end;
|
|
|
|
SClassMappingCache.Add(typeInfoHandle, Result);
|
|
finally
|
|
SClassMappingCacheLock.EndWrite;
|
|
end;
|
|
finally
|
|
SClassMappingCacheLock.EndRead;
|
|
end;
|
|
end;
|
|
|
|
|
|
class function TDaintyRttiMapperFactory.GetFieldName(AMember: TRttiNamedObject): string;
|
|
var
|
|
attribute: TCustomAttribute;
|
|
|
|
begin
|
|
for attribute in AMember.GetAttributes do
|
|
begin
|
|
if attribute is FieldName then
|
|
Exit(FieldName(attribute).FieldName);
|
|
end;
|
|
|
|
Result := AMember.Name;
|
|
end;
|
|
|
|
|
|
class function TDaintyRttiMapperFactory.GetConverter(AMember: TRttiMember): TDaintyConverter;
|
|
var
|
|
member: TDaintyRttiMember;
|
|
registration: TConverterFactoryRegistration;
|
|
hasConverter: Boolean;
|
|
|
|
begin
|
|
if AMember is TRttiField then
|
|
member := TDaintyRttiFieldMember.Create(TRttiField(AMember))
|
|
else if AMember is TRttiProperty then
|
|
member := TDaintyRttiPropertyMember.Create(TRttiProperty(AMember))
|
|
else
|
|
raise EInvalidOpException.CreateFmt('Member type not supported: %s', [AMember.ClassName]);
|
|
|
|
hasConverter := False;
|
|
|
|
SConverterFactoriesLock.BeginRead;
|
|
try
|
|
for registration in SConverterFactories do
|
|
begin
|
|
if registration.Factory.Construct(member, Result) then
|
|
begin
|
|
hasConverter := True;
|
|
SMembers.Add(member);
|
|
Break;
|
|
end;
|
|
end;
|
|
finally
|
|
SConverterFactoriesLock.EndRead;
|
|
end;
|
|
|
|
if not hasConverter then
|
|
begin
|
|
FreeAndNil(member);
|
|
raise ENotSupportedException.CreateFmt('Member %s has unsupported type for Dainty: %d', [AMember.Name, Ord(member.RttiType.TypeKind)]);
|
|
end;
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiMapperFactory.TConverterFactoryRegistration }
|
|
constructor TDaintyRttiMapperFactory.TConverterFactoryRegistration.Create(AFactory: TDaintyRttiConverterFactoryClass; APriority: Integer);
|
|
begin
|
|
Factory := AFactory;
|
|
Priority := APriority;
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiReader<T> }
|
|
constructor TDaintyRttiReader<T>.Create(AClassMapping: TDaintyRttiClassMapping; ADataSet: TDataSet);
|
|
var
|
|
fieldNameMapping: TDaintyRttiFieldNameMapping;
|
|
|
|
begin
|
|
inherited Create;
|
|
|
|
FConstruct := AClassMapping.Construct;
|
|
FFieldMapping := TList<TDaintyRttiFieldMapping>.Create;
|
|
|
|
for fieldNameMapping in AClassMapping.FieldNameMapping do
|
|
FFieldMapping.Add(TDaintyRttiFieldMapping.Create(ADataSet.FieldByName(fieldNameMapping.FieldName), fieldNameMapping.Converter));
|
|
end;
|
|
|
|
|
|
destructor TDaintyRttiReader<T>.Destroy;
|
|
begin
|
|
FreeAndNil(FFieldMapping);
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
function TDaintyRttiReader<T>.MapRow: T;
|
|
var
|
|
fieldMapping: TDaintyRttiFieldMapping;
|
|
|
|
begin
|
|
Result := FConstruct() as T;
|
|
|
|
for fieldMapping in FFieldMapping do
|
|
fieldMapping.Converter.FieldReader(Result, fieldMapping.Field);
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiWriter<T> }
|
|
constructor TDaintyRttiWriter<T>.Create(AClassMapping: TDaintyRttiClassMapping; AParams: TParams; AMatching: TDaintyParamsMatching);
|
|
var
|
|
fieldNameMapping: TDaintyRttiFieldNameMapping;
|
|
remainingParams: TList<TParam>;
|
|
paramIndex: Integer;
|
|
param: TParam;
|
|
paramNames: string;
|
|
|
|
begin
|
|
inherited Create;
|
|
|
|
FParamMapping := TList<TDaintyRttiParamMapping>.Create;
|
|
try
|
|
remainingParams := TList<TParam>.Create;
|
|
try
|
|
for paramIndex := 0 to Pred(AParams.Count) do
|
|
remainingParams.Add(AParams[paramIndex]);
|
|
|
|
|
|
for fieldNameMapping in AClassMapping.FieldNameMapping do
|
|
begin
|
|
param := AParams.FindParam(fieldNameMapping.FieldName);
|
|
if Assigned(param) then
|
|
begin
|
|
remainingParams.Remove(param);
|
|
FParamMapping.Add(TDaintyRttiParamMapping.Create(param, fieldNameMapping.Converter));
|
|
end else if AMatching in [dpmExact, dpmAllMembers] then
|
|
raise EProgrammerNotFound.CreateFmt('Parameter not found: %s', [fieldNameMapping.FieldName]);
|
|
end;
|
|
|
|
|
|
{ Check if all parameters have a corresponding member }
|
|
if (AMatching in [dpmExact, dpmAllParams]) and (remainingParams.Count > 0) then
|
|
begin
|
|
paramNames := '';
|
|
for param in remainingParams do
|
|
paramNames := paramNames + IfThen(Length(paramNames) = 0, '', ', ') + param.Name;
|
|
|
|
raise EProgrammerNotFound.CreateFmt('The following parameters do not have a corresponding member: %s', [paramNames]);
|
|
end;
|
|
finally
|
|
FreeAndNil(remainingParams);
|
|
end;
|
|
except
|
|
FreeAndNil(FParamMapping);
|
|
raise;
|
|
end;
|
|
end;
|
|
|
|
|
|
destructor TDaintyRttiWriter<T>.Destroy;
|
|
begin
|
|
FreeAndNil(FParamMapping);
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
procedure TDaintyRttiWriter<T>.ApplyParams(AValue: T);
|
|
var
|
|
paramMapping: TDaintyRttiParamMapping;
|
|
|
|
begin
|
|
for paramMapping in FParamMapping do
|
|
paramMapping.Converter.ParamWriter(AValue, paramMapping.Param);
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiFieldNameMapping }
|
|
constructor TDaintyRttiFieldNameMapping.Create(const AFieldName: string; const AConverter: TDaintyConverter);
|
|
begin
|
|
FieldName := AFieldName;
|
|
Converter := AConverter;
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiFieldMapping }
|
|
constructor TDaintyRttiFieldMapping.Create(const AField: TField; const AConverter: TDaintyConverter);
|
|
begin
|
|
Field := AField;
|
|
Converter := AConverter;
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiParamMapping }
|
|
constructor TDaintyRttiParamMapping.Create(const AParam: TParam; const AConverter: TDaintyConverter);
|
|
begin
|
|
Param := AParam;
|
|
Converter := AConverter;
|
|
end;
|
|
|
|
|
|
{ TDaintyRttiClassMapping }
|
|
constructor TDaintyRttiClassMapping.Create(AConstruct: TFunc<TObject>);
|
|
begin
|
|
inherited Create;
|
|
|
|
FConstruct := AConstruct;
|
|
FFieldNameMapping := TList<TDaintyRttiFieldNameMapping>.Create;
|
|
end;
|
|
|
|
|
|
destructor TDaintyRttiClassMapping.Destroy;
|
|
begin
|
|
FreeAndNil(FFieldNameMapping);
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
{ FieldName }
|
|
constructor FieldName.Create(const AFieldName: string);
|
|
begin
|
|
inherited Create;
|
|
|
|
FFieldName := AFieldName;
|
|
end;
|
|
|
|
|
|
initialization
|
|
TDaintyRttiMapperFactory.Initialize;
|
|
|
|
finalization
|
|
TDaintyRttiMapperFactory.Finalize;
|
|
|
|
end.
|