Chapter 5. Modifying Entities and Saving Changes

So far, we have focused on many ways to retrieve data from the database through the Entity Data Model (EDM). This is only part of the Entity Framework story and the beginning of the life cycle of an entity. Once you have retrieved entities you can modify them, delete them, or even add new ones and then save all of these changes back to the database. In this chapter, we’ll take a high-level look at the default way in which the Entity Framework tracks these changes and performs updates. This will prepare you for most of what you will be learning throughout the rest of the book, whether you are using high-level data-binding features or working deeply under the covers.

Note

In the next chapter, you’ll learn about overriding the default behavior for both querying and updating by using stored procedures.

How ObjectContext Manages Entities

In Chapters 3 and 4, you used an ObjectContext, the ProgrammingEFDB1Entities class, which inherits from ObjectContext, to create and execute queries. You also worked with the objects that were returned by those queries, whether they were entities, anonymous types, or objects within a DbDataRecord. The nature of this interaction was to iterate through the objects and extract a few properties to display in the console windows.

Those objects were created by an internal process called object materialization, which takes the returned data and builds the relevant objects for you. Depending on the query, these could be EntityObjects, anonymous types, or DbDataRecords. By default, for any EntityObjects that are materialized, the ObjectContext creates an extra object behind the scenes, called an ObjectStateEntry. It will use these ObjectStateEntry objects to keep track of any changes to their related entities. If you execute an additional query using the same context, more ObjectStateEntry objects will be created for any newly returned entities and the context will manage all of these as well. The context will keep track of its entries as long as it remains in memory.

The ObjectContext can track only entities. It cannot keep track of anonymous types or nonentity data that is returned in a DbDataRecord.

Note

You will learn about object materialization, ObjectStateEntry, change tracking, and more in great detail in Chapter 9.

Remembering Original Values and Keeping Track of Changes

ObjectStateEntry takes a snapshot of an entity’s values as it is first created, and then stores the original values and the current values as two separate sets. ObjectStateEntry also has an EntityState property whose value reflects the state of the entity (Unchanged, Modified, Added, Deleted). As the user modifies the objects, the ObjectContext updates the current values of the related ObjectStateEntry as well as its EntityState. As you learn more about the Entity Framework, you’ll discover how to locate and inspect the details of an ObjectStateEntry.

Note

The object itself also has an EntityState property. As long as the object is being managed by the context, its EntityState will always match the EntityState of the ObjectStateEntry. If the object is not being managed by the context, its state is Detached.

Entities have two different types of properties: scalar properties and navigation properties. ObjectStateEntry keeps track of only the scalar values of its related entity. The navigations are tracked in a very different way that is out of scope for this high-level overview but that you will learn quite a lot about in Chapter 9 as well as in Chapter 15, which focuses on relationships and associations.

As the scalar properties are changed—for example, Contact.LastName—the new value of LastName is stored in the ObjectStateEntry’s set of current values for that contact, and if the ObjectStateEntry.EntityState value was Unchanged at the time of the modification, its value will be set to Modified.

The SaveChanges Method

ObjectContext has a single method, SaveChanges, which persists back to the database all of the changes made to the entities. A call to SaveChanges will check for any ObjectStateEntry objects being managed by that context whose EntityState is not Unchanged, and then will use its details to build separate Insert, Update, and Delete commands to send to the database. We’ll start by focusing on entities that have come into the context as a result of queries and have been modified.

Example 5-1 shows a simple ObjectQuery to retrieve the first contact from the Contacts EntitySet. Remember that context.Contacts is a method that will return an ObjectQuery of Contact types. The example then uses the LINQ method First to pull back only the first result.

The FirstName and ModifiedDate properties are given new values, and then SaveChanges is called.

Example 5-1. Querying for a contact, editing, and then saving back to the database

VB
Using context As New PEF
  Dim contact = context.Contacts.First()
  contact.FirstName = "Julia"
  contact.ModifiedDate = DateTime.Now
  context.SaveChanges
End Using
C#
using (PEF context = new PEF())
{
  var contact = context.Contacts.First();
  contact.FirstName = "Julia";
  contact.ModifiedDate = DateTime.Now;
  context.SaveChanges();
}

Looking at the SQL Profiler, you can see the following parameterized Update command, which was sent to the database when SaveChanges was called:

exec sp_executesql N'update [dbo].[Contact]
set [FirstName] = @0, [ModifiedDate] = @1
where ([ContactID] = @2)
',N'@0 nchar(5),@1 datetime2(7),@2 int',
@0=N'Julia',@1='2008-11-22 18:12:18.7210000',@2=1

This command updates the Contact table, setting the FirstName and ModifiedDate properties for the Contact whose ContactID is 1. The values are passed in via parameters, and the last parameter, @2, shows the value used for the ContactID.

By comparing the original values in the ObjectStateEntry for that Contact entity to the current values, the ObjectContext determined that only the FirstName and ModifiedDate properties had changed, and therefore those are the only values that it sends into the command. It uses the value of the property that is marked as the EntityKey, ContactID, to identify which row to update.

Let’s see what happens when we have more than one entity.

Example 5-2 queries for all of those Roberts once again, including their addresses, and returns a List of the entity graphs: Contacts with Addresses. The example then randomly selects one of these contacts and changes its FirstName to Bobby. Another contact is selected and the Street property of the first Address is edited. Finally, SaveChanges is called.

Example 5-2. Editing various entities and calling SaveChanges

VB
Dim contacts = context.Contacts.Include("Addresses") _
                      .Where(Function(c) c.FirstName="Robert").ToList()
Dim contact = contacts(3)
contact.FirstName = "Bobby"
contact = contacts(5)
Dim address = contact.Addresses.ToList(0)
address.Street1 = "One Main Street"
context.SaveChanges
C#
var contacts = context.Contacts.Include("Addresses")
                      .Where(c =>c.FirstName=="Robert").ToList();
var contact = contacts[3];
contact.FirstName = "Bobby";
contact = contacts[5];
var address = contact.Addresses.ToList()[0];
address.Street1 = "One Main Street";
context.SaveChanges();

Initially, 12 contacts and 13 addresses were retrieved. Let’s look at the SQL commands sent to the database when SaveChanges is called:

exec sp_executesql N'update [dbo].[Address]
set [Street1] = @0
where ([addressID] = @1)
',N'@0 nchar(15),@1 int',@0=N'One Main Street',@1=2418

exec sp_executesql N'update [dbo].[Contact]
set [FirstName] = @0
where ([ContactID] = @1)
',N'@0 nchar(5),@1 int',@0=N'Bobby',@1=288

The first command sent to the database updates the single Address that was modified, and only its Street value and identity, AddressID, are included. Next, the command to update the contact was sent. None of the other entities was modified, so the ObjectContext doesn’t bother to construct or send any commands for those entities. The call to SaveChanges is very efficient in this aspect.

ObjectContext learned everything it needed to know to create these commands, not by looking at the Contact and Address objects that it was managing but by looking at the ObjectStateEntry objects that it was maintaining for each of the 12 Contact and 13 Address entities. ObjectContext first checked the EntityState to see whether anything needed to be processed. Because the EntityState for the untouched entities was Unchanged, it ignored them. For the two that were Modified, it compared the original values to the current values to determine what properties needed to be included in the Update command.

When the update completes, the two entities will be refreshed so that their EntityState is Unchanged, and the original values will be set to match the current values.

From Entity Framework Command to Native Command

In between the call to SaveChanges and the execution of SQL commands in the database, the Entity Framework did a lot of work under the covers to construct the command. The process is similar to how the commands and queries are compiled and converted into store queries.

As noted earlier, the first step in the process is to inspect all of the ObjectStateEntry objects for the entities that the context is managing. Those that have an EntityState of Unchanged are ignored. The Modified entities that you worked with earlier, as well as any that are Added or Deleted, are processed by the context. As the commands are built, the model’s metadata (conceptual, store, and mapping layers) are read and the mapping information is used to translate the entities and their properties into table and column names. The mappings also provide the knowledge to move from model relationships to database foreign keys. The ADO.NET provider, such as SqlClient, does the final job of constructing the appropriate native command.

You’ll look more closely at this process in later chapters.

Adding New Entities

Now that you have an idea of how edits are handled, let’s look at entities that are newly created in your application and then sent to the database as part of SaveChanges.

In Example 5-3, a new address is created in memory. Then, after attaching the address to a contact that was queried from the database, SaveChanges is called.

Note

There are many different ways to link entities to one another based on particular scenarios. You will learn about this in Chapter 15.

Example 5-3. Creating a new address in memory

VB
Dim contact = context.Contacts.Where(Function(c) c.FirstName = "Robert").First()
Dim address = New Address()
With address
  .Street1 = "One Main Street"
  .City = "Burlington"
  .StateProvince = "VT"
  .AddressType = "Business"
  .ModifiedDate = DateTime.Now
  'join the new address to the contact
  .Contact = contact
End With
context.SaveChanges()
C#
var contact = context.Contacts.Where(c => c.FirstName == "Robert").First();
var address = new Address();
address.Street1 = "One Main Street";
address.City = "Burlington";
address.StateProvince = "VT";
address.AddressType = "Business";
address.ModifiedDate = DateTime.Now;
//join the new address to the contact
address.Contact = contact;
context.SaveChanges();

When the newly created address is joined with the contact, because ObjectContext is managing the contact the context will recognize that it needs to create a new ObjectStateEntry for the address. Because the address has no identity key, its EntityState will be set to Added. When SaveChanges is called, because the EntityState is Added an Insert command is constructed and sent to the database. Here is that command:

exec sp_executesql N'insert [dbo].[Address]([Street1], [Street2], [City],
 [StateProvince], [CountryRegion], [PostalCode], [AddressType], [ContactID],
 [ModifiedDate])
values (@0, null, @1, @2, null, null, @3, @4, @5)
select [addressID]
from [dbo].[Address]
where @@ROWCOUNT > 0 and [addressID] = scope_identity()',
N'@0 nchar(15),@1 nchar(10),@2 nchar(2),@3 nchar(8),@4 int,@5 datetime2(7)',
@0=N'One Main Street',@1=N'Burlington',@2=N'VT',@3=N'Business',
@4=209,@5='2008-11-22 20:19:58.7090000'

Breaking Down the Native Insert Command

The preceding parameterized command does a number of notable things.

First, it has an Insert command that inserts a new address using the values of each property of the entity. Notice that even though the code did not set all of the properties, the command uses all of the properties and inserts defaults, in this case null, where the properties weren’t explicitly set in the code.

The fifth line down is the beginning of a Select command. In addition to inserting the new address, the command will return to the application the primary key value that the database generated for the new address. As part of the call to SaveChanges, the new address in the application code will receive its AddressID from the database so that you can continue working with it in code if you wish.

The last thing to point out in the command is that one of the fields in the Insert command is the ContactID. Remember that the Address entity does not have a ContactID property. The relationship between Contact and Address is through the navigation properties, and it is the mappings that interact with the ContactID foreign key value. When SaveChanges is called, part of the process that creates the native command identifies the relationship and uses the mappings in the model to determine that the ContactID of the related contact needs to be used for the ContactID field of the address being inserted into the database.

When the insert completes, not only will the address in memory have its new AddressID value, but like the update in the preceding section, the entity will be refreshed and its EntityState will be set to Unchanged.

Inserting New Parents and Children

The preceding example inserted a new address to an existing contact. What if you wanted to create a new contact with a new address? In typical data access scenarios, you would have to first insert the new contact, retrieve its ContactID, and then use that to insert the new address. SaveChanges does all of this for you when it sees that both are new and that they are related. It also uses the model’s mappings to figure out which is the dependent entity (in this case, Address) and needs the foreign key (ContactID). With this information, it executes the database inserts in the correct order.

The code in Example 5-4 creates a new contact on the fly using the Contact class’s CreateContact factory method.

Note

Recall that the model’s code generator creates a factory class for every EntityObject, which uses all of the non-nullable properties as its arguments.

The example then creates a new address in the same manner as with Example 5-3. Next, it joins the new contact to the new address. At this point, the context has no knowledge of these new entities; therefore, they need to be added to the context. Because the entities are joined, you can add either entity, and it will bring along the rest of the graph. So in this case, the contact is added explicitly and the address is pulled into the context along with the contact.

Note

AddToContacts is a method that the model’s code generator created. The generated context class has an AddTo method for each entity in the model, as well as a generic AddObject method. You’ll learn more about adding and attaching entities to the context and to each other in Chapter 15. You will also see a variety of examples of these methods in many of the samples throughout the book.

Finally, SaveChanges is called.

Example 5-4. Inserting a new contact with a new address

VB
Dim contact = Contact.CreateContact _
              (0, "Camey", "Combs", DateTime.Now, DateTime.Now)
Dim address = New Address()
address.Street1 = "One Main Street"
address.City = "Olympia"
address.StateProvince = "WA"
address.AddressType = "Business"
address.ModifiedDate = DateTime.Now
'join the new address to the contact
address.Contact = contact
'add the new graph to the context
context.AddToContacts(contact)
context.SaveChanges
C#
var contact = Contact.CreateContact
              (0, "Camey", "Combs", DateTime.Now, DateTime.Now);
var address = new Address();
address.Street1 = "One Main Street";
address.City = "Olympia";
address.StateProvince = "WA";
address.AddressType = "Business";
address.ModifiedDate = DateTime.Now;
//join the new address to the contact
address.Contact = contact;
//add the new graph to the context
context.AddToContacts(contact);
context.SaveChanges();

As the entities are added to the context, the context creates a new ObjectStateEntry for each one. Because they are being added and have no identity key, the EntityState for each is Added. SaveChanges handles these as it did with the previous insert, except that it also takes care of using the contact’s new ContactID when inserting the address.

The following SQL is the result of the call to SaveChanges. There are two commands. The first command inserts the new contact and does a Select to return the new contact’s ContactID.

The second command inserts the new address, and as you can see in the second-to-last line, the @4 parameter has a value of 709. This is the new ContactID. This command also selects the new address’s AddressID value to return to the application:

exec sp_executesql N'insert [dbo].[Contact]([FirstName], [LastName],
 [Title], [AddDate], [ModifiedDate])
values (@0, @1, null, @2, @3)
select [ContactID]
from [dbo].[Contact]
where @@ROWCOUNT > 0 and [ContactID] = scope_identity()',N'@0 nchar(5),
@1 nchar(5),@2 datetime2(7),@3 datetime2(7)',
@0=N'Camey',@1=N'Combs',@2='2008-11-23 08:14:41.6420000',
@3='2008-11-23 08:14:41.6420000'

exec sp_executesql N'insert [dbo].[Address]([Street1], [Street2], [City],
 [StateProvince], [CountryRegion], [PostalCode], [AddressType], [ContactID],
 [ModifiedDate])
values (@0, null, @1, @2, null, null, @3, @4, @5)
select [addressID]
from [dbo].[Address]
where @@ROWCOUNT > 0 and [addressID] = scope_identity()',
N'@0 nchar(15),@1 nchar(7),@2 nchar(2),@3 nchar(8),@4 int,@5 datetime2(7)',
@0=N'One Main Street',@1=N'Olympia',@2=N'WA',@3=N'Business',@4=709,
@5='2008-11-23 08:14:41.6470000'

As you build more complex models later in the book, you will see how the insert can handle various types of entities with data that is related through navigation properties. In addition, with other types of mappings, such as inheritance, you will see entities that map back to multiple database tables and even entities in a many-to-many relationship.

Deleting Entities

The last type of modification to look at is deleting entities. The Entity Framework has a very specific requirement for deleting data: it must have an entity in hand in order to delete it from the database. ObjectContext has a DeleteObject method that takes an EntityObject as a parameter—for example, an instance of a Contact. When DeleteObject is called, the context sets the EntityState of that object’s ObjectState Entry to Deleted. When SaveChanges is called, the context notes the Deleted EntityState and constructs a Delete command to send to the database.

If the entity has already been retrieved from the database, this will not pose a problem. But sometimes you might want to delete data from the database that has not been queried. Unfortunately, there is no way with the Entity Framework to delete data in the database directly unless you use your own stored procedures. Therefore, even in this case, you will need to first query for the object and then delete it.

Note

Although you will work with stored procedures in the next chapter, this particular usage is more advanced and will be covered in Chapter 13.

Example 5-5 demonstrates the scenario where the contact to be deleted has not yet been retrieved. It uses the GetObjectByKey method described in Chapter 4 to retrieve the contact.

Here you can also see how an EntityKey is constructed on the fly using the strongly typed EntitySet name (which includes the name of the EntityContainer, PEF), the name of the property that is the EntityKey, and the value of the key. Therefore, the EntityKey is for a Contact whose ContactID is 438.

The EntityKey, which is in the System.Data namespace, is passed into the GetObjectByKey method, which will first inspect the existing EntityObjects being managed by the context to see whether that contact has already been retrieved. If it is not found there, the context will create and execute a query to retrieve that contact from the data store.

Once the contact is in hand, it is passed into the DeleteObject method, which marks it for deletion by setting the EntityState to Deleted.

Example 5-5. Retrieving and deleting a contact entity

VB
Dim contactKey = New EntityKey("PEF.Contacts", "ContactID", 438)
Dim contact = context.GetObjectByKey(contactKey)
context.DeleteObject(contact)
context.SaveChanges
C#
var contactKey = new EntityKey("PEF.Contacts", "ContactID", 438);
var contact = context.GetObjectByKey(contactKey);
context.DeleteObject(contact);
context.SaveChanges();

Here is the Store command that GetObjectByKey executed, as well as the Delete command that was executed as a result of the call to SaveChanges:

exec sp_executesql N'SELECT
[Extent1].[ContactID] AS [ContactID],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Title] AS [Title],
[Extent1].[AddDate] AS [AddDate],
[Extent1].[ModifiedDate] AS [ModifiedDate]
FROM [dbo].[Contact] AS [Extent1]
WHERE [Extent1].[ContactID] = @p0',N'@p0 int',@p0=438

exec sp_executesql N'delete [dbo].[Contact]
where ([ContactID] = @0)',N'@0 int',@0=438

The Delete command simply passes in the ContactID to delete the appropriate data.

Summary

This chapter provided you with a high-level overview of how the Entity Framework creates the necessary Insert, Update, and Delete commands to store your changes to the database with a single call to SaveChanges. This is the default behavior of the Entity Framework and one of its core features.

However, you are not bound to the default behavior. It is possible to override this mechanism to leverage your own stored procedures. The Entity Framework has a number of ways to use stored procedures. The next chapter will introduce you to overriding the dynamic generation of Insert, Update, and Delete commands with your own stored procedures, as well as show you how to use stored procedures to query data. You also can work with stored procedures that the Designer does not support as easily. We will cover these more advanced techniques in Chapter 13.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.65.130