In Chapter 6, you learned about function mapping for stored procedures in the Entity Data Model (EDM). Mapping insert, update, and delete stored procedures to entities is the simplest way to use stored procedures in the EDM. But the scenarios in Chapter 6 cover only a narrow scope of the types of stored procedures many developers leverage in their applications.
Although version 1 of the Entity Framework has plenty of support for stored procedures, the Designer doesn’t support everything the model can do, which in some cases makes implementing stored procedures somewhat taxing.
This chapter will cover a number of different ways to implement
stored procedures beyond the function mapping you already performed in the
Designer that overrides the SaveChanges
behavior. These additional implementations will create functions that you
can call directly in your code.
In addition to implementing stored procedures from your database, you’ll also learn how to create native stored procedures directly in your model.
The first part of the chapter will focus on stored procedures that are used for querying the database. The latter part of the chapter will address stored procedures for performing inserts, updates, and deletes in your database.
In previous chapters, you learned to use functions for database
stored procedures—for example, the InsertPayment
and CustomersbyState
functions, which line up with
the Payment
entity and Customer
entity, respectively.
But what about procedures whose columns do not precisely match a particular entity type, such as a query stored procedure whose results do not match an entity, or an update procedure that takes values from a number of entities?
First we’ll look at read stored procedures that fall into this category, and later in the chapter you’ll see how to work with stored procedures that perform inserts, updates, and deletes, or even a combination of commands.
The Entity Framework supports stored procedures and user-defined functions (UDFs). Version 1 of the Entity Framework does not provide support for table-valued functions (TVFs), but this is planned for the next release.
You can map stored procedures to entities, as you have seen in
previous chapters. But you will find that many stored procedures can’t
be mapped to entities. Instead, you will call their functions directly.
You can call some of the functions as a method of the ObjectContext
, but others you can call only
with EntityClient
(EntityConnection
, EntityCommand
, etc.).
You can define UDFs in the store layer of your EDM, and the Entity Data Model Wizard and Update Model Wizard will pick them up. You can call UDFs only as part of an Entity SQL string, but not with LINQ. We’ll look at UDFs at the end of this chapter.
You can include functions in queries; however, only the UDF functions are truly composable. When the function is from a stored procedure only the procedure itself will be processed on the server side. The rest of the query is processed on the client side in memory. This is because in most databases, stored procedures are not composable.
For example, if you have a stored procedure that returns all orders for a particular company, and you write a LINQ to Entities query adding an additional filter to it, such as the following:
From o in context.OrdersforaCustomer(12345) Where order.Total>10000 Select order
the stored procedure will execute on the database, returning all orders for the customer; then, in memory, LINQ will query all of those orders and return only the subset. This is not a limitation of the Entity Framework, but the nature of stored procedures.
UDFs are composable, and therefore their EDM functions are composable as well.
A number of different query (or read) stored procedures need special handling in the EDM. Those that return randomly shaped resultsets might be an obvious target, but there’s also another scenario that is the result of your ability to customize the model. If you modify the property names of an entity, this can cause a conflict with stored procedures that return the fields that match those properties.
In this section, we’ll explore these scenarios and other query stored procedures that require special mapping in the EDM.
If the schema of the return type matches up exactly with an existing type in your model, you are a few clicks away from mapping the function. However, there is one caveat to this. The function truly expects an exact match. If you have changed property names in entities and they do not match column names being returned, the function will fail.
One function in the model that demonstrates this problem is
ActivitiesonaTrip
. Example 13-1 shows the
database procedure for this function. Because of the SELECT *
, the procedure returns all of the
columns from Activities
.
Example 13-1. The ActivitiessonaTrip stored procedure
SELECT * FROM [Activities] WHERE activityid IN (SELECT ActivityID FROM [EventActivities] WHERE eventid=@tripid)
In the model, the Activity
entity has a one-to-one mapping to the Activities
table, so the fields and
properties should line up exactly. The Activity
entity has the same fields; or does
it? The field names in the Activities
table are ActivityID
, Activity
, imagepath
, and Category
. You may recall that when changing
the original entity name from Activities
to Activity
, there was a conflict with the
property named Activity
, so you
changed the property name to ActivityName
. Even this minor change causes
the function to fail when it attempts to match up the results of the
returned data with the Activity
entity.
You’ll be able to implement the mapping function in the model, but when you try to execute the function you will get this error:
The data reader is incompatible with the specified 'BAModel.Activity'. A member of the type, 'ActivityName', does not have a corresponding column in the data reader with the same name.
Because neither the model nor the Designer gives you an
opportunity to define the mapping between the results and Activity
, you can’t provide the necessary
information to make this work.
The next version of the Entity Framework Design Tools, part of Visual Studio 2010, will have this capability.
One solution to this problem takes advantage of EntityClient
. You can perform an EntityClient
query to call the function.
Remember that EntityClient
does
not materialize its results as objects. But you can read through the
results and coerce them into the entity you want. This is the same
solution you will be using later in this chapter when we look at
solutions for using stored procedures that return scalar values or
randomly shaped results.
You could also leverage a DefiningQuery
, which you will learn about
a bit later in this chapter. You’ll be able to rename the fields
that result so that they can automatically line up with the Activity
entity.
What about inherited types? If you have a procedure whose
results match up with a derived type such as Customer
, you can map the function in the
Designer with no problem. The CustomersWhoTravelledinDateRange
stored
procedure returns all of the appropriate fields to match up with the
Customer
type. This includes fields
from the Customer
table, fields from the
Contact
table, and fields from the
ContactPersonalInfo
table.
You will see the originally misspelled Customer
table field, PrimaryDesintation
,
in the stored procedure as a nice reminder that you don’t have to
live with these problems in your EDM.
PROCEDURE CustomersWhoTravelledinDateRange --returns customer records with contact info for customers @startdate DATETIME, @enddate datetime AS SELECT Customers.ContactID, Customers.PrimaryDesintation, Customers.CustomerTypeID, Customers.InitialDate, Customers.SecondaryDestination, Customers.PrimaryActivity, Customers.SecondaryActivity, Customers.Notes, Contact.FirstName, Contact.LastName, Contact.Title, Contact.AddDate, Contact.ModifiedDate, ContactPersonalInfo.BirthDate, ContactPersonalInfo.HeightInches, ContactPersonalInfo.WeightPounds, ContactPersonalInfo.DietaryRestrictions,Contact.TimeStamp FROM Customers INNER JOIN Contact ON Customers.ContactID = Contact.ContactID INNER JOIN ContactPersonalInfo ON Customers.ContactID = ContactPersonalInfo.ContactID WHERE customers.contactid IN (SELECT Customers.ContactID FROM Customers INNER JOIN Reservations ON Customers.ContactID = Reservations.ContactID INNER JOIN Events ON Reservations.EventID = Events.EventID WHERE events.startdate>=@startdate AND events.startdate<=@enddate GROUP BY Customers.contactid)
You can use the Model Browser to create a function import for
this stored procedure and point the return type to the Customer
entity. You can test the function
with the code in Example 13-2.
Example 13-2. Calling a function that returns a derived type
VB
Using context As New BAEntities
Dim customers = context.CustomersWhoTravelledinDateRange _
(New Date(2006, 1, 1), New Date(2006, 12, 31))
End Using
C#
using (BAEntities context = new BAEntities())
{
var customers = context.CustomersWhoTravelledinDateRange=
(new DateTime(2006, 1, 1), new DateTime(2006, 12, 31));
}
More commonly, your existing stored procedures will not conveniently return results that match up with a predefined entity. Unfortunately, the Designer does not directly support these procedures. However, there are two ways to incorporate them into your model.
The first is to create an entity that matches up with the returned data. This is a little involved because the model requires every entity to map to something in the store layer. Therefore, although creating an entity in the conceptual layer is simple, you’ll need to manually create a matching entity in the store layer and then map the two. In effect, you are creating in the Store Schema Definition Layer (SSDL) a “fake” table; a representation of a table that does not actually exist in the database. This isn’t so bad for a handful of stored procedures, but it may become tiresome if you have a lot of stored procedures of this type.
If you have control of the database, a “trick” to get around the problem of stored procedures that return randomly shaped results is to create a view in the database that returns data of the same structure as the stored procedure. Remember that views become read-only entities in the model.
Once you have done this, you now have two roads you can follow.
The first is to just use the view in your model and forget the stored procedure. A nice benefit of this is that a view is composable, whereas the function derived from the stored procedure is not. You can write queries against the view and those queries will become native store commands, executed on the server. When using the function, though, you can call the function and you can even use it in a query; however, the function itself, as you saw in Chapter 6, will execute the stored procedure on the server, and then the results will be further manipulated on the client side by the rest of the query operators. This could result in very inefficient queries if your stored procedure returns many more entities than your query specifies.
If you have Insert
, Update
, and Delete
procedures that align with the
results of that view, you can map them back to that new entity using
function mapping and use it as your object. If you do this, you’ll
want to remove the entity that this is replacing so that you don’t
have update collisions.
The other path to follow is useful if you still want to use the
function that represents the read stored procedure. You can map the
FunctionImport
to that
entity.
However, you are still looking at a hack here, and this particular hack of using a view leaves you with your results sitting in a new entity type.
Let’s walk through the scenario with the PaymentsforPeriod
stored procedure. This
procedure pulls fields from a number of tables defined by INNER JOIN
s. The results do not match up
with any entities in the model. Example 13-3 shows the SQL
for this procedure.
Example 13-3. The PaymentsforPeriod stored procedure in the BreakAway database
PROCEDURE PaymentsforPeriod --returns payment, reservation, trip, and contact information @startDate AS DATETIME, @endDate AS DATETIME AS SELECT Payments.PaymentDate, Payments.Amount, Reservations.ReservationDate, Contact.FirstName, Contact.LastName, Events.StartDate, Events.EndDate, Locations.LocationName FROM Payments INNER JOIN Reservations ON Payments.ReservationID = Reservations.ReservationID INNER JOIN Contact ON Reservations.ContactID = Contact.ContactID INNER JOIN Events ON Reservations.EventID = Events.EventID INNER JOIN Locations ON Events.LocationID = Locations.LocationID WHERE payments.[PaymentDate] BETWEEN @startdate AND @enddate
The problem with this procedure is that it returns fields with the original column names from the table, and these don’t match the modified property names in the entity.
You could create a new view in the database, such as vPaymentsforPeriod
, that matches the
procedure with the exception that it takes no input parameters and has
no WHERE
clause. When this view is
pulled into the model, you will have a new entity named vPaymentsforPeriod
. Now, as shown in Figure 13-1, you can simply
create a FunctionImport
from the PaymentsforPeriod
function that returns a
vPaymentsforPeriod
type.
Now you can call that function and work with the read-only results. Example 13-4 shows this function being called in code.
Example 13-4. Calling a function that returns entities
VB
Using context As New BAEntities
Dim pmts = context.PaymentsforPeriod
(New Date(2008, 1, 1), New Date(2008, 12, 31))
End Using
C#
using (BAEntities context = new BAEntities())
{
var pmts = context.PaymentsforPeriod
(new DateTime(2008, 1, 1), new DateTime(2008, 12, 31));
}
The benefit of using the view to create an entity for
capturing the results of the stored procedure is that you will
receive an object that can be change-tracked and that will have its
relationships managed by the ObjectContext
.
The Entity Framework does not directly support queries that return multiple resultsets either. However, Colin Meek, one of the members of the Entity Framework team, created a project called EFExtensions that contains a method for using stored procedures that return multiple resultsets. In its current iteration, each resultset can match up with an existing entity or it can return arbitrary types. You can find EFExtensions on the MSDN Code Gallery at http://code.msdn.microsoft.com/EFExtensions/. Colin wrote an in-depth explanation of how these extensions work, along with a walkthrough of his sample application, on his blog, at http://blogs.msdn.com/meek/archive/2008/03/26/ado-entity-framework-stored-procedure-customization.aspx/.
Although the Designer supports queries that return primitive
types, such as Int32
and String
, the Designer does not generate code
for them. Therefore, you won’t be able to call the function directly
from the context, as you saw with some of the functions discussed
earlier in this chapter.
However, you can use EntityClient
to call these function
imports.
The TripSalesforDateRange
procedure shown in Example 13-5 returns a scalar
value representing the total sales from reservations made within a
specific date range.
Example 13-5. The TripSalesforDateRange stored procedure
PROCEDURE TripSalesforDateRange @startdate DATETIME, @enddate datetime AS SET NOCOUNT ON; SELECT SUM(Events.TripCostUSD) AS Expr1 FROM Reservations INNER JOIN Events ON Reservations.EventID = Events.EventID WHERE reservationdate BETWEEN @startdate AND @enddate
When creating a FunctionImport
from this, you would specify
that the return type is a scalar value of type Int32
, as shown in Example 13-6.
Example 13-6. Using EntityClient to call TripSalesforDateRange
VB
Private Sub ScalarFunction()
Dim result As Int32
Dim eConn = New EntityConnection("Name = BAEntities")
Dim eComm = eConn.CreateCommand
With eComm
.CommandType = CommandType.StoredProcedure
.CommandText = "BAEntities.TripSalesforDateRange"
.Parameters.AddWithValue("startdate", New Date(2006, 1, 1))
.Parameters.AddWithValue("enddate", New Date(2006, 12, 31))
eConn.Open()
result = Convert.ToInt32(.ExecuteScalar())
eConn.Close()
End With
End Sub
C#
private static void ScalarFunction()
{
Int32 result = 0;
var eConn = new EntityConnection("Name = BAEntities");
var eComm = eConn.CreateCommand();
eComm.CommandType = CommandType.StoredProcedure;
eComm.CommandText = "BAEntities.TripSalesforDateRange";
eComm.Parameters.AddWithValue("startdate", new DateTime(2006, 1, 1));
eComm.Parameters.AddWithValue("enddate", new DateTime(2006, 12, 31));
eConn.Open();
result = Convert.ToInt32(eComm.ExecuteScalar());
eConn.Close();
}
Sometimes a query you would like to use in your application does
not exist in the database. Although you cannot define native queries on
the fly in code, you can add native queries directly into the model
using the Function
element, and then
add your own query into the function using the CommandText
element.
As an example, it would be very convenient to use the PaymentbyPeriod
query to return payments for a
particular contact, rather than for all contacts in a date range. You
could do this by changing the WHERE
clause. The results of this new query would still match up with the
vPaymentsbyPeriod
entity in the
model.
You can do this directly in the SSDL without adding a new stored procedure to the database.
If the procedure did exist in the database, the following function would represent it in the model:
<Function Name="PaymentsforContact" IsComposable="false"> <Parameter Name="ContactID" Type="int" Mode="In"/> </Function>
The Function
element has a
child element called CommandText
. You
can enter native store commands, such as a SQL Server T-SQL query,
directly into the CommandText
element. Therefore, you can add the new query directly into the SSDL of
the model.
The entire function would now look like this:
<Function Name="PaymentsforContact" IsComposable="false"> <CommandText> SELECT Payments.PaymentDate, Payments.Amount, Reservations.ReservationDate, Contact.FirstName, Contact.LastName, Events.StartDate, Events.EndDate, Locations.LocationName FROM Payments INNER JOIN Reservations ON Payments.ReservationID = Reservations.ReservationID INNER JOIN Contact ON Reservations.ContactID = Contact.ContactID INNER JOIN Events ON Reservations.EventID = Events.EventID INNER JOIN Locations ON Events.LocationID = Locations.LocationID WHERE Contact.ContactID=@ContactID </CommandText> <Parameter Name="ContactID" Type="int" Mode="In"/> </Function>
If you have any less than (<
) signs in your query, you’ll need to
use the escaped notation (<
)
so that there is no conflict with the XML, which interprets <
as the beginning of a node. You’ll see
this in action in Implementing a DefiningQuery.
Now you can create a FunctionImport
for the PaymentsforContact
function and map it to the
vPaymentsbyPeriod
entity, and then
call this function in your code.
In addition to being able to add your own stored procedures to the
model, you can also add views that don’t exist in the database. However,
rather than using a function, you use an element called DefiningQuery
.
A DefiningQuery
is comparable
to a database view, which is a virtual table. Like a view, a DefiningQuery
cannot take parameters or
return multiple resultsets, as a stored procedure can.
However, because a DefiningQuery
is a virtual table, when you
write LINQ or Entity SQL queries against these views, query operators
such as WHERE
filters will be
processed on the server side. This is quite different from working
with functions that are embedded in the model. Queries written against
functions will have filters performed on the client side.
If you open the BreakAway model in the XML Editor, you will find
that you already have two DefiningQuery
elements. The EDM Wizard
turned all of the database views into DefiningQuery
elements in the SSDL.
The first one is for vOfficeAddresses
and contains a T-SQL query
against the database’s vOfficeAddresses
view:
<EntitySet Name="vOfficeAddresses" EntityType="BreakAwayModel.Store.vOfficeAddresses" store:Type="Tables" store:Schema="dbo" store:Name="vOfficeAddresses"> <DefiningQuery> SELECT [vOfficeAddresses].[FirstName] AS [FirstName], [vOfficeAddresses].[LastName] AS [LastName], [vOfficeAddresses].[addressID] AS [addressID], [vOfficeAddresses].[Street1] AS [Street1], [vOfficeAddresses].[Street2] AS [Street2], [vOfficeAddresses].[City] AS [City], [vOfficeAddresses].[StateProvince] AS [StateProvince], [vOfficeAddresses].[CountryRegion] AS [CountryRegion], [vOfficeAddresses].[PostalCode] AS [PostalCode], [vOfficeAddresses].[AddressType] AS [AddressType], [vOfficeAddresses].[ContactID] AS [ContactID], [vOfficeAddresses].[ModifiedDate] AS [ModifiedDate] FROM [dbo].[vOfficeAddresses] AS [vOfficeAddresses] </DefiningQuery> </EntitySet>
The EntitySet
attributes are
different from standard EntitySet
definitions in the store layer. For the sake of comparison, here is
the EntitySet
for the Payments
table:
<EntitySet Name="Payments" EntityType="BreakAwayModel.Store.Payments" store:Type="Tables" />
The DefiningQuery
in the
vOfficeAddresses EntitySet
surfaces
the results of the vOfficeAddresses
view as a SELECT
query. Rather than just duplicating
the SQL of the existing view, it does a SELECT
against the existing view. Since
database views are most often read-only, using this SELECT
explicitly restricts the Entity
Framework from attempting to perform inserts, updates, or deletes
against the view.
Because a database view has no primary key, the wizard infers an
EntityKey
by combining the non-nullable fields of the
view. In the SSDL, the wizard also inserts a comment indicating this
action:
<!--Errors Found During Generation: warning 6002: The table/view BreakAway.dbo.vOfficeAddresses' does not have a primary key defined. The key has been inferred and the definition was created as a read-only table/view. --> <EntityType Name="vOfficeAddresses"> <Key> <PropertyRef Name="FirstName" /> <PropertyRef Name="LastName" /> <PropertyRef Name="addressID" /> <PropertyRef Name="AddressType" /> <PropertyRef Name="ContactID" /> <PropertyRef Name="ModifiedDate" /> </Key> <-- Property Elements --> </EntityType>
The fact that the EntitySet
is defined with a DefiningQuery
has
no other impact on the entity in the CSDL or the mappings. Figures
13-2 and 13-3 show the entity in the model and
its mappings back to the entity defined in the SSDL. The only
difference from table-based entities is the inability to persist
changes to the database from the view-based entities without using
stored procedures for updating.
“DefiningQuery provides an ultimate escape hatch for cases where the mapping is too complex to define in MSL.”
—Mike Pizzo, principal architect on the Data Programmability team at Microsoft, in the MSDN forums for the Entity Framework
DefiningQuery
really is the
ultimate escape hatch. Even with the incredible flexibility that the
model’s various mapping capabilities provide, there still may be some
things that you just cannot manage to pull off.
A DefiningQuery
lets you add
queries using the store’s native language—for example, T-SQL or
PL/SQL—directly to the store layer of the model. It’s the last step
before swallowing your modeling wizardry pride and asking the database
administrator to add another view or stored procedure to the database;
or in cases where modifying the database is not a possibility.
In addition to creating completely new database views with a
DefiningQuery
, there are other uses
for DefiningQuery
. One example is
to write a DefiningQuery
that
returns an entity with properties that don’t exist in the database,
such as a calculated property. Although you could achieve the same
effect using partial classes, that does not get the property into the
model itself, and this could make a big difference if the model is to
be shared with other application developers, report builders, or
additional EDM consumers in your enterprise.
Also, when you create your own DefiningQuery
, if the model does not already
have an entity that lines up with its results, you will have to create
all of the model elements yourself: the Entity
and EntitySet
in the CSDL, the Entity
and EntitySet
in the SSDL, and the mappings. You
already did that the easy way by creating a new view in the database.
In the next walkthrough, along with creating a DefiningQuery
, you will see how to implement
these additional necessary elements manually in the model.
A view that would be very useful for BreakAway’s team to have is one that displays reservations that have not been fully paid. Although you can express the query in Entity SQL, as shown in Example 13-7, it is somewhat challenging to construct.
Example 13-7. A complicated Entity SQL query
(SELECT r.reservationID,r.Customer.ContactID,r.Trip.TripID, SUM(SELECT VALUE p.amount from r.Payments as p) AS PaymentsTotal, r.Trip.TripCostUSD AS TripCost FROM BAEntities.reservations AS r WHERE SUM(SELECT VALUE p.amount FROM r.Payments AS p) < r.Trip.TripCostUSD) UNION (SELECT r.reservationID,r.Customer.ContactID,r.Trip.TripID,0, r.Trip.TripCostUSD FROM BAEntities.Reservations AS r WHERE EXISTS(SELECT p from r.Payments AS p)= false)
It would be nice to have this view be part of the model, since
it would come in handy in a number of places. You could get at this
particular data in other ways, such as by having a PaidinFull
Boolean added to the Reservation
table in the database and then
creating a derived type, but that would result in the need for
additional logic to keep that Boolean valid.
Another option might be to create a QueryView
. But because it is a complex
query, you might want to have more control over the SQL that hits the
database. More importantly, you may be unable to work out the Entity
SQL even if you are able to express the query in your native query
syntax (e.g., T-SQL). A DefiningQuery
allows you to do just that:
add the native query into your model.
To create the DefiningQuery
you’ll need the following elements in your model:
The native command to express the query
An Entity
, and an
EntitySet
in the CSDL
A virtual table in the SSDL in the form of an Entity
An EntitySet
in the SSDL
to contain the DefiningQuery
A mapping between the entity and the virtual table
You can create items 2 and 5 in the preceding list using the Designer, whereas you must create the others using the XML Editor.
The first step to implementing the UnpaidReservations DefiningQuery
is to
rewrite the Entity SQL expression in Example 13-7 as a native database
query. For SQL Server, that would look like Example 13-8. This is much
simpler than the query that would be constructed from the Entity SQL
expression shown earlier.
Example 13-8. The Entity SQL query from Example 13-7 expressed in T-SQL
SELECT r.reservationid, SUM(amount) AS PaymentTotal, MIN(events.[TripCostUSD]) AS cost FROM reservations r JOIN payments p ON r.[ReservationID]=p.[ReservationID] JOIN events ON r.[EventID]=events.[EventID] GROUP BY r.[ReservationID] HAVING SUM(amount)<min(TripCostUSD) UNION SELECT r.reservationid, 0,MIN(events.[TripCostUSD]) AS cost FROM reservations r JOIN events ON r.[EventID]=events.[EventID] WHERE r.[ReservationID] NOT IN (SELECT reservationID FROM payments) GROUP BY r.[ReservationID]
This DefiningQuery
will be a
permanent addition to the BreakAway model, not the test model you used
for QueryView
s. Be sure to switch
back to the BreakAway model when making these changes:
Create a new entity in the Designer, named UnpaidReservation
, and name its EntitySet
UnpaidReservations
.
Add the following scalar properties which match the resultset of the query:
ReservationID
(Type=Int32
, Nullable=false
)
PaymentTotal
(Type=Decimal
, Nullable=false
)
Cost
(Type=Int32
, Nullable=false
)
Make ReservationID
the
EntityKey
for this
entity.
Open the model in the XML Editor and scroll down to the
EntityContainer
element of the
SSDL section. This is where you will add the EntitySet
with the DefiningQuery
.
Add the EntitySet
and
DefiningQuery
element:
<EntitySet Name="UnpaidReservations" EntityType="BAModel.Store.UnpaidReservation"> <DefiningQuery> </DefiningQuery> </EntitySet>
The additional attributes that the vOfficeAddress EntitySet
uses (that
is, store:Schema
and store:Name
) are not used here. Those
attributes are necessary so that when the Update Model Wizard is
called, the Designer knows how to resolve the views properly.
Because the EntitySet
you are creating is
virtual and it does not exist in the database, those attributes
are not required.
Within the DefiningQuery
tags, enter the stored procedure listed in Example 13-8, with one
exception. The XML will be confused by the <
, so you will need to replace it
with an HTML-encoded version, <
. That line should look like
this:
HAVING SUM(amount)<min(TripCostUSD
Next, you’ll need to create the virtual table to which the
UnpaidReservation
entity will
map.
In the section where EntityType
elements are defined within
the SSDL, add the UnpaidReservation
virtual table:
<EntityType Name="UnpaidReservation" > <Key> <PropertyRef Name="ReservationID" /> </Key> <Property Name="ReservationID" Type="int" Nullable="false" /> <Property Name="PaymentTotal" Type="money" Nullable="false" /> <Property Name="Cost" Type="money" Nullable="false" /> </EntityType>
Save and close the model, and then open it in the Designer so that you can map the entity to the virtual table you just created. The mapping should look like Figure 13-4.
Now that you have the entity in the model, you can create an
association back to the Reservation
entity and tie right into the
model and all of the other relationships. This will be a great
benefit because you will be able to provide additional details from
queries against UnpaidReservation
.
Although we know the relationship to be a One to Zero or One
relationship, where there may be an UnpaidReservation
entity for a
particular reservation but never more than one, you should not
define the association as a 1:0..1. This “virtual” entity will
create a problem when you attempt to delete a reservation entity.
The model constraints will expect you to delete an UnpaidReservation
as well. With entities
that are mapped to database tables, this is not a problem. In this
case you can avoid this problem by defining a One to Many
relationship between Reservation
and UnpaidReservation
.
You can do this in a few ways; one is by right-clicking the
UnpaidReservation
entity, and
then choosing Add and then Association from the context menu. Fill
out the New Association form as shown in Figure 13-5.
The last step is to map the new association. Create the mapping so that it looks like Figure 13-6.
Now you can see the DefiningQuery
and the association in
action.
Add the method in Example 13-9 into the test module
you created at the beginning of this chapter, and then run it to see
how UnpaidReservation
is plugged
right into the model, leveraging its relationship to the Reservation
entity.
Example 13-9. Code to test the new DefiningQuery
VB
Private Sub DefiningQuery()
Using context As New BAEntities
Dim unpaidReservations =
From ur In context.UnpaidReservations _
.Include("Reservation.Customer") _
Select ur
For Each unpaid In unpaidReservations
Dim custOwes = Convert.ToDecimal(unpaid.Cost-unpaid.PaymentTotal)
Dim custname As String
With unpaid.Reservation.Customer
custname = .LastName.Trim & ", " & .FirstName.Trim
End With
Console.WriteLine("Cust: {0} owes {1} for the {2} Res", _
custname, custOwes,
unpaid.Reservation.ReservationDate)
Next
End Using
End Sub
C#
private static void DefiningQuery()
{
using (BAEntities context = new BAEntities())
{
var unpaidReservations =
from ur in context.UnpaidReservations
.Include("Reservation.Customer")
select ur;
foreach (var unpaid in unpaidReservations)
{
var custOwes = Convert.ToDecimal(unpaid.Cost - unpaid.PaymentTotal);
string custname = null;
custname = unpaid.Reservation.Customer.LastName.Trim() + ", " +
unpaid.Reservation.Customer.FirstName.Trim();
Console.WriteLine("Cust: {0} owes {1} for the {2} Reservation",
custname, custOwes,
unpaid.Reservation.ReservationDate);
}
}
}
This example demonstrates that the entity created by DefiningQuery
behaves like any other
entity. The query uses Include
to
show eager loading through its association to Reservation
and then Reservation
’s association to Customer
. After the data is returned, you
can traverse the relationships to get at the rest of the information
that makes UnpaidReservation
much
more meaningful.
As you learned earlier, the Designer’s Update Model Wizard
overwrites portions of the SSDL content of the .edmx file. However, what it overwrites
are the nodes that represent objects in the database. Because the
DefiningQuery
is independent of
the data store, the elements you have added to the SSDL for this
mapping should not be affected if you update the model using the
wizard.
DefiningQuery
also allows you
to solve more complex problems. Here is another quote from Mike Pizzo,
taken from the MSDN forums, describing the ability to create mappings
that you cannot create with entities that map directly to
tables:
…with DefiningQuery, you can map multiple entities to the same table outside of a type hierarchy, or a single entity to multiple rows within a single table (I did a demo at TechEd where I mapped an “Activity” Entity to a Sharepoint schema in which the properties of the Activity were actually mapped to different rows within a single “universal” table according to a row ordinal). The list goes on… In fact, every time I think I’ve found a mapping scenario that we don’t support in Entity Framework 1.0, I find a way to do it using DefiningQuery.
So far, this chapter has focused on retrieving data from the database. In addition to the many views and stored procedures for read operations that have been implemented for your databases, you also probably have many Database Manipulation Language (DML) procedures for performing updates, inserts, and deletes.
In Chapter 6, you
learned how to use the simplest form of these in the model, by
performing function mapping for the Insert
, Update
, and Delete
functions of particular entities. You
also did this in an earlier chapter with the Payment
entity. Yet you can use DML procedures
in many other scenarios. Leveraging them in your model and using them
with the Entity Framework is possible, if not always pretty.
The BreakAway database has a stored procedure called CreateCustomerfromContact
. This is an
important function for BreakAway’s business model. The company has
many contacts who are potential customers, yet they are not customers
until they book their first trip. That’s when BreakAway begins to
track more details regarding the customer. Sometimes the company needs
to create a new Customer
that does
not already have a Contact
in the
database, and the inheritance in the model takes care of
that.
But if the Contact
record
already exists and you want to create a new Customer
record to tie back to that Contact
, inserting a Customer
entity won’t work, because that
will attempt to insert a new Contact
as well.
CreateCustomerfromContact
solves this problem, and not only extends the Contact
to be a Customer
but also passes back the newly
created customer so that it can be used immediately. It takes a
ContactID
as a parameter, inserts a
new row into the Customer
table
using that ContactID
, and then returns a complete
Customer
. The results map directly
back to a Customer
entity. Here is the T-SQL for
the procedure:
INSERT INTO customers (ContactID,customers.[InitialDate]) VALUES (@contactid,GETDATE()) INSERT INTO ContactPersonalInfo (ContactID) VALUES (@contactid) SELECT Customers.*, Contact.FirstName, Contact.LastName, Contact.Title, Contact.AddDate, Contact.ModifiedDate, CPI.BirthDate, CPI.HeightInches, CPI.WeightPounds, CPI.DietaryRestrictions FROM Customers INNER JOIN Contact ON Customers.ContactID = Contact.ContactID INNER JOIN ContactPersonalInfo CPI ON Customers.ContactID = CPI.ContactID WHERE customers.contactid
You can create a FunctionImport
for this stored procedure and
set its return type to Customer
.
Then you can call the function pretty easily in your code. Example 13-10 shows a method
that calls the function and retrieves the newly created
customer.
Before it executes the function, it first tests to ensure that the contact already exists in the database and that it is not already a customer.
Example 13-10. Using a function to turn an existing contact into a customer
VB
Public Function ConvertContacttoCustomer(ByVal contactID As Integer)
As Customer
Using context As New BAEntities
Dim contact = context.Contacts _
.Where(Function(c) c.ContactID = contactID) _
.FirstOrDefault
If contact IsNot Nothing Then
If Not TypeOf (contact) Is Customer Then
'remove contact from the context and then from memory
context.Detach(contact)
contact = Nothing
'call the function which returns a customer
Return context.CreateCustomerfromContact(contactID).First
End If
End If
End Using
Return Nothing
End Function
C#
public static BAGA.Customer ConvertContacttoCustomer(int contactID)
{
using (BAEntities context = new BAEntities())
{
var contact = context.Contacts
.Where((c) => c.ContactID == contactID)
.FirstOrDefault();
if (contact != null)
{
if (!((contact) is Customer))
{
//remove contact from the context and then from memory
context.Detach(contact);
contact = null;
//call the function which returns a customer
return context.CreateCustomerfromContact(contactID).First();
}
}
}
return null;
}
It’s necessary to remove the contact from memory before
returning the new customer, because you would otherwise have a
conflict. When you first queried for the contact, the query returned
a non-Customer
type of contact.
When you call the function and it attempts to return a contact with
the same ContactID
but of a
different type, you will get an exception. The customer will have
been created; the problem is just a conflict when an attempt is made
to add the newly returned Customer
into the context.
This is nice for a test module, but what about the real world?
This function gives you the ability to turn a contact into a customer,
and you have a number of options for leveraging it. You may want it to
be an explicit action whereby a user would select a contact and do
something in the UI to tell the system to create a customer from the
contact. Another option is to have it happen implicitly, whereby if
the user attempts to perform a Customer
-like action on a non-Customer
(e.g., add a reservation), the
system could, in the background, go ahead and create the customer
identity for that contact if necessary.
What if you have a stored procedure in your database that
performs a modification to the database, but doesn’t return any
results? Or returns a scalar value as the result? Or returns randomly
shaped results? Only functions that return entities will be built into
the classes that are generated from the model. Functions that return
scalars or nothing won’t be available as methods of the context.
Instead, you’ll need to call them from EntityClient
.
You can use EntityClient
and
one of EntityCommand
’s Execute
methods to call these functions.
Like the other ADO.NET Command
classes, you can call ExecuteNonQuery
, ExecuteScalar
, or ExecuteReader
from EntityCommand
. You already used ExecuteReader
to
perform queries. Let’s take a look at these commands for calling
functions.
CancelTrip
is a procedure
in the database that cancels all reservations and adds negative
payments (to balance out the existing payments) for all clients who
were on a particular trip. The procedure returns a Boolean
indicating whether the procedure executed successfully.
The stored procedure takes a single parameter, the EventID
(remember that in the database, the table for trips is
called Events
), and then performs
all of the necessary actions. The procedure would also appear in the
SSDL as a function such as the following:
<Function Name="CancelTrip" Aggregate="false" BuiltIn="false" NiladicFunction="false" IsComposable="false" ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo"> <Parameter Name="eventID" Type="int" Mode="In" /> </Function>
You can call this function in the same way you called the
ScalarFunction
in Example 13-7. The only difference is
that you are capturing a Boolean as the return:
VB
Private Sub CancelTrip(ByVal TripID As Integer)
Dim result As Boolean
Using eConn = New EntityConnection("Name = BAEntities")
Dim eComm = eConn.CreateCommand
With eComm
.CommandType = CommandType.StoredProcedure
.CommandText = "BAEntities.CancelTrip"
.Parameters.AddWithValue("eventid", TripID)
eConn.Open()
result = Convert.ToBoolean(.ExecuteScalar())
eConn.Close()
End With
End Using
End Sub
C#
private static void CancelTrip(int TripID)
{
bool result = false;
using (var eConn = new EntityConnection("Name = BAEntities"))
{
var eComm = eConn.CreateCommand();
eComm.CommandType = CommandType.StoredProcedure;
eComm.CommandText = "BAEntities.CancelTrip";
eComm.Parameters.AddWithValue("eventid", TripID);
eConn.Open();
result = Convert.ToBoolean(eComm.ExecuteScalar());
eConn.Close();
}
}
Earlier in this chapter, you defined a T-SQL query directly in the
SSDL by using the <CommandText>
element of <Function>
. That
example was for a read stored procedure.
You can do the same for insert, update, and delete procedures, as well. And as with the other functions, how you call these functions depends on the return type.
You already have worked with functions for database stored
procedures in previous chapters. For example, the InsertPayment
stored procedure is wrapped by
the InsertPayment
function in the
model.
If you want to try out this function on your own, place it in
the extra model you created to test out QueryView
rather than in the BreakAway
model.
Imagine that the BreakAway database didn’t already have stored
procedures for inserting contacts, or that you wanted to change the
definition of the InsertContact
procedure that exists in the database. Here’s how you would construct
a new function:
<Function Name="InsertContactVirtual" IsComposable="false"> <CommandText> INSERT INTO Contact ([FirstName] ,[LastName] ,[Title] ,[AddDate] ,[ModifiedDate]) VALUES (@FirstName,@LastName,@Title,GETDATE(),GETDATE()) SELECT SCOPE_IDENTITY() as ContactID </CommandText> <Parameter Name="FirstName" Type="nchar" Mode="In"/> <Parameter Name="LastName" Type="nchar" Mode="In"/> <Parameter Name="Title" Type="nchar" Mode="In"/> </Function>
You can then map this function to the Contact
entity using the Designer just as
you would map other functions.
Compare this to the function the wizard created from the
existing InsertContact
function:
<Function Name="InsertContact" Aggregate="false" BuiltIn="false" NiladicFunction="false" IsComposable="false" ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo"> <Parameter Name="FirstName" Type="nchar" Mode="In" /> <Parameter Name="LastName" Type="nchar" Mode="In" /> <Parameter Name="Title" Type="nchar" Mode="In" /> </Function>
As described in Chapter 6, the two required
attributes for a function are Name
and IsComposable
.
Like the DefiningQuery
, the
Designer only partially supports custom functions. You need to create
them in the XML Editor, but once they are there you can still open the
Designer and use it to map the virtual functions to entities.
Additionally, because these are not contained in actual database
objects, they should remain intact if you use the Update Model Wizard
to refresh the database elements in the store layer of your
model.
One more rule regarding stored procedures in the EDM may come as a surprise, whether you are using the Designer or implementing the stored procedures by hand.
When mapping stored procedures to base or derived types, you are
also required to map the stored procedures to any other type within the
inheritance structure. Therefore, if you map a function to a base type,
you must also map a function to its derived types. Conversely, mapping
to a derived type, such as Customer
,
requires that you also map functions to the base type (Contact
) and any other derived types (NonCustomer
). If you forget this rule, the
compiler will happily remind you with an error message. The following
error message, which results when you have mapped to the Customer
entity but not the Contact
, is an example:
If an EntitySet mapping includes a function binding, function bindings must be included for all types. The following types do not have function bindings: BreakAwayModel.Contact.
You can take a few approaches when working with stored
procedures. The procedures in the BreakAway database take the more
standard route, which is to simply perform the tasks at hand. The
database contains stored procedures for performing inserts, updates,
and deletes on customers as well as contacts. InsertCustomer
, UpdateCustomer
, and DeleteCustomer
interact with the Customer
, Contact
, and ContactPersonalInfo
tables, and
InsertContact
, UpdateContact
, and DeleteContact
interact with only the
Contact
table.
When the Entity Framework uses these stored procedures it will
not overlap them. When saving changes to Customer
s it will call only the Customer
entity’s functions. When saving
changes to Contact
s it will call
only the Contact
entity’s
functions.
You can try to map these functions, or just be prepared for when you are defining your own model with inherited entities and stored procedures.
Many databases allow you to create your own functions, called user-defined functions, or UDFs. In SQL Server, these can be table-valued functions, scalar functions, or array functions. The Entity Framework’s EDM supports UDFs, with the exception of table-valued functions.
The EDM Wizard and the Update Model Wizard list UDFs along with stored procedures in the Stored Procedures node. Like stored procedures, UDFs are resolved as functions in the store layer of your model.
In the BreakAway database, because a customer’s weight is stored
in U.S. pounds, a function is defined to convert pounds into kilograms.
It’s called ufnLBtoKG
. If you were to
select this UDF in either of the wizards, you would find the following
function in the SSDL section of the EDMX file:
<Function Name="ufnLBtoKG" ReturnType="nvarchar" Aggregate="false" BuiltIn="false" NiladicFunction="false" IsComposable="true" ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo"> <Parameter Name="Pounds" Type="int" Mode="In" /> </Function>
Notice that the IsComposable
attribute is true
. This is different
from the stored procedures whose IsComposable
attribute must be false
. You can use UDFs as parts of queries.
Another big difference between UDFs and stored procedures is that you call them directly from the store layer rather than doing function mapping and calling them from the conceptual model.
This means that the functions are not available in LINQ. You can
access them only in Entity SQL statements. You can use this with
ObjectQuery
or with EntityClient
.
Example 13-11 uses the ufnLBtoKG
function with Entity SQL. You will
find that there is a surprising difference between this expression and
those you wrote earlier. This is due to the fact that the function is
only in the store.
The example will return a list of customer names, their weight in pounds, and their weight in kilograms.
Because Customer
is a derived
type, you will need to use the TREAT
AS
Entity SQL operator that you learned about in Chapter 12. Remember that Entity SQL
points back to the assembly namespace, not the model namespace when
casting to derived types.
Example 13-11. Querying with a UDF
VB
Dim esql = "select TREAT(c as BAGA.Customer).WeightPounds," & _
"BAModel.Store.ufnLBtoKG(TREAT(c as BAGA.Customer).WeightPounds) " & _
"from BAEntities.Contacts AS c where c is of(BAGA.Customer)"
Dim query = context.CreateQuery(Of DbDataRecord)(esql)
Dim weightList = query.ToList
C#
var esql = "select TREAT(c as BAGA.Customer).WeightPounds," +
"BAModel.Store.ufnLBtoKG(TREAT(c as BAGA.Customer).WeightPounds) " +
"from BAEntities.Contacts AS c where c is of(BAGA.Customer)"
Dim query == context.CreateQuery<DbDataRecord>(esql);
var weightList = query.ToList();
Notice how the function is called: BAModel.Store.ufnLBtoKG
. It is using the
strongly typed name of the function in the store layer, not the
CSDL.
Why do you need to use the SSDL schema to reference the UDF function in the query? This is definitely not expected, and I may not have even figured it out on my own if it weren’t for an example I happened to find hiding in the MSDN forums. You’ll learn the answer at the end of this section.
In fact, if you use function mapping to map this function back to the conceptual layer, at runtime you will get the following error when executing a query that uses the function:
"A FunctionImport is mapped to a storage function 'BAModel.Store.ufnLBtoKG' that can be composed. Only stored procedure functions may be mapped."
Entity Framework does not currently support mapping UDFs into the model. This is why you must access it directly from the SSDL.
As you learned in this chapter, the Entity Framework supports stored procedures in many more ways than the Designer-supported function mappings. And there’s not much that you can’t pull off. The only drawback is that in some cases, much more manual effort may be involved than you might want to employ.
For some read stored procedures, you may find
that it is easier to create a view that returns a similarly shaped
result and implement that in your model instead of the stored procedure.
You can also define native stored procedures directly in your model for
reading or writing to the database. Some of the functions that result
from stored procedures can be called as a method of the ObjectContext
, whereas others must be called
from EntityConnection
.
For organizations that have an investment in stored procedures but want to leverage the model and the change tracking of the Entity Framework, additional effort will be required to get the best of both worlds.
If you have myriad stored procedures that you must use, you may find the amount of manual work in the model to be too much. Although version 1 of the Entity Framework is focused more on query composition and query processing—which is why many of these scenarios require more effort to implement—Microsoft promises better support for stored procedures in future versions of the Entity Framework, starting with the next version, which will be part of the Visual Studio 2010 release.
18.188.178.181