I'm juggling with One-To-Many relationship using Entity Framework:
I need to insert a bunch of data from C# objects into my SQL Server database, so I have been using Entity Framework with the Code First approach.
In my example, I have a product which have one category, yet categories can obviously belong to multiple products.
My Product-class looks like this.
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
I have tried multiple solutions for my Category class, both with and without the outcommented line but here we go:
public class Category
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CategoryId { get; set; }
public string CategoryName { get; set; }
// public ICollection<Product> Products { get; set; }
}
When inserting multiple products, with the same category, I get a Primary key constraint violation - which I understand why happens, but I would think Entity Framework would care of!
I have a long list of indexes, that I use to get an XML-file (based on that index) from an API. I then create an object based on that XML file. The method is called GetProductFromXML:
Foreach(int index in listOfIndexes){
Product product = GetProductFromXML(index);
productContext.Products.Add(product);
}
Context.SaveChanges();
When ever I get a product, where the category already exists, I get an violation of primary key constraint-exception from my Category-table.
What I want is obviously that EF understands, that the second object should use the category from the first object.
What do I do? I find this such a simple action, that would be easily done with normal querying, but with Entity Framework I wrap my head around it and I'm going nuts!
Hope someone will give me the logic answer!
You're creating two new Category instances and explicitly giving them both the same CategoryId. (You're also not actually using either instance for your products, and you never set any properties on the second product. I'm assuming these are typos.)
Only create one instance:
Category category = new Category();
category.CategoryId = 1;
category.CategoryName = "categoryA";
Then use it for both of your Product instances:
Product product = new Product();
product.ProductId = 1;
product.Name = "ProductA";
product.Category = category;
context.Products.Add(product);
Product productB = new Product();
productB.ProductId = 2;
productB.Name = "ProductB";
productB.Category = category;
context.Products.Add(productB);
Edit: From a lengthy comment thread below (and the updated question which obscures the failing code behind a new method), the original problem still remains... You're creating new Category instances where you should be re-using existing instances.
Consider keeping them in a list outside of your method. As a logical guide, something like this:
// Note: If there are categories from previous runs of this logic
// then you should fetch them from the backing data here
var categories = new List<Category>();
foreach (int index in listOfIndexes)
{
var product = GetProductFromXML(index, categories);
productContext.Products.Add(product);
}
Context.SaveChanges();
And witin your GetProductFromXML method:
int id = // ... get the Product ID from your XML
string name = // ... get the Product Name from your XML
//... etc.
string categoryName = // ... get the Category Name from your XML
var category = categories.SingleOrDefault(c => c.Name == categoryName);
if (category = null)
{
// It doesn't exist yet, so add it
category = new Category();
category.Name = categoryName;
category.CategoryId = // ... whatever logic you use to determine this
categories.Add(category);
}
return new Product
{
ProductId = id,
Name = name,
// etc.
Category = category
};
Related
I have two entities, one is Product and looks something like this:
[Key]
public int ID {get; set;}
public virtual ICollection<Category> Categories { get; set; }
and the other is Category and looks like this:
[Key]
public int ID {get; set;}
[JsonIgnore]
public virtual ICollection<Product> Products { get; set; }
Both objects are simplified massively here.
My insert method looks like this:
public static Product AddProduct(ProductDTO product)
{
using var context = new ProjectDbContext();
Product newProduct = Product.ConvertDTO(product);
var contr = context.Products;
contr.Add(newProduct);
context.SaveChanges();
if (product.Categories != null && product.Categories.Count() > 0)
{
var list = from r in context.categories
where product.Categories.Contains(r.ID)
select r;
newProduct.Categories = list.ToList();
}
contr.Update(newProduct);
context.SaveChanges();
return newProduct;
}
The ProductDTO is just an object that has the product data and a list of category ids.
The product is inserted and the data is also written into the generated connection table inside the database. However when I now try to get the inserted product, its categories are null, even though it should have three category objects.
Ok, I think I found a solution. I forgot to eager load, when getting the entity:
contr = context.Products.Include(x => x.Categories).Where(x=> x.ID == id).FirstOrDefault();
This is my first dip into Entity Framework - as a long time Linq2Sql guy I never had anyone show me what EF did better. I’ve followed some MS tutorials and I have a context that creates a database and stores stuff easily. I can query it with LINQ so it’s still familiar.
What isn’t working though is a relationship. I have three classes, call them Product, Place, and Price. They all have an int Id. Product and Place both have a virtual icollection, and Price has a property each for Product and Place.
The logical model is that I have a list of products that are sold in zero or more places, and any product might have a different price in each location (think local tax adjustments).
My first test wouldn’t populate the list of prices from the product, so myProduct.Prices was null. I worked around that by getting the list of prices myself
var prices = dB.Prices.Where(...)
My problem is when I have more than one Place (Id 1 and 2), and I put in data for multiple prices, the Place is null where the Id is 2.
I have tried adding data directly to the tables (a place with Id 2, anD a price with Place_Id = 2). I have tried adding them with code.
If I go in to the Price table and change both Place_Id to 1, it works in that they both retrieve Place 1. If I set them both to two they both give me null Place. If I set one each way, the one with 1 works and the other is null.
So my relationship works but only when the FK is 1. WTF have I done?
edit: code samples (sorry, I was on my phone earlier)
{ //declared in my dbcontext
...
public DbSet<Product> Products { get; set; }
public DbSet<Place> Places{ get; set; }
public DbSet<Price> Prices{ get; set; }
...
}
//Just to be clear, below are separate from the dbcontext
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
...
[InverseProperty("Product")]
public ICollection<Price> Prices { get; set; }
}
public class Place
{
public int Id { get; set; }
public string Name { get; set; }
...
[InverseProperty("Place")]
public ICollection<Price> Prices { get; set; }
}
public class Price{
public int Id { get; set; }
public Product Product { get; set; }
public Place Place { get; set; }
...
}
I tried inserting rows directly, also I tried
//In the Seed(Context) method of my DbContextInitializer, where db is an instance of my dbcontext
var a1 = new Place { Name = "Place 1"};
var a2 = new Place { Name = "Place 2"};
var p = new Product { Name = "Test Product" };
db.Products.Add(p);
db.Associations.Add(a1);
db.Associations.Add(a2);
db.Prices.Add(new Price { Amount = 10.1m, Place= a1, Product = p });
db.Prices.Add(new Price { Amount = 3.45m, Place= a2, Product = p });
db.SaveChanges();
All of that data gets inserted and I can interrogate the database to see it.
The code that is coming unstuck is:
foreach (var p in db.Products.ToList()) //NB if I try this without the ToList I get an exception about an open data reader
{
var price = 0m;
foreach (var o in db.Prices)
{
//there are two price records which we created above.
//In the first Price, o = {Id = 1, Place_Id = 1, Product_Id = 1}. This works.
//In the second Price, o = {Id = 2, Place_Id = 2, Product_Id = 1}. o.Place is null
// Yes, there is absolutely a Place {Name = "Place 2", Id = 2} in the database
if (o.Place.Id == ...)price += o.Amount;
}
...
}
Something interesting I noticed. If I jam in more products, it works with any product Id. Also (and I suspect this is the underlying issue), I notice that o.Product is of type Product, however o.Place is of type DynamicProxies.Place_Guid - but I'm not understanding why these are different I have declared the properties in identical fashion as you can see above.
I have two classes with a many-to-many relationship in a ASP.NET EF application. I'm trying to find all Listings that have any Categories which is posted from a view. The categories are checkboxes on the view form.
These are the classes with navigation properties simplified for example:
public class Listing
{
public int ID { get; set; }
public ICollection<Category> Categories { get; set; }
...
}
public class Category
{
public int ID { get; set; }
public ICollection<Listing> Listings { get; set; }
...
}
// this is the join table created by EF code first for reference
public class CategoryListings
{
public int Category_ID { get; set; }
public int Listing_ID { get; set; }
}
This is the query I am trying to use in my MVC Controller but it doesn't work and I don't really know what else to try:
if (model.Categories !=null && model.Categories.Any(d => d.Enabled))
{
List<Listing> itemsSelected = null;
foreach (var category in model.Categories.Where(d => d.Enabled))
{
var itemsTemp = items.Select(x => x.Categories.Where(d => d.ID == category.ID));
foreach (var item1 in itemsTemp)
{
itemsSelected.Add((Listing)item1); //casting error here
}
}
items = itemsSelected;
}
In SQL, I would write this using a subquery (the subquery represents the multiple categories that can be searched for):
select l.id, cl.Category_ID
from
listings as l inner join CategoryListings as cl
on l.id=cl.Listing_ID
inner join Categories as c on c.ID = cl.Category_ID
where c.id in (select id from Categories where id =1 or id=3)
How do I write that SQL query in EF using navigators or lambda? The subquery in the SQL will change each search and can be any id or IDs.
You forgot to tell us what objects are in your collection items. I think they are Listings. Your case doesn't work, because itemsTemp is a collection of Categories, and every item1 is a Category, which of course can't be cast to a Listing.
Advice: to debug casting problems, replace the word var
with the type you actually expect. The compiler will warn you about
incorrect types. Also use proper identifiers in your lambda expressions.
This makes them easier to read
IQueryable<???> items = ... // collection of Listings?
List<Listing> itemsSelected = null;
IQueryable<Category> enabledCategories = model.Categories.Where(category => category.Enabled));
foreach (Category category in enabledCategories)
{
IEnumerable<Category> itemsTemp = items
.Select(item => item.Categories
.Where(tmpCategory => tmpCategory.ID == category.ID));
foreach (Category item1 in itemsTemp)
{
// can't cast a Category to a Listing
We'll come back to this code later.
If I look at your SQL it seems that you want the following:
I have a DbContext with (at least) Listings and Categories.
I want all Listings with their Categories that have category Id 1 or 3
It's good to see that you followed the entity framework code-first conventions, however you forgot to declare your collections virtual:
In entity framework the columns in a table are represented by
non-virtual properties. The virtual properties represent the relations
between the table.
With a slight change your many-to-many relation can be detected automatically by entity framework. Note the virtual before the ICollection
class Listing
{
public int ID { get; set; }
// every Listing has zero or more categories (many-to-many)
public virtual ICollection<Category> Categories { get; set; }
...
}
class Category
{
public int ID { get; set; }
// every Category is used by zero or more Listings (many-to-many)
public ICollection<Listing> Listings { get; set; }
...
public bool Enabled {get; set;}
}
And the DbContext
public MyDbContext : DbContext
{
public DbSet<Listing> Listings {get; set;}
public DbSet<Category> Categories {get; set;}
}
Although a relational database implements a many-to-many relationship with a junction table, you don't need to declare it in your DbContext. Entity framework detects that you want to design a many-to-many and creates the junction table for you.
But how can I perform my joins without access to the junction table?
Answer: Don't do joins, use the ICollections!
Entity Framework knows which inner joins are needed and will do the joins for you.
Back to your SQL code:
Give me all (or some) properties of all Listings that have at least one Category with Id equal to 1 or 3
var result = myDbcontext.Listings
.Select(listing => new
{ // select only the properties you plan to use
Id = listing.Id,
Name = listing.Name,
...
Categories = listing.Categories
// you don't want all categories, you only want categories with id 1 or 3
.Where(category => category.Id == 1 || category.Id == 3)
.Select(category => new
{
// again select only the properties you plan to use
Id = category.Id,
Enabled = category.Enabled,
...
})
.ToList(),
})
// this will also give you the Listings without such Categories,
// you only want Listings that have any Categories left
.Where(listing => listing.Categories.Any());
One of the slower parts of database queries is the transfer of the selected data from the DBMS to your local process. Hence it is wise to only transfer the properties you actually plan to use. For example, you won't need the foreign keys of one-to-many relationships, you know it equals the Id value of the one part in the one-to-many.
Back to your code
It seems to me, that your items are Listings. In that case your code wants all Listings that have at least one enabled Category
var result = myDbContext.Listings
.Where(listing => ...) // only if you don't want all listings
.Select(listing => new
{
Id = listing.Id,
Name = list.Name,
Categories = listing.Categories
.Where(category => category.Enabled) // keep only the enabled categories
.Select(category => new
{
Id = category.Id,
Name = category.Name,
...
})
.ToList(),
})
// this will give you also the Listings that have only disabled categories,
// so listings that have any categories left. If you don't want them:
.Where(listing => listing.Categories.Any());
Do you have a relation between Listing/Category and CategoryListings?
Here is example for EF 6: http://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx
If you have it the query will be simple, something like that:
CategoryListing.Where(cl => new List<int>{1, 3}.Contains(cl.CategoryRefId))
.Select(x => new {x.ListingRefId, x.CategoryRefId});
If you need all properties of Listing or Category, Include() extension will help.
I want select the all restaurants, and for the each restaurant load the list of the attached categories.
There is a many-to-many relationship between the Restaurant and Category:
public class Restaurant
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Value { get; set; }
}
In my current implementation I'm selecting a raw data with all restaurants and categories, and process them on the client side: group by restaurant and select categories. In this case, the generated SQL looks very simple and executed fast:
var plainData = (
from restaurant in RestaurantsRepository.GetAll()
from category in restaurant.Categories
select new
{
Id = restaurant.Id,
Name = restaurant.Name,
Category = category.Value
}).ToList();
var restaurants = (
from restaurant in plainData
group restaurant by new
{
restaurant.Id,
restaurant.Name
}
into grp
select new RestaurantModel
{
Id = grp.Key.Id,
Name = grp.Key.Name,
Categories = grp.Select(c => c.Category).ToList()
});
The another variant is to use Entity Framework and the relation between restaurants and categories. In this case, the generated SQL is very complicated and executed four times slower:
var restaurants =(
from restaurant in RestaurantsRepository.GetAll()
select new RestaurantModel
{
Id = restaurant.Id,
Name = restaurant.Name,
Categories = restaurant.Categories
}).ToList();
The question is: There is there a more efficient way (then 1 or 2) to select my data?
Your collection is virtual, thus, I suppose, you're using lazy loading.
The second variant executes N + 1 queries, where N is a count of items, returned from RestaurantsRepository.GetAll(): one query to get all restaurants, and N queries to get all categories for the particular restaurant.
Try to use eager loading of collection:
RestaurantsRepository
.GetAll()
.Include(r => r.Categories)
This should execute single query with JOIN against database, like this (real SQL will differ):
SELECT
*
FROM
[Restaurants] JOIN [Categories] ON [Restaurants].Id = [Categories].[RestaurantId]
Also, think about lazy loading - do you really need it, if you're mapping query result into another types (RestaurantModel in your sample).
I have pored through StackOverflow, Google and asp.net trying to find a clear cut, basic example of how to do this. All the examples have been abstract or involved complications that do not apply. I haven't been able to extract much useful from them. So far, none of them have completely answered my question or addressed my issue(s).
I am working on an MVC project with the following model:
Article.cs:
public class Article
{
public int ArticleId { get; set; }
public string Title { get; set; }
.
.
.
public virtual ICollection<Category> Categories { get; set; }
public Article()
{
Categories = new HashSet<Category>();
}
}
Category.cs:
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
public Category()
{
Articles = new HashSet<Article>();
}
}
ArticleEntities.cs:
public class ArticleEntities : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<Category> Categories { get; set; }
}
An article can have many categories and a category can belong to many articles.
So far I can save/update/create all the article fields except the Categories.
I am representing them as a checkboxes in the view. I can get the values for the selected checkboxes into the controller, but, every attempt I have made to store them in the db with the article has failed.
How do I:
1) When saving an edited article, update the existing relations in the relation table without creating duplicates?
2) When saving a new article, create the chosen relations in the relation table?
I assume that you get a list of CategoryIds from the controller post action, a List<int> or more general just an IEnumerable<int>.
1) When saving an edited article, update the existing relations in the
relation table without creating duplicates?
Article article; // from post action parameters
IEnumerable<int> categoryIds; // from post action parameters
using (var ctx = new MyDbContext())
{
// Load original article from DB including its current categories
var articleInDb = ctx.Articles.Include(a => a.Categories)
.Single(a => a.ArticleId == article.ArticleId);
// Update scalar properties of the article
ctx.Entry(articleInDb).CurrentValues.SetValues(article);
// Remove categories that are not in the id list anymore
foreach (var categoryInDb in articleInDb.Categories.ToList())
{
if (!categoryIds.Contains(categoryInDb.CategoryId))
articleInDb.Categories.Remove(categoryInDb);
}
// Add categories that are not in the DB list but in id list
foreach (var categoryId in categoryIds)
{
if (!articleInDb.Categories.Any(c => c.CategoryId == categoryId))
{
var category = new Category { CategoryId = categoryId };
ctx.Categories.Attach(category); // this avoids duplicate categories
articleInDb.Categories.Add(category);
}
}
ctx.SaveChanges();
}
Note that the code also works when you have a ArticleViewModel instead of an Article, given that the property names are the same (SetValues takes an arbitrary object).
2) When saving a new article, create the chosen relations in the relation
table?
More or less the same idea as above but simpler because you don't need to compare with an original state in the database:
Article article; // from post action parameters
IEnumerable<int> categoryIds; // from post action parameters
using (var ctx = new MyDbContext())
{
foreach (var categoryId in categoryIds)
{
var category = new Category { CategoryId = categoryId };
ctx.Categories.Attach(category); // this avoids duplicate categories
article.Categories.Add(category);
// I assume here that article.Categories was empty before
}
ctx.Articles.Add(article);
ctx.SaveChanges();
}