ManyValuedAssociation does not want to play ball

Hello!

We have a situation where we wish to load, in a lump, three tables that are related to each other (AmbitiTerritoriali, AmbitiTerritoriali_Comuni, Comuni). As you can guess the two ends of the relationship are AmbitiTerritoriali and Comuni with the middle one being a many-to-many (ie one Comuni can be linked to more than one AmbitiTerritoriali and viceversa).
We need to load it all in a lump because loading Comuni one by one simply doesn't work for us from a performance standpoint.

To this end, I wish to adjust the model and I am thinking of using a ManyValuedAssociation, like so:

[Entity]
  [Table('CMTAMBITITERRITORIALI')]
  TCMTAMBITITERRITORIALI = class(TIDNullableDescription100WithLog)
  private
    [Column('CODICE', [TColumnProp.Required], 30)]
    FCODICE: string;
    [Column('ATTIVA', [TColumnProp.Required], 1)]
    FATTIVA: string;
  public
    property CODICE: string read FCODICE write FCODICE;
    property ATTIVA: string read FATTIVA write FATTIVA;
  end;

  [Entity]
  [Table('CMTAMBITITERRITORIALI_COMUNI')]
  TCMTAMBITITERRITORIALI_COMUNI = class(TIdLogModelBase)
  private
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_CMTAMBITITERRITORIALI', [TColumnProp.Required], 'ID')]
    FID_CMTAMBITITERRITORIALI: Proxy<TCMTAMBITITERRITORIALI>;
    [ManyValuedAssociation([], CascadeTypeAll - [TCascadeType.Remove])]
//    [JoinColumn('ID_CMTCOMUNI', [TColumnProp.Required], 'ID')]
    FID_CMTCOMUNI: TList<TCMTCOMUNI>;
    function GetID_CMTAMBITITERRITORIALI: TCMTAMBITITERRITORIALI;
    procedure SetID_CMTAMBITITERRITORIALI(const Value: TCMTAMBITITERRITORIALI);
  public
    property ID_CMTAMBITITERRITORIALI: TCMTAMBITITERRITORIALI read GetID_CMTAMBITITERRITORIALI write SetID_CMTAMBITITERRITORIALI;
    property ID_CMTCOMUNI: TList<TCMTCOMUNI> read FID_CMTCOMUNI write FID_CMTCOMUNI;
  end;
[Entity]
  [Table('CMTCOMUNI')]
  TCMTCOMUNI = class( TIDDescription250WithLog )
  private
    [Column('CODICE_ISTAT', [TColumnProp.Required], 8)]
    FCODICE_ISTAT: string;
   
    [Column('ID_SYTNAZIONI', [])]
    FID_SYTNAZIONI: Nullable<Integer>;
    
    [Column('ATTIVA', [], 1)]
    FATTIVA: Nullable<string>;
    [Association([TAssociationProp.Lazy], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_CMTPROVINCE', [], 'ID')]
    FID_CMTPROVINCE: Proxy<TCMTPROVINCE>;

    [Column('RADON', [], 1)]
    FRADON: Nullable<string>;

    function GetID_CMTPROVINCE: TCMTPROVINCE;
    procedure SetID_CMTPROVINCE(const Value: TCMTPROVINCE);
  public
    property CODICE_ISTAT: string read FCODICE_ISTAT write FCODICE_ISTAT;
    property ID_SYTNAZIONI: Nullable<Integer> read FID_SYTNAZIONI write FID_SYTNAZIONI;
    property ATTIVA: Nullable<string> read FATTIVA write FATTIVA;
    property ID_CMTPROVINCE: TCMTPROVINCE read GetID_CMTPROVINCE write SetID_CMTPROVINCE;
    property RADON: Nullable<string> read FRADON write FRADON;
  end;

Unfortunately, when I try this model all I get is

ThreadId=7360
ProcessId=9
ThreadName="Main Thread"
ExceptionMessage="Property "D_CMTAMBITITERRITORIALI" not found on class "TCMTAMBITITERRITORIALI_COMUNI"."
ExceptionName="EPropertyNotFound"
ExceptionDisplayName="EPropertyNotFound"
ExceptionAddress=76DB5AA2
FileName=<not available>
LineNumber=<not available>
ExceptionObject=08387278
Classes=[EPropertyNotFound,EAureliusException,Exception,TObject]

Which to my mind makes zero sense.
Please find the rest of the inheritance here:

  IDataLog = interface
  ['{BC565DE6-E578-482A-B631-CBC0111918D3}']
    function GetUserIns: string;
    function GetDataIns: Int64;
    procedure SetUserIns( const Value: string );
    procedure SetDataIns( const Value: Int64 );
    function GetUserUpd: string;
    function GetDataUpd: Int64;
    procedure SetUserUpd( const Value: string );
    procedure SetDataUpd( const Value: Int64 );
    property UserIns: string read GetUserIns write SetUserIns;
    property DataIns: Int64 read GetDataIns write SetDataIns;
    property UserUpd: string read GetUserUpd write SetUserUpd;
    property DataUpd: Int64 read GetDataUpd write SetDataUpd;
  end;

  [AbstractEntity]
  TModelBase = class( TBFivePersistentObject )

  end;
  [AbstractEntity]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TIDModelBase = class( TModelBase )
  strict private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    procedure SetID(const Value: Integer);
  public
    property ID: Integer read FID write SetID;
  end;
  
  [AbstractEntity]
  TIDLogModelBase = class( TIDModelBase, IDataLog )
  strict private
    [Column('USER_INS', [], 50)]
    FUSER_INS: Nullable<string>;

    [Column('DATA_INS', [])]
    FDATA_INS: Nullable<TDateTime>;

    [Column('USER_UPD', [], 50)]
    FUSER_UPD: Nullable<string>;

    [Column('DATA_UPD', [])]
    FDATA_UPD: Nullable<TDateTime>;

    FUnixConvert: IUnixDateTimeConvert;
    FStringConvert: INullableString;
  strict protected
    function GetUserIns: string;
    function GetDataIns: Int64;
    procedure SetUserIns( const Value: string );
    procedure SetDataIns( const Value: Int64 );
    function GetUserUpd: string;
    function GetDataUpd: Int64;
    procedure SetUserUpd( const Value: string );
    procedure SetDataUpd( const Value: Int64 );
  public
    constructor Create; override;
    property USER_INS: Nullable<string> read FUSER_INS write FUSER_INS;
    property DATA_INS: Nullable<TDateTime> read FDATA_INS write FDATA_INS;
    property USER_UPD: Nullable<string> read FUSER_UPD write FUSER_UPD;
    property DATA_UPD: Nullable<TDateTime> read FDATA_UPD write FDATA_UPD;
  end;
  
  [AbstractEntity]
  TIDDescription250WithLog = class( TIDLogModelBase )
  strict private
    [Column('DESCRIZIONE', [TColumnProp.Required], 250)]
    FDescrizione: string;
    procedure SetDescription(const Value: string);
  public
    property Descrizione: string read FDescrizione write SetDescription;
  end;

Can you please advice where I am going wrong with this?

Thank you!

I believe your many-to-many mapping is wrong. In class TCMTAMBITITERRITORIALI_COMUNI you should have two simple associations for each of the other related tables.

The many-valued association should be in the other classes (Comuni and AmbitiTerritoriali) and both should point to TCMTAMBITITERRITORIALI_COMUNI.

More info here: Many to many with Aurelius - #2 by wlandgraf

Hi!

So, here is my current model, updated with your suggestions:

  [Entity,Automapping]
  [Table('CMTAMBITITERRITORIALI')]
  TCMTAMBITITERRITORIALI = class(TIDNullableDescription100WithLog)
  private
    [Column('CODICE', [TColumnProp.Required], 30)]
    FCODICE: string;
    [Column('ATTIVA', [TColumnProp.Required], 1)]
    FATTIVA: string;
    FID_CMTComuni: Proxy<TList<TCMTCOMUNI>>;
    function GetComuni: TList<TCMTCOMUNI>;
    procedure SetComuni(const Value: TList<TCMTCOMUNI>);
  public
    property CODICE: string read FCODICE write FCODICE;
    property ATTIVA: string read FATTIVA write FATTIVA;
    property ID_CMTComuni: TList<TCMTCOMUNI> read GetComuni write SetComuni;
  end;
  
  [Entity]
  [Table('CMTAMBITITERRITORIALI_COMUNI')]
  TCMTAMBITITERRITORIALI_COMUNI = class(TIdLogModelBase)
  private
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_CMTAMBITITERRITORIALI', [TColumnProp.Required], 'ID')]
    FID_CMTAMBITITERRITORIALI: Proxy<TCMTAMBITITERRITORIALI>;
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    FID_CMTCOMUNI: Proxy<TCMTCOMUNI>;
    function GetID_CMTAMBITITERRITORIALI: TCMTAMBITITERRITORIALI;
    procedure SetID_CMTAMBITITERRITORIALI(const Value: TCMTAMBITITERRITORIALI);
    function GetID_CMTCOMUNI: TCMTCOMUNI;
    procedure SetID_CMTCOMUNI(const Value: TCMTCOMUNI);
  public
    property ID_CMTAMBITITERRITORIALI: TCMTAMBITITERRITORIALI read GetID_CMTAMBITITERRITORIALI write SetID_CMTAMBITITERRITORIALI;
    property ID_CMTCOMUNI: TCMTCOMUNI read GetID_CMTCOMUNI write SetID_CMTCOMUNI;
  end;
  
  [Entity,Automapping]
  [Table('CMTCOMUNI')]
  TCMTCOMUNI = class( TIDDescription250WithLog )
  private
    [Column('CODICE_ISTAT', [TColumnProp.Required], 8)]
    FCODICE_ISTAT: string;
   
    [Column('ID_SYTNAZIONI', [])]
    FID_SYTNAZIONI: Nullable<Integer>;
    
    [Column('ATTIVA', [], 1)]
    FATTIVA: Nullable<string>;
    [Association([TAssociationProp.Lazy], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_CMTPROVINCE', [], 'ID')]
    FID_CMTPROVINCE: Proxy<TCMTPROVINCE>;

    [Column('RADON', [], 1)]
    FRADON: Nullable<string>;

    function GetID_CMTPROVINCE: TCMTPROVINCE;
    procedure SetID_CMTPROVINCE(const Value: TCMTPROVINCE);
  public
    property CODICE_ISTAT: string read FCODICE_ISTAT write FCODICE_ISTAT;
    property ID_SYTNAZIONI: Nullable<Integer> read FID_SYTNAZIONI write FID_SYTNAZIONI;
    property ATTIVA: Nullable<string> read FATTIVA write FATTIVA;
    property ID_CMTPROVINCE: TCMTPROVINCE read GetID_CMTPROVINCE write SetID_CMTPROVINCE;
    property RADON: Nullable<string> read FRADON write FRADON;
  end;

Right now, the direction I am most interested in is from AMBITITERRITORIALI to COMUNI (CMT is a prefix).
I have a feeling that the AMBITITERRITORIALI_COMUNI is still incorrect but would love a tip on exactly how it is incorrect.

My main goal here is, when I access the Comuni list that list to be loaded at once and not record-by-record.
What else am I doing wrong?

Cheers and thank you!

TCMTAMBITITERRITORIALI_COMUNI is correct.

TCMTAMBITITERRITORIALI is incorrect, though. It should have a list of TCMTAMBITITERRITORIALI_COMUNI, not TCMTCOMUNI.

Once it's done, you can query TCMTAMBITITERRITORIALI_COMUNI directly and apply filters to its associations.

Okay, I have recreated the kind of situation I would like to get to:

[Entity,Automapping]
  [Table('DETAIL_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TDETAIL_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TestStr', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeWideMemo]
    FTestStr: TBlob;
  public
    property ID: Integer read FID write FID;
    property TestStr: TBlob read FTestStr write FTestStr;
  end;
  
  [Entity,Automapping]
  [Table('MASTER_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMASTER_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TestStr', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeWideMemo]
    FTestStr: TBlob;
    FDetails: Proxy<TList<TDETAIL_IN_DB>>;
    function GetDetails: TList<TDETAIL_IN_DB>;
    procedure SetDetails(const Value: TList<TDETAIL_IN_DB>);
  public
    property ID: Integer read FID write FID;
    property TestStr: TBlob read FTestStr write FTestStr;
    property Details: TList<TDETAIL_IN_DB> read GetDetails write SetDetails;
  end;
  
  [Entity,Automapping]
  [Table('MIDDLE_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMIDDLE_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('DETAIL_ID', [TColumnProp.Required], 'ID')]
    FDETAIL_ID: Proxy<TDETAIL_IN_DB>;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('MASTER_ID', [TColumnProp.Required], 'ID')]
    FMASTER_ID: Proxy<TMASTER_IN_DB>;
    function GetDETAIL_ID: TDETAIL_IN_DB;
    procedure SetDETAIL_ID(const Value: TDETAIL_IN_DB);
    function GetMASTER_ID: TMASTER_IN_DB;
    procedure SetMASTER_ID(const Value: TMASTER_IN_DB);
  public
    property ID: Integer read FID write FID;
    property DETAIL_ID: TDETAIL_IN_DB read GetDETAIL_ID write SetDETAIL_ID;
    property MASTER_ID: TMASTER_IN_DB read GetMASTER_ID write SetMASTER_ID;
  end;

This is the code I am using in the client:

procedure TForm1.btnTestClick(Sender: TObject);
var
  ObjMgr: TObjectManager;
  Master: TMASTER_IN_DB;
  Detail: TDETAIL_IN_DB;
  MasterList: TObjectList<TMASTER_IN_DB>;
  DetailList: TList<TDETAIL_IN_DB>;
begin
  ObjMgr := TObjectManager.Create(AureliusConnection.CreateConnection);
  MasterList := ObjMgr.Find<TMASTER_IN_DB>.List;
  if MasterList.Count = 0 then
  begin
    Master := TMASTER_IN_DB.Create;
    Master.TestStr.AsUnicodeString := 'This is a test in master';
    ObjMgr.Save(Master);
    DetailList := TList<TDETAIL_IN_DB>.Create;
    Detail := TDETAIL_IN_DB.Create;
    Detail.TestStr.AsUnicodeString := 'This is a test in detail';
    DetailList.Add(Detail);
    Detail := TDETAIL_IN_DB.Create;
    Detail.TestStr.AsUnicodeString := 'This is a second test in detail';
    DetailList.Add(Detail);
    Master.Details := DetailList;
  end;
  ObjMgr.SaveOrUpdate(Master);
  ObjMgr.Free;
end;

I also have an UpdateDatabase call in FormCreate:

procedure TForm1.FormCreate(Sender: TObject);
begin
  with TDatabaseManager.Create(AureliusConnection.CreateConnection) do
  begin
    UpdateDatabase;
    Free;
  end;
end;

The middle table hasn't been touched and instead a new field has been added to the detail table. When I create the objects, I would expect the middle table to be filled but it's not. Which tellls me I am missing something and I am not sure what that is.
Can you please help with this trimmed-down version?

Thanks!

Okay and will this wind up loading all Comuni associated to an AmbitoTerritoriale in one go and not one-by-one?
This is my final goal, the one thing I need to achieve and I am happy to modify how the model is mapped in order to achieve this singular goal. I can't easily change the table structure, only the way it's mapped. The reason for this is that there are other points that would benefit from this and thus I will be replicating this across the other tables as well.

Thanks.

To make this even clearer: if I am using Find without parameters, I want a list of AmbitiTerritoriali and so the associated Comuni are of no interest to me.
When I am instead loading by ID, I need to be able to load all associated Comuni (which will be accessed). I need a proxy for that and mark something as lazy. That's fine. My issue is what to mark and how so that I get the behaviour I want.

Cheers!

Okay, so, here is how that part of the model looks atm:

[Entity,Automapping]
  [Table('CMTAMBITITERRITORIALI')]
  TCMTAMBITITERRITORIALI = class(TIDNullableDescription100WithLog)
  private
    [Column('CODICE', [TColumnProp.Required], 30)]
    FCODICE: string;
    [Column('ATTIVA', [TColumnProp.Required], 1)]
    FATTIVA: string;
    [Association([TAssociationProp.Lazy],CascadeTypeAllButRemove)]
    [JoinColumn('ID',[TColumnProp.Lazy],'ID_CMTAMBITITERRITORIALI')]
    FAmbTerrComuni: Proxy<TCMTAMBITITERRITORIALI_COMUNI>;
    function GetAmbTerrComuni: TCMTAMBITITERRITORIALI_COMUNI;
    procedure SetAmbTerrComuni(const Value: TCMTAMBITITERRITORIALI_COMUNI);
  public
    property CODICE: string read FCODICE write FCODICE;
    property ATTIVA: string read FATTIVA write FATTIVA;
    property AmbTerrComuni: TCMTAMBITITERRITORIALI_COMUNI read GetAmbTerrComuni write SetAmbTerrComuni;
  end;

  [Entity,Automapping]
  [Table('CMTAMBITITERRITORIALI_COMUNI')]
  TCMTAMBITITERRITORIALI_COMUNI = class(TIdLogModelBase)
  private
    [ManyValuedAssociation([TAssociationProp.Lazy], CascadeTypeAll - [TCascadeType.Remove])]
//    [JoinColumn('ID_CMTAMBITITERRITORIALI', [TColumnProp.Required], 'ID')]
    FID_CMTAMBITITERRITORIALI: Proxy<TList<TCMTAMBITITERRITORIALI>>;
    [ManyValuedAssociation([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
//    [JoinColumn('ID_CMTCOMUNI', [TColumnProp.Required], 'ID')]
    FID_CMTCOMUNI: Proxy<TList<TCMTCOMUNI>>;
    function GetID_CMTAMBITITERRITORIALI: TList<TCMTAMBITITERRITORIALI>;
    procedure SetID_CMTAMBITITERRITORIALI(const Value: TList<TCMTAMBITITERRITORIALI>);
    function GetID_CMTCOMUNI: TList<TCMTCOMUNI>;
    procedure SetID_CMTCOMUNI(const Value: TList<TCMTCOMUNI>);
  public
    property ID_CMTAMBITITERRITORIALI: TList<TCMTAMBITITERRITORIALI> read GetID_CMTAMBITITERRITORIALI write SetID_CMTAMBITITERRITORIALI;
    property ID_CMTCOMUNI: TList<TCMTCOMUNI> read GetID_CMTCOMUNI write SetID_CMTCOMUNI;
  end;

When I try to load the model, I get this exception:

ThreadId=7680
ProcessId=15
ThreadName="Main Thread"
ExceptionMessage="Invalid referenced column name "ID_CMTAMBITITERRITORIALI" defined in join column on class TCMTAMBITITERRITORIALI_COMUNI."
ExceptionName="EInvalidReferencedColumnName"
ExceptionDisplayName="EInvalidReferencedColumnName"
ExceptionAddress=76DB5AA2
FileName=<not available>
LineNumber=<not available>
ExceptionObject=0E8FCA30
Classes=[EInvalidReferencedColumnName,EAureliusException,Exception,TObject]

To me this sounds it should be right, not wrong.
What I am doing here is grab hold of the record in AmbitiTerritoriali_Comuni referring to this AmbitoTerritoriale and subsequently grab hold of the linked comuni.
Either the documentation, the exception or both are not nearly complete enough and the samples don't sample this scenario so those might need work as well.
What exactly is wrong here?

Thanks!

Yo, I haven't received an answer - can you please guide me?

Thanks!

If you can proceed from this, with classes with more descriptive and English names, that will be very helpful, not only for me, and maybe you, but for other users. It's confusing to use business model names, especially in other language. Thank you.

So, your code has a flaw I mentioned before: your "master" and "detail" should not have a list of each other, instead they need to have a list for the "middle" class.

So this is wrong:

  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMASTER_IN_DB = class
  private
...
    FDetails: Proxy<TList<TDETAIL_IN_DB>>;

This is the correct thing to do:

  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMASTER_IN_DB = class
  private
...
    FMiddleItems: Proxy<TList<TMIDDLE_IN_DB>>;

Hi!

Using Delphi 11.3 .

Okay, so I dropped my test db and recreated it and these are the SQL scripts for the tables:

CREATE TABLE [dbo].[MASTER_IN_DB](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[TEST_STR] [varchar](max) NOT NULL,
 CONSTRAINT [PK_MASTER_IN_DB] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[DETAIL_IN_DB](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[TEST_STR] [varchar](max) NOT NULL,
 CONSTRAINT [PK_DETAIL_IN_DB] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[MIDDLE_IN_DB](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[ID_MASTER] [int] NOT NULL,
	[ID_DETAIL] [int] NOT NULL,
 CONSTRAINT [PK_MIDDLE_IN_DB] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[MIDDLE_IN_DB]  WITH CHECK ADD  CONSTRAINT [FK_MIDDLE_IN_DB_DETAIL_IN_DB] FOREIGN KEY([ID_DETAIL])
REFERENCES [dbo].[DETAIL_IN_DB] ([ID])
GO

ALTER TABLE [dbo].[MIDDLE_IN_DB] CHECK CONSTRAINT [FK_MIDDLE_IN_DB_DETAIL_IN_DB]
GO

ALTER TABLE [dbo].[MIDDLE_IN_DB]  WITH CHECK ADD  CONSTRAINT [FK_MIDDLE_IN_DB_MASTER_IN_DB] FOREIGN KEY([ID_MASTER])
REFERENCES [dbo].[MASTER_IN_DB] ([ID])
GO

ALTER TABLE [dbo].[MIDDLE_IN_DB] CHECK CONSTRAINT [FK_MIDDLE_IN_DB_MASTER_IN_DB]
GO

I then imported this with the Delphi wizard and got this:

[Entity]
  [Table('DETAIL_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TDETAIL_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TEST_STR', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeMemo]
    FTEST_STR: TBlob;
  public
    property ID: Integer read FID write FID;
    property TEST_STR: TBlob read FTEST_STR write FTEST_STR;
  end;
  
  [Entity]
  [Table('MASTER_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMASTER_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TEST_STR', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeMemo]
    FTEST_STR: TBlob;
  public
    property ID: Integer read FID write FID;
    property TEST_STR: TBlob read FTEST_STR write FTEST_STR;
  end;
  
  [Entity]
  [Table('MIDDLE_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMIDDLE_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_DETAIL', [TColumnProp.Required], 'ID')]
    FID_DETAIL: Proxy<TDETAIL_IN_DB>;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_MASTER', [TColumnProp.Required], 'ID')]
    FID_MASTER: Proxy<TMASTER_IN_DB>;
    function GetID_DETAIL: TDETAIL_IN_DB;
    procedure SetID_DETAIL(const Value: TDETAIL_IN_DB);
    function GetID_MASTER: TMASTER_IN_DB;
    procedure SetID_MASTER(const Value: TMASTER_IN_DB);
  public
    property ID: Integer read FID write FID;
    property ID_DETAIL: TDETAIL_IN_DB read GetID_DETAIL write SetID_DETAIL;
    property ID_MASTER: TMASTER_IN_DB read GetID_MASTER write SetID_MASTER;
  end;

You will probably notice that there's a single association and not a ManyValues. Fine, let's make the required changes:

[Entity]
  [Table('DETAIL_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TDETAIL_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TEST_STR', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeMemo]
    FTEST_STR: TBlob;
  public
    property ID: Integer read FID write FID;
    property TEST_STR: TBlob read FTEST_STR write FTEST_STR;
  end;
  
  [Entity]
  [Table('MASTER_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMASTER_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TEST_STR', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeMemo]
    FTEST_STR: TBlob;
  public
    property ID: Integer read FID write FID;
    property TEST_STR: TBlob read FTEST_STR write FTEST_STR;
  end;
  
  [Entity]
  [Table('MIDDLE_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMIDDLE_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [ManyValuedAssociation([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
//    [JoinColumn('ID_DETAIL', [TColumnProp.Required], 'ID')]
    FID_DETAIL: Proxy<TList<TDETAIL_IN_DB>>;
    
    [ManyValuedAssociation([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
//    [JoinColumn('ID_MASTER', [TColumnProp.Required], 'ID')]
    FID_MASTER: Proxy<TList<TMASTER_IN_DB>>;
    function GetID_DETAIL: TList<TDETAIL_IN_DB>;
    procedure SetID_DETAIL(const Value: TList<TDETAIL_IN_DB>);
    function GetID_MASTER: TList<TMASTER_IN_DB>;
    procedure SetID_MASTER(const Value: TList<TMASTER_IN_DB>);
  public
    property ID: Integer read FID write FID;
    property ID_DETAIL: TList<TDETAIL_IN_DB> read GetID_DETAIL write SetID_DETAIL;
    property ID_MASTER: TList<TMASTER_IN_DB> read GetID_MASTER write SetID_MASTER;
  end;

Now this should be correct, yes? If not, where am I going wrong?
Let's assume it is indeed correct (as it probably is), I try adding some data to it with the following code:

procedure TForm1.AddObjects(MasterStrings, DetailStrings: TArray<string>);
var
  MasterObj: TMASTER_IN_DB;
  MiddleObj: TMIDDLE_IN_DB;
  DetailObj: TDETAIL_IN_DB;
  MasterStr, DetailStr: string;
  MiddleListMaster: TList<TMASTER_IN_DB>;
  MiddleListDetail: TList<TDETAIL_IN_DB>;
begin
  MiddleListMaster := nil;
  MiddleListDetail := nil;
  MiddleObj := nil;
  for MasterStr in MasterStrings do
  begin
    MasterObj := TMASTER_IN_DB.Create;
    MasterObj.TEST_STR.AsUnicodeString := MasterStr;
    FObjMgr.Save(MasterObj);
    for DetailStr in DetailStrings do
    begin
      DetailObj := TDETAIL_IN_DB.Create;
      DetailObj.TEST_STR.AsUnicodeString := DetailStr;
      FObjMgr.Save(DetailObj);
      if not Assigned(MiddleListMaster) then
      begin
        MiddleListMaster := TList<TMASTER_IN_DB>.Create;
        MiddleListMaster.Capacity := Length(MasterStrings);
        if not Assigned(MiddleObj) then
          MiddleObj := TMIDDLE_IN_DB.Create;
        MiddleObj.ID_MASTER := MiddleListMaster;
      end;
      MiddleListMaster.Add(MasterObj);
      if not Assigned(MiddleListDetail) then
      begin
        MiddleListDetail := TList<TDETAIL_IN_DB>.Create;
        MiddleListDetail.Capacity := Length( DetailStrings );
      end;
      MiddleObj.ID_DETAIL := MiddleListDetail;
      MiddleListDetail.Add(DetailObj);
      FObjMgr.SaveOrUpdate(MiddleObj);
    end;
    FreeAndNil(MiddleListMaster);
    FreeAndNil(MiddleListDetail);
  end;
end;

I then put it to work in a button's OnClick:

procedure TForm1.btnAddClick(Sender: TObject);
begin
  AddObjects(['Test master 1'],['Test detail master 1','Test detail master 2']);
end;

When I execute the code I get this exception:

ThreadId=11848
ProcessId=5
ThreadName=""
ExceptionMessage="Assertion failure (C:\Users\Administrator\Documents\tmssoftware\business11\aurelius\source\core\Aurelius.Sql.AnsiSqlGenerator.pas, line 403)"
ExceptionName="EAssertionFailed"
ExceptionDisplayName="EAssertionFailed"
ExceptionAddress=76DB5AA2
FileName=<not available>
LineNumber=<not available>
ExceptionObject=0534CA80
Classes=[EAssertionFailed,Exception,TObject]

Which obviously means that something is not quite right.
What is happening here?

Thank you!

Further testing of that code reveals that if I save the middle object outright then the same assert happens even with the lists being unassigned.

I don't know what the issue is but there has to be one.

Cheers!

No, it's not correct. The lists have to be added to master and detail, this way:

[Entity]
  [Table('DETAIL_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TDETAIL_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TEST_STR', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeMemo]
    FTEST_STR: TBlob;

    [ManyValuedAssociation([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAllRemoveOrphan, 'FID_DETAIL')]
    FMIDDLE_ITEMS: Proxy<TList<TMIDDLE_IN_DB>>;
  public
    property ID: Integer read FID write FID;
    property TEST_STR: TBlob read FTEST_STR write FTEST_STR;
  end;
  
  [Entity]
  [Table('MASTER_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMASTER_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Column('TEST_STR', [TColumnProp.Required, TColumnProp.Lazy])]
    [DBTypeMemo]
    FTEST_STR: TBlob;

    [ManyValuedAssociation([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAllRemoveOrphan, 'FID_MASTER')]
    FMIDDLE_ITEMS: Proxy<TList<TMIDDLE_IN_DB>>;
  public
    property ID: Integer read FID write FID;
    property TEST_STR: TBlob read FTEST_STR write FTEST_STR;
  end;
  
  [Entity]
  [Table('MIDDLE_IN_DB')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TMIDDLE_IN_DB = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_DETAIL', [TColumnProp.Required], 'ID')]
    FID_DETAIL: Proxy<TDETAIL_IN_DB>;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('ID_MASTER', [TColumnProp.Required], 'ID')]
    FID_MASTER: Proxy<TMASTER_IN_DB>;
    function GetID_DETAIL: TDETAIL_IN_DB;
    procedure SetID_DETAIL(const Value: TDETAIL_IN_DB);
    function GetID_MASTER: TMASTER_IN_DB;
    procedure SetID_MASTER(const Value: TMASTER_IN_DB);
  public
    property ID: Integer read FID write FID;
    property ID_DETAIL: TDETAIL_IN_DB read GetID_DETAIL write SetID_DETAIL;
    property ID_MASTER: TMASTER_IN_DB read GetID_MASTER write SetID_MASTER;
  end;

(Please note that I didn't complete the classes, as in both MASTER_IN_DB and DETAIL_IN_DB you need to add Create and Destroy methods and create/destroy the lists using SetInitialValue and DestroyValue as explained here.