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.
In the next chapter, you’ll learn about overriding the default behavior for both querying and updating by using stored procedures.
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 EntityObject
s,
anonymous types, or DbDataRecord
s. By
default, for any EntityObject
s 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
.
You will learn about object materialization, ObjectStateEntry
, change tracking, and more
in great detail in Chapter 9.
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
.
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
.
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.
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.
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.
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'
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
.
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.
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.
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.
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.
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 EntityObject
s
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.
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.
18.116.65.130