Related collection will not save on recursive save

Posts   
 
    
melman
User
Posts: 13
Joined: 26-Apr-2024
# Posted on: 05-Jun-2024 03:50:54   

I am running LLBLGen Runtime Framework 5.10.1. The database is Azure SQL. This is in a .Net 6.0 WebAPI. We have inherited a 20+ year old application to enhance and ultimately re-platform. The client app is .Net Framework based Windows form app that was making direct calls to the database via SQLClient and Stored Procedures. Any new functionality that we have been adding, we have been creating API endpoints in the new WebAPI app and calling with Refit. Customer's existing workflow moved data (jobs) through a series of tables with duplicate columns so we are tied to that. We have a method that makes a copy of an entity based on one table (Job) and moves it to another table (JobHistory).

We accomplish this by creating an extension method for the JobEntity called ConvertToJobHistory(). The code is below.

public static JobHistoryEntity ConvertToJobHistory(this JobEntity job)
{
    var jobHistory = new JobHistoryEntity();
    jobHistory.JobId = job.JobId;
    jobHistory.UpdateFrom(job);
    foreach (var jobTechnician in job.Technicians)
    {
        var jobHistoryTechnician = new JobHistoryTechnicianEntity();
        jobHistoryTechnician.UpdateFrom(jobTechnician);
        jobHistory.Technicians.Add(jobHistoryTechnician);
    }
    foreach (var jobDetail in job.JobDetails)
    {
        var jobHistoryDetail = new JobHistoryDetailEntity();
        jobHistoryDetail.UpdateFrom(jobDetail);
        jobHistory.JobDetails.Add(jobHistoryDetail);
    }
    foreach (var equipmentDetail in job.EquipmentDetails)
    {
        var equipmentHistoryDetail = new EquipmentHistoryDetailEntity();
        equipmentHistoryDetail.UpdateFrom(equipmentDetail);
        jobHistory.EquipmentDetails.Add(equipmentHistoryDetail);
    }
    foreach (var billingDocument in job.BillingDocuments)
    {
        var jobHistoryBillingDocument = new JobHistoryBillingDocumentEntity();
        jobHistoryBillingDocument.UpdateFrom(billingDocument);
        jobHistory.BillingDocuments.Add(jobHistoryBillingDocument);
    }
    foreach (var jobNote in job.JobNotes)
    {
        var jobHistoryNote = new JobHistoryNoteEntity();
        jobHistoryNote.UpdateFrom(jobNote);
        jobHistory.JobNotes.Add(jobHistoryNote);
    }
    foreach (var jobSignature in job.Signatures)
    {
        var jobHistorySignature = new JobHistorySignatureEntity();
        jobHistorySignature.UpdateFrom(jobSignature);
        jobHistory.Signatures.Add(jobHistorySignature);
    }

    return jobHistory;
}

The UpdateFrom() method that is called is below

public static void UpdateFrom(this JobHistoryEntity jobHistory, JobEntity job)
{
    jobHistory.JobId = job.JobId;
    jobHistory.StatKey = 0;
    jobHistory.SelectJob = job.SelectJob.HasValue ? job.SelectJob.Value : false;
    jobHistory.NewCust = job.NewCust;
    jobHistory.TimeSpecific = job.TimeSpecific;
    jobHistory.WetVac = job.WetVac;
    jobHistory.SameDay = job.SameDay;
    jobHistory.HasNotes = job.HasNotes;
    jobHistory.TechCount = job.Technicians.Count;
    jobHistory.WorkAreaId = job.WorkAreaId;
    jobHistory.CompId = job.CompId;
    jobHistory.CustId = job.CustId;
    jobHistory.Ov = job.Ov;
    jobHistory.Ponum = job.Ponum;
    jobHistory.InvNum = job.InvNum;
    jobHistory.InvDate = job.InvDate;
    jobHistory.Unit = job.Unit;
    jobHistory.JobDescr = job.JobDescr;
    jobHistory.RedoPay = job.RedoPay;
    jobHistory.RedoNopay = job.RedoNopay;
    jobHistory.Credit = job.Credit;
    jobHistory.InvAmnt = job.InvAmnt;
    jobHistory.FirstAm = job.FirstAm;
    jobHistory.DateTimeRec = job.DateTimeRec;
    jobHistory.CalledBy = job.CalledBy;
    jobHistory.DateTimeSched = job.DateTimeSched;
    jobHistory.SchedBy = job.SchedBy;
    jobHistory.DateTimeDisp = job.DateTimeDisp;
    jobHistory.DispBy = job.DispBy;
    jobHistory.DateTimeComp = job.DateTimeComp;
    jobHistory.CompBy = job.CompBy;
    jobHistory.JobNote = job.JobNote;
    jobHistory.Digital = job.Digital;
    jobHistory.InvoiceTypeId = job.InvoiceTypeId;
    jobHistory.ApprovedForBilling = job.ApprovedForBilling;
    jobHistory.ApprovedForBillingDateTime = job.ApprovedForBillingDateTime;
    jobHistory.ApprovedForBillingByUserId = job.ApprovedForBillingByUserId;
    jobHistory.ApprovedForPayroll = job.ApprovedForPayroll;
    jobHistory.ApprovedForPayrollDateTime = job.ApprovedForPayrollDateTime;
    jobHistory.ApprovedForPayrollByUserId = job.ApprovedForPayrollByUserId;
    jobHistory.Billed = job.Billed;
    jobHistory.BillingMethodId = job.BillingMethodId;
    jobHistory.BilledByUserId = job.BilledByUserId;
    jobHistory.BilledDateTime = job.BilledDateTime;
    jobHistory.CalledByCallbackNumber = job.CalledByCallbackNumber;
    jobHistory.OverrideAddress = job.OverrideAddress;
    jobHistory.OverrideAddress1 = job.OverrideAddress1;
    jobHistory.OverrideAddress2 = job.OverrideAddress2;
    jobHistory.OverrideCity = job.OverrideCity;
    jobHistory.OverrideStateCode = job.OverrideStateCode;
    jobHistory.OverrideZip = job.OverrideZip;
    jobHistory.InvoiceStatusId = job.InvoiceStatusId;
    jobHistory.TaggedTechnicianId = job.TaggedTechnicianId;
    jobHistory.InvoiceStatusId = job.InvoiceStatusId;
    jobHistory.TechnicianDateTimeCompleted = job.TechnicianDateTimeCompleted;
    jobHistory.TechnicianStatusId = job.TechnicianStatusId;
    jobHistory.Wonum = job.Wonum;
    jobHistory.PayrollDate = job.PayrollDate;
}

The code that deletes the record (and related records) from the Job table and adds the record (and related records to JobHistory in a single transaction is below

                job.ApprovedForPayroll = true;
                job.ApprovedForPayrollDateTime = DateTime.Now;
                job.ApprovedForPayrollByUserId = userId;
                job.PayrollDate = ComputePayrollDate(job.ApprovedForPayrollDateTime.Value);
                await job.SaveAsync(true);
                if(job.InvoiceTypeId != InvoiceTypes.Invoice)
                {
                    response.Job = null;
                    var jobHistory = job.ConvertToJobHistory();
                    jobHistory.DispatchStatusId = DispatchStatuses.Invoiced;
                    using (var transactionManager = new DAL.HelperClasses.Transaction(System.Data.IsolationLevel.ReadCommitted, Guid.NewGuid().ToString()))
                    {
                        try
                        {
                            transactionManager.Add(jobHistory);
                            transactionManager.Add(job);
                            await job.DeleteAsync();
                            await jobHistory.SaveAsync(true);
                            transactionManager.Commit();                 
                        }
                        catch (Exception ex)
                        {
                            transactionManager.Rollback();
                            response.Success = false;
                            response.Messages.Add(ex.Message);
                        }
                    }
                }
                else
                {
                    response.Job = job.MapToJobSummary();
                }
            }

The problem we are facing is that even though jobHistory.Technicians ends up with 1 dirty/new entity in it, no insert statement is executed when we call await jobHistory.SaveAsync(true). The only statements that get created for execution are as follows:

Method Enter: CreateDeleteDQ(6)
Method Enter: CreateSingleTargetDeleteDQ(3)
Method Enter: CreateSingleTargetDeleteDQ(4)
Generated Sql query: 
    Query: DELETE FROM [Creative].[dbo].[Job] WHERE ( ( [Creative].[dbo].[Job].[JobID] = @p1))
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4916618.

Method Exit: CreateSingleTargetDeleteDQ(4)
Method Exit: CreateSingleTargetDeleteDQ(3)
Method Exit: CreateDeleteDQ(6)
Method Enter: CreateInsertDQ
Method Enter: CreateSingleTargetInsertDQ
Generated Sql query: 
    Query: INSERT INTO [Creative].[dbo].[JobHistory] ([ApprovedForBilling], [ApprovedForBillingByUserID], [ApprovedForBillingDateTime], [ApprovedForPayroll], [ApprovedForPayrollByUserID], [ApprovedForPayrollDateTime], [Billed], [BilledByUserID], [BilledDateTime], [BillingMethodID], [CalledBy], [CalledByCallbackNumber], [CompBy], [CompID], [Credit], [CustID], [DateTimeComp], [DateTimeDisp], [DateTimeRec], [DateTimeSched], [Digital], [DispBy], [FirstAM], [HasNotes], [InvAmnt], [InvDate], [InvNum], [InvoiceStatusID], [InvoiceTypeID], [JobDescr], [JobID], [JobNotes], [NewCust], [OV], [OverrideAddress], [OverrideAddress1], [OverrideAddress2], [OverrideCity], [OverrideStateCode], [OverrideZip], [PayrollDate], [PONum], [RedoNopay], [RedoPay], [SameDay], [SchedBy], [SelectJob], [StatKey], [TaggedTechnicianID], [TechCount], [TechnicianDateTimeComp], [TechnicianStatusID], [TimeSpecific], [Unit], [WetVac], [WONum], [WorkAreaID]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23, @p24, @p25, @p26, @p27, @p28, @p29, @p30, @p31, @p32, @p33, @p34, @p35, @p36, @p37, @p38, @p39, @p40, @p41, @p42, @p43, @p44, @p45, @p46, @p47, @p48, @p49, @p50, @p51, @p52, @p53, @p54, @p55, @p56, @p57)
    Parameter: @p1 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p2 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p3 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p4 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 449.
    Parameter: @p6 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T21:41:25.4430000.
    Parameter: @p7 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p8 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p9 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p10 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p11 : AnsiString. Length: 30. Precision: 0. Scale: 0. Direction: Input. Value: "STEVEN HILL".
    Parameter: @p12 : String. Length: 10. Precision: 0. Scale: 0. Direction: Input. Value: "6784726510".
    Parameter: @p13 : AnsiString. Length: 30. Precision: 0. Scale: 0. Direction: Input. Value: "shill".
    Parameter: @p14 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 3.
    Parameter: @p15 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p16 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4344.
    Parameter: @p17 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T00:00:00.0000000.
    Parameter: @p18 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:30:00.6100000.
    Parameter: @p19 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:29:10.7900000.
    Parameter: @p20 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T10:00:00.0000000.
    Parameter: @p21 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p22 : AnsiString. Length: 30. Precision: 0. Scale: 0. Direction: Input. Value: "shill".
    Parameter: @p23 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p24 : Byte. Length: 1. Precision: 3. Scale: 0. Direction: Input. Value: 2.
    Parameter: @p25 : Currency. Length: 0. Precision: 19. Scale: 4. Direction: Input. Value: 275.0000.
    Parameter: @p26 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:32:25.0500000.
    Parameter: @p27 : AnsiString. Length: 21. Precision: 0. Scale: 0. Direction: Input. Value: "CR987123".
    Parameter: @p28 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
    Parameter: @p29 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 2.
    Parameter: @p30 : AnsiString. Length: 255. Precision: 0. Scale: 0. Direction: Input. Value: "STEAM CLEAN".
    Parameter: @p31 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4916618.
    Parameter: @p32 : AnsiString. Length: 800. Precision: 0. Scale: 0. Direction: Input. Value: "EDITED BY: SHILL; EDITED BY: SHILL; EDITED BY: SHILL; EDITED BY: SHILL; Credit Ticket".
    Parameter: @p33 : Byte. Length: 1. Precision: 3. Scale: 0. Direction: Input. Value: 0.
    Parameter: @p34 : AnsiStringFixedLength. Length: 1. Precision: 0. Scale: 0. Direction: Input. Value: "O".
    Parameter: @p35 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p36 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "BLAH".
    Parameter: @p37 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p38 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "BLAH".
    Parameter: @p39 : String. Length: 2. Precision: 0. Scale: 0. Direction: Input. Value: "BLAH".
    Parameter: @p40 : String. Length: 10. Precision: 0. Scale: 0. Direction: Input. Value: "BLAH".
    Parameter: @p41 : Date. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 6/14/2024 12:00:00 AM.
    Parameter: @p42 : AnsiString. Length: 21. Precision: 0. Scale: 0. Direction: Input. Value: "JOBTECHHISTORY".
    Parameter: @p43 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p44 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p45 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p46 : AnsiString. Length: 30. Precision: 0. Scale: 0. Direction: Input. Value: "Frans".
    Parameter: @p47 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p48 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 0.
    Parameter: @p49 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p50 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
    Parameter: @p51 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T00:00:00.0000000.
    Parameter: @p52 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 8.
    Parameter: @p53 : Byte. Length: 1. Precision: 3. Scale: 0. Direction: Input. Value: 9.
    Parameter: @p54 : AnsiString. Length: 21. Precision: 0. Scale: 0. Direction: Input. Value: "185".
    Parameter: @p55 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: False.
    Parameter: @p56 : String. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p57 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.

Method Exit: CreateSingleTargetInsertDQ
Method Exit: CreateInsertDQ
Method Enter: CreateInsertDQ
Method Enter: CreateSingleTargetInsertDQ
Generated Sql query: 
    Query: INSERT INTO [Creative].[dbo].[JobHistoryDetail] ([EnteredBy], [EnteredOn], [JobDetailDescriptionID], [JobDetailLocationID], [JobID], [OnInvoice], [OtherDescription], [OtherLocation], [PartNumber], [PartNumberID], [Price], [Quantity], [UID]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13)
    Parameter: @p1 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "449".
    Parameter: @p2 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:30:20.1400000.
    Parameter: @p3 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 69.
    Parameter: @p4 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4916618.
    Parameter: @p6 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p7 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p8 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p9 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "Repair".
    Parameter: @p10 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p11 : Currency. Length: 0. Precision: 19. Scale: 4. Direction: Input. Value: 50.0000.
    Parameter: @p12 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
    Parameter: @p13 : Guid. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 7471d4c8-0bb6-4d75-a0fc-30aaad78b9a5.

Method Exit: CreateSingleTargetInsertDQ
Method Exit: CreateInsertDQ
Method Enter: CreateInsertDQ
Method Enter: CreateSingleTargetInsertDQ
Generated Sql query: 
    Query: INSERT INTO [Creative].[dbo].[JobHistoryDetail] ([EnteredBy], [EnteredOn], [JobDetailDescriptionID], [JobDetailLocationID], [JobID], [OnInvoice], [OtherDescription], [OtherLocation], [PartNumber], [PartNumberID], [Price], [Quantity], [UID]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13)
    Parameter: @p1 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "449".
    Parameter: @p2 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:30:39.7800000.
    Parameter: @p3 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 69.
    Parameter: @p4 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 23.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4916618.
    Parameter: @p6 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p7 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p8 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p9 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "Repair".
    Parameter: @p10 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p11 : Currency. Length: 0. Precision: 19. Scale: 4. Direction: Input. Value: 50.0000.
    Parameter: @p12 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
    Parameter: @p13 : Guid. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2772fb2b-80f2-418c-afe2-50615d9ca0d7.

Method Exit: CreateSingleTargetInsertDQ
Method Exit: CreateInsertDQ
Method Enter: CreateInsertDQ
Method Enter: CreateSingleTargetInsertDQ
Generated Sql query: 
    Query: INSERT INTO [Creative].[dbo].[JobHistoryDetail] ([EnteredBy], [EnteredOn], [JobDetailDescriptionID], [JobDetailLocationID], [JobID], [OnInvoice], [OtherDescription], [OtherLocation], [PartNumber], [PartNumberID], [Price], [Quantity], [UID]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13)
    Parameter: @p1 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "449".
    Parameter: @p2 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:30:54.6670000.
    Parameter: @p3 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 49.
    Parameter: @p4 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 22.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4916618.
    Parameter: @p6 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p7 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p8 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p9 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p10 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p11 : Currency. Length: 0. Precision: 19. Scale: 4. Direction: Input. Value: 100.0000.
    Parameter: @p12 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
    Parameter: @p13 : Guid. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: c13c7f49-1b92-49ed-9125-60799b2c2e24.

Method Exit: CreateSingleTargetInsertDQ
Method Exit: CreateInsertDQ
Method Enter: CreateInsertDQ
Method Enter: CreateSingleTargetInsertDQ
Generated Sql query: 
    Query: INSERT INTO [Creative].[dbo].[JobHistoryDetail] ([EnteredBy], [EnteredOn], [JobDetailDescriptionID], [JobDetailLocationID], [JobID], [OnInvoice], [OtherDescription], [OtherLocation], [PartNumber], [PartNumberID], [Price], [Quantity], [UID]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13)
    Parameter: @p1 : String. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "449".
    Parameter: @p2 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: 2024-06-04T20:31:14.3730000.
    Parameter: @p3 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 49.
    Parameter: @p4 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 5.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 4916618.
    Parameter: @p6 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p7 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p8 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p9 : String. Length: 250. Precision: 0. Scale: 0. Direction: Input. Value: "".
    Parameter: @p10 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p11 : Currency. Length: 0. Precision: 19. Scale: 4. Direction: Input. Value: 75.0000.
    Parameter: @p12 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.
    Parameter: @p13 : Guid. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: b130e133-4102-44dc-9f97-7143196ec842.

Method Exit: CreateSingleTargetInsertDQ
Method Exit: CreateInsertDQ

If we are following the exact same pattern for the related collection classes, why would one related entity, JobHistoryDetail, save and another not?!?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39760
Joined: 17-Aug-2003
# Posted on: 05-Jun-2024 09:49:37   

Although there's a relationship defined between JobHistory and JobHistoryTechnician, is the FK side -> PK side visible? (I.e. is there a JobHistory property in JobHistoryTechnician)? if not, the PK side is invisible to the FK side and the FK side won't be saved.

To dig into this further, please enable ORMPersistenceExecution tracing so you can see why the save function exits. After calling SaveAsync(true), you should see what path the code follows.

I assume the for loop traverses technician entities? It might be a good idea to prefetch these collections with a prefetch path as you're now lazy loading the collections. (a set of temporal tables might be even better but you're likely stuck with this setup).

The UpdateFrom for JobHistoryTechnician is ok?

Frans Bouma | Lead developer LLBLGen Pro
melman
User
Posts: 13
Joined: 26-Apr-2024
# Posted on: 05-Jun-2024 14:21:53   

Hi Frans, thanks for the reply. Here is the JobHistory property on the JobTechnicianEntity

/// <summary>Gets / sets related entity of type 'JobHistoryEntity'. This property is not visible in databound grids.
/// Setting this property to a new object will make the load-on-demand feature to stop fetching data from the database, until you set this
/// property to null. Setting this property to an entity will make sure that FK-PK relations are synchronized when appropriate.<br/><br/></summary>
[Browsable(false)]
public virtual JobHistoryEntity JobHistory
{
    get { return GetSingleJobHistory(false); }
    set { SetSingleRelatedEntityNavigator(value, "JobHistory"); }
}

At the time ConvertToJobHistory() is called, the entity doesn't exist in that table. Originally, in this loop, I was doing something like var jobHistoryTechnician = jobHistory.Technicians.AddNew(), but I noticed it was firing a SELECT query to the database each time AddNew() was called. Can I alleviate that with a Prefetch path? I always use prefetch paths on my QuerySpec queries, but when I do that, I am always fetching data that already exists in the database, not creating new entities.

This loop...

foreach (var jobTechnician in job.Technicians)
{
    var jobHistoryTechnician = new JobHistoryTechnicianEntity();
    jobHistoryTechnician.UpdateFrom(jobTechnician);
    jobHistory.Technicians.Add(jobHistoryTechnician);
}

calls the UpdateFrom() extension method...

public static void UpdateFrom(this JobHistoryTechnicianEntity historyTechnicianEntity, JobTechnicianEntity jobTechnicianEntity)
{
    historyTechnicianEntity.JobId = jobTechnicianEntity.JobId;
    historyTechnicianEntity.EmpId = jobTechnicianEntity.EmpId;
    historyTechnicianEntity.Split = jobTechnicianEntity.BillOut;
    historyTechnicianEntity.JobTechId = jobTechnicianEntity.JobTechId;
    historyTechnicianEntity.BillOut = jobTechnicianEntity.BillOut;    
}

The Uid field on JobTechHistory is a Guid (uniqueidentifier in the db) and is set as the RowGuid property on the table, so it generates a new value if one is not specified. This has always worked for me, but I could try setting it explicitly to see if that gets the save working.

a set of temporal tables might be even better but you're likely stuck with this setup

I am not sure what temporal tables are, but I will check the docs

melman
User
Posts: 13
Joined: 26-Apr-2024
# Posted on: 05-Jun-2024 14:26:53   

Also, WRT...

To dig into this further, please enable ORMPersistenceExecution tracing so you can see why the save function exits. After calling SaveAsync(true), you should see what path the code follows.

Would I enable that via code? Somewhere in here?

RuntimeConfiguration.AddConnectionString("ConnectionString.SQL Server (SqlClient)", connString);
// Configure the DQE
var catalogOverwrite = builder.Configuration.GetSection("CatalogOverwrites")["Creative"];

if(string.IsNullOrWhiteSpace(catalogOverwrite))
{
    RuntimeConfiguration.ConfigureDQE<SQLServerDQEConfiguration>(
                                c => c.SetTraceLevel(TraceLevel.Verbose)
                                        .AddDbProviderFactory(typeof(System.Data.SqlClient.SqlClientFactory))
                                        .SetDefaultCompatibilityLevel(SqlServerCompatibilityLevel.SqlServer2012));

}
else
{
    RuntimeConfiguration.ConfigureDQE<SQLServerDQEConfiguration>(
                                c => c.SetTraceLevel(TraceLevel.Verbose)
                                        .AddDbProviderFactory(typeof(System.Data.SqlClient.SqlClientFactory))
                                        .AddCatalogNameOverwrite("Creative", catalogOverwrite)
                                        .SetDefaultCompatibilityLevel(SqlServerCompatibilityLevel.SqlServer2012));
}
melman
User
Posts: 13
Joined: 26-Apr-2024
# Posted on: 05-Jun-2024 22:01:16   

Frans, one of my developers figured it out. There was a setting in the designer for allowed actions and it was only set to select. I am thinking this is because the table didn't have a primary key when it was initially reverse engineered.