DuplicateKeyException using BO within DataPortal_Execute

DuplicateKeyException using BO within DataPortal_Execute

Old forum URL: forums.lhotka.net/forums/t/5813.aspx


cmclernon posted on Monday, November 17, 2008

Hi all,

I get the DuplicateKeyException “Cannot add an entity with a key that is already in use.” whether or not I have transaction scope setup on execute and update methods.  Here is an excerpt from my code:

 protected override void DataPortal_Execute()
            {
                using (var mgr = ContextManager<DalLinq.MyDBDataContext>
                            .GetManager(DalLinq.Database.MyDB))
                {
                    var _part = BusinessObjects.Part.GetPart(_no, _rev);

                    _part.Description = "Test update";

                    _part.Save();  <-- Error happens when calling this, see below...

        protected override void DataPortal_Update()
        {
            bool cancel = false;
            OnUpdating(ref cancel);
            if (cancel) return;

            using (var mgr = ContextManager<DalLinq.MyDBDataContext>
                        .GetManager(DalLinq.Database.MyDB))
            {
                var data = new DalLinq.Part()
                {
                    PartNo = ReadProperty<string>(NoProperty),
                    PartRev = ReadProperty<int>(RevProperty)
                };
                data.LastChanged = _lastChanged;
                mgr.DataContext.Part.Attach(data); <-- Error happens here

I'm pretty new to L2S and I can't see where I'm going wrong.  Anyone any ideas?  I was under the impression the above should work based on Rocky's reply in the following post:

http://forums.lhotka.net/forums/thread/23484.aspx

Thanks,

Colm 

ajj3085 replied on Monday, November 17, 2008

Hi,

What is the key property for DalLinq.Part?   If you're attaching, you'll need to make sure the key column has a value set.  If you do make sure of that.. then you have another object in your Part table that has the same key.  I'd check the values of DalLinq.Part and look through mgr.DataContext.Part as well.

Andy

cmclernon replied on Monday, November 17, 2008

Thanks Andy,

PartNo and PartRev are the key fields and are populated with proper values.  It also seems mgr.DataContext.Part is populated with all the records in the database, well when I call the Count() method anyway - unless this is causing the database to be queried??

The DataPortal_Update portion of the code works fine when the "using (var mgr = ContextManager<DalLinq.MyDBDataContext>.GetManager(DalLinq.Database.MyDB))" line is removed from the DataPortal_Execute method.  In this instance the mgr.DataContext.Part.Count() method returns same count as above...so now I'm really confused as I would have expected the same DuplicateKeyException.

Removal of the using ContextManager command in the DataPortal_Execute method presumably invalidates the Transaction logic within the CSLA framework so is not an option.

Any other ideas?

Thanks,

Colm

ajj3085 replied on Monday, November 17, 2008

It's possible that calling Count is populating the table.. in fact I wouldn't see how it'd work otherwise.  To count, it has to enumerate the collection, which would populate all the records.  So, I'd figure out what code is causing the enumeration to fire and execute the query. 

If it's necessary, your other option is just to select the existing record instead of new'ing one up..
var part = mgr.DataContext.Part.Where( p => p.PartNo == partNo && p.PartRev == partRev ), and then simply updating part.  That will also generate the proper sql.

cmclernon replied on Monday, November 17, 2008

Thanks for your replies Andy.  That code is generated by the Codesmith CSLA templates so not sure if I can change that or not, from the point of view of wanting to go down that road or not...

Can anyone else shed any light on the subject?

Thanks,

Colm

cmclernon replied on Tuesday, November 18, 2008

Hi all,

The following code is a Role BO generated using Codesmith with the CSLAcontrib template against the ProjectTracker database.  I've included an UpdateName command to fully show what is going on here.  Andy's solution above works and proves that the transaction can be persisted across BO's as Rocky suggests but his solution requires a trip to the database which is essentially redundant and doubles the amount of database hits.

Including the using statement in the DataPortal_Execute causes an error so I can only assume that there's something happening within the ContextManager that is causing this behaviour.

Perhaps I am going wrong in my use of the BO's or not doing something I should be.  Any help would be greatly appreciated.

Thanks,

Colm

using System;

using System.Linq;

using Csla;

using Csla.Data;

using Csla.Security;

using Csla.Validation;

namespace CSLAExample

{

[Serializable()]

public partial class Role : Csla.BusinessBase<Role>

{

#region Business Properties and Methods

//register properties

private static PropertyInfo<int> IdProperty = RegisterProperty<int>(typeof(Role), new PropertyInfo<int>("Id"));

private static PropertyInfo<string> NameProperty = RegisterProperty<string>(typeof(Role), new PropertyInfo<string>("Name"));

private byte[] _lastChanged;

[System.ComponentModel.DataObjectField(true, false)]

public int Id

{

get { return GetProperty<int>(IdProperty); }

}

public string Name

{

get { return GetProperty<string>(NameProperty); }

set { SetProperty<string>(NameProperty, value); }

}

#endregion //Business Properties and Methods

#region Validation Rules

protected override void AddBusinessRules()

{

//

// Name

//

ValidationRules.AddRule(CommonRules.StringRequired, NameProperty);

ValidationRules.AddRule(CommonRules.StringMaxLength, new CommonRules.MaxLengthRuleArgs(NameProperty, 50));

AddCustomRules();

}

partial void AddCustomRules();

#endregion //Validation Rules

#region Factory Methods

public static Role NewRole(int id)

{

return DataPortal.Create<Role>(new SingleCriteria<Role, int>(id));

}

public static Role GetRole(int id)

{

return DataPortal.Fetch<Role>(new SingleCriteria<Role, int>(id));

}

public static void DeleteRole(int id)

{

DataPortal.Delete(new SingleCriteria<Role, int>(id));

}

private Role()

{ /* require use of factory method */ }

#endregion //Factory Methods

#region Data Access

#region Data Access - Create

[RunLocal]

private void DataPortal_Create(SingleCriteria<Role, int> criteria)

{

bool cancel = false;

OnCreating(criteria, ref cancel);

if (cancel) return;

LoadProperty<int>(IdProperty, criteria.Value);

ValidationRules.CheckRules();

OnCreated();

}

partial void OnCreating(SingleCriteria<Role, int> criteria, ref bool cancel);

partial void OnCreated();

#endregion //Data Access - Create

#region Data Access - Fetch

private void DataPortal_Fetch(SingleCriteria<Role, int> criteria)

{

bool cancel = false;

OnFetching(criteria, ref cancel);

if (cancel) return;

using (var mgr = ContextManager<DalLinq.PTRACKERDataContext>

.GetManager(Database.PTRACKER))

{

var data = mgr.DataContext.Roles

.Single(r => r.Id == criteria.Value);

OnMemberLoading(data);

LoadProperty<int>(IdProperty, data.Id);

LoadProperty<string>(NameProperty, data.Name);

_lastChanged = data.LastChanged.ToArray();

OnMemberLoaded();

ValidationRules.CheckRules();

}//using

OnFetched();

}

partial void OnFetching(SingleCriteria<Role, int> criteria, ref bool cancel);

partial void OnFetched();

partial void OnMemberLoading(DalLinq.Roles data);

partial void OnMemberLoaded();

#endregion //Data Access - Fetch

#region Data Access - Insert

protected override void DataPortal_Insert()

{

bool cancel = false;

OnInserting(ref cancel);

if (cancel) return;

using (var mgr = ContextManager<DalLinq.PTRACKERDataContext>

.GetManager(Database.PTRACKER))

{

var data = new DalLinq.Roles();

OnMemberReading(data);

data.Id = ReadProperty<int>(IdProperty);

data.Name = ReadProperty<string>(NameProperty);

OnMemberRead();

mgr.DataContext.Roles.InsertOnSubmit(data);

mgr.DataContext.SubmitChanges();

_lastChanged = data.LastChanged.ToArray();

}//using

OnInserted();

}

partial void OnInserting(ref bool cancel);

partial void OnInserted();

partial void OnMemberReading(DalLinq.Roles data);

partial void OnMemberRead();

#endregion //Data Access - Insert

#region Data Access - Update

protected override void DataPortal_Update()

{

bool cancel = false;

OnUpdating(ref cancel);

if (cancel) return;

using (var mgr = ContextManager<DalLinq.PTRACKERDataContext>

.GetManager(Database.PTRACKER))

{

var data = new DalLinq.Roles()

{

Id = ReadProperty<int>(IdProperty)

};

data.LastChanged = _lastChanged;

mgr.DataContext.Roles.Attach(data);

OnMemberReading(data);

if (IsSelfDirty)

{

data.Name = ReadProperty<string>(NameProperty);

}

OnMemberRead();

mgr.DataContext.SubmitChanges();

if (IsSelfDirty)

{

_lastChanged = data.LastChanged.ToArray();

}

}//using

OnUpdated();

}

partial void OnUpdating(ref bool cancel);

partial void OnUpdated();

#endregion //Data Access - Update

 

#region Data Access - Delete

protected override void DataPortal_DeleteSelf()

{

bool cancel = false;

OnSelfDeleting(ref cancel);

if (cancel) return;

using (var mgr = ContextManager<DalLinq.PTRACKERDataContext>

.GetManager(Database.PTRACKER))

{

OnMemberSelfDeleting();

var data = new DalLinq.Roles()

{

Id = ReadProperty<int>(IdProperty)

};

data.LastChanged = _lastChanged;

mgr.DataContext.Roles.Attach(data);

mgr.DataContext.Roles.DeleteOnSubmit(data);

OnMemberSelfDeleted();

mgr.DataContext.SubmitChanges();

}//using

OnSelfDeleted();

}

partial void OnSelfDeleting(ref bool cancel);

partial void OnSelfDeleted();

partial void OnMemberSelfDeleting();

partial void OnMemberSelfDeleted();

private void DataPortal_Delete(SingleCriteria<Role, int> criteria)

{

bool cancel = false;

OnDeleting(criteria, ref cancel);

if (cancel) return;

using (var mgr = ContextManager<DalLinq.PTRACKERDataContext>

.GetManager(Database.PTRACKER))

{

var data = mgr.DataContext.Roles

.Single(r => r.Id == criteria.Value);

OnMemberDeleting(data);

mgr.DataContext.Roles.DeleteOnSubmit(data);

OnMemberDeleted();

mgr.DataContext.SubmitChanges();

}//using

OnDeleted();

}

partial void OnDeleting(SingleCriteria<Role, int> criteria, ref bool cancel);

partial void OnDeleted();

partial void OnMemberDeleting(DalLinq.Roles data);

partial void OnMemberDeleted();

#endregion //Data Access - Delete

#endregion //Data Access

#region UpdateName

public static bool UpdateName(int id, string newName)

{

return UpdateNameCommand.UpdateName(id, newName);

}

[Serializable()]

private class UpdateNameCommand : CommandBase

{

private int _id;

private string _newName;

public bool UpdateSuccessful { get; internal set; }

public static bool UpdateName(int id, string newName)

{

UpdateNameCommand result = null;

result = DataPortal.Execute<UpdateNameCommand>(new UpdateNameCommand(id, newName));

return result.UpdateSuccessful;

}

private UpdateNameCommand(int id, string newName)

{

_id = id;

_newName = newName;

}

protected override void DataPortal_Execute()

{

using (var ctx = ContextManager<DalLinq.PTRACKERDataContext>

.GetManager(Database.PTRACKER))

{

Role _role = Role.GetRole(_id);

_role.Name = _newName;

_role.Save();

}

}

}

#endregion

}

}

ajj3085 replied on Tuesday, November 18, 2008

The only time I see you selecting the record and then doing something is in the delete.. so it looks like that's the only round trip to the database.  The other option appears to be how you have the update / insert coded, which doesn't cause a round trip.

cmclernon replied on Tuesday, November 18, 2008

Yeah, the CRUD methods here are generated by the CSLAContrib template.  I've added the UpdateNameCommand object, that inherits from CommandBase, and this is what I'm particularly interested in.  This is an example of updating the BO in the DataPortal_Execute method and calling the BO.Save method.  Am I correct in doing this?  I am trying to use the BO to update the database in an attempt to only have to code update logic once and re-use in both the front-end and DAL.

Theoretically this code works when I don't include the using contextmanager statement in the DataPortal_Execute method but that means I need to use DTS and this performance hit is not acceptable for our users.

Your solution above, Andy, was to use "var part = mgr.DataContext.Part.Where( p => p.PartNo == partNo && p.PartRev == partRev )" in the DataPortal_Update method which stops the duplicate key exception and essentially makes the code work.  The downfall of this solution is that this is a redundant roundtrip to the database, just to populate DataContext.Part with the DalLinq.Part I wish to update.

So I need Rocky (or someone) to confirm that I am indeed correct using the BO within the DataPortal_Execute method to update the database and I'm hoping that they can tell me there's an issue with doing this that needs some property set or if there is a bug that it will be fixed in 3.6.

Thanks,

Colm

ajj3085 replied on Tuesday, November 18, 2008

Well, it looks like your mgr.DataContext.Part entity set is already populated; it shouldn't perform a subsequent call to the database because it's already in memory.  At least that's how I would expect it to work; did you verify if a second database hit is actually occurring?

Also, I don't think your issue has anything to do with Csla itself; this would happen outside of Csla, as it's a Linq behavior. 

As far as your update command, if you have a use case that allows a user to just update the name outside of loading the full object, I don't see any issue.  It depends on what you're trying to do.

cmclernon replied on Wednesday, November 19, 2008

Hi Andy,

No, I haven't verified that as yet - can I do that with SQL Express installed and Linq logs, or do I need the full version of SQL Server installed?

As I'm new to both Linq and CSLA, I'm not sure where the problem lies but I was querying here because it's when the BO update is done inside a using CSLA contextmanager block that the error occurs and I thought that this would have been a common problem with an easy solution.

Thanks,

Colm

ajj3085 replied on Wednesday, November 19, 2008

I usually use Sql Profiler, but I don't recall if that comes with the express version or not.  There may be other ways too, but I can't think of them off the top of my head.

I've hit the problem before... but it's because the object already existed in memory, or I was trying to attach an object that had null for a key, and there was already another object in memory with null as the key.  I don't think InsertOnSubmit checks the key value.. I believe just attach will.

Oh, I think I've also had it occur when I forget to set the primary key as an Identity.  linq expected me to supply a unique key, but my code assumed the database would do that.. so that might be something to look into as well.  Linq also needs to know that; the Column attribute for the PK should have IsDbGenerated = true.

cmclernon replied on Thursday, November 20, 2008

No, profiler just comes with full version.

I think the primary key is setup correctly because the same code works without being included in the using contextmanager block...so being in that using block is causing the object to exist in memory - so that's what we need to get to the bottom of.

Thanks,

Colm

ianr replied on Thursday, November 20, 2008

There is an open source profiler for Sql Express (just google "Sql Express Profiler").  It does most of the tasks that the full version does.

ajj3085 replied on Thursday, November 20, 2008

No... the object exists because you're all the code which uses the contextmanager is using the exact same instance of your DataContext.  This is what you want normally, if you want to use one open connection, and don't want  your transactionscope transaction to kick in to DTC mode. 

When you take out your contextmanager, you're probably creating a NEW data context each time.  So all your data code is isolated from each other, and thus code which may have already attached your object with a certain key can't affect latter code that does the same thing.  But as soon as you add in your contextmanager, your code is colliding by trying to attach two different objects with the same key to your context.  I'd step through the code and use a watch window for your mgr variable..  inspect the DataConetxt prior to the line which throws the exception, and then figure out which of your other code atttached an instance with the same key, or caused the context to populate the table.

cmclernon replied on Thursday, November 20, 2008

Cheers Ian,

Andy, when you put it like that it suddenly becomes clear.  The GetRole will be adding the Role object in the DataPortal_Fetch and then the save will be calling DataPortal_Update, which will try and add it again and therefore produce the error.

Role _role = Role.GetRole(_id);

_role.Name = _newName;

_role.Save();

Thanks for your help.

Colm

ajj3085 replied on Thursday, November 20, 2008

Colm,

Glad I could help.  Post back if you run into any more problems!

Andy

Copyright (c) Marimer LLC