Dainty/Source/Dainty.pas

846 lines
22 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.ValueSetter.Default;
type
TDaintyMapper<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>
/// 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>
/// Note that the DataSet is not reset to First and will instead start at the current record.
/// </remarks>
function Rows<T: class>: TEnumerable<T>;
/// <summary>
/// Provides access to the mapper which allows control over the DataSet loop.
/// </summary>
function GetMapper<T: class>: TDaintyMapper<T>;
/// <summary>
/// Returns the current row mapped to the specified class. Throws an exception if no
/// row is active.
/// </summary>
function GetFirst<T: class>: T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active.
/// </summary>
function GetFirstOrDefault<T: class>(const ADefault: T): 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>
function GetSingle<T: class>: T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active or if more than one row is remaining.
/// </summary>
function GetSingleOrDefault<T: class>(const ADefault: T): T;
end;
/// <summary>
/// Provides row to object mapping functionality. Usually accessed using the TDaintyDataSetHelper.
/// </summary>
TDainty = class
public
/// <summary>
/// Returns a typed enumerable which iterates the DataSet and returns the mapped
/// object for each row.
/// </summary>
/// <remarks>
/// 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): TEnumerable<T>;
/// <summary>
/// Provides access to the mapper which allows control over the DataSet loop.
/// </summary>
class function GetMapper<T: class>(ADataSet: TDataSet): TDaintyMapper<T>;
/// <summary>
/// Returns the current row mapped to the specified class. Throws an exception if no
/// row is active.
/// </summary>
class function GetFirst<T: class>(ADataSet: TDataSet): T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active.
/// </summary>
class function GetFirstOrDefault<T: class>(ADataSet: TDataSet; const ADefault: T): 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>
class function GetSingle<T: class>(ADataSet: TDataSet): T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active or if more than one row is remaining.
/// </summary>
class function GetSingleOrDefault<T: class>(ADataSet: TDataSet; const ADefault: T): T;
end;
/// <summary>
/// Performs the mapping of the current row to the specified type.
/// </summary>
TDaintyMapper<T: class> = class
public
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(TEnumerable<T>)
private
FMapper: TDaintyMapper<T>;
FDataSet: TDataSet;
protected
function DoGetEnumerator: TEnumerator<T>; override;
public
constructor Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
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(TEnumerator<T>)
private
FMapper: TDaintyMapper<T>;
FDataSet: TDataSet;
FCurrent: T;
protected
function DoGetCurrent: T; override;
function DoMoveNext: Boolean; override;
public
constructor Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
end;
TDaintyValueSetter = reference to procedure(AInstance: TObject; AField: TField);
TDaintyRttiFieldNameMapping = record
FieldName: string;
ValueSetter: TDaintyValueSetter;
constructor Create(const AFieldName: string; const AValueSetter: TDaintyValueSetter);
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;
ValueSetter: TDaintyValueSetter;
constructor Create(const AField: TField; const AValueSetter: TDaintyValueSetter);
end;
TDaintyRttiMapper<T: class> = class(TDaintyMapper<T>)
private
FConstruct: TFunc<TObject>;
FFieldMapping: TList<TDaintyRttiFieldMapping>;
public
constructor Create(AClassMapping: TDaintyRttiClassMapping; ADataSet: TDataSet);
function MapRow: T; override;
end;
TDaintyRttiMember = class
private
FRttiMember: TRttiMember;
protected
function GetRttiType: TRttiType; virtual; abstract;
public
constructor Create(ARttiMember: TRttiMember);
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);
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);
procedure SetValue(AInstance: TObject; const AValue: TValue); override;
end;
TDaintyAbstractValueSetterFactoryClass = class of TDaintyAbstractValueSetterFactory;
TDaintyAbstractValueSetterFactory = class
public
class function Construct(AMember: TDaintyRttiMember): TDaintyValueSetter; virtual; abstract;
end;
TDaintyRttiMapperFactory = class
private type
TValueSetterRegistration = record
Factory: TDaintyAbstractValueSetterFactoryClass;
Priority: Integer;
constructor Create(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer);
end;
private class var
SContext: TRttiContext;
SClassMappingCacheLock: TMultiReadExclusiveWriteSynchronizer;
SClassMappingCache: TDictionary<Pointer, TDaintyRttiClassMapping>;
SValueSettersLock: TMultiReadExclusiveWriteSynchronizer;
SValueSetters: TList<TValueSetterRegistration>;
SMembers: TObjectList<TDaintyRttiMember>;
private
class function GetClassMapping<T: class>: TDaintyRttiClassMapping;
class function GetFieldName(AMember: TRttiNamedObject): string;
class function GetValueSetter(AMember: TRttiMember): TDaintyValueSetter;
protected
class procedure Initialize;
class procedure Finalize;
public
class function Construct<T: class>(ADataSet: TDataSet): TDaintyRttiMapper<T>;
class procedure RegisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer = 0);
class procedure UnregisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass);
end;
implementation
uses
System.TypInfo;
{ TDaintyDataSetHelper }
function TDaintyDataSetHelper.Rows<T>: TEnumerable<T>;
begin
Result := TDainty.Rows<T>(Self);
end;
function TDaintyDataSetHelper.GetMapper<T>: TDaintyMapper<T>;
begin
Result := TDainty.GetMapper<T>(Self);
end;
function TDaintyDataSetHelper.GetFirst<T>: T;
begin
Result := TDainty.GetFirst<T>(Self);
end;
function TDaintyDataSetHelper.GetFirstOrDefault<T>(const ADefault: T): T;
begin
Result := TDainty.GetFirstOrDefault<T>(Self, ADefault);
end;
function TDaintyDataSetHelper.GetSingle<T>: T;
begin
Result := TDainty.GetSingle<T>(Self);
end;
function TDaintyDataSetHelper.GetSingleOrDefault<T>(const ADefault: T): T;
begin
Result := TDainty.GetSingleOrDefault<T>(Self, ADefault);
end;
{ TDainty }
class function TDainty.Rows<T>(ADataSet: TDataSet): TEnumerable<T>;
var
mapper: TDaintyMapper<T>;
begin
mapper := GetMapper<T>(ADataSet);
Result := TDaintyEnumerable<T>.Create(mapper, ADataSet);
end;
class function TDainty.GetMapper<T>(ADataSet: TDataSet): TDaintyMapper<T>;
begin
Result := TDaintyRttiMapperFactory.Construct<T>(ADataSet);
end;
class function TDainty.GetFirst<T>(ADataSet: TDataSet): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if not enumerator.MoveNext then
raise EDatabaseError.Create('Expected at least 1 record but none found');
Result := enumerator.Current;
finally
FreeAndNil(enumerator);
end;
end;
class function TDainty.GetFirstOrDefault<T>(ADataSet: TDataSet; const ADefault: T): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if enumerator.MoveNext then
Result := enumerator.Current
else
Result := ADefault;
finally
FreeAndNil(enumerator);
end;
end;
class function TDainty.GetSingle<T>(ADataSet: TDataSet): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if not enumerator.MoveNext then
raise EDatabaseError.Create('Expected 1 record but none found');
Result := enumerator.Current;
if enumerator.MoveNext then
raise EDatabaseError.Create('Expected 1 record but more found');
finally
FreeAndNil(enumerator);
end;
end;
class function TDainty.GetSingleOrDefault<T>(ADataSet: TDataSet; const ADefault: T): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if not enumerator.MoveNext then
Exit(ADefault);
Result := enumerator.Current;
if enumerator.MoveNext then
Exit(ADefault);
finally
FreeAndNil(enumerator);
end;
end;
{ TDaintyEnumerable<T> }
constructor TDaintyEnumerable<T>.Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
begin
inherited Create;
FMapper := AMapper;
FDataSet := ADataSet;
end;
function TDaintyEnumerable<T>.DoGetEnumerator: TEnumerator<T>;
begin
Result := TDaintyEnumerator<T>.Create(FMapper, FDataSet);
end;
{ TDaintyEnumerator<T> }
constructor TDaintyEnumerator<T>.Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
begin
inherited Create;
FMapper := AMapper;
FDataSet := ADataSet;
end;
function TDaintyEnumerator<T>.DoGetCurrent: T;
begin
Result := FCurrent;
end;
function TDaintyEnumerator<T>.DoMoveNext: Boolean;
begin
if FDataSet.Eof then
Exit(False);
FCurrent := FMapper.MapRow;
Result := True;
FDataSet.Next;
end;
{ TDaintyRttiMapper<T> }
constructor TDaintyRttiMapper<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.ValueSetter));
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;
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;
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]);
SValueSettersLock := TMultiReadExclusiveWriteSynchronizer.Create;
SValueSetters := TList<TValueSetterRegistration>.Create;
SMembers := TObjectList<TDaintyRttiMember>.Create;
end;
class procedure TDaintyRttiMapperFactory.RegisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer);
var
registration: TValueSetterRegistration;
registrationIndex: Integer;
begin
{ Initialization for Dainty.ValueSetter.Default runs before ours, make sure we are initialized }
Initialize;
SValueSettersLock.BeginWrite;
try
registration := TValueSetterRegistration.Create(AFactory, APriority);
for registrationIndex := 0 to Pred(SValueSetters.Count) do
if SValueSetters[registrationIndex].Priority <= APriority then
begin
SValueSetters.Insert(registrationIndex, registration);
Exit;
end;
SValueSetters.Add(registration);
finally
SValueSettersLock.EndWrite;
end;
end;
class procedure TDaintyRttiMapperFactory.UnregisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass);
var
registrationIndex: Integer;
begin
SValueSettersLock.BeginWrite;
try
for registrationIndex := Pred(SValueSetters.Count) downto 0 do
if SValueSetters[registrationIndex].Factory = AFactory then
SValueSetters.Delete(registrationIndex);
finally
SValueSettersLock.EndWrite;
end;
end;
class procedure TDaintyRttiMapperFactory.Finalize;
begin
FreeAndNil(SValueSetters);
FreeAndNil(SValueSettersLock);
FreeAndNil(SClassMappingCache);
FreeAndNil(SClassMappingCacheLock);
SContext.Free;
end;
class function TDaintyRttiMapperFactory.Construct<T>(ADataSet: TDataSet): TDaintyRttiMapper<T>;
var
mapping: TDaintyRttiClassMapping;
begin
mapping := GetClassMapping<T>;
Result := TDaintyRttiMapper<T>.Create(mapping, ADataSet);
end;
class function TDaintyRttiMapperFactory.GetClassMapping<T>: TDaintyRttiClassMapping;
var
typeInfoHandle: Pointer;
classInfo: TRttiType;
method: TRttiMethod;
instanceClassType: TClass;
fieldInfo: TRttiField;
propertyInfo: TRttiProperty;
valueSetter: TProc<TObject, TField>;
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), GetValueSetter(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), GetValueSetter(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.GetValueSetter(AMember: TRttiMember): TDaintyValueSetter;
var
member: TDaintyRttiMember;
registration: TValueSetterRegistration;
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]);
SValueSettersLock.BeginRead;
try
for registration in SValueSetters do
begin
Result := registration.Factory.Construct(member);
if Assigned(Result) then
begin
SMembers.Add(member);
Break;
end;
end;
finally
SValueSettersLock.EndRead;
end;
if not Assigned(Result) then
begin
FreeAndNil(member);
raise ENotSupportedException.CreateFmt('Member %s has unsupported type for Dainty: %d', [AMember.Name, Ord(member.RttiType.TypeKind)]);
end;
end;
{ TDaintyRttiMapperFactory.TValueSetterRegistration }
constructor TDaintyRttiMapperFactory.TValueSetterRegistration.Create(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer);
begin
Factory := AFactory;
Priority := APriority;
end;
{ TDaintyRttiMapper<T> }
function TDaintyRttiMapper<T>.MapRow: T;
var
fieldMapping: TDaintyRttiFieldMapping;
begin
Result := FConstruct() as T;
for fieldMapping in FFieldMapping do
fieldMapping.ValueSetter(Result, fieldMapping.Field);
end;
{ TDaintyRttiFieldNameMapping }
constructor TDaintyRttiFieldNameMapping.Create(const AFieldName: string; const AValueSetter: TDaintyValueSetter);
begin
FieldName := AFieldName;
ValueSetter := AValueSetter;
end;
{ TDaintyRttiFieldMapping }
constructor TDaintyRttiFieldMapping.Create(const AField: TField; const AValueSetter: TDaintyValueSetter);
begin
Field := AField;
ValueSetter := AValueSetter;
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.