From ab92225d1291e05d021971d3c6027e52f3f522dc Mon Sep 17 00:00:00 2001 From: Daniele Teti Date: Wed, 13 Mar 2024 13:00:11 +0100 Subject: [PATCH] First implementation for foDoNotInsert and foDoNotUpdate --- lib/loggerpro/LoggerPro.FileAppender.pas | 2 - samples/activerecord_showcase/EntitiesU.pas | 26 +++++++ samples/activerecord_showcase/MainFormU.dfm | 34 ++++++--- samples/activerecord_showcase/MainFormU.pas | 74 +++++++++++++++++++ sources/MVCFramework.ActiveRecord.pas | 26 +++++-- .../MVCFramework.SQLGenerators.PostgreSQL.pas | 4 +- 6 files changed, 145 insertions(+), 21 deletions(-) diff --git a/lib/loggerpro/LoggerPro.FileAppender.pas b/lib/loggerpro/LoggerPro.FileAppender.pas index 1523c1d0..542b4828 100644 --- a/lib/loggerpro/LoggerPro.FileAppender.pas +++ b/lib/loggerpro/LoggerPro.FileAppender.pas @@ -207,7 +207,6 @@ var lModuleName: string; lPath: string; lFormat: string; - lNow: TDateTime; begin {$IF Defined(Android)} lModuleName := TAndroidHelper.ApplicationTitle.Replace(' ', '_', [rfReplaceAll]); @@ -221,7 +220,6 @@ begin lFormat := fLogFileNameFormat; lPath := fLogsFolder; - lNow := Now(); lFormat := lFormat .Replace('{module}', lModuleName, [rfReplaceAll]) .Replace('{number}', aFileNumber.ToString.PadLeft(2,'0') , [rfReplaceAll]) diff --git a/samples/activerecord_showcase/EntitiesU.pas b/samples/activerecord_showcase/EntitiesU.pas index 9daf4743..291dd561 100644 --- a/samples/activerecord_showcase/EntitiesU.pas +++ b/samples/activerecord_showcase/EntitiesU.pas @@ -231,6 +231,32 @@ type property City: string read fCity write fCity; end; + + [MVCNameCase(ncLowerCase)] + [MVCTable('customers')] + TCustomerWithOptions = class(TCustomEntity) + private +{$IFNDEF USE_SEQUENCES} + [MVCTableField('id', [foPrimaryKey, foAutoGenerated])] +{$ELSE} + [MVCTableField('id', [foPrimaryKey, foAutoGenerated], + 'SEQ_CUSTOMERS_ID' { required for interbase } )] +{$ENDIF} + fID: Integer; + [MVCTableField('code', [foDoNotInsert, foDoNotUpdate])] + fCode: NullableString; + [MVCTableField('description', [foDoNotInsert])] + fCompanyName: string; + [MVCTableField('city', [foDoNotUpdate])] + fCity: string; + public + property ID: Integer read fID write fID; + property Code: NullableString read fCode write fCode; + property CompanyName: string read fCompanyName write fCompanyName; + property City: string read fCity write fCity; + end; + + [MVCNameCase(ncLowerCase)] [MVCTable('order_details')] TOrderDetail = class(TCustomEntity) diff --git a/samples/activerecord_showcase/MainFormU.dfm b/samples/activerecord_showcase/MainFormU.dfm index bef09e66..d4e738a0 100644 --- a/samples/activerecord_showcase/MainFormU.dfm +++ b/samples/activerecord_showcase/MainFormU.dfm @@ -2,7 +2,7 @@ object MainForm: TMainForm Left = 0 Top = 0 Caption = 'TMVCActiveRecord - ShowCase' - ClientHeight = 626 + ClientHeight = 660 ClientWidth = 1094 Color = clBtnFace Font.Charset = DEFAULT_CHARSET @@ -14,7 +14,7 @@ object MainForm: TMainForm OnShow = FormShow DesignSize = ( 1094 - 626) + 660) TextHeight = 13 object btnCRUD: TButton Left = 8 @@ -27,7 +27,7 @@ object MainForm: TMainForm end object btnSelect: TButton Left = 8 - Top = 242 + Top = 283 Width = 121 Height = 33 Caption = 'Queries' @@ -38,7 +38,7 @@ object MainForm: TMainForm Left = 280 Top = 8 Width = 806 - Height = 610 + Height = 644 Anchors = [akLeft, akTop, akRight, akBottom] Ctl3D = True DoubleBuffered = True @@ -55,10 +55,11 @@ object MainForm: TMainForm TabOrder = 2 WantReturns = False WordWrap = False + ExplicitHeight = 610 end object btnRelations: TButton Left = 8 - Top = 281 + Top = 322 Width = 121 Height = 35 Caption = 'Relations' @@ -67,7 +68,7 @@ object MainForm: TMainForm end object btnInheritance: TButton Left = 8 - Top = 322 + Top = 363 Width = 121 Height = 34 Caption = 'Inheritance' @@ -76,7 +77,7 @@ object MainForm: TMainForm end object btnValidation: TButton Left = 8 - Top = 362 + Top = 403 Width = 121 Height = 34 Caption = 'Validation' @@ -94,7 +95,7 @@ object MainForm: TMainForm end object btnRQL: TButton Left = 8 - Top = 402 + Top = 443 Width = 121 Height = 34 Caption = 'RQL Query' @@ -240,7 +241,7 @@ object MainForm: TMainForm end object btnReadOnly: TButton Left = 8 - Top = 442 + Top = 483 Width = 121 Height = 34 Caption = 'Read/Only Entities' @@ -249,7 +250,7 @@ object MainForm: TMainForm end object btnSpeed: TButton Left = 8 - Top = 482 + Top = 523 Width = 121 Height = 34 Caption = 'Metadata Speed Test' @@ -285,7 +286,7 @@ object MainForm: TMainForm end object btnIntegersAsBool: TButton Left = 8 - Top = 522 + Top = 563 Width = 121 Height = 34 Caption = 'Integers As Booleans' @@ -294,7 +295,7 @@ object MainForm: TMainForm end object btnObjectVersion: TButton Left = 8 - Top = 562 + Top = 603 Width = 121 Height = 34 Caption = 'Object Version' @@ -310,6 +311,15 @@ object MainForm: TMainForm TabOrder = 30 OnClick = btnCustomTableClick end + object btnCRUDWithOptions: TButton + Left = 8 + Top = 242 + Width = 121 + Height = 33 + Caption = 'CRUD With Fields Opts' + TabOrder = 31 + OnClick = btnCRUDWithOptionsClick + end object FDConnection1: TFDConnection Left = 312 Top = 40 diff --git a/samples/activerecord_showcase/MainFormU.pas b/samples/activerecord_showcase/MainFormU.pas index 427000b7..ffa7561d 100644 --- a/samples/activerecord_showcase/MainFormU.pas +++ b/samples/activerecord_showcase/MainFormU.pas @@ -63,6 +63,7 @@ type btnIntegersAsBool: TButton; btnObjectVersion: TButton; btnCustomTable: TButton; + btnCRUDWithOptions: TButton; procedure btnCRUDClick(Sender: TObject); procedure btnInheritanceClick(Sender: TObject); procedure btnMultiThreadingClick(Sender: TObject); @@ -96,6 +97,7 @@ type procedure btnIntegersAsBoolClick(Sender: TObject); procedure btnObjectVersionClick(Sender: TObject); procedure btnCustomTableClick(Sender: TObject); + procedure btnCRUDWithOptionsClick(Sender: TObject); private procedure Log(const Value: string); procedure LoadCustomers(const HowManyCustomers: Integer = 50); @@ -412,6 +414,78 @@ begin end; end; +procedure TMainForm.btnCRUDWithOptionsClick(Sender: TObject); +var + lCustomer: TCustomerWithOptions; + lID: Integer; +begin + Log('** CRUD test with fields options'); + lCustomer := TCustomerWithOptions.Create; + try + { + 'Code' will not be persisted on table because defined as 'foReadOnly' + } + lCustomer.Code := '1234'; // "Code" will be skipped in insert and in update as well + lCustomer.CompanyName := 'Google Inc.'; // "CompanyName" will be skipped in insert + lCustomer.City := 'Montain View, CA'; // "City" will be skipped in update + lCustomer.Insert; + lID := lCustomer.ID; + Log('Just inserted Customer ' + lID.ToString + ' with fields options'); + finally + lCustomer.Free; + end; + + //let's check that code is empty + lCustomer := TMVCActiveRecord.GetByPK(lID); + try + Assert(lCustomer.Code.IsNull); // it's null + Assert(lCustomer.CompanyName.IsEmpty); //empty string + Assert(lCustomer.City = 'Montain View, CA'); //inserted + + lCustomer.Code := '1234'; // "Code" will be skipped in insert and in update as well + lCustomer.CompanyName := 'Google Inc.'; // "CompanyName" will be saved + lCustomer.City := 'Via Roma 10, ITALY'; // "City" will be skipped in update + lCustomer.Update; + finally + lCustomer.Free; + end; + + //let's check + lCustomer := TMVCActiveRecord.GetByPK(lID); + try + Assert(lCustomer.Code.IsNull); // it's null + Assert(lCustomer.CompanyName = 'Google Inc.'); //correctly updated + Assert(lCustomer.City = 'Montain View, CA'); // not updated, mantains old value + finally + lCustomer.Free; + end; + + { + //if underlying field is not null, it is loaded as usual + TMVCActiveRecord.CurrentConnection.ExecSQL('update customers set code = ''XYZ'' where id = ?', [lID]); + lCustomer := TMVCActiveRecord.GetByPK(lID); + try + Assert('XYZ' = lCustomer.Code); + lCustomer.CompanyName := lCustomer.CompanyName + ' changed!'; + lCustomer.Code := 'this code will not be saved'; + lCustomer.Update; //do not save field "code" + Log('Just updated Customer ' + lID.ToString); + finally + lCustomer.Free; + end; + + //but being foReadOnly is not updated + lCustomer := TMVCActiveRecord.GetByPK(lID); + try + Assert('XYZ' = lCustomer.Code); + lCustomer.Delete; + Log('Just deleted Customer ' + lID.ToString + ' with a R/O field'); + finally + lCustomer.Free; + end; + } +end; + procedure TMainForm.btnCRUDWithStringPKsClick(Sender: TObject); var lCustomer: TCustomerWithCode; diff --git a/sources/MVCFramework.ActiveRecord.pas b/sources/MVCFramework.ActiveRecord.pas index a1dc4c63..0947808f 100644 --- a/sources/MVCFramework.ActiveRecord.pas +++ b/sources/MVCFramework.ActiveRecord.pas @@ -66,9 +66,11 @@ type TMVCActiveRecord = class; TMVCActiveRecordFieldOption = (foPrimaryKey, { it's the primary key of the mapped table } foAutoGenerated, { not written, read - similar to readonly } - foReadOnly, { not written, read } + foReadOnly, { not written, read - like foDoNotInsert+foDoNotUpdate } foWriteOnly, { written, not read } - foVersion {used for versioning, only one field with foVersion is allowed in class} + foVersion, {used for versioning, only one field with foVersion is allowed in class} + foDoNotInsert, { this field is not included in SQL INSERT commands } + foDoNotUpdate { this field is not included in SQL UPDATE commands } ); TMVCActiveRecordFieldOptions = set of TMVCActiveRecordFieldOption; TMVCEntityAction = (eaCreate, eaRetrieve, eaUpdate, eaDelete); @@ -104,7 +106,7 @@ type FieldName: string; FieldOptions: TMVCActiveRecordFieldOptions; DataTypeName: string; - Writeable, Readable, IsVersion: Boolean; + Writeable, Readable, Insertable, Updatable, IsVersion: Boolean; procedure EndUpdates; end; @@ -3899,7 +3901,7 @@ begin begin Result := Result + GetFieldNameForSQL(lPair.Value.FieldName) + ' = ' + GetParamNameForSQL(lPair.Value.FieldName) + ' + 1,'; - end else if lPair.Value.Writeable then + end else if lPair.Value.Updatable then begin Result := Result + GetFieldNameForSQL(lPair.Value.FieldName) + ' = :' + GetParamNameForSQL(lPair.Value.FieldName) + ','; @@ -4236,13 +4238,27 @@ begin begin Writeable := false; Readable := false; + Insertable := False; + Updatable := False; end else begin Writeable := ((FieldOptions * [foReadOnly, foAutoGenerated]) = []); - Readable := (FieldOptions * [foWriteOnly]) = []; + Readable := not (foWriteOnly in FieldOptions); + Insertable := not (foDoNotInsert in FieldOptions); + Updatable := not (foDoNotUpdate in FieldOptions); end; IsVersion := foVersion in FieldOptions; + + // field options consistency check +// if Writeable and (not Updatable) then +// begin +// raise EMVCActiveRecord.CreateFmt('Field "%s" cannot be Writeable but not Updateable', [FieldName]); +// end; +// if (not Writeable) and (Readable) and (Updatable or Insertable) then +// begin +// raise EMVCActiveRecord.CreateFmt('Field "%s" cannot be ReadOnly but (Updateable or Insertable)', [FieldName]); +// end; end; { TMVCUnitOfWork } diff --git a/sources/MVCFramework.SQLGenerators.PostgreSQL.pas b/sources/MVCFramework.SQLGenerators.PostgreSQL.pas index 3a807337..cc9acf5b 100644 --- a/sources/MVCFramework.SQLGenerators.PostgreSQL.pas +++ b/sources/MVCFramework.SQLGenerators.PostgreSQL.pas @@ -85,7 +85,7 @@ begin for lKeyValue in TableMap.fMap do begin - if lKeyValue.Value.Writeable then + if lKeyValue.Value.Insertable then begin lSB.Append(GetFieldNameForSQL(lKeyValue.Value.FieldName) + ','); end; @@ -109,7 +109,7 @@ begin if lKeyValue.Value.IsVersion then begin lSB.Append(OBJECT_VERSION_STARTING_VALUE + ','); - end else if lKeyValue.Value.Writeable then + end else if lKeyValue.Value.Insertable then begin lSB.Append(':' + GetParamNameForSQL(lKeyValue.Value.FieldName) + ','); end;