Stored Procedure Mapping

Published 26 March 08 04:38 PM | dpblogs 

In this blog post, we’d like to walk through the EDM designer’s support for specifying insert, update, and delete stored procedures for entity types. Each of these stored procedure types has some non-obvious requirements and capabilities and we’d especially like to call those out here. We assume that you know why you want your types mapped to stored procedures and instead focus on the how. We also assume a small degree of familiarity with the EDM designer – you should know how to create a new entity data model edmx file from a database.

 

For this walkthrough, we are using a very simple product-orders SQL 2005 database. The tables look are:

CREATE TABLE [dbo].[Product](

      [id] [int] IDENTITY(1,1) NOT NULL,

      [name] [varchar](50) NOT NULL,

      [price] [float] NOT NULL,

 CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED

(

      [id] ASC

)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

) ON [PRIMARY]

 

CREATE TABLE [dbo].[Order](

      [id] [int] IDENTITY(1,1) NOT NULL,

      [productId] [int] NOT NULL,

      [quantity] [int] NOT NULL,

      [timestamp] [timestamp] NOT NULL,

 CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED

(

      [id] ASC

)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

) ON [PRIMARY]

 

ALTER TABLE [dbo].[Order]  WITH CHECK ADD  CONSTRAINT [FK_Order_Product] FOREIGN KEY([productId])

REFERENCES [dbo].[Product] ([id])

GO

ALTER TABLE [dbo].[Order] CHECK CONSTRAINT [FK_Order_Product]

 

We also set up a number of stored procedures for managing the rows in these tables:

CREATE procedure [dbo].[Product_Insert](@name varchar(50),@price float) as

insert into Product([name], price) values (@name, @price)

select SCOPE_IDENTITY() as id

 

CREATE procedure [dbo].[Product_Update](@id int, @name varchar(50), @price float) as

update Product set [name] = @name, price = @price where id=@id

 

CREATE procedure [dbo].[Product_Delete](@id int) as

Delete from [Product] where id=@id

 

CREATE procedure [dbo].[Order_Insert](@productId int, @quantity int) as

insert into [Order] (productId, quantity) values(@productId, @quantity)

select SCOPE_IDENTITY() as id

 

CREATE procedure [dbo].[Order_Update](@id int, @productId int, @quantity int, @originalTimestamp timestamp) as

Update [Order] set quantity=@quantity

where id=@id and [timestamp]=@originalTimestamp

 

CREATE procedure [dbo].[Order_Delete](@id int, @productId int) as

Delete from [Order] where id=@id

 

Create a database and run the script against it. Then, add a new ADO.NET Entity Data Model item to a console application project. Generate a model from this database, including both the Product and Order tables and the six stored procedures.

 

Once you are done, your model should look like this:

 

 

In addition, if you expand your model browser’s conceptual and store schema nodes, you should see this:

 

 

Looking above, you can see the stored procedures that were added.

 

We are now ready to map these stored procedures to the appropriate actions. At this point it is appropriate to note the first stumbling block: At this time, the Entity Framework requires you to map all three stored procedures. You cannot map only the delete stored procedure. In addition, the entity type must still be mapped to a table, view, or query view.

 

We’ll begin with the simpler of the two types to map – the Product type. Right-click on the product type and bring up the mapping view. By default, the mapping view displays in the table mapping view. Click on the stored procedure mapping view icon, which is on the left side of the view and which looks like this: . In the soon-to-be-released version, you will also be able to right-click an entity type and select “Stored Procedure Mapping”.

 

Insert Product

 

Click on the <Select Insert Function> and the view should look like this:

 

 

Click on the drop-down button and select “Product_Insert”. At this point, the view should look like this:

 

 

Now let’s take a look at the stored procedure to which we are mapping:

CREATE procedure [dbo].[Product_Insert](@name varchar(50),@price float) as

insert into Product([name], price) values (@name, @price)

select SCOPE_IDENTITY() as id

 

When a product is inserted,  the stored procedure returns the id that was created for the row. We need to write this id back to the product entity instance in our client. The way to do this is by using the result column bindings: Click once on <Add Result Binding>, type “id”, and hit the Tab or Enter key:

 

 

The value of the id column from the stored procedure’s return table will now be mapped back to the entity once the stored procedure completes.

 

Update Product

 

The is a straightforward mapping: We assign the Product_Update stored procedure to this task:

 

 

We will come back to the update stored procedure in more detail when we map the Order type.

 

Delete Product

 

This is also a simple mapping task: We assign the Product_Delete stored procedure to this task:

 

 

Now we move on to the more interesting of the types.

 

Insert Order

 

Inserting an order adds a small twist: We need to provide a productId, but the productId column is not surfaced in the entity. Luckily, we have our navigation property, so we can map the productId parameter to Product_1.id:

 

 

Note that we also map a result column binding to Order’s key property.

 

Delete Order

We will discuss the update stored procedure last, because I would like to run some code against it, which requires all stored procedures to be mapped. The delete stored procedure looks like this:

CREATE procedure [dbo].[Order_Delete](@id int, @productId int) as

Delete from [Order] where id=@id

 

Note that we have an unused parameter called @productId in this stored procedure. This is another subtle issue that you should be aware of: The runtime requires associations to be consistently mapped for all three stored procedure operations, and since we’ve mapped the product-orders relationship in the insert stored procedure, we must also map it here.

 

We hope to provide a solution for this issue in future versions of the Entity Framework, but for now, this is what needs to be done for associations whose target multiplicity, with respect to the type being mapped, is 1 or 0..1. In other words, the “reference” end of the association needs to be mapped to the keys of the target type.

As an alternative, it is possible to define a function in the SSDL which defines its own CommandText element and which declares two parameters, and then turns around and calls a single-parameter version of Order_Delete. However, this is not supported through the designer, and furthermore, the designer’s “Update Model from Database” feature currently regenerates the entire SSDL section of the EDMX file.

 

So, the mapping for the Order_Delete stored procedure looks like this:

 

 

Update Order

 

Here, for demonstration purposes, we add a small wrinkle: We’re going to use SQL Server’s timestamp datatype to provide us with the basis for a simple optimistic concurrency mechanism. The Order_Update stored procedure looks like this:

CREATE procedure [dbo].[Order_Update](@id int, @productId int, @quantity int, @originalTimestamp timestamp) as

Update [Order] set quantity=@quantity

where id=@id and [timestamp]=@originalTimestamp

 

The idea here is that we only update the order if the timestamp sent back to the database is identical to the timestamp that was originally retrieved from it, indicating that no other process has modified the Order in the meantime. In order to ensure that the original value is sent to the database, we make use ot the “use Original Value” checkbox, which you see at far right here:

 

 

Now that we’ve mapped the stored procedures for the model, we can write some code in the console application’s Program.cs file to test this optimistic concurrency behavior:

 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using StoredProcsDemoModel;

 

namespace SprocsWalkthrough

{

    class Program

    {

        private static StoredProcsDemoEntities context1 = null, context2 = null;

 

        static void Main(string[] args)

        {

            try {

                context1 = new StoredProcsDemoEntities();

                context2 = new StoredProcsDemoEntities();

                deleteExistingEntities();

                createTestEntities();

                causeConcurrencyConflict();

                Console.Read();

            }

            finally {

                if (context1 != null) context1.Dispose();

                if (context2 != null) context2.Dispose();

            }

        }

 

        //Clear the order and product tables of values so that we can run this code multiple times

        //without worrying about data left over from previous runs

        static void deleteExistingEntities()

        {

            foreach(Product p in context1.Product) {

                context1.DeleteObject(p);

            }

            foreach(Order o in context1.Order) {

                context1.DeleteObject(o);

            }

            context1.SaveChanges();

        }

 

        //Create a product and an order that is associated with it

        static void createTestEntities()

        {

            Product product = new Product();

            product.name = "My Product";

            product.price = 100;

            context1.AddToProduct(product);

            Order order = new Order();

            order.Product_1 = product;

            order.quantity = 5;

            context1.AddToOrder(order);

            context1.SaveChanges();

            Console.Out.WriteLine("Created a product with id " + product.id);

            Console.Out.WriteLine("Created an order with id " + order.id);

        }

 

        //Finally, update the order from two separate context to simulation a concurrency

        //exception.

        static void causeConcurrencyConflict()

        {

            Order order = (from o in context1.Order select o).First();

            Order sameOrder = (from o in context2.Order select o).First();

            Console.Out.WriteLine("order id = " + order.id);

            Console.Out.WriteLine("sameOrder id = " + sameOrder.id);

 

            sameOrder.quantity++;

            context2.SaveChanges();

            order.quantity++;

            try {

                context1.SaveChanges();

            }

            catch (System.Data.OptimisticConcurrencyException oce) {

                Console.Out.WriteLine(oce.Message);

                Console.Out.WriteLine("The conflict occurred on "

                    + oce.StateEntries[0].Entity + " with key value "

                    + oce.StateEntries[0].EntityKey.EntityKeyValues[0].Value);

            }

        }

    }

}

 

When we run this code, we see the following output:

Created a product with id 14

Created an order with id 9

order id = 9

sameOrder id = 9

Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.

The conflict occurred on StoredProcsDemoModel.Order with key value 9

 

What the Entity Framework did automatically on our behalf was detect that that the number of rows affected by the last call to context1.SaveChanges() was 0.  It then threw exactly the kind of exception we need, and in it put useful information about which entities were involved in the update.

(No rows were affected because the call call to context2.SaveChanges() changed the value of the timestamp column, meaning that Order_Update did not update any row in the database.)

 

We hope this post was useful to you. Please send us any questions or comments, or post on the ADO.NET forum to let us know your thoughts.

 

Noam Ben-Ami

Program Manager, ADO.NET Entity Framework Tools

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# Matthieu MEZIL said on March 28, 2008 4:53 AM:

L'ADO .Net Team vient de poster deux nouveaux posts : le premier concerne l'utilisation des procédures

# Hot Topics said on March 29, 2008 11:43 AM:

Noam Ben-Ami, from the EF Tools team shows how to use the Entity Framework Designer tools to map Store

# Tony said on March 31, 2008 8:54 PM:

After attempting a similar example on my own and I get the following exception:

Error 2027: If an entity set or association set includes a function mapping, all related entity and association sets in the entity container must also define function mappings. The following sets require function mappings: Report2Queries.

THe Report2Queries is an 1 to many association between a Report and its queries.  

# Noam Ben-Ami said on April 1, 2008 1:45 PM:

Tony,

I assume that you have a report entity and a query entity, and that a report can contain many queries. I also assume that the query table has a foreign key to the report entity, something like reportId. Finally, I assume that your query entity has a navigation property, probably called report, which means that the insert, update, and delete stored procedures must have a parameter called reportId, as detailed in the blog entry.

Given all of that, you need to map report.reportId on the query type to the reportId parameter on the insert, update, and delete stored procedures.

# Roman Dynnik said on April 16, 2008 8:18 AM:

>>>The value of the id column from the stored procedure’s return table will now be mapped back to the entity...

Good article, thanks!

I have some quetions:

1. Is it possible map back to the entity a out(inout)-parameter from stored procedure?

2. Is it possible map to a parameter of stored procedure any variable or property from other object (for example, I need to pass a ticketID)

# Andrey Balykov said on April 23, 2008 3:24 AM:

I've a problem with using Stored proc in Entity Framework Model.

For simple Example, I've 2 tables (and Entities), Prices (PriceID primary key, CategoryID, price) and Category (CategoryID primary key, CategoryName). I've relation FK_Prices_Category

I was made EDM with stored proc - all OK. But I can't make Deleting prom Price entity.

My code:

01 VmzkoPricesEntities ctx = new VmzkoPricesEntities();

02 VmzkoPricesModel.Prices pr = ctx.GetPriceByID(newsID).First<VmzkoPricesModel.Prices>();

03 ctx.DeleteObject(pr);

04 ctx.SaveChanges(true);

GetPriceByID - is an imported stored proc with output of all fields of Prices (PriceID, CategoryID, price) BUT if I expand 'pr' object (row 02) then property Category is NULL.

After that deleting is not available: Entities in 'VmzkoPricesEntities.Prices' participate in the 'FK_Prices_Category' relationship. 0 related 'Category' were found. Between 1 and 1 'Category' are expected.

How can I fill Category property inside Prices object by using stored proc???

How can I fill that by alternative method?

# Arye Friedman said on May 19, 2008 10:12 AM:

Hi Noam,

Do you plan to implement a wizard that will be able to port (not just map)  stored procedures with a non-trivial logic to LINQ code?  The purpose is to run these converted stored procedures using EF approach against another data source which does not contain any logic but just data.

# <Jose A. Fernandez /> said on August 23, 2008 5:47 PM:

Con un proyecto donde estoy probando Entity Framework , para ir mas alla de lo simple a lo que estamos

# Skeptic said on October 15, 2008 4:14 PM:

I stopped reading after "We hope to provide a solution for this issue in future versions of the Entity Framework..." because this told me everything I need to know: EF is "not ready for prime time".  

I look forward to revisiting EF once it becomes realistic enough to use out of the box without a lot of "magic work-around coding" that requires a PHD in Computer Sciene or worse, breaks when the next edition of EF comes out.

# richardcollette said on October 30, 2008 2:36 PM:

Definitely frustrated by limitations myself as well.  I simply want to get a collection of read only entities from a table (a list of US States) using  a stored procedure.  Issues are:

I must map the entity to a store table or view, even when updates are not going to be performed.

The entity must map all primary keys to the store table, even though I only require a portion of the table values returned.

Clearly the read only use case was not focused on in the design much.  As a result, EF seems to tie my data access to the physical store more so than ever.

It's interesting that this is so poor because it works fairly well in Linq to SQL except that I cannot specify a name for the return type or its properties.  I have to live with the autogenerated return type.

Please take this use case into account in the next release.

# davin said on December 20, 2008 11:14 AM:

如果对于sp在EDM的映射有一些了解的话,可以很清楚的明白,在EDM中映射一个sp返回的类型包括:Entity Type,Scalars Type,None;这三中类型分别对应的sp返回内容对应到数据库,表的层面就是:(Entity)一张表的所有字段或若干字段(必须包括主键);(Scalars)返回一个常量(int, varchar)可以是数据库的一个字段,可以是sum,count,left...等函数操作的结果;(none)自然是什么都不返回.这里自然就有一个严重的问题,对于返回(multiple

# Margaret said on December 30, 2008 8:08 AM:

I got a similar error 2027 to Tony, and provided mappings for the association set.  However, in my case, the relationship is 1 to 0..1 and when I mapped the Key of the dependent table to the PK of the independent table in all three Function mappings (as instructed), I got a slightly different error:

Error 2027: If an EntitySet or AssociationSet includes a function mapping, all related entity and AssociationSets in the EntityContainer must also define function mappings.

It went on to say that the Independent table required function mappings, even though up to now I was able to save changes using the regular .SaveChanges Method.  Is this because of the 1 to 0..1 relationship?

# D.Asuncion said on January 19, 2009 10:07 PM:

Olahhhh.. Amigo... I Cannot Run.. I got some error..  like this :

error 2037: A mapping function bindings specifies a function EDMXModel.Store.Order_Insert but does not map the following function parameters: productId.

5 Ado.Net Team Reference.App_Code.Model.msl(61,6) : error 2048: The EntitySet 'Order' includes function mappings for AssociationSet 'FK_Order_Product', but none exists in element 'InsertFunction' for type 'EDMXModel.Order'. AssociationSets must be consistently mapped for all operations.

# D.Asuncion said on January 20, 2009 12:29 AM:

I fixed it now... Thank you very much..for the article.

# David J. said on January 23, 2009 7:25 PM:

I've started today working on an ADO.NET entity model using stored procedures, and I have to say I'm underwhelmed. Some of the serious limitations:

1) Cannot map output parameters from the insert sproc back to entity properties - only the return value. If you have an existing sproc that returns the identity value as an output parameter, guess what - you'll have to rewrite it. If you're procedure passes back multiple output parameters, then you're screwed - you'll have to rethink your design.

2) If you choose sproc mapping you MUST provide an insert, update and delete procedure. Never mind if you don't intent to support all of those semantics. I found a bug reported on this. The fix was postponed, and to paraphrase the workaround provided: "If you're not doing full CRUD, then don't use EF." I guess a model where there are some entities that don't actually support full CRUD semantics is beyond the EF team's comprehension.

3) You can apparently only have one procedure per operation. So let's say you want multiple delete procedures - one to delete a specific order, one to delete all orders for a customer, etc. There's apparently no way to do that unless you cram it all into one sproc.

4) While futzing with these limitation I had to iterate on DB changes to workaround them and the entity designer got confused several times. After mucking with the raw XML and not getting anywhere, I regenerated the project from scratch and it worked fine. This doesn't give me much confidence in the ability of the designer to cope with an evolving data model.

# jason chan said on April 28, 2009 8:12 PM:

what is the point of mapping all the stored procedures with all the insert/update/delete?

what if i don't do it?  what will happen?

# jason chan said on April 28, 2009 8:27 PM:

i just tried removing the stored procedure mapping for delete function with the delete stored procedure, so the code will stop at this line

foreach(Product p in context1.Product) {

   context1.DeleteObject(p)

}

it will say:

Mapping Exception was unhandled

Schema specified is not valid. Errors: 2025

So, I think if you do all the stored procedure mapping with all your entities...then you can just use all the add, delete, update easier.

# jason chan said on April 28, 2009 8:33 PM:

i tried to remove the stored procedure mapping for insert function on product...

so i will get the same error (mapping exception was unhandled) from this line

foreach(Product p in context1.Product)

so, I must do all the mapping before I can use the above code?

# Kreshiv said on September 11, 2009 6:28 PM:

Thanks Man. I was searching for the meaning of this error for 2 days..

Hope I can try and resolve it now.

Leave a Comment

(required) 
(optional)
(required) 

  
Enter Code Here: Required

Search

This Blog

Syndication

Page view tracker