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

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.

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

Entity Framework self referencing - Parent-Child

I have a self referencing table and i need to bind the date from table to tree view. Parent-Child. My question is How to get tree view from that table using Entity Framework using anonymous type
some like this:
var tree = db.Categories.Select(g => new
{
id = g.CategoryId,
text = g.CategoryName,
children = g.children.Select(w => new
{
id = w.CategoryId,
parent = w.ParentCategoryId,
text = w.CategoryName,
}).ToList(),
}
).ToList();
Here is the code:
public partial class Category
{
public Category()
{
this.children = new HashSet<Category>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public Nullable<int> ParentCategoryId { get; set; }
public virtual ICollection<Category> children { get; set; }
public virtual Category Parent { get; set; }
}
I suppose to create a type for your purpose rather then using anonymous type and fill model via recursive method.
var three = BuildThree(db.Categories);
public IEnumerable<CategoryVm> BuildThree(IEnumerable<Categories> categories, int? parentCategoryId = null)
{
if (categories == null)
return null;
var result = categories.select(c => new CategoryVm()
{
id = c.CategoryId,
text = c.CategoryName,
parent = parentCategoryId,
children = BuildThree(c.children, c.CategoryId)
}
return result;
}
This solution there is on drawback - each time time when you call navigation property (children) you will make request to database. If you want to make it in one request and you have only one level of nested categories then .Include(c => c.Children) enough otherwise you have to make a choice one of the next options:
Write a common table expression (CTE) query, put it to view or stored procedure and map it by means of EF. The role of EF is not really big because the most tricky part is the SQL query
Especially for this kind of purpose, Microsoft SQL Server has hierarchyid but EF does not support it from the box. However there are some workarounds: Entity Framework HierarchyId Workarounds
You can add something like rootId to the Comment entity, when each child replay will has a link to root comment. After that you can load all hierarchy to memory in one sql query and map it manually. Given that database is bottleneck it will much faster then make new query for each level of hierarchy.

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

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

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

Any of the properties equals any of a list of objects

I have a problem in Entity-Framework, using Code-First, that I couldn't solve.
Having entities of the type
public class Product {
public int ID {get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category {
public int ID {get; set;}
public virtual ICollection<Product> Products { get; set; }
// rest omitted
}
in my database, i try to get all Products that have at least one Category from a list of given Categories. I need an Expression as this expression is combined with other expressions later.
Ie. i tried:
var searchFor = new List<Category>{...};
var expression = product => product.Categories.Any(cat => searchFor.Contains(cat))
Executing this later against a DbContext
context.Products.Where(expression).ToList();
creates an exception stating mainly that This context supports primitive types only.
Changing it to
var expression = product => product.Categories.Any(
cat => searchFor.Any(d => d.ID == cat.ID));
to get rid of the object comparison didn't help. I'm stuck. How can I manage that?
You should get rid of List<Category>, replacing it with a list of IDs, like this:
// I'm assuming that ID is of type long; please fix as necessary
var searchFor = new List<long>{...};
var expression = product =>
product.Categories.Any(cat => searchFor.Contains(cat.ID))
If you've already got a list of categories, you can build a list of IDs outside the query:
var searchForIds = searchFor.Select(x => x.ID).ToList();
var query = context.Products
.Where(product => product.Categories
.Any(cat => searchForIds.Contains(cat.ID)));
I don't know that that will work, but it might. (Apologies for the indentation... it's just to avoid scrolling.)

Categories