Inserting Many to Many Relationships in EF with or without a Join Entity

Many to Many relationships are defined in a database using a join table.

 

manytomanydb

Depending on the schema of that join table, they are represented differently in an Entity Framework EDM. The Person...PersonStores...StoreLocations becomes a clean *:* relationship between Person and Store. Person has an EntityCollection of stores and vice versa. The join table is hidden in the mappings. However the Person...FavoriteBreweries...Brewery relationship includes the FavoriteBreweries join entity.

manytomanymodel

The reason that these are different is because the PersonStores table contains only the PrimaryKeys of the joined tables, whereas the FavoriteBreweries table contains the Primary Keys as well as another column, DateAdded. The EDM is not able to hide this join table because it won't know how to deal with the "extra" property.

When working with the simple *:* entities, you can query, update, insert and delete the involved entities without having to worry about the join table.

Here's an example of creating a relationship between an existing Person and an existing Store.

//grab a random person and random store from the db
Person person = context.People.First();
Store store = context.StoreLocations.First();
person.FavoriteStores.Add(store);

It's not quite as simple when there is a join entity involved.

Here is an example of creating a relationship between an existing Person and an existing Brewery

 //grab a random person and a random brewery from the db
Person person = context.People.First();
Brewery brewery = context.Breweries.First();
//create new favorite brewery and set it's person and brewery
FavoriteBrewery fb = new FavoriteBrewery();fb
.DateAdded = DateTime.Now;
fb.Person = person;
fb.Brewery = brewery;
context.AddToFavoriteBreweries(fb);

This code makes sense now that it's spelled out, but it took a few tries before I figured out the pattern.

You can also use new entities for person/store

Person newPerson = Person.CreatePerson(0, "Julie", "Lerman");
Store newStore = Store.CreateStore(0, "Burlington Vermont");
newPerson.FavoriteStores.Add(newStore);
context.SaveChanges();

or person/brewery,

 //Create new person and new brewery and then new 
 //favorite brewery between them
Person newPerson = Person.CreatePerson(0, "Julie", "Lerman");
Brewery newbrewery=Brewery.CreateBrewery(0,"Magic Hat");
FavoriteBrewery fb = new FavoriteBrewery(); 
fb.DateAdded = DateTime.Now;fb.Person = newPerson; fb.Brewery = newbrewery; context.AddToFavoriteBreweries(fb);

Just make sure that any required EntityReferences (FKs) for those new entities are spelled out before you call SaveChanges. For example, if the person has a 1:1 relationship with PersonalDetails (e.g., an entity that contains things like Birthday and shoe size), then the constraints will require that the new person has a related PersonalDetails record as well. Although this has nothing to do with the *:* relationship, it could easily trip you up when doing an insert like this.

#1 Dave on 3.31.2009 at 3:39 PM

This post helped alot!Thank you!Here's what I am doing using stored procedures.I don't think it is very efficient having to call 4 stored procedures to accomplish an insert into a junction table -UserGroupRole userGroupRoleObjectResult = new UserGroupRole();userGroupRoleObjectResult.UserProfile = _db.SpGetUserProfileByID(user.ID).First();userGroupRoleObjectResult.BusinessGroup = _db.SpGetBusinessGroupByID(membership.MembershipGroup.ID).First();userGroupRoleObjectResult.BusinessRole = _db.SpGetBusinessRoleByID(membership.MembershipRole.ID).First();userGroupRoleObjectResult.UserGroupRoleIsLead = membership.IsLead;userGroupRoleObjectResult.AddedBy = membership.AddedBy;userGroupRoleObjectResult.DateAdded = membership.DateAdded;userGroupRoleObjectResult.UpdatedBy = membership.UpdatedBy;userGroupRoleObjectResult.DateUpdated = membership.DateUpdated;_db.AddToUserGroupRole(userGroupRoleObjectResult);_db.SaveChanges();Stored procedures doesn't seem to be effective with Entity Framework do to simple CRUD operations.

#2 Julia Lerman on 3.31.2009 at 3:44 PM

This might be a place where, if you could get the dba's blessing :-), letting EF compose the commands and do the insert/update directly would be beneficial.

#3 Dave on 3.31.2009 at 4:08 PM

Agree Juila.Also, fully leverage the benefits of LINQ IQueryable and deferred loading.Use stored procedures for complex operations through the Function Import.

#4 NSS on 4.20.2009 at 9:34 AM

How to Insert New/(Not the Exisiting) Relation Ship when there is Man-To-Many Relation using third table(Just a reference Table)

#5 Julia Lerman on 4.20.2009 at 9:40 AM

Hi NSS - I can't tell if this is a question and if it is, can you rephrase it? It seems as though the original blog post itself would be the answer.

#6 Barry on 5.27.2009 at 4:08 PM

I'm looking for a solution to the problem of manipulating the "PersonStoreLocations" table in your example, I have my tables set up the same way. But then all of the sudden a FavoriteStores entity popped up in your example to join the Person and StoreLocations tables. Where did that come from. You explain in your example that if the join table only contains the primary keys of the two tables that there is no entity governing the relationship, and indeed, in my configuration there is no entity that I can add a "store" to. I use Person.Stores.Add(store) and .Remove(store) to try to do this but the framework seems to ignore it. Could you clarify your example of adding a person/store relationship using the tables shown in your example or clarify where the FavoriteStores entity came from? Thanks.

#7 Julie on 5.28.2009 at 9:33 PM

Hi Barry.

In the model I renamed the StoreLocations navigation property to FavoriteStores.

Sorry for the confusion.

Knowing this, are you now able to get the sample working?

#8 Barry on 6.03.2009 at 9:45 AM

Thanks for the rewrite of the blog, it's now clear and Yes, I got it working programmatically and it works just like one would expect. Unfortunately I was using a GridView and DetailsView in my own example and it seems that the EntityDataSource doesn't support many-to-many relationships out of the box. I had to intercept the OnInserting, OnDeleting and OnUpdating events and manually update and save my entities. The only problem I now have is with the Insert. I keep getting errors concerning foreign key relationships that supposedly aren't present and up to now I haven't been able to solve the problem. What a headache.

#9 Norwill Gutierrez to Venezuela on 7.23.2009 at 3:22 PM

Thanks for the help, i aprecious to much...

regards

#10 Stephen Study on 8.04.2009 at 2:41 PM

You know, I've got a table set up just like your Person to Store tables in this example. I'm doing the Person.Store.Add(newStore) and when I execute the dbContext.SaveChanges() call I get the following error: Unable to update the EntitySet 'CONTRACT_JOBS' because it has a DefiningQuery and no <InsertFunction> element exists in the <ModificationFunctionMapping> element to support the current operation.

Indeed there is a DefiningQuery in the emdx file for ContractJobs. Your book and the above example say this will work. Various Bing/Google hits say may problem is due to the fact that I don't have a PK defined in my many-to-many table ContractJob.

I'm confused.

#11 Julie on 8.04.2009 at 2:49 PM

Hi Stephen

The table in question - the JOIN table in your db should have a composite primary key defined from both of the IDs. I have that set up here. In personstores, see how both IDs have the key next to them in the db schema screenshot?

does that help?

#12 Stephen Study on 8.04.2009 at 3:18 PM

Ok, figured out what was required. I made the ContractJobs (many to many table between Contract and Job) have a PK of the combined ContractId and JobId fields in the table. That solved the problem I was seeing concerning "DefiningQuery" and allowed the changes to be saved. Cool!

#13 Chris on 9.17.2009 at 4:44 PM

When I attempt this I get error 3034 about mapping fragments. Any thoughts?

#14 Chris on 9.18.2009 at 10:08 AM

Further more I attempted to add a condition based on information I found on another site and received an error about having a condition on a primary key. I am interested in seeing how this association is displayed in the xml. I do not have the relationship defined in my DB, but I am trying to create it in the model based on this article.

#15 Julie on 9.21.2009 at 10:38 AM

Chris, is this what you were looking for? Hoping the comment block doesn't totally mangle the xml formatting.

<AssociationSetMapping Name="PersonStores"

TypeName="EFWorkshopModel.PersonStores"

StoreEntitySet="PersonStores">

<EndProperty Name="Person">

<ScalarProperty Name="PersonID" ColumnName="PersonID" />

</EndProperty>

<EndProperty Name="StoreLocations">

<ScalarProperty Name="storeid" ColumnName="storeid" />

</EndProperty>

</AssociationSetMapping>

#16 Chris on 9.22.2009 at 9:25 AM

Yes, exactly what I wanted to see. The solution to my problem was changing the StoreEntitySet of the association to the name of my join table and removing that table from the model. I see on a lot of websites people posting that many:many relations are not supported in EF, hopefully more people find this page before surrendering. Thanks a lot!

#17 Perry on 11.30.2009 at 12:00 PM

Hi Julie, I've been following along on your blog for sometime now, and today I actually have a question that was never answered on the MSDN boards. Perhaps you can help me since EF4 and the new CTP have hit. I must be doing something wrong, because I can't get this to work with my schema. I think I know why, but I wanted to float this by you. I have 2 tables that represent entities, and one JOIN table. They are: Locations, AccessRules, and LocationAccessRules. There are 2 keys on LocationAccessRules. LocationID, and AccessRuleID. It also has to extra columns, an active flag and an emulate flag. Anyway, this is an old schema, and guess what, there are no proper FK between the 2 entities and the JOIN table. It's all been by luck I guess. Since the DB schema doesn't have any FKs, my entities in EF don't seem to match up. I try to set up a *:* between Locations and AccessRules, but the mappings don't line up. Do I need FK in my schema to get this to work? Should I be including my LocationAccessRules (the join table) into my EF diagram?! I'd really like to use EF to create a new architecture here, but I can't change the mistakes in the past, re:the db schema. Does this make sense?

#18 Julie on 11.30.2009 at 1:43 PM

hi perry

The first thing to be sure to undersand (and you may already but I have to check) is that if you have any fields in your join table besides the Fk fields then you can't set up a *:* in the model. The model needs a join entity that maps to the join table. Do you think this is the cause of your problem or does it go deeper than this?

#19 Perry on 11.30.2009 at 2:51 PM

Ok, so you are right. I did not understand that a *:* mapping isn't possible in the model if I have extra columns in the JOIN table. And I do have extra columns in the join table. 2 to be exact. I thought my problem was the fact I had a join table to begin with. Shows you what I know. Should I not have all 3 tables in the EDMX GUI then? I've tried to create an association between just the 2 tables that represent the true entities, but that leaves me with unmapped columns in the association. I keep getting a variation on Error 3034 or 3032. I'm clearly running in circles here!

#20 Julie on 11.30.2009 at 3:02 PM

That "little" rule about *:* in the model is not an obvious thing. :)

You'll have to add that extra join table into your model if you want the relationships between Locations and Accessrules to be worked out.

#21 Perry on 11.30.2009 at 5:26 PM

Ok. I took your advice and I have added the join table to my edmx. And now I am getting problems when attempting to create the associations.

I'll give it the old college try again tonight. Thanks for the advice.

#22 Julie on 11.30.2009 at 5:43 PM

if you have the pk/fk relatoinships defined in the database (like in the screenshot at the top of the blog post), the wizard should fix those up for you. Youy said in your first comment that you don't have them defined. Any chance you can define them in the db? I'm not sure how the app will behave even if you define them correctly in the conceptual model.

#23 Perry on 12.01.2009 at 5:41 PM

Nope. Can't define them in the db. That's the thing. There is no way to change the schema. This is a legacy SQL server db and changes to it are going to be tough to make. So, I am trying to emulate exactly what you have in the right hand side of your diagram, by creating an association between Locations table and the JOIN table LocationAccessRules. This is like your Person and FavoriteBrewery. The problem is that the second key is not being mapped (AccessRuleID). So I keep getting Error 11011 errors.

#24 Germán on 2.15.2010 at 12:35 PM

Hi Julie,

I'm enjoying your EF/EF4 series of posts. I would like to know if there's a way to force EF to materialize related entities in an abritrary order.

For example, I'm dealing with travel itineraries. Each TravelItinerary visits multiple Cities and a City can be visited in multiple itineraries. It sounds like a many-to-many relationship, right?

The thing is that every itinerary defines an arbitrary order in wich each city gets visited (no alphabetical order, no id ordering) and I would like to know if EF can arrange the navigation collection with an order clause based on a column in the association table.

In pseudocode:

City

====

int Id,

int Name,

int CountryId

Itinerary

=========

int Id,

string Name,

decimal PricePerPerson,

Collection<City> VisitedCities

ItineraryCity : Association

=============

int ItineraryId,

int CityId,

int Ordering

In my association table I have an Ordering column that predefines the order in wich each itinerary visits the cities.

Is there a way to tell EF/EF4 that I want the Itinerary.VisitedCities collection materialized in the right order specified in the ordering column?

Thanks a lot.

Germán. Spain.

#25 Renata on 2.16.2010 at 3:01 PM

Hi Julie,

Sorry for my english, im brazilian.

I am trying to insert a new Person with many new FavoriteBreweries, and with existents brewery.

Thanks

#26 Darryl on 2.16.2010 at 5:58 PM

Hi Julie,

Great post.

I have something very similar set up and want to do something like what Renata just described.

Say I am trying to update the Person table with a call like the following:

public void SavePerson(Person p1) {

...

var ptemp = context.Person.Where(s => s.PersonId == p1.PersonID).FirstOrDefault();

if( ptemp != null) //update fav stores

{

ptemp = p1; //no error, doesn't update db

}

else //else doesn't exist so add it

{

context.AddToPersons(p1); //ERROR

}

context.SaveChanges();

}

Just looking at the AddToPersons call, it gives an error saying that it's duplicate entry in the Store table. Because p1.FavouriteStores is already populated with a list of stores that exist in the Store table. However if it has a lisf of Store entries NOT already in the Store table, it'll add entries into the Store table and the PersonStores table. I just want the PersonStores populated with the list p1.FavouteStores list that has existing stores.

Sorry if this isn't that clear, but I think you can see what I'm asking.

Thanks!

#27 Dave on 3.05.2010 at 4:17 AM

Hi Julie, Great post.

Quick question. I have two entities with a one to one relation Customer and Address. How do I save these both together. I took out the AddressID from the Customer entity and placed it on the Navigation Property but when I try to save them with <br/>Cust.Address = (the new address) <br/>

Context.Customer.AddObject(Cust)

Context.SaveChanges.... it gives me an error as follows:

Entities in 'Model.Addresses' participate in the 'CustomerAddress' relationship. 0 related 'Customer' were found. 1 'Customer' is expected.

Any ideas?

#28 Julie on 3.05.2010 at 11:20 AM

@dave - error tells me that there's no two way fix up going on. re you using pocos? Do you need to explicitly set address.customer=Cust?

BTW - your q has nothing to do with this blog post. (sheepish grin) If you have random questions, you might find the MSDN forums for EF to be very helpful.

#29 George on 3.19.2010 at 10:32 AM

I have tables like your person, personStores and storeLocations tables. My person and storeLocation tables are populated; now I am trying to populate the personStores table. When I execute the context.SaveChanges(), I don't get an error but my personStores table doesn't get populated either. How can I try to debug this?

#30 Shaleen on 4.29.2010 at 10:13 AM

Person newPerson = Person.CreatePerson(0, "Julie", "Lerman");

Store newStore = Store.CreateStore(0, "Burlington Vermont");

newPerson.FavoriteStores.Add(newStore);

//This line is missing:

context.AddToPeople(newPerson);

context.SaveChanges();

#31 Butch on 5.11.2010 at 4:34 PM

So, what I'm trying to to boils down to getting all the store locations for a person ( given the PersonId )...using RIA Services & EF4

#32 Radomir on 5.14.2010 at 1:09 PM

Hello,

can you help me with this?

I need add one new Person and five new Brewery and save PersonID and all BreweryID to FavoriteBrewery??

Thanks

#33 Julie on 5.14.2010 at 3:15 PM

@butch I haven't screwed around with *:* in RIA services. You might want to try on the forums on silverlight.net.

@radomir I'd iterate through the breweries, add the relationship for each one (like in the example in my post) and then call save changes.

#34 Tom van Stiphout on 5.25.2010 at 2:47 PM

Hi Julie,

Can you show how EF can be used to delete a record from the *:* junction table only? I want to delete only from PersonStore table, indicating that PersonX no longer works at StoreY.

Thanks,

-Tom.

(a Microsoft Access MVP and an EF beginner)

#35 Julie on 5.25.2010 at 2:52 PM

Hey Tom

I'm super busy trying to finish the new book. Can you check on the MSDN forums?

social.msdn.microsoft.com/.../threads

#36 headscratcher on 9.07.2010 at 5:09 AM

Hi Julie,

I have a problem. I have 2 tables Users and Groups. They both are associated in Many:Many relation and this association is stored in a table without unique key.

How can i store the association in the physical table whenever there is a mapping.