ManyToOne associations Transient capability.

Issue #94 resolved
Todd Flora created an issue

Save All saves both OneToMany and ManyToOne related classes. Of course with a method named SaveAll that is what you would expect, and so this is excellent.

But for us there are times when you want all the one to many related classes to save but not the manyToOne relations.

We use the manyToOne relations to provide lookup data into a given class but don't want it to save when we save that same class.

For instance look at the following definition: the TSubsidiaryAssociate is a ManyToOne but it is just there so that we can lookup SBSNo and SBSName and provide these fields as part of the Vendor Payload, We never want this to save when saving a vendor as Subsidiaries are lookup data that are seeded by an administration console not by a vendor being saved.

[Entity]
  [Table('VENDOR', 'rps')]
  [Sequence('Select GetSid as Sid from dual')]
  TVendor = class
  private
    FSid: Int64;
    FVendCode: string;
    FActive: Int16;
    FVendName: Nullable<string>;
    FVendId: Nullable<Integer>;
    FFirstName: Nullable<string>;
    FLastName: Nullable<string>;
    FSbsSid: Nullable<Int64>;
    [ManyToOne(false, [ckCascadeAll], 'SbsSid')]
    FSubsidiary : TSubsidiaryAssociate;  < -- DONT WANT THIS TO SAVE

    [OneToMany(False, [ckCascadeAll])]
    FVendorContactAddresses: Lazy<IList<TVendorContactAddress>>;
    [OneToMany(False, [ckCascadeAll])]
    FVendorEmails: Lazy<IList<TVendorEmail>>;
    [OneToMany(False, [ckCascadeAll])]
    FVendorPhones: Lazy<IList<TVendorPhone>>;
    [OneToMany(False, [ckCascadeAll])]
    FVendorTerms: Lazy<IList<TVendorTerm>>;
    function GetSbsName: Nullable<string>;
    function GetSbsNo: Int16;
    procedure SetSbsName(const Value: Nullable<string>);
    procedure SetSbsNo(const Value: Int16);
  public
    constructor Create; virtual;
    destructor Destroy; override;
    [Column('SID',[cpRequired,cpNotNull,cpPrimaryKey],19,0)]
    property Sid: Int64 read FSid write FSid;
    [Column('VEND_CODE',[cpRequired,cpNotNull],6)]
    property VendCode: string read FVendCode write FVendCode;
    [Column('ACTIVE',[cpRequired,cpNotNull],1,0)]
    property Active: Int16 read FActive write FActive;
    [Column('VEND_NAME',[],25)]
    property VendName: Nullable<string> read FVendName write FVendName;
    [Column('VEND_ID',[],10,0)]
    property VendId: Nullable<Integer> read FVendId write FVendId;
    [Column('FIRST_NAME',[],30)]
    property FirstName: Nullable<string> read FFirstName write FFirstName;
    [Column('LAST_NAME',[],30)]
    property LastName: Nullable<string> read FLastName write FLastName;
    [Column('SBS_SID',[],19,0)]
    property SbsSid: Nullable<Int64> read FSbsSid write FSbsSid;
    property SbsNo: Int16 read GetSbsNo write SetSbsNo;
    property SbsName: Nullable<string> read GetSbsName write SetSbsName;

    property VendorContactAddresses: IList<TVendorContactAddress> read getVendorContactAddresses;
    property VendorEmails: IList<TVendorEmail> read getVendorEmails;
    property VendorPhones: IList<TVendorPhone> read getVendorPhones;
    property VendorTerms: IList<TVendorTerm> read getVendorTerms;
  end;

Therefore in order to Mark certain ralationships as not savable I would like to suggest a TransientAttribute that can be added to those related classes that you don't want to save, even when calling Save All.

I have tried this and it works great. I Made the following two changes.

In Spring.Persistence.Mapping Attributes I added a transient Attribute based on TCustomAttribute

 TransientAttribute = class(TCustomAttribute);

In Spring.Persistence.Mapping.RttiExplorer I modified the GetRelationsOf method to ignore those relations marked as Transient. This method is currently only being used by the SaveAll process so for me it made sense to add it here. Maybe not the right place if you plan to use this method in other places. In that case maybe something similar to this that would accomplish this same goal. of ignoring relations marked transient.

class function TRttiExplorer.GetRelationsOf(AEntity: TObject; relationAttributeClass: TAttributeClass): IList<TObject>;
var
  LType: TRttiType;
  LField: TRttiField;
  LProperty: TRttiProperty;
  LEntities: IList<TObject>;
begin
  Result := TCollections.CreateList<TObject>;

  LType := FRttiCache.GetType(AEntity.ClassType);
  for LField in LType.GetFields do
  begin
    if (LField.HasCustomAttribute(relationAttributeClass)) and
       (not LField.HasCustomAttribute(TransientAttribute)) then  <-- Added this Line
    begin
      LEntities := GetSubEntityFromMemberDeep(AEntity, LField);
      if LEntities.Any then
        Result.AddRange(LEntities);
    end;
  end;

  for LProperty in LType.GetProperties do
  begin
    if LProperty.HasCustomAttribute(relationAttributeClass) then
    begin
      LEntities := GetSubEntityFromMemberDeep(AEntity, LProperty);
      if LEntities.Any then
        Result.AddRange(LEntities);
    end;
  end;
end;

So now with this new Attribute I can do the following and the Subsidiary is not saved when I call SaveAll

    [Transient]
    [ManyToOne(false, [ckCascadeAll], 'SbsSid')]
    FSubsidiary : TSubsidiaryAssociate;

I think this is a feature that others would also use, and so I would like to suggest it as an enhancement. What do you think?

Comments (3)

  1. Todd Flora reporter

    BTW. If there is a better way to include lookup data in a class from another class, I would greatly appreciate knowing how you would suggest doing this.

  2. Log in to comment