My setup
I have two classes, here shown stripped down to what is needed in this example:
public class Photo : Entity
{
[Key]
public int Id { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public Photo()
{
Tags = new List<Tag>();
}
}
public class Tag : Entity
{
[Key]
public string Text { get; set; }
public virtual ICollection<Photo> Photos { get; set; }
public Tag()
{
Photos = new List<Photo>();
}
}
As shown above theres is a many-to-many relationship between the two entities.
I'm using EF 4.1 code-first.
Example:
"photo1" has "tag1", "tag2" and "tag3" in its tags navigation property.
"photo2" has "tag2", "tag3" and "tag4" in its tags navigation property.
"photo3" has "tag2" and "tag4" in its tags navigation property.
Total tag count in all photos:
"tag1": 1
"tag2": 3
"tag3": 2
"tag4": 2
Total tags: 8
Note
My end goal is this tag cloud, but using MVC3:
http://www.geekzilla.co.uk/View960C74AE-D01B-428E-BCF3-E57B85D5A308.htm
First question
How do I (using EF) find out how many times the most used tag(s) is used (finding the count of "tag2")?
And the same for the least used tag(s) (count of "tag1" in above example).
In the link above these to lines of code is used:
double.TryParse(tagData.Compute("min(count)", null).ToString(), out min);
double.TryParse(tagData.Compute("max(count)", null).ToString(), out max);
What is the EF/LINQ equivalent?
Second question
How do I get the count for each tag or the count for the 50 most used tags?
Counts by tag:
from t in Context.Tags
select new
{
t.Text,
t.Photos.Count()
}
Most used tag:
(from t in Context.Tags
let photoCount = t.Photos.Count()
orderby photoCount descending
select new
{
t.Text,
photoCount
}).FirstOrDefault()
50 most used tags (may not actually have 50 there):
(from t in Context.Tags
let photoCount = t.Photos.Count()
orderby photoCount descending
select new
{
t.Text,
photoCount
}).Take(50)
Freehand, so might not be 100% syntactically correct or the smallest/most readable way to do it.
Edit: Added Example:
foreach(var result in Context.Tags.Select(x => new { t.Text, t.Photos.Count() }))
{
Console.WriteLine(string.Format("Text: {0}, Count: {1}", result.Text, result.Count.ToString());
}
Related
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 have a data model in MVC5 entity framework in which a post has a category. This category can be nested such as.
Top Level: 0
-> Lower Level: 1
-> Lowest Level: 2
This is represented in my model as:
public class CategoryModel
{
public int Id { get; set; }
public CategoryModel ParentCategory { get; set; }
public string Title { get; set; }
}
Now when I display my post which has (from the above example) category "Lowest Level 2", I would like to display
"Top level: 0 > Lower Level: 1 > Lowest Level: 2"
somewhere on that page to inform the user where they are.
Problem is I dont have any idea of how to do this.
Propably really simple (as with all things in lambda) but I don't really know how and my googling skills are really off.
Edit as per comment question:
The post is defined as this:
public class PostModel
{
public int Id { get; set; }
public CategoryModel Category { get; set; } // a post can only have one category
public string Text { get; set; }
}
What I want to do is follow the CategoryModel relation, and then keep following the Categories ParentCategory untill it is null. This is always a 1 to 1 relation.
More Edit:
I was fairly simply able to do this with a TSQL-CTE expression but still no idea how to convert this to lambda.
SQL:
;WITH Hierarchy(Title, CatId, LEVEL, CategoryPath)
AS
(
Select c.Title, c.Id, 0, c.Title
FROM Categories c
WHERE c.[ParentCategory_Id] IS NULL
UNION ALL
SELECT c.Title, c.Id, H.LEVEL+1, H.CategoryPath+' > '+c.Title
FROM Categories c
INNER JOIN Hierarchy H ON H.CatId = c.[ParentCategory_Id]
)
SELECT SPACE(LEVEL*4) + H.Title, *
FROM Hierarchy H
ORDER BY H.CategoryPath
Result:
Assuming you have an instance of CategoryModel you could write a function that will build a string list with the chain of all titles:
private void FormatCategories(CategoryModel model, List<string> result)
{
result.Add(model.Title);
if (model.ParentCategory != null)
{
FormatCategories(model.ParentCategory, result);
}
}
and then:
CategoryModel model = ... // fetch your model from wherever you are fetching it
var result = new List<string>();
FormatCategories(model, result);
Now all that's left is to reverse the order of elements in the list and join them to retrieve the final result:
result.Reverse();
string formattedCategories = string.Join(" -> ", result);
// At this stage formattedCategories should contain the desired result
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()}
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 **/
});
I have the following entity collections in RavenDB:
public class EntityA
{
public string Id { get; set; }
public string Name { get; set; }
public string[] Tags { get; set; }
}
public class EntityB
{
public string Id { get; set; }
public string Name { get; set; }
public string[] Tags { get; set; }
}
The only thing shared is the Tags collection: a tag of EntityA may exist in EntityB, so that they may intersect.
How can I retrieve every EntityA that has intersecting tags with EntityB where the Name property of EntityB is equal to a given value?
Well, this is a difficult one. To do it right, you would need two levels of reducing - one by the tag which would expand out your results, and another by the id to collapse it back. Raven doesn't have an easy way to do this.
You can fake it out though using a Transform. The only problem is that you will have skipped items in your result set, so make sure you know how to deal with those.
public class TestIndex : AbstractMultiMapIndexCreationTask<TestIndex.Result>
{
public class Result
{
public string[] Ids { get; set; }
public string Name { get; set; }
public string Tag { get; set; }
}
public TestIndex()
{
AddMap<EntityA>(entities => from a in entities
from tag in a.Tags.DefaultIfEmpty("_")
select new
{
Ids = new[] { a.Id },
Name = (string) null,
Tag = tag
});
AddMap<EntityB>(entities => from b in entities
from tag in b.Tags
select new
{
Ids = new string[0],
b.Name,
Tag = tag
});
Reduce = results => from result in results
group result by result.Tag
into g
select new
{
Ids = g.SelectMany(x => x.Ids),
g.First(x => x.Name != null).Name,
Tag = g.Key
};
TransformResults = (database, results) =>
results.SelectMany(x => x.Ids)
.Distinct()
.Select(x => database.Load<EntityA>(x));
}
}
See also the full unit test here.
There is another approach, but I haven't tested it yet. That would be to use the Indexed Properties Bundle to do the first pass, and then map those results for the second pass. I am experimenting with this in general, and if it works, I will update this answer with the results.