Entity Framework - How to improve query with many-to-many relationship - c#

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

Related

Entity Framework: One-To-Many?

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

How to write T-SQL many-to-many with subquery in EF

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.

Linq to Entities filter navigation collection properties

I have an order class that has two navigation properties of type collections; OrderDetails and Categories. There is a one to many relationship between Order and both OrderDetail and Category. An Order may or may not have a Category associated to it. An OrderDetail record has a CustomerID field.
I am trying to retrieve a list of Orders that have categories associated to them and their corresponding OrderDetail records for a specific customer. I want to achieve this using linq to entities if possible.
public class order
{
public order()
{
OrderDetails = new list<OrderDetail>();
Categories = new list<Category>();
}
public int OrderID { get; set; }
public DateTime OrderDate { get; set; }
public virtual List<OrderDetail> OrderDetails { get; set; }
public virtual List<Category> Categories{ get; set; }
}
public class OrderDetail
{
public int OrderDetailID { get; set; }
public int CustomerID { get; set; }
public virtual Order Order { get; set; }
}
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public virtual Order Order { get; set; }
}
I can get it to work if I start with the OrderDetail entity first as shown below but how would I write this if I want to start with the Order entity first?
var query = from od in _dbCtx.OrderDetails
.Include("Order")
.Include("Order.Categories")
where od.CustomerID == custID && od.Order.Categories.Count > 0
select od;
You can try this:
var query =_dbCtx.Orders.Include("OrderDetails")
.Include("Categories")
.Where(o=>o.Categories.Count>0)
.SelectMany(o=>o.OrderDetails.Where(od=>od.CustomerID == custID));
The key in this query is the SelectMany extension method, which is used to flatten the Where's result into one single collection.
Edit 1
Due to you have disabled lazy loading, the Order navigation property in the OrderDetails that you get when you execute my query are null. One option could be using the Load method when you use the result:
foreach(var od in query)
{
// Load the order related to a given OrderDetail
context.Entry(od).Reference(p => p.Order).Load();
// Load the Categories related to the order
context.Entry(blog).Collection(p => p.Order.Categories).Load();
}
Another option could be returning an anonymous type:
var query =_dbCtx.Orders.Include("OrderDetails")
.Include("Categories")
.Where(o=>o.Categories.Count>0)
.SelectMany(o=>o.OrderDetails.Where(od=>od.CustomerID == custID).Select(od=>new {Order=o,OrderDetail=od}));
But I don't like anyone of these solutions.The most direct way is the query that you already had from the beginning.
The default setting for Entity Framework is to allow lazy loading and dynamic proxies.
And in this case when you are using the virtual keyword on the relational properties these 'should' (in case you have not disabled it in EF) load with Lazy Loading.
Lazy Loading Loads the relational properties when you need it. Example:
var load = data.Orders.OrderDetails.Tolist() // Would load all OrderDetails to a list.
//Below would load all OrderDetails that has a OrderId smaller than 5
var loadSpecific = data.Orders.Where(x=> x.OrderId < 5).OrderDetails.ToList()
The case you are describing is Eager Loading('Include' statements), Nothing wrong with it. But if you are planning on using it I would consider using below syntax instead. This would give compilation error if you decide to change the name of the relational property.
var load = data.Orders
.Include(x => x.OrderDetails)
.Include(x => x.Categories)
I suggest you take 10-15 minutes of time and read up on it in this article:
https://msdn.microsoft.com/en-us/data/jj574232.aspx

Group By object within a nested list linq

What i am trying to do is i have sales order object which contains
sales order header
and list of order lines
within the order lines i have the actual order line, product information object and stock information object:
public class SalesOrder
{
public Header SalesHeader { get; set; }
public List<OrderLineProductInfo> OrderLines { get; set; }
}
public class OrderLineProductInfo
{
public Line salesOrderLine { get; set; }
public Info ProductInfo { get; set; }
public Stock ProductStock { get; set; }
}
so i can get a list of SalesOrder Objects so example sales order index 0
has 2 lines the ProductStockObject within one of these lines has Preferred Supplier of abc and the other line has Preferred Supplier 123
i want to be able to group on the Preferred Supplier property
var separatePreferredSuppliers =
(from b in x.OrderLines
.GroupBy(g => g.ProductStock.PreferredSupplier )
select ...
).ToList();
not quite sure what comes next what needs to be selected? a new list of SalesOrder?
I want it so that it gives two instances of the sales order but split in 2 one for each preferred supplier
I think I get what you mean
from line in x.OrderLines
group line by line.ProductStock.PreferredSupplier into grouped
select new SalesOrder
{
OrderLines = grouped.ToList()
}
though I'm not sure how you populate your SalesHeader
easy when you know what the groupby function ruturns -- you want the key and the list:
.Select( (g) => new { supplier = g.Key, prodlist = g.ToList()}

Entity Framework - Get 'fake' navigation property within one query

I have a Product table that has no relation defined to the translation table. I added a Translation property to the Product POCO as [NotMapped].
**My Product POCO: **
public partial class Product
{
public int ProductID { get; set; }
public double Price { get; set; }
[NotMapped]
public virtual Translation Translation{ get; set; }
/** Other properties **/
}
I also have a Translation table, and like the name says, it contains all the translations.
Now, the right translation can be retrieved from the database by providing three parameters: LanguageID, TranslationOriginID and ValueID.
LanguageID: ID from the language that the user has defined.
TranslationOriginID: Simply said, 'What table contains the entity that I want the translation for?' In other words, this ID points to another table that contains all possible origins. An origin is a table/entity that can have a translation. E.g: The origin in this example is Product.
ValueID: This is the ID of the entity that I want a translation for.
My Translation POCO:
public partial class Translation
{
public int TranslationID { get; set; }
public byte LanguageID { get; set; }
public short TranslationOriginID { get; set; }
public int ValueID { get; set; }
public string TranslationValue { get; set; }
/** Other properties **/
public virtual TranslationOrigin TranslationOrigin { get; set; }
public virtual Language Language { get; set; }
}
When I want to retrieve all products with their Translation, I execute this code:
List<Product> products = context.Products.ToList();
foreach (Product product in products)
{
product.Translation = context.Translations.FirstOrDefault(y => y.LanguageID == 1 && y.TranslationOriginID == 2 && y.ValueID == product.ProductID);
}
Like you can see, I execute for every product in the list another query to get the translation.
My question:
Is it possible to get all the products and their translation in one query? Or even that I automatically retrieve the right translation when I select a product?
I already tried an .Include() and a .Select(). It didn't work, maybe I did something wrong?
I also tried this method, didn't work either.
Btw, I use Entity framework 5 with .NET 4 (so, Entity Framework 4.4).
Thanks in advance.
Greetings
Loetn
Answer
With the example given by Ed Chapel, I came up with a solution.
return (from p in context.Products
join t in context.Translations
on new
{
Id = p.ProductID,
langId = languageID,
tOriginId = translationOriginID
}
equals new
{
Id = d.ValueID,
langId = d.LanguageID,
tOriginId = d.TranslationOriginID
}
into other
from x in other.DefaultIfEmpty()
select new
{
Product = p,
Translation = x
})
.ToList().ConvertAll(x => new Product()
{
Code = x.Product.Code,
Translation = x.Translation,
/** Other properties **/
});
I don't like proper LINQ in most cases. However, join is one scenario where the LINQ is easy than the extensions methods:
from p in context.Products
join t in context.Translations
on t.ValueID equals p.ValueID
&& t.LanguageID == 1
&& t.TranslationOriginID == 2
into joinT
from x in joinT
select new {
Product = p,
Translation = t,
};
You then loop over the result setting x.Product.Translation = x.Translation.
First of all you should realize that your translations table is not structured like a dba would like it You have a non enforced relationship because depending on the OriginId your valueId references a different table.
Because of this you cannot use lazy loading or includes from EF.
My best idea at this point would to manually join the table on an anonymous type(to include your originId). Afterwards you can iterate over the results to set the translation property
The result would look like this :
var data = from p in context.Products
join pt in context.Translations on new{p.Id,2} equals new {pt.ValueId, pt.OriginId} into trans
select new {p, trans};
var result = data.ToList().Select( a =>
{
a.p.Translations = a.trans;
return a.p;
}).ToList();
With the example that Ed Chapel proposed as a solution, I came up with this.
return (from p in context.Products
join t in context.Translations
on new
{
Id = p.ProductID,
langId = languageID,
tOriginId = translationOriginID
}
equals new
{
Id = d.ValueID,
langId = d.LanguageID,
tOriginId = d.TranslationOriginID
}
into other
from x in other.DefaultIfEmpty()
select new
{
Product = p,
Translation = x
})
.ToList().ConvertAll(x => new Product()
{
Code = x.Product.Code,
Translation = x.Translation,
/** Other properties **/
});

Categories