Deep Clone Entity Graph

Posts   
 
    
Posts: 56
Joined: 08-Jun-2010
# Posted on: 31-Jan-2011 19:39:24   

Hello

I'm trying to clone a graph of related entities using v3 / adapter / sql server.

I've seen lots of posts on doing this on these forums and I'm really close to getting it to work.

I have 3 entities to clone, Customer, Address and Contact. All have identity fields as primary keys.

relationships are

Customer -> Address 1:m Customer -> Contact 1:m Address -> Contact 1:0..m

The problem I am having is with the Address-> Contact relation. If I have 1 customer, 1 Address and 1 Contact all tied together. After a clone and a save I end up with 2 addresses in the db with the address which hangs off of contact having its fk pointing to the original customer.

I've tried to reset the address foreign key to null and zero on contact after the clone and before saving with no success.

heres a snippet of my code




//prefetch for customer I'm using
IPrefetchPath2 prefetch = new PrefetchPath2(EntityModel.EntityType.CustomerEntity);
            prefetch.Add(CustomerEntity.PrefetchPathAddresses);
            prefetch.Add(CustomerEntity.PrefetchPathContacts)
                .SubPath.Add(ContactEntity.PrefetchPathAddress);

......... 

CustomerEntity cloned = (CustomerEntity) CloneHelper.CloneEntity(customer);

//try and fix up the fk - doesnt work
for (int i = 0; i < cloned.Contacts.Count; i++)
{
    var contact = cloned.Contacts[i];
      Contact.Fields[(int)ContactFieldIndex.AddressId].ForcedCurrentValueWrite(0);
//  contact.AddressId = null; // doesn't work either
}
adapter.SaveEntity(cloned, true, true);

The clone helper I built up from previous threads and is as follows


    internal class CloneHelper
    {
        private CloneHelper()
        {
        }

        internal static object CloneObject(object o)
        {
            MemoryStream ms = new MemoryStream();
            BinaryFormatter bf = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone));
            bf.Serialize(ms, o);
            ms.Seek(0, SeekOrigin.Begin);
            object oOut = bf.Deserialize(ms);
            ms.Close();
            return oOut;
        }

        internal static void ResetEntityAsNew(IEntity2 entity)
        {
            entity.IsNew = true;
            entity.IsDirty = true;
            entity.Fields.IsDirty = true;
            for (int f = 0; f < entity.Fields.Count; f++)
            {
                var field = entity.Fields[f];
                if (field.IsPrimaryKey) field.ForcedCurrentValueWrite(null);
                field.IsChanged = true;
            }
        }

        internal static IEntity2 CloneEntity(IEntity2 Entity)
        {
            IEntity2 newEntity;

            newEntity = (IEntity2)CloneObject(Entity);
            ObjectGraphUtils ogu = new ObjectGraphUtils();
            List<IEntity2> flatList = ogu.ProduceTopologyOrderedList(newEntity);

            for (int f = 0; f < flatList.Count; f++)
                ResetEntityAsNew(flatList[f]);

            return newEntity;
        }
    }

Does anybody have any idea how to sort this out? My next plan of attack is to clone one object at a time and manually fix up the foreign keys but this seems a little inefficent.

Thanks ~Brett

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 01-Feb-2011 02:19:20   

Hi Brett,

I think that code should work without the need of setting the fk to another value (it's already attached to a new Contact) so the PK-FK should be synchronized.

Could you please post the generated sql?

David Elizondo | LLBLGen Support Team
Posts: 56
Joined: 08-Jun-2010
# Posted on: 01-Feb-2011 11:58:05   

As requested the trace output is below.

I've removed my my attempts at trying to manually fix the fk's

From the code below you will notice that it inserts 2 addresses (there should be only 1)

the second address is created against the original customer instead of the clone (@p5) original cust is #10 cloned cust #11

when the the contact is created it should really be pointing at the first address @p1 = 16 instead of @p1=17.

I think that after the clone llblgen does not recognise that the address hanging off customer and the address hanging off of contact are the same instance.


    Method Enter: CreateInsertDQ
    Method Enter: CreateSingleTargetInsertDQ
    Generated Sql query: 
    Query: INSERT INTO [NUnit].[dbo].[Customer] .......... edited .......

Method Exit: CreateSingleTargetInsertDQ
    Method Exit: CreateInsertDQ
    Method Enter: CreatePagingSelectDQ
    Method Enter: CreateSelectDQ
    Method Enter: CreateSelectDQ
    Generated Sql query: 
    Query: SELECT [NUnit].[dbo].[Customer].[Id],  .... edited .... 
    FROM [NUnit].[dbo].[Customer]   WHERE ( ( [NUnit].[dbo].[Customer].[Id] = @p1))
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 11.
Method Exit: CreateSelectDQ
    Method Exit: CreatePagingSelectDQ: no paging.
    Method Enter: CreateInsertDQ
    Method Enter: CreateSingleTargetInsertDQ
    Generated Sql query: 
    Query: INSERT INTO [NUnit].[dbo].[Address] ([Address1], [Address2], [AddressType], [County], [CustomerId], [HouseName], [HouseNumber], [MonthsAtAddress], [PostCodePart1], [PostCodePart2], [Town]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p9, @p10, @p11, @p12) ;SELECT @p8=SCOPE_IDENTITY()
    Parameter: @p1 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "3 Groovy on Downs".
    Parameter: @p2 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p3 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 0.
    Parameter: @p4 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 11.
    Parameter: @p6 : AnsiString. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p7 : AnsiString. Length: 10. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p8 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Output. Value: <undefined value>.
    Parameter: @p9 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 12.
    Parameter: @p10 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "DT10".
    Parameter: @p11 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "WTV".
    Parameter: @p12 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "DownTown".

Method Exit: CreateSingleTargetInsertDQ
    Method Exit: CreateInsertDQ
    Method Enter: CreatePagingSelectDQ
    Method Enter: CreateSelectDQ
    Method Enter: CreateSelectDQ
    Generated Sql query: 
    Query: SELECT [NUnit].[dbo].[Address].[Address1], [NUnit].[dbo].[Address].[Address2], [NUnit].[dbo].[Address].[AddressType], [NUnit].[dbo].[Address].[County], [NUnit].[dbo].[Address].[CustomerId], [NUnit].[dbo].[Address].[HouseName], [NUnit].[dbo].[Address].[HouseNumber], [NUnit].[dbo].[Address].[Id], [NUnit].[dbo].[Address].[MonthsAtAddress], [NUnit].[dbo].[Address].[PostCodePart1], [NUnit].[dbo].[Address].[PostCodePart2], [NUnit].[dbo].[Address].[Town] FROM [NUnit].[dbo].[Address]   WHERE ( ( [NUnit].[dbo].[Address].[Id] = @p1))
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 16.
Method Exit: CreateSelectDQ
    Method Exit: CreatePagingSelectDQ: no paging.
    Method Enter: CreateInsertDQ
    Method Enter: CreateSingleTargetInsertDQ
    Generated Sql query: 
    Query: INSERT INTO [NUnit].[dbo].[Address] ([Address1], [Address2], [AddressType], [County], [CustomerId], [HouseName], [HouseNumber], [MonthsAtAddress], [PostCodePart1], [PostCodePart2], [Town]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p9, @p10, @p11, @p12) ;SELECT @p8=SCOPE_IDENTITY()
    Parameter: @p1 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "3 Groovy on Downs".
    Parameter: @p2 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p3 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 0.
    Parameter: @p4 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p5 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 10.
    Parameter: @p6 : AnsiString. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p7 : AnsiString. Length: 10. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p8 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Output. Value: <undefined value>.
    Parameter: @p9 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 12.
    Parameter: @p10 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "DT10".
    Parameter: @p11 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "WTV".
    Parameter: @p12 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: "DownTown".

Method Exit: CreateSingleTargetInsertDQ
    Method Exit: CreateInsertDQ
    Method Enter: CreatePagingSelectDQ
    Method Enter: CreateSelectDQ
    Method Enter: CreateSelectDQ
    Generated Sql query: 
    Query: SELECT [NUnit].[dbo].[Address].[Address1], [NUnit].[dbo].[Address].[Address2], [NUnit].[dbo].[Address].[AddressType], [NUnit].[dbo].[Address].[County], [NUnit].[dbo].[Address].[CustomerId], [NUnit].[dbo].[Address].[HouseName], [NUnit].[dbo].[Address].[HouseNumber], [NUnit].[dbo].[Address].[Id], [NUnit].[dbo].[Address].[MonthsAtAddress], [NUnit].[dbo].[Address].[PostCodePart1], [NUnit].[dbo].[Address].[PostCodePart2], [NUnit].[dbo].[Address].[Town] FROM [NUnit].[dbo].[Address]   WHERE ( ( [NUnit].[dbo].[Address].[Id] = @p1))
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 17.
Method Exit: CreateSelectDQ
    Method Exit: CreatePagingSelectDQ: no paging.
    Method Enter: CreateInsertDQ
    Method Enter: CreateSingleTargetInsertDQ
    Generated Sql query: 
    Query: INSERT INTO [NUnit].[dbo].[Contact] ([AddressId], [CustomerId], [DateOfBirth], [Email], [Fax], [FirstName], [IsPrimary], [LastName], [MiddleName], [Position], [Telephone], [Title]) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p8, @p9, @p10, @p11, @p12, @p13) ;SELECT @p7=SCOPE_IDENTITY()
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 17.
    Parameter: @p2 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 11.
    Parameter: @p3 : DateTime. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p4 : AnsiString. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p5 : AnsiString. Length: 15. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p6 : AnsiString. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: "Fred".
    Parameter: @p7 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Output. Value: <undefined value>.
    Parameter: @p8 : Boolean. Length: 0. Precision: 0. Scale: 0. Direction: Input. Value: True.
    Parameter: @p9 : AnsiString. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: "Flintstone".
    Parameter: @p10 : AnsiString. Length: 50. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p11 : AnsiString. Length: 100. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p12 : AnsiString. Length: 15. Precision: 0. Scale: 0. Direction: Input. Value: <undefined value>.
    Parameter: @p13 : AnsiString. Length: 5. Precision: 0. Scale: 0. Direction: Input. Value: "Mr".

Method Exit: CreateSingleTargetInsertDQ
    Method Exit: CreateInsertDQ
    Method Enter: CreatePagingSelectDQ
    Method Enter: CreateSelectDQ
    Method Enter: CreateSelectDQ
    Generated Sql query: 
    Query: SELECT [NUnit].[dbo].[Contact].[AddressId], [NUnit].[dbo].[Contact].[CustomerId], [NUnit].[dbo].[Contact].[DateOfBirth], [NUnit].[dbo].[Contact].[Email], [NUnit].[dbo].[Contact].[Fax], [NUnit].[dbo].[Contact].[FirstName], [NUnit].[dbo].[Contact].[Id], [NUnit].[dbo].[Contact].[IsPrimary], [NUnit].[dbo].[Contact].[LastName], [NUnit].[dbo].[Contact].[MiddleName], [NUnit].[dbo].[Contact].[Position], [NUnit].[dbo].[Contact].[Telephone], [NUnit].[dbo].[Contact].[Title] FROM [NUnit].[dbo].[Contact]   WHERE ( ( [NUnit].[dbo].[Contact].[Id] = @p1))
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 10.
Method Exit: CreateSelectDQ
    Method Exit: CreatePagingSelectDQ: no paging.


MTrinder
User
Posts: 1461
Joined: 08-Oct-2008
# Posted on: 01-Feb-2011 21:01:03   

Are they actually the same instance - or they actually two instances of the same data? The difference is subtle, but important...

Matt

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 02-Feb-2011 06:24:45   

To elaborate the Matt's question:

  1. Describe the original fetched graph (Is any Address attached to Customer?)

  2. Do that instances of Customer and Contact share the same Address? If they do, then your original graph should have one Address instance for Customer and one Address instance for Contact. That means that if you make changes to both addresses, the next save will result in two updates for those Addresses (original graph, no the cloned one). So if this is the case, the cloning is not the problem, but the instances you fetched. The cloning just make a copy of the graph and set the IsNew flags to true. If this is your situation, you can overcome this by using the Context class.

To resume, the cloning routine just make a copy of the graph and set all to 'new', you can check that by making changes to your original graph and see what happen in the updates.

David Elizondo | LLBLGen Support Team
Posts: 56
Joined: 08-Jun-2010
# Posted on: 02-Feb-2011 11:47:17   

Matt / David

I have 1 customer with 1 address and 1 contact. The contact is related to the same address so in db terms there is 1 row in each table.

I had assumed (but not checked) that the fetch in my first post brought back 1 instance each of address, customer and contact and that customer.addresses[0] == customer.contacts[0].address

does this answer your question?

David, I'm not sure I understand what you are advising? Are you saying that the initial prefetch brings back 2 instances of address (one off of customer and one off of contact) even though they represent the same row in the address table? And that If I create a new instance of Context and pass that into my original fetch and then pass the same instance of context when I save the clone it will fix my problem?

Thanks

~Brett

Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 02-Feb-2011 15:30:59   

I'm really confused here.

If the following is true:

I have 1 customer with 1 address and 1 contact. The contact is related to the same address so in db terms there is 1 row in each table.

So why do you need that last relation (Address -> Contact)?

Customer -> Address 1:m Customer -> Contact 1:m Address -> Contact 1:0..m

Posts: 56
Joined: 08-Jun-2010
# Posted on: 02-Feb-2011 16:42:48   

So the 1 Customer, 1 Address, 1 Contact is just an example set of data.

For a given customer we have adresses and contacts.

Some contacts, but not all, are associated with an address.

Not all addresses have a contact.

Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 02-Feb-2011 16:50:54   

Some contacts, but not all, are associated with an address.

Not all addresses have a contact.

I think you should say, not all Contacts have addresses.

Otherwise you should use the following code instead.

            IPrefetchPath2 prefetch = new PrefetchPath2(EntityModel.EntityType.CustomerEntity);
            prefetch.Add(CustomerEntity.PrefetchPathAddresses).SubPath.Add(AddresstEntity.PrefetchPathContacts);
            prefetch.Add(CustomerEntity.PrefetchPathContacts);
Posts: 56
Joined: 08-Jun-2010
# Posted on: 02-Feb-2011 16:58:38   

I don't see how that would make a difference over my original?


PrefetchPath2 prefetch = new PrefetchPath2(EntityModel.EntityType.CustomerEntity);
            prefetch.Add(CustomerEntity.PrefetchPathAddresses);
            prefetch.Add(CustomerEntity.PrefetchPathContacts)
                .SubPath.Add(ContactEntity.PrefetchPathAddress);

It would change the shape of the object graph but instead of ending up with duplicate address I would end up with a duplicate contacts

Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 02-Feb-2011 17:39:18   

There is some confusion over here, that I would like to clear.

        IPrefetchPath2 prefetch = new PrefetchPath2(EntityModel.EntityType.CustomerEntity);
        prefetch.Add(CustomerEntity.PrefetchPathAddresses).SubPath.Add(AddresstEntity.PrefetchPathContacts);
        prefetch.Add(CustomerEntity.PrefetchPathContacts);

If the above code (my suggestion), can correctly fetch your graph, then it's obvious that you should get 2 different instances of the same entity fetched back from the database.

You may use a Context to use unique instaces, and prevent duplicate instances in the same graph..

Posts: 56
Joined: 08-Jun-2010
# Posted on: 02-Feb-2011 17:51:05   

yes, your above code will fetch my graph.

My question isn't to do with fetching the data its to do with copying it.

I'm starting out with 3 related records in the database and I need to finish with another 3 related records in the database.

I'm trying to use llblgen to acheive this. I don't really mind if llblgen has 3 or 4 object instances in memory when I load it so long as I end up with 3 new records in the database.

How do I acheive this?

Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 02-Feb-2011 18:08:27   

you will have to use a context for that.

Here is why:

1- There are 3 instances in the database. 2- You fetch them into a graph, they become 4 different objects. 3- You fiddle with them to clone them and hence you reset the objects to appear as new instances. This is were things get wrong. As then you have 2 new instances in memory, but they are should be one in the database.

And hence a context should make sure you only receive one instance for each database instance in the same graph.

Posts: 56
Joined: 08-Jun-2010
# Posted on: 02-Feb-2011 18:13:34   

Thanks thats really helpfull I'll give it a go.

out of interest do you think I actualy need to do the binary copy via the memory stream? Isn't setting the whole graph to "IsNew" and "IsDirty" enough?

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 03-Feb-2011 06:08:05   

BrettBailey wrote:

out of interest do you think I actualy need to do the binary copy via the memory stream? Isn't setting the whole graph to "IsNew" and "IsDirty" enough?

It's enough, but you 'loose' the original graph. Cloning is generally used when you need the original graph and a copy of that so you can make comparisons. So if you don't need the original graph for reference, you can create a routine similar to ResetEntityAsNew(IEntity2 entity) to make your graph look as a new one.

David Elizondo | LLBLGen Support Team