Chapter 13. Working with Stored Procedures When Function Mapping Won’t Do

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.

Does the Procedure Line Up with an Entity?

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.

Overview of Procedures, UDFs, and TVFs in the EDM

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.

Composing Queries Against Functions

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.

Mapping and Executing Query Stored Procedures

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.

Using Functions That Match an Entity Whose Property Names Have Been Changed

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.

Note

The next version of the Entity Framework Design Tools, part of Visual Studio 2010, will have this capability.

How can you get around this problem?

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.

Query Stored Procedures and Inherited Types

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.

Note

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));
}

Queries That Return Randomly Shaped Results

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.

Note

It would be nice to see a third-party tool that is able to do this automatically. I will definitely be on the lookout for this.

Replacing Stored Procedures with Views

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 JOINs. 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.

Mapping a function to an entity that was created from a view

Figure 13-1. Mapping a function to an entity that was created from a view

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.

Queries That Return Multiple Resultsets

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/.

Queries That Return Primitive Types

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();
}

Adding Native Queries to the Model

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>

Note

If you have any less than (<) signs in your query, you’ll need to use the escaped notation (&lt;) 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.

Adding Native Views to the Model

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.

DefiningQuery Is Already in Your Model

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.

An entity that represents a view in the database and is mapped to a DefiningQuery in the SSDL of the model

Figure 13-2. An entity that represents a view in the database and is mapped to a DefiningQuery in the SSDL of the model

The mappings for the view entity, which are the same as any other entity’s mappings

Figure 13-3. The mappings for the view entity, which are the same as any other entity’s mappings

Using DefiningQuery to Create Your Own Views

“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.

Implementing a DefiningQuery

To create the DefiningQuery you’ll need the following elements in your model:

  1. The native command to express the query

  2. An Entity, and an EntitySet in the CSDL

  3. A virtual table in the SSDL in the form of an Entity

  4. An EntitySet in the SSDL to contain the DefiningQuery

  5. 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 QueryViews. Be sure to switch back to the BreakAway model when making these changes:

  1. Create a new entity in the Designer, named UnpaidReservation, and name its EntitySet UnpaidReservations.

  2. 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)

  3. Make ReservationID the EntityKey for this entity.

  4. 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.

  5. Add the EntitySet and DefiningQuery element:

    <EntitySet Name="UnpaidReservations"
       EntityType="BAModel.Store.UnpaidReservation">
      <DefiningQuery>
    
      </DefiningQuery>
    </EntitySet>

    Note

    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.

  1. 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, &lt;. That line should look like this:

    HAVING SUM(amount)&lt;min(TripCostUSD

    Next, you’ll need to create the virtual table to which the UnpaidReservation entity will map.

  2. 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>
  3. 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.

Mapping the new entity to the new virtual table

Figure 13-4. Mapping the new entity to the new virtual table

Creating an association from the new entity

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.

Note

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.

Creating a 1:* association between the new entity (UnpaidReservation) and the Reservation entity

Figure 13-5. Creating a 1:* association between the new entity (UnpaidReservation) and the Reservation entity

The last step is to map the new association. Create the mapping so that it looks like Figure 13-6.

The Mapping Details window for the new association

Figure 13-6. The Mapping Details window for the new association

Testing the DefiningQuery

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.

Note

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.

Using DefiningQuery to Solve More Complex Problems

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.

Using Commands That Affect the Persisted Database

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.

DML Functions That Return Entities

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;
}

Note

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.

Insert, Update, and Delete Functions That Don’t Return an Entity

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.

Returning a scalar value

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();
  }
}

Warning

The stored procedure needs to return its data using the SELECT clause, not RETURN. If you use RETURN, you will get the following error:

The data reader returned by the store data provider does not have enough columns for the query requested.

Returning nothing

In the FunctionImport mapping, make sure the return type is set to None. In your code, execute the command using the EntityCommand.NonQuery method.

Defining Insert, Update, and Delete Stored Procedures Directly in the Model

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.

What Do the Functions Look Like?

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.

Warning

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.

Mapping Insert/Update/Delete to Types Within an Inheritance Structure

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.

What If Stored Procedures Affect Multiple Entities in an Inheritance Structure?

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 Customers it will call only the Customer entity’s functions. When saving changes to Contacts 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.

Implementing and Querying with User-Defined Functions (UDFs)

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.

Note

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.

Note

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.

Summary

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.

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

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