Improved dataset handling for functional actions.

This commit is contained in:
Daniele Teti 2023-07-19 11:10:21 +02:00
parent e6fc21dff9
commit 61d021b92a
10 changed files with 308 additions and 28 deletions

View File

@ -231,12 +231,89 @@ Congratulations to Daniele Teti and all the staff for the excellent work!" -- Ma
## What's New in the next "repo version" a.k.a. 3.4.0-neon
- Added support for dotEnv multiline keys - added dotEnv show case
- Added MSHeap memory manager for Win32 and Win64 (https://github.com/RDP1974/DelphiMSHeap)
- ⚡ Added support for dotEnv multiline keys - added dotEnv show case
- ⚡ Added MSHeap memory manager for Win32 and Win64 (https://github.com/RDP1974/DelphiMSHeap)
- 🐞 FIX [Issue 664](https://github.com/danieleteti/delphimvcframework/issues/664) Thanks to [MPannier](https://github.com/MPannier)
- 🐞 FIX [Issue 667](https://github.com/danieleteti/delphimvcframework/issues/667)
- 🐞 FIX Wrong comparison in checks for ro/RW/PK fields in `TMVCActiveRecord`
- Wizard updated to be dotEnv aware
- ⚡ Wizard updated to be dotEnv aware
- ⚡ Better error message in case of serialization of `TArray<TObject>`
- ⚡ Improved serialization of `TObjectList<TDataSet>` (however `ObjectDict` is still the preferred way to serialize multiple datasets).
- ⚡ Added static method for easier cloning of FireDAC dataset into `TFDMemTable`.
- `class function CloneFrom(const FDDataSet: TFDDataSet): TFDMemTable`
- Check sample "function_actions_showcase.dproj" for more info.
- ⚡ Functional Actions
- In addition to the classic `procedure` based actions, now it's possibile to use functions as actions. The `Result` variable is automatically rendered and, if it is an object, its memory is freed.
```pascal
type
[MVCNameCase(ncCamelCase)]
TPersonRec = record
FirstName, LastName: String;
Age: Integer;
class function Create: TPersonRec; static;
end;
[MVCNameCase(ncCamelCase)]
TPerson = class
private
fAge: Integer;
fFirstName, fLastName: String;
public
property FirstName: String read fFirstName write fFirstName;
property LastName: String read fLastName write fLastName;
property Age: Integer read fAge write fAge;
end;
[MVCPath('/api')]
TMyController = class(TMVCController)
public
{ actions returning a simple type }
[MVCPath('/sumsasinteger/($A)/($B)')]
function GetSum(const A, B: Integer): Integer;
[MVCPath('/sumsasfloat/($A)/($B)')]
function GetSumAsFloat(const A, B: Extended): Extended;
{ actions returning records }
[MVCPath('/records/single')]
function GetSingleRecord: TPersonRec;
[MVCPath('/records/multiple')]
function GetMultipleRecords: TArray<TPersonRec>;
{ actions returning objects }
[MVCPath('/objects/single')]
function GetSingleObject: TPerson;
[MVCPath('/objects/multiple')]
function GetMultipleObjects: TObjectList<TPerson>;
{ actions returning datasets }
[MVCPath('/datasets/single')]
function GetSingleDataSet: TDataSet;
[MVCPath('/datasets/multiple')]
function GetMultipleDataSet: TEnumerable<TDataSet>;
[MVCPath('/datasets/multiple2')]
function GetMultipleDataSet2: IMVCObjectDictionary;
{ customize response headers }
[MVCPath('/headers')]
function GetWithCustomHeaders: TObjectList<TPerson>;
end;
```
Check sample "function_actions_showcase.dproj" for more info.

View File

@ -0,0 +1 @@
{"FDBS":{"Version":16,"Manager":{"UpdatesRegistry":true,"TableList":[{"class":"Table","Name":"FDMemTable1","SourceName":"FDMemTable1","TabID":0,"EnforceConstraints":false,"MinimumCapacity":50,"CheckNotNull":false,"ColumnList":[{"class":"Column","Name":"id","SourceName":"id","SourceID":1,"DataType":"Int64","Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OInKey":true,"OriginColName":"id"},{"class":"Column","Name":"last_name","SourceName":"last_name","SourceID":2,"DataType":"WideString","Size":100,"Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"last_name","SourceSize":100},{"class":"Column","Name":"first_name","SourceName":"first_name","SourceID":3,"DataType":"WideString","Size":100,"Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"first_name","SourceSize":100},{"class":"Column","Name":"dob","SourceName":"dob","SourceID":4,"DataType":"Date","Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"dob"},{"class":"Column","Name":"full_name","SourceName":"full_name","SourceID":5,"DataType":"WideString","Size":80,"Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"full_name","SourceSize":80},{"class":"Column","Name":"is_male","SourceName":"is_male","SourceID":6,"DataType":"Boolean","Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"is_male"},{"class":"Column","Name":"note","SourceName":"note","SourceID":7,"DataType":"WideMemo","Searchable":true,"AllowNull":true,"BlobData":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"note"},{"class":"Column","Name":"photo","SourceName":"photo","SourceID":8,"DataType":"Blob","Searchable":true,"AllowNull":true,"BlobData":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"photo"},{"class":"Column","Name":"person_type","SourceName":"person_type","SourceID":9,"DataType":"WideString","Size":8190,"Searchable":true,"AllowNull":true,"BlobData":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"person_type","SourceSize":8190},{"class":"Column","Name":"salary","SourceName":"salary","SourceID":10,"DataType":"Currency","Precision":19,"Scale":4,"Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"salary","SourcePrecision":19,"SourceScale":4},{"class":"Column","Name":"annual_bonus","SourceName":"annual_bonus","SourceID":11,"DataType":"Currency","Precision":19,"Scale":4,"Searchable":true,"AllowNull":true,"Base":true,"OAllowNull":true,"OInUpdate":true,"OInWhere":true,"OriginColName":"annual_bonus","SourcePrecision":19,"SourceScale":4}],"ConstraintList":[],"ViewList":[],"RowList":[{"RowID":0,"Original":{"id":235,"last_name":"Richards","first_name":"Reed","dob":"19851104","full_name":"Reed Richards","is_male":true,"person_type":"person","photo":""}},{"RowID":1,"Original":{"id":236,"last_name":"Parker","first_name":"Peter","dob":"19851104","full_name":"Peter Parker","is_male":true,"person_type":"employee","salary":2100,"photo":""}},{"RowID":2,"Original":{"id":237,"last_name":"Storm","first_name":"Sue","dob":"19751014","full_name":"Sue Storm","is_male":false,"person_type":"employee","salary":2200,"photo":""}},{"RowID":3,"Original":{"id":238,"last_name":"Banner","first_name":"Bruce","dob":"19751104","full_name":"Bruce Banner","is_male":true,"person_type":"manager","salary":2800,"annual_bonus":5000,"photo":""}}]}],"RelationList":[],"UpdatesJournal":{"SavePoint":4,"Changes":[]}}}}

View File

@ -4,7 +4,7 @@ interface
uses
MVCFramework, MVCFramework.Commons, MVCFramework.Serializer.Commons,
System.Generics.Collections;
System.Generics.Collections, Data.DB;
type
[MVCNameCase(ncCamelCase)]
@ -46,6 +46,14 @@ type
[MVCPath('/objects/multiple')]
function GetMultipleObjects: TObjectList<TPerson>;
{ actions returning datasets }
[MVCPath('/datasets/single')]
function GetSingleDataSet: TDataSet;
[MVCPath('/datasets/multiple')]
function GetMultipleDataSet: TEnumerable<TDataSet>;
[MVCPath('/datasets/multiple2')]
function GetMultipleDataSet2: IMVCObjectDictionary;
{ customize response headers }
[MVCPath('/headers')]
function GetWithCustomHeaders: TObjectList<TPerson>;
@ -54,10 +62,38 @@ type
implementation
uses
System.SysUtils, MVCFramework.Logger, System.StrUtils, System.DateUtils;
System.SysUtils, MVCFramework.Logger, System.StrUtils, System.DateUtils,
MainDMU, FireDAC.Comp.Client, MVCFramework.FireDAC.Utils;
{ TMyController }
function TMyController.GetMultipleDataSet: TEnumerable<TDataSet>;
begin
var lDM := TdmMain.Create(nil);
try
lDM.dsPeople.Open;
var lList := TObjectList<TDataSet>.Create;
lList.Add(TFDMemTable.CloneFrom(lDM.dsPeople));
lList.Add(TFDMemTable.CloneFrom(lDM.dsPeople));
Result := lList;
finally
lDM.Free;
end;
end;
function TMyController.GetMultipleDataSet2: IMVCObjectDictionary;
begin
var lDM := TdmMain.Create(nil);
try
lDM.dsPeople.Open;
Result := ObjectDict()
.Add('people1', TFDMemTable.CloneFrom(lDM.dsPeople))
.Add('people2', TFDMemTable.CloneFrom(lDM.dsPeople));
finally
lDM.Free;
end;
end;
function TMyController.GetMultipleObjects: TObjectList<TPerson>;
begin
Result := TObjectList<TPerson>.Create;
@ -90,6 +126,17 @@ begin
Inc(Result[2].Age, 20);
end;
function TMyController.GetSingleDataSet: TDataSet;
begin
var lDM := TdmMain.Create(nil);
try
lDM.dsPeople.Open;
Result := TFDMemTable.CloneFrom(lDM.dsPeople);
finally
lDM.Free;
end;
end;
function TMyController.GetSingleObject: TPerson;
begin
Result := TPerson.Create;

View File

@ -0,0 +1,101 @@
object dmMain: TdmMain
Height = 162
Width = 312
object dsPeople: TFDMemTable
Active = True
FetchOptions.AssignedValues = [evMode]
FetchOptions.Mode = fmAll
ResourceOptions.AssignedValues = [rvPersistent, rvSilentMode]
ResourceOptions.Persistent = True
ResourceOptions.SilentMode = True
UpdateOptions.AssignedValues = [uvCheckRequired, uvAutoCommitUpdates]
UpdateOptions.CheckRequired = False
UpdateOptions.AutoCommitUpdates = True
Left = 176
Top = 88
Content = {
4144425310000000CA060000FF00010001FF02FF03040016000000460044004D
0065006D005400610062006C0065003100050016000000460044004D0065006D
005400610062006C0065003100060000000000070000080032000000090000FF
0AFF0B04000400000069006400050004000000690064000C00010000000E000D
000F000110000111000112000113000114000115000116000400000069006400
FEFF0B0400120000006C006100730074005F006E0061006D0065000500120000
006C006100730074005F006E0061006D0065000C00020000000E001700180064
0000000F00011000011100011200011300011400011600120000006C00610073
0074005F006E0061006D006500190064000000FEFF0B04001400000066006900
7200730074005F006E0061006D00650005001400000066006900720073007400
5F006E0061006D0065000C00030000000E0017001800640000000F0001100001
110001120001130001140001160014000000660069007200730074005F006E00
61006D006500190064000000FEFF0B04000600000064006F0062000500060000
0064006F0062000C00040000000E001A000F0001100001110001120001130001
14000116000600000064006F006200FEFF0B040012000000660075006C006C00
5F006E0061006D006500050012000000660075006C006C005F006E0061006D00
65000C00050000000E0017001800500000000F00011000011100011200011300
01140001160012000000660075006C006C005F006E0061006D00650019005000
0000FEFF0B04000E000000690073005F006D0061006C00650005000E00000069
0073005F006D0061006C0065000C00060000000E001B000F0001100001110001
12000113000114000116000E000000690073005F006D0061006C006500FEFF0B
0400080000006E006F00740065000500080000006E006F00740065000C000700
00000E001C000F00011000011D00011100011200011300011400011600080000
006E006F0074006500FEFF0B04000A000000700068006F0074006F0005000A00
0000700068006F0074006F000C00080000000E001E000F00011000011D000111
000112000113000114000116000A000000700068006F0074006F00FEFF0B0400
1600000070006500720073006F006E005F007400790070006500050016000000
70006500720073006F006E005F0074007900700065000C00090000000E001700
1800FE1F00000F00011000011D00011100011200011300011400011600160000
0070006500720073006F006E005F0074007900700065001900FE1F0000FEFF0B
04000C000000730061006C0061007200790005000C000000730061006C006100
720079000C000A0000000E001F002000130000002100040000000F0001100001
11000112000113000114000116000C000000730061006C006100720079002200
13000000230004000000FEFF0B04001800000061006E006E00750061006C005F
0062006F006E007500730005001800000061006E006E00750061006C005F0062
006F006E00750073000C000B0000000E001F002000130000002100040000000F
000110000111000112000113000114000116001800000061006E006E00750061
006C005F0062006F006E0075007300220013000000230004000000FEFEFF24FE
FF25FEFF26FF27280000000000FF290000EB0000000000000001001000000052
00690063006800610072006400730002000800000052006500650064000300D5
0F0B0004001A0000005200650065006400200052006900630068006100720064
0073000500FFFF08000C00000070006500720073006F006E00FEFEFF27280001
000000FF290000EC0000000000000001000C0000005000610072006B00650072
0002000A000000500065007400650072000300D50F0B00040018000000500065
0074006500720020005000610072006B00650072000500FFFF08001000000065
006D0070006C006F007900650065000900406F400100000000FEFEFF27280002
000000FF290000ED0000000000000001000A000000530074006F0072006D0002
000600000053007500650003007B010B00040012000000530075006500200053
0074006F0072006D000500000008001000000065006D0070006C006F00790065
006500090080B14F0100000000FEFEFF27280003000000FF290000EE00000000
00000001000C000000420061006E006E006500720002000A0000004200720075
0063006500030090010B00040018000000420072007500630065002000420061
006E006E00650072000500FFFF08000E0000006D0061006E0061006700650072
000900003FAB01000000000A0080F0FA0200000000FEFEFEFEFEFF2AFEFF2B2C
0004000000FF2DFEFEFE0E004D0061006E0061006700650072001E0055007000
6400610074006500730052006500670069007300740072007900120054006100
62006C0065004C006900730074000A005400610062006C00650008004E006100
6D006500140053006F0075007200630065004E0061006D0065000A0054006100
620049004400240045006E0066006F0072006300650043006F006E0073007400
7200610069006E00740073001E004D0069006E0069006D0075006D0043006100
700061006300690074007900180043006800650063006B004E006F0074004E00
75006C006C00140043006F006C0075006D006E004C006900730074000C004300
6F006C0075006D006E00100053006F007500720063006500490044000E006400
740049006E007400360034001000440061007400610054007900700065001400
530065006100720063006800610062006C006500120041006C006C006F007700
4E0075006C006C000800420061007300650014004F0041006C006C006F007700
4E0075006C006C0012004F0049006E0055007000640061007400650010004F00
49006E00570068006500720065000C004F0049006E004B00650079001A004F00
72006900670069006E0043006F006C004E0061006D0065001800640074005700
69006400650053007400720069006E0067000800530069007A00650014005300
6F007500720063006500530069007A0065000C00640074004400610074006500
12006400740042006F006F006C00650061006E00140064007400570069006400
65004D0065006D006F00100042006C006F00620044006100740061000C006400
740042006C006F006200140064007400430075007200720065006E0063007900
120050007200650063006900730069006F006E000A005300630061006C006500
1E0053006F00750072006300650050007200650063006900730069006F006E00
160053006F0075007200630065005300630061006C0065001C0043006F006E00
730074007200610069006E0074004C0069007300740010005600690065007700
4C006900730074000E0052006F0077004C00690073007400060052006F007700
0A0052006F0077004900440010004F0072006900670069006E0061006C001800
520065006C006100740069006F006E004C006900730074001C00550070006400
61007400650073004A006F00750072006E0061006C0012005300610076006500
50006F0069006E0074000E004300680061006E00670065007300}
end
end

View File

@ -0,0 +1,26 @@
unit MainDMU;
interface
uses
System.SysUtils, System.Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option,
FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf,
FireDAC.DApt.Intf, FireDAC.Stan.StorageBin, Data.DB, FireDAC.Comp.DataSet,
FireDAC.Comp.Client;
type
TdmMain = class(TDataModule)
dsPeople: TFDMemTable;
private
{ Private declarations }
public
{ Public declarations }
end;
implementation
{%CLASSGROUP 'System.Classes.TPersistent'}
{$R *.dfm}
end.

View File

@ -15,7 +15,8 @@ uses
IdContext,
IdHTTPWebBrokerBridge,
ControllerU in 'ControllerU.pas',
WebModuleU in 'WebModuleU.pas' {MyWebModule: TWebModule};
WebModuleU in 'WebModuleU.pas' {MyWebModule: TWebModule},
MainDMU in 'MainDMU.pas' {dmMain: TDataModule};
{$R *.res}

View File

@ -120,6 +120,11 @@
<FormType>dfm</FormType>
<DesignClass>TWebModule</DesignClass>
</DCCReference>
<DCCReference Include="MainDMU.pas">
<Form>dmMain</Form>
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>

View File

@ -29,7 +29,8 @@ unit MVCFramework.FireDAC.Utils;
interface
uses
FireDAC.Comp.Client, FireDAC.Stan.Param, System.Rtti, JsonDataObjects;
FireDAC.Comp.Client, FireDAC.Stan.Param, System.Rtti, JsonDataObjects,
Data.DB, FireDAC.Comp.DataSet;
type
TFireDACUtils = class sealed
@ -51,13 +52,13 @@ type
TFDCustomMemTableHelper = class helper for TFDCustomMemTable
public
procedure InitFromMetadata(const AJSONMetadata: TJSONObject);
class function CloneFrom(const FDDataSet: TFDDataSet): TFDMemTable; static;
end;
implementation
uses
System.Generics.Collections,
Data.DB,
System.Classes,
MVCFramework.Serializer.Commons,
System.SysUtils;
@ -222,6 +223,12 @@ begin
end;
end;
class function TFDCustomMemTableHelper.CloneFrom(const FDDataSet: TFDDataSet): TFDMemTable;
begin
Result := TFDMemTable.Create(nil);
TFDMemTable(Result).CloneCursor(FDDataSet);
end;
procedure TFDCustomMemTableHelper.InitFromMetadata(const AJSONMetadata: TJSONObject);
begin
TFireDACUtils.CreateDatasetFromMetadata(Self, AJSONMetadata);

View File

@ -3033,8 +3033,15 @@ begin
for Obj in ObjList do
begin
if Obj <> nil then
begin
if Obj is TDataSet then
begin
DataSetToJsonArray(TDataSet(Obj), JSONArray.AddArray, TMVCNameCase.ncLowerCase, nil,nil,);
end
else
begin
ObjectToJsonObject(Obj, JSONArray.AddObject, GetSerializationType(Obj, AType), AIgnoredAttributes)
end;
end
else
begin
@ -3074,6 +3081,12 @@ begin
begin
lJObj := lJSONArr.AddObject;
lCurrentArrayItem := ATValueContainingAnArray.GetArrayElement(I);
if lCurrentArrayItem.IsObjectInstance then
begin
raise EMVCSerializationException.CreateFmt('Found a "%s" while serializing array. Instance types not allowed in arrays - [HINT] Use list of objects instead of array', [lCurrentArrayItem.AsObject.ClassName]);
end
else
begin
InternalRecordToJsonObject(
lCurrentArrayItem.GetReferenceToRawData,
lCurrentArrayItem.TypeInfo,
@ -3085,6 +3098,7 @@ begin
nil
);
end;
end;
Result := lJSONArr.ToJSON();
finally
lJSONArr.free;

View File

@ -2515,7 +2515,11 @@ begin
lResponseObject := lInvokeResult.AsObject;
try
// https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-7.0
if lResponseObject is TStream then
if lResponseObject is TDataSet then
begin
lSelectedController.Render(TDataSet(lResponseObject), False);
end
else if lResponseObject is TStream then
begin
lContext.Response.RawWebResponse.Content := EmptyStr;
lContext.Response.RawWebResponse.ContentType := lContext.Response.ContentType;
@ -2523,9 +2527,7 @@ begin
lContext.Response.RawWebResponse.FreeContentStream := True;
lResponseObject := nil; //do not free it!!
end
else
begin
if TDuckTypedList.CanBeWrappedAsList(lResponseObject, lObjList) then
else if TDuckTypedList.CanBeWrappedAsList(lResponseObject, lObjList) then
begin
lSelectedController.Render(lObjList);
end
@ -2533,7 +2535,6 @@ begin
begin
lSelectedController.Render(lResponseObject, False);
end;
end;
finally
lResponseObject.Free;
end