- Home
- LLBLGen Pro
- Bugs & Issues
Cloning a self-referencing entity
Joined: 15-May-2009
I'm having a problem using the entity cloning method referenced at http://www.llblgen.com/tinyforum/Messages.aspx?ThreadID=7568
My current environment: - LLBLGen Pro 3.1 Final (February 7th, 2011) - adapter model - SQL Server 2008 R2
Problem Description:
I have to create a copy of a some records for business reasons. I'm not aware of a cloning method specific to LLBLGen entities other than the one referenced above (other than manually creating the copies).
This cloning method was working fine until I attempted to use it on a table that has a self-referencing hierarchy. The hierarchy is defined at the database level (see below).
The problem appears to be that when it creates the new records for the self-referencing hierarchy table, it correctly creates new parent records but the newly created children records still point back to the old parent record instead of pointing at the newly created parent.
Example table definition:
SelfReferencingHierarchyTable
ID Guid NOT NULL (PK) ParentID Guid NULL (FK back to SelfReferencingHierarchyTable) [SomeDataFields]
Source records:
ID ParentID SomeDataFields
abc NULL parent record data 123 abc child record 1 data 456 abc child record 2 data
"Cloned" records:
ID ParentID SomeDataFields
xyz NULL copy of 'abc' parent record data 789 abc copy of child record 1 data 012 abc copy of child record 2 data
As you can see, the cloned child records are pointing at the original parent record instead of the new parent record.
I began to write a custom cloning routine that recursively adds the child records, but I though I'd ask and see if this is something the referenced cloning code should take care of or if it's just outside the scope of its capabilities.
Any help is greatly appreciated
dm0527
Here is my test using the EmployeId-ReportsTo Self-referencing hierarchy of table Emloyees in Norhwind:
[TestMethod]
public void CloneSelfReferencingHierarchy()
{
// fetch some known employee
var originalEmployee = FetchEmployeeAndTestTheChildren(2, 5);
// clone it
EmployeeEntity clonedEmployee = (EmployeeEntity) CloneHelper.CloneEntity(originalEmployee);
// check if the in-memory copy is ok
Assert.AreEqual(5, clonedEmployee.Employees.Count);
Assert.AreEqual(5, clonedEmployee.Employees.Where(e => e.ReportsTo == 2).Count());
// save the copy
using (var adapter = new DataAccessAdapter())
{
adapter.SaveEntity(clonedEmployee, false, true);
}
// test the saved cloned hierarchy
var checkEmployee = FetchEmployeeAndTestTheChildren(clonedEmployee.EmployeeId, 5);
}
private EmployeeEntity FetchEmployeeAndTestTheChildren(int employeeId, int expectedChildren)
{
// employee to be fetched
var employee = new EmployeeEntity(employeeId);
// fetch its children as well
var employeePath = new PrefetchPath2(EntityType.EmployeeEntity);
employeePath.Add(EmployeeEntity.PrefetchPathEmployees);
// fetch
using (var adapter = new DataAccessAdapter())
{
adapter.FetchEntity(employee, employeePath);
}
// fetch the expected children
Assert.AreEqual(expectedChildren, employee.Employees.Count);
Assert.AreEqual(expectedChildren, employee.Employees.Where(e => e.ReportsTo == employeeId).Count());
return employee;
}
Everything is working. So my guess is that the problem is in the way you are setting the new Id and saving the cloned copy back to DB. I noticed your ID is GUID, and I guess you are setting it in your code (not auto generated by DB), right? I will see if I can reproduce it with a table like yours and will come back with my results.
I tried with something more close to your scenario. Here it's:
The table DDL
CREATE TABLE [dbo].[Friend](
[Id] [uniqueidentifier] NOT NULL,
[ParentId] [uniqueidentifier] NULL,
[SomeOtherData] [varchar](50) NULL,
CONSTRAINT [PK_Friend] PRIMARY KEY CLUSTERED
(
[Id] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Friend] WITH CHECK ADD CONSTRAINT [FK_Friend_Friend] FOREIGN KEY([ParentId])
REFERENCES [dbo].[Friend] ([Id])
GO
The tests
[TestMethod]
public void CloneSelfReferencingFriendHierarchy()
{
// fetch some known friend
int numberOfChildresToCheck = 3;
var originalFriend = FillTheFriends(numberOfChildresToCheck);
// clone it
FriendEntity clonedFriend = (FriendEntity)CloneHelper.CloneEntity(originalFriend);
// set new ids
clonedFriend.Id = Guid.NewGuid();
foreach (var child in clonedFriend.Friends)
{
child.Id = Guid.NewGuid();
}
// check if the in-memory copy is ok
Assert.AreEqual(numberOfChildresToCheck, clonedFriend.Friends.Count);
Assert.AreEqual(numberOfChildresToCheck, clonedFriend.Friends.Where(e => e.ParentId == originalFriend.Id).Count());
// save the copy
using (var adapter = new DataAccessAdapter())
{
adapter.SaveEntity(clonedFriend, false, true);
}
// test the saved cloned hierarchy
var checkFriend = FetchFriendAndTestTheChildren(clonedFriend.Id, numberOfChildresToCheck);
}
private FriendEntity FillTheFriends(int numberOfChilds)
{
var supertFriend = new FriendEntity(Guid.NewGuid());
supertFriend.SomeOtherData = "The SuperFriend";
for (int i = 1; i <= numberOfChilds; i++)
{
var child = new FriendEntity(Guid.NewGuid());
child.SomeOtherData = "Child Friend " + i.ToString();
supertFriend.Friends.Add(child);
}
using (var adapter = new DataAccessAdapter())
{
adapter.SaveEntity(supertFriend, true, true);
}
return supertFriend;
}
private FriendEntity FetchFriendAndTestTheChildren(Guid friendId, int expectedChildren)
{
// friend to be fetched
var friend = new FriendEntity(friendId);
// fetch its children as well
var friendPath = new PrefetchPath2(EntityType.FriendEntity);
friendPath.Add(FriendEntity.PrefetchPathFriends);
// fetch
using (var adapter = new DataAccessAdapter())
{
adapter.FetchEntity(friend, friendPath);
}
// fetch the expected children
Assert.AreEqual(expectedChildren, friend.Friends.Count);
Assert.AreEqual(expectedChildren, friend.Friends.Where(e => e.ParentId == friendId).Count());
return friend;
}
Everything seems to work as expected. As I said before, maybe it's the way you are setting up the new Id's or the way you are adding the new children. If you look at my example I added them by reference ( parent.Children.Add(someChild) ), this makes LLBLGen to keep them in sync with their parent so when you are saving the whole graph, LLBLGen first save the parent and then updates the children's reference to it (parentId) and then LLBLGen runtime "saves the children"
Joined: 15-May-2009
My database is set up identically to yours (as far as the self-referencing UNIQUEIDENTIFIER field goes). I did notice that I had modified some of the CloneHelper code to use IEntity2 rather than IEntity. I'm also cloning some other entities along with the self-referencing table. I'm wondering if I'm doing something incorrect in one of those places.
Here's more detail:
DB Setup: ReconConfig (1 entity, base of the fetch and clone) --ReconConfigAdjustments (n entities, no self-referencing) ----Rule (maximum of 1 entity per ReconConfigAdjustment) ------RuleExpressions (n entities per Rule, this is where the self-referencing is at)
RuleExpressions is the self-referencing table. Definition (sans superfluous indexes, etc)
CREATE TABLE [dbo].[RuleExpression](
[RuleExpressionID] [uniqueidentifier] NOT NULL,
[FieldName] [varchar](255) NOT NULL,
[OperatorID] [int] NULL,
[Value] [varchar](255) NOT NULL,
[CalculationTypeID] [int] NULL,
[CalculationFunctionID] [int] NULL,
[RuleID] [uniqueidentifier] NULL,
[JoinTypeID] [int] NULL,
[ParentID] [uniqueidentifier] NULL,
[Sequence] [int] NULL,
CONSTRAINT [PK_QueryCriteria] PRIMARY KEY CLUSTERED (
[RuleExpressionID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
ALTER TABLE [dbo].[RuleExpression] WITH CHECK ADD CONSTRAINT [FK_RuleExpression_RuleExpression_Parent] FOREIGN KEY([ParentID])
REFERENCES [dbo].[RuleExpression] ([RuleExpressionID])
Refactored Cloning Code (other than these changes, it's identical to the CloneHelper from the referenced post)
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++)
{
Entity.Fields[f].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;
}
Here's the function that's handling the cloning
public static ReconConfigEntity PerformCopyForBranch(Guid FromReconConfigID, string CopyToBranchID)
{
ReconConfigEntity oldRC = ReconConfigManager.Fetch(FromReconConfigID, FetchTypes.DeepCopy);
ReconConfigEntity clone = CloneHelper.CloneEntity(oldRC) as ReconConfigEntity;
// reset IDs
clone.ReconConfigID = Guid.NewGuid();
foreach (ReconConfigAdjustmentEntity adj in clone.ReconConfigAdjustments)
{
adj.ReconConfigAdjustmentID = Guid.NewGuid();
if (adj.Rule != null)
{
adj.Rule.RuleID = Guid.NewGuid();
foreach (RuleExpressionEntity exp in adj.Rule.RuleExpressions)
exp.RuleExpressionID = Guid.NewGuid();
}
}
GetAdapter().SaveEntity(clone, true, true);
return clone;
}
#endregion
And the prefetch code for ReconConfigManager.Fetch:
q = q.WithPath(
new PathEdge<ReconConfigAdjustmentEntity>(ReconConfigEntity.PrefetchPathReconConfigAdjustments,
new PathEdge<RuleEntity>(ReconConfigAdjustmentEntity.PrefetchPathRule,
new PathEdge<RuleExpressionEntity>(RuleEntity.PrefetchPathRuleExpressions)
)
)
);
I have massaged the code quite a bit trying to get it to work, but it seems that no matter what I do, I always end up with the scenario I originally described.
Any thoughts?
Thanks again for all the help!
Scott
It's not that obvious at first because all the hierarchy, however the problem is that you are missing the 'last' level. The deepest level should be RuleExpression-RuleExpressions. When you have a self-referencing table like this, a relation and a navigator are generated. Like in my example: FriendEntity has a Friends navigator property and its type is EntityCollection<FriendEntity>. So your RuleExpression should have a RuleExpressions navigator property of type EntityCollection<RuleExpression>. You are missing that in two important places: the prefetchPath and the routine that reset Id's.
However the deepest RuleExpressions (children of the parent RuleExpression(s)) are cloned, so my guess is that the Rule.RuleExpressions contains both the parent and the children. So they are fetched. But you are not reseting the children Id's correctly.
My guess is that the key of your problem is the Rule - RuleExpression relationship. After all it's an 1:n relation. So in your last loop:
adj.Rule.RuleID = Guid.NewGuid();
foreach (RuleExpressionEntity exp in adj.Rule.RuleExpressions)
exp.RuleExpressionID = Guid.NewGuid();
... you are actually changing all the RuleExpression hierarchy without parent-child references. So when you modify a ruleExpressionId it wont sync its children's ParentId as they are not related in memory. You are treating all ruleExpressions as if they don't belong to a self-referencing hierarchy. In other words how you have it now, it's just like a flat list of RuleExpressions.
As I see, you have two options:
A. If possible, modify the way you store RuleExpression in your table. Ideally you would only have RuleId stored in that table for the parent rule expressions. If so, when you say someRule.RuleExpressions you are obtaining only the parent ones. Then you should add a level to your prefetchPath to load the children of that parents. So, the involved code should look like:
q = q.WithPath(
new PathEdge<ReconConfigAdjustmentEntity>(ReconConfigEntity.PrefetchPathReconConfigAdjustments,
new PathEdge<RuleEntity>(ReconConfigAdjustmentEntity.PrefetchPathRule,
new PathEdge<RuleExpressionEntity>(RuleEntity.PrefetchPathRuleExpressions,
new PathEdge<RuleExpressionEntity>(RuleExpressionEntity.PrefetchPathRuleExpressions
)
)
)
);
adj.Rule.RuleID = Guid.NewGuid();
foreach (RuleExpressionEntity exp in adj.Rule.RuleExpressions)
exp.RuleExpressionID = Guid.NewGuid();
foreach (RuleExpression childExp in exp.RuleExpressions)
childExp.RuleExpressionID = Guid.NewGuid();
B. Just like you are doing now, but take into account that your rule.RuleExpressions collection has both the parent and the children, so you will have to add code to update the ParentId of the children. Something like:
adj.Rule.RuleID = Guid.NewGuid();
foreach (RuleExpressionEntity exp in adj.Rule.RuleExpressions)
{
var newId = Guid.NewGuid();
// check if there are children to update
foreach (RuleExpressionEntity childCandidate in adj.Rule.RuleExpressions)
if (childCandidate.ParentID == exp.RuleExpressionID)
childCandidate.ParentID = newId;
// set the new id
exp.RuleExpressionID = newId;
}
Hope helpful