Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DbSet.Attach(entity) vs DbContext.Entry(entity).State = EntityState.Modified

When I am in a detached scenario and get a dto from the client which I map into an entity to save it I do this:

context.Entry(entity).State = EntityState.Modified; context.SaveChanges(); 

For what is then the DbSet.Attach(entity)

or why should I use the .Attach method when EntityState.Modified already attaches the entity?

like image 582
Elisabeth Avatar asked Jun 22 '15 19:06

Elisabeth


People also ask

What is the primary difference between the add and attach methods on a DbContext?

Add is to indicate to the Entity Framework that we are going to want to insert a new record in the database. In contrast, Entry and Attach are used in scenarios where the record already exists in the database, and we simply want to make some kind of modification on it. var attach = context.

What is DbSet and DbContext?

DbContext generally represents a database connection and a set of tables. DbSet is used to represent a table. Your code sample doesn't fit the expected pattern. First, it is incomplete. Also, there are properties that really don't belong.

What is EntityState modified?

EntityState.Added : EntityState.Modified; context.SaveChanges(); } } Note that when you change the state to Modified all the properties of the entity will be marked as modified and all the property values will be sent to the database when SaveChanges is called.

What does DbSet attach do?

Attach is used to repopulate a context with an entity that is known to already exist in the database. SaveChanges will therefore not attempt to insert an attached entity into the database because it is assumed to already be there.


2 Answers

When you do context.Entry(entity).State = EntityState.Modified;, you are not only attaching the entity to the DbContext, you are also marking the whole entity as dirty. This means that when you do context.SaveChanges(), EF will generate an update statement that will update all the fields of the entity.

This is not always desired.

On the other hand, DbSet.Attach(entity) attaches the entity to the context without marking it dirty. It is equivalent to doing context.Entry(entity).State = EntityState.Unchanged;

When attaching this way, unless you then proceed to update a property on the entity, the next time you call context.SaveChanges(), EF will not generate a database update for this entity.

Even if you are planning on making an update to an entity, if the entity has a lot of properties (db columns) but you only want to update a few, you may find it advantageous to do a DbSet.Attach(entity), and then only update the few properties that need updating. Doing it this way will generate a more efficient update statement from EF. EF will only update the properties you modified (in contrast to context.Entry(entity).State = EntityState.Modified; which will cause all properties/columns to be updated)

Relevant documentation: Add/Attach and Entity States.

Code example

Let's say you have the following entity:

public class Person {     public int Id { get; set; } // primary key     public string FirstName { get; set; }     public string LastName { get; set; } } 

If your code looks like this:

context.Entry(personEntity).State = EntityState.Modified; context.SaveChanges(); 

The SQL generated will look something like this:

UPDATE person SET FirstName = 'whatever first name is',     LastName = 'whatever last name is' WHERE Id = 123; -- whatever Id is. 

Notice how the above update statement will update all the columns, regardless or whether you've actually changed the values or not.

In contrast, if your code uses the "normal" Attach like this:

context.People.Attach(personEntity); // State = Unchanged personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty. context.SaveChanges(); 

Then the generated update statement is different:

UPDATE person SET FirstName = 'John' WHERE Id = 123; -- whatever Id is. 

As you can see, the update statement only updates the values that were actually changed after you attached the entity to the context. Depending on the structure of your table, this can have a positive performance impact.

Now, which option is better for you depends entirely on what you are trying to do.

like image 130
sstan Avatar answered Sep 22 '22 17:09

sstan


Just in addition (to the marked answer) there is an important difference between context.Entry(entity).State = EntityState.Unchanged and context.Attach(entity) (in EF Core):

I did some tests to understand it more by myself (therefore this also includes some general reference testing), so this is my test-scenario:

  • I used EF Core 3.1.3
  • I used QueryTrackingBehavior.NoTracking
  • I used only attributes for mapping (see below)
  • I used different contexts to get the order and to update the order
  • I wiped the whole db for every test

These are the models:

public class Order {     public int Id { get; set; }     public string Comment { get; set; }     public string ShippingAddress { get; set; }     public DateTime? OrderDate { get; set; }     public List<OrderPos> OrderPositions { get; set; }     [ForeignKey("OrderedByUserId")]     public User OrderedByUser { get; set; }     public int? OrderedByUserId { get; set; } }  public class OrderPos {     public int Id { get; set; }     public string ArticleNo { get; set; }     public int Quantity { get; set; }     [ForeignKey("OrderId")]     public Order Order { get; set; }     public int? OrderId { get; set; } }  public class User {     public int Id { get; set; }     public string FirstName { get; set; }     public string LastName { get; set; } } 

This is the (original) test data in the database: enter image description here

To get the order:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault(); 

Now the tests:

Simple Update with EntityState:

db.Entry(order).State = EntityState.Unchanged; order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 2 Calls: // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5) // UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1 

Simple Update with Attach:

db.Attach(order); order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 1 Call: // UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1 // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5) // UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1 // UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1 

Update with changing Child-Ids with EntityState:

db.Entry(order).State = EntityState.Unchanged; order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUser.Id = 3; // will be IGNORED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED order.OrderPositions[0].Id = 3; // will be IGNORED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 2 Calls: // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5) // UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1 

Update with changing Child-Ids with Attach:

db.Attach(order); order.ShippingAddress = "Germany"; // would be UPDATED order.OrderedByUser.Id = 3; // will throw EXCEPTION order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED order.OrderPositions[0].Id = 3; // will throw EXCEPTION order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED db.SaveChanges(); // Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.) 

Note: This throws Exception, no matter if the Id was changed or was set to the original value, seems like the state of Id is set to "changed" and this is not allowed (because it's the primary key)

Update with changing Child-Ids as new (no difference between EntityState and Attach):

db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged; order.OrderedByUser = new User(); order.OrderedByUser.Id = 3; // // Reference will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3) db.SaveChanges(); // Will generate SQL in 2 Calls: // UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1 // UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3 

Note: See the difference to the Update with EntityState without new (above). This time the Name will be updated, because of the new User instance.

Update with changing the Reference-Ids with EntityState:

db.Entry(order).State = EntityState.Unchanged; order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUserId = 3; // will be UPDATED order.OrderedByUser.Id = 2; // will be IGNORED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED order.OrderPositions[0].Id = 3; // will be IGNORED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 2 Calls: // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5) // UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1 

Update with changing the Reference-Ids with Attach:

db.Attach(order); order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUserId = 3; // will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!) order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 1 Call: // UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1 // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5) // UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1 // UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1 

Note: The reference will be changed to User 3, but also the user 1 will be updated, I guess this is because the order.OrderedByUser.Id is unchanged (it's still 1).

Conclusion With EntityState you have more control, but you have to update sub-properties (second-level) by yourself. With Attach you can update everything (I guess with all levels of properties), but you have to keep an eye on references. Just for example: If User (OrderedByUser) would be a dropDown, changing the value via a dropDown might be overwrite the whole User-object. In this case the original dropDown-Value would be overwritten instead of the reference.

For me the best case is setting objects like OrderedByUser to null and only set the order.OrderedByUserId to the new value, if I only want change the reference (no matter if EntityState or Attach).

Hope this helps, I know it's a lot of text :D

like image 41
StewieG Avatar answered Sep 21 '22 17:09

StewieG