0

I have Users - Rules tables with many-to-many relationship. I want to delete some rules of a certain user and update one field of that user in the same step / database transaction, so I have the following pseudo-code in BL:

this.UnitOfWork.ExecuteTransaction(() =>
{
   // delete rules
   foreach (var rule in ruleList)
       repoRules.DeleteRulesForUser(user, rule);

   // update user field
   repoUser.UpdateUserField(user);
}

In EF repositories:

public void DeleteRulesForUser(User user, Rule rule)
{
   // check user
   EFUser u = this.dbContext.EFUsers.Find(user.UID);
   if (u == null)
      throw new DBModuleException();

   // check rule
   EFRule r = this.dbContext.EFRules.Find(rule.UID);
   if (r == null)
      throw new DBModuleException();


   // detach previous entities, so that we can attach with relation 
   this.dbContext.ObjectContext.Detach(u);
   this.dbContext.ObjectContext.Detach(r);

   // prepare entities
   var efUser = new EFUser { US_UID = user.UID };
   var efRule = new EFRule { RU_UID = rule.UID };
   efRule.Users.Add(efUser);

   // attach
   this.dbContext.EFRules.Attach(efRule);

   // delete relation
   efRule.Users.Remove(efUser);
}

public void UpdateUserField(User user)
{
    // get updating user
    EFUser efUser = this.dbContext.EFUsers.FirstOrDefault(u => u.UID == user.UID);
    if (efUser == null)
       throw new DBModuleException("Not found!");

    // perform update
    efUser.US_Field = user.Field;
}

My UnitOfWork implementation (only with EF6 or above):

public override void ExecuteTransaction(Action transAction)
{
    // make sure to make IsolationLevel flexible later
    using (var dbTransaction = this.dbContext.Database
                              .BeginTransaction(IsolationLevel.ReadCommitted)) 
    {
       try
       {
           transAction();

           // save changes if not done yet
           this.dbContext.SaveChanges();

           // commit transaction
           dbTransaction.Commit();
       }
       catch (System.Data.DataException ex)
       {
           // rollback
           dbTransaction.Rollback();

           // handling exception ceremony comes here
       }
    }

My problem is that DeleteRulesForUser attaches that user with only UID (as specified in the method), but then, later in the UpdateUserField EF is providing this user with only UID, instead of getting the user with complete data because FirstOrDefault calls the database. I assume this is the way how EF cache works(?). Anyhow, the update operation fails and I want to learn how to resolve this best.

I see the need of loading sometimes the entity with only UIDs, but sometimes with complete data.

I was thinking to detach the entities at the end of DeleteRulesForUser method, but I don't think is good for performance, due to the fact there are for-loops sometimes in BL. I also thought maybe I should rewrite my Update method, but don't know a good way to do that yet.

What would you recommend?

FYI: I have LazyLoading disabled.

Note: BL doesn't use EF classes, but proprietary domain classes (EF support has been added later). Don't criticize the existing architecture, I want a solution for this as is.

EDIT1: As I read a bit more, maybe AutoDetectChangesEnabled could help, but I need more info to find out.

Learner
  • 3,297
  • 4
  • 37
  • 62

2 Answers2

1

If you have the requirement to delete thousands of rules, I would suggest to drop Entity Framework and use ADO.NET and sql statements (or stored procedures), which is a lot faster and simpler. See Batch update/delete EF5.

There's also no reason for attaching or detaching objects. If you use EF, create a context from the moment you need it and dispose it as soon as possible.

Community
  • 1
  • 1
L-Four
  • 13,345
  • 9
  • 65
  • 109
  • Actually I already have a solution with plain Ado.Net and works ok, but I have to provide Oracle support besides MS SQL, so I thought EF may come to the rescue (that is why EF repositories have been created later), but it seems it comes with disadvatages as well – Learner Apr 02 '14 at 14:31
  • Then look for an Oracle provider (http://stackoverflow.com/questions/682773/how-to-choose-an-oracle-provider-for-net-application) – L-Four Apr 02 '14 at 14:33
  • I forgot to mention that my DAL is a thin wrapper over Stored Procedures, which will be a bit of a pain to migrate to Oracle. That is why I thought is good idea of having one ORM to satisfy multiple databases. – Learner Apr 02 '14 at 14:37
  • In the end, I almost have it, but I noticed this behavior which I want to fix somehow. – Learner Apr 02 '14 at 14:48
0

There is no reason to detach the objects from the object state manager. Just by looking at the code, it doesn't make much sense at what you are trying to do. Perhaps you don't have much experience with EF, but I would recommend reading up on how EF works before using it.

You could simply load the User entity and include the rules in the result set for a batch operation: var dbUser = ctx.Users.Include(x => x.Rules).FirstOrDefault(x => x.UID == user.UID);

Now that you have your fully populated user object and all the rules associated with them, you can find the rule you want to remove: var ruleToDelete = dbUser.Rules.FirstOrDefault(x => x.UID == rule.UID);.

Now you have the rule from the user you want to delete and it didn't cost you a trip back to the database. Now you can remove the rule: ctx.Rules.Remove(ruleToDelete);

Now when you call ctx.SaveChanges() the changes will be pushed back to the database. You do need to understand that Entity Framework wraps all insert, update, and delete statements in a transaction automatically for you. Also, EF doesn't have support for batch insert, update, and delete operations.

//Get the user from the db, should check for null after this
var dbUser = ctx.Users.Include(x => x.Rules).FirstOrDefault(x => x.UID == user.UID);

//Get the rule from the user object you want to remove
var ruleToDelete = dbUser.Rules.FirstOrDefault(x => x.UID == rule.UID);

//Set the state of the object to "Deleted" in the state manager so it will issue a delete statement
ctx.Rules.Remove(ruleToDelete);

//Save the changes to the database in a transaction
ctx.SaveChanges()
Justin
  • 3,337
  • 3
  • 16
  • 27
  • True, I am a beginner... I understand your answer, but I cannot do as you propose, because in my real application I will have thousand of "rules" and don't want to load them in memory. I need to do it somehow in a detached manner – Learner Apr 02 '14 at 14:18
  • How does someone remove a rule from a user then? I assume there is a screen with a list of all the rules, so aren't you already loading all the rules at one point or another? – Justin Apr 02 '14 at 15:47