Possible to do two Group Bys and an if count check in one LINQ lambda statement? - entity-framework

Kind of a specific question but I wasn't sure how to approach it. I've got a list of rooms, that I am trying to group first by type, then by owner. I am doing this to check if there are duplicate rooms for a given owner and type (which shouldn't be possible so I need to prune them out). Right now my code looks like this:
IQueryable<IGrouping<Guid, Room>> allRoomsByOwner = _dbContext.Rooms.GroupBy(x => x.OwnerId);
List<Room> duplicates = new List<Room>();
foreach (IGrouping<Guid, Room> roomsByOwner in allRoomsByOwner)
{
IEnumerable<IGrouping<Guid, Room>> roomsOfOwnerByType = roomsByOwner.ToList().GroupBy(x => x.TypeId);
foreach (IGrouping<Guid, Room> grouping in roomsOfTypeByType)
{
if (grouping.Count() > 1)
{
duplicates.AddRange(grouping.ToList());
}
}
}
I'm just wondering if it's possible to put this all into one LINQ statement? I've got similar things before, but not quite this complex and not using two group bys. Thanks.

You can group by multiple columns ( OwnerId and TypeId) and flatten the groups with more than one elements (using the SelectMany method) to get the duplicates:
var duplicates = _dbContext.Rooms.GroupBy(x => new{x.OwnerId,x.TypeId})
.Where(g=>g.Count()>1)
.SelectMany(g=>g.Skip(1))// If you like you can skip the first element as representative of the group and the treat the rest as a duplicate.
.ToList();

Related

Filtering on Many to Many relationship

I have 3 related entities with 1 to many relationships.
Account 1-* Collection 1-* items
Each of these has a many to many relationship with Users, (with join tables AccountManagers , CollectionManagers, ItemManagers)
I'm trying to get a list of Collections for a user if he's in any of the join tables.
I was thinking create 3 expressions and then merge the 3 results at the end and somehow remove duplicates.
It seemed like Lambda expressions are the way to go, but I'm still learning them.
I think the middle one is easier , like
db.Collections.where(C => C.CollectionManagers.UserID == CurrentUserID)
But how would you gather a list of collections from the users account manager and item manager entries?
Thanks in advance
Using LINQ, the Union operator will return only the unique Collection rows, so assembling each category and combining them should work.
For Items, I thought it would be most efficient to find all Items managed by the current user and then find all collections they belong to:
var iCollections = Items.Where(i => i.ItemManagers.Any(im => im.UserId == CurrentUserID)).SelectMany(i => Collections.Where(c => c.Items.Contains(i)));
It is also possible to do this the other way, e.g. find all Collections that contain an Item managed by the current user:
var iCollections = Collections.Where(c => c.Items.Any(i => i.ItemManagers.Any(im => im.UserId == CurrentUserID)));
For Collections, as you pointed out, you just need to find all collections where the current user manages the collection:
var cCollections = Collections.Where(c => c.CollectionManagers.Any(cm => cm.UserId == CurrentUserID));
For Accounts, we find all accounts managed by the current user and then all collections owned by the account:
var aCollections = Accounts.Where(a => a.AccountManagers.Any(am => am.UserId == CurrentUserID)).SelectMany(a => a.Collections);
Then you can Union the results together:
var CurrentUserCollections = iCollections.Union(cCollections).Union(aCollections);

Doing subqueries in Mybatis, or query recursively the selected values

UPDATE:
I understood that the solution to my problem is doing subqueries, which apply a different filter each time, and they have a reduced result set. But I can't find a way to do that in MyBatis logic. Here is my query code
List<IstanzaMetadato> res = null;
SqlSession sqlSession = ConnectionFactory.getSqlSessionFactory().openSession(true);
try {
IstanzaMetadatoMapper mapper = sqlSession.getMapper(IstanzaMetadatoMapper.class);
IstanzaMetadatoExample example = new IstanzaMetadatoExample();
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> entry = it.next();
example.createCriteria().andIdMetadatoEqualTo(entry.getKey()).andValoreEqualTo(entry.getValue());
}
example.setDistinct(true);
res = mapper.selectByExample(example);
I need to execute a new selectByExample but inside the while cycle, and it has to query the previus "SELECTED" results....
Is there a Solution ?
ORIGINAL QUESTION:
I have this table structure
I have to select rows from the table with different filters, specified by the final user.
Those filters are specified by a couple (id_metadato, valore), in example you can have id_metadato = 3 and valore = "pippo";
the user can specify 0-n filters from the web page typing 0-n values inside the search boxes which are based on id_metadato
Obviusly, the more filters the users specifies, the more restriction would have the final query.
In example if the user fills only the first search box, the query will have only a filter and would provide all the rows that will have the couple (id_metadato, valore) specified by the user.
If he uses two search boxes, than the query will have 2 filters, and it will provide all the rows that verify the first condition AND the second one, after the "first subquery" is done.
I need to do this dinamically, and in the best efficient way. I can't simply add AND clause to my query, they have to filter and reduce the result set every time.
I can't do 0-n subqueries (Select * from ... IN (select * from ....) ) efficiently.
Is there a more elegant way to do that ? I'm reading dynamic SQL queries tutorials with MyBatis, but I'm not sure that is the correct way. I'm still trying to figure out the logic of the resosultio, then I will try to implement with MyBatis.
Thanks for the answers
MyBatis simplified a lot this process of nesting subqueries, it was sufficient to concatenate the filter criterias and to add
the excerpt of the code is the following
try {
IstanzaMetadatoMapper mapper = sqlSession.getMapper(IstanzaMetadatoMapper.class);
IstanzaMetadatoExample example = new IstanzaMetadatoExample();
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> entry = it.next();
if (listaIdUd.isEmpty()) {
example.createCriteria().andIdMetadatoEqualTo(entry.getKey()).andValoreEqualTo(entry.getValue());
example.setDistinct(true);
listaIdUd = mapper.selectDynamicNested(example);
continue;
}
example.clear();
example.createCriteria().andIdMetadatoEqualTo(entry.getKey()).andValoreEqualTo(entry.getValue()).andIdUdIn(listaIdUd);
example.setDistinct(true);
listaIdUd = mapper.selectDynamicNested(example);
}

entity framework 4.0 multiple joins

This is my real world example.
I have 4 tables:
Person
Plan
Coverage
CoveredMembers
Each person can have many plans, each of those plans can have many coverages. Each of those coverages can have many CoveredMembers.
I need a query that will apply a filter on Plan.PlanType == 1 and CoveredMembers.TermDate == null.
This query should bring back any person who has a medical type plan that is not terminated.
This SQL statement would do just that:
SELECT Person.*, Plans.*, Coverages.*, CoveredMembers.*
FROM Person P
INNER JOIN Plan PL ON P.PersonID = PL.PersonID
INNER JOIN Coverage C on PL.PlanID = C.PlanID
INNER JOIN CoveredMember CM on C.CoverageID = CM.CoverageID
WHERE CM.TermDate = NULL AND PL.PlanType = 1
I have figured out how to do this using anonymous types, but I sometimes need to update the data and save back to the database - and anonymous types are read only.
I was given a solution that did work using JOIN but it only brought back the persons (albeit filtered the way I needed). I can then loop through each person:
foreach (var person in persons) {
foreach (var plan in person.Plans{
//do stuff
}
}
But wouldn't that make a db call for each iteration of the loop? I have 500 persons with 3 unterminated medical plans each, so it would call the db 1500 times?
This is why I want to bring the whole data tree from Persons to CoveredMembers back in one shot. Is this not possible?
I believe this is accomplished in two parts:
Your query to determine the people you wish to have returned based on your criteria as discussed in this question previously: Entity framework. Need help filtering results
Properly setting the navigation properties for entities you want brought together to be eagerly loaded: http://msdn.microsoft.com/en-us/data/jj574232.aspx
For example if your Person entity looks like:
public class Person {
public List<Plan> Plans {get; set;}
...
}
When returning data from the dbcontext you can also use explicit eager loading with the include option:
var people = context.People
.Include(p => p.Plans)
.ToList();
....
If these are nested - coverage is part of plan, etc (which it looks like, it goes something like):
var people = context.People
.Include(p => p.Plans.Select(pl=>pl.Coverage).Select(c=>c.CoveredMembers)))
.ToList();
....
I am making some assumptions about your data model here, and my code above probably needs a little tweaking.
EDIT:
I might need someone else to weigh in here, but I don't think you can add the where clause into an include like that (my example above leads you that way a bit by putting the include on the context object, instead return an IQueryable with your conditions set as solved in your first post (without a ToList() called on it) and then use the code you wrote above without the Where clauses:
From first post (you supplied different criteria in this one, but same concept)
var q = from q1 in dbContext.Parent
join q2 in dbContext.Children
on q1.key equals q2.fkey
join q3 in ........
where q4.col1 == 3000
select q1;
Then:
List<Person> people = q.Include(p => p.Plans
.Select(pl => pl.Coverages)
.Select(c => c.CoveredMembers).ToList();
Again, doing this without being able to troubleshoot - I am sure it would take me a few attempts to iron this one out too.

Entity Framework IN clause on navigation property

I have a object IQueryable which I am trying to dynamically add Where clauses, this works great for columns on Listing object, however now conditionally I would like to add a IN clause to a navigation property (ListingAmenities) which has columns ListingID, AmenityID
I have critiera.AmenityIDs which may contain Amenities I like to filter the results on.
I am trying to achieve the following if there is any amenityIDs in my array
select * from Listings l inner join ListingAmenities a on l.ListingID = a.ListingID where a.AmenityID IN(1,2,3)
here is my code (I am using EF5)
if (criteria.AmenityIDs.Count > 0)
{
listings = listings.Where(x => x.ListingAmenities.Any(y => y.AmenityID == criteria.AmenityIDs));
}
which of course does not work.
Important note is that I am adding these WHERE clauses dynamically so that is why I am building up a IQueryable object
Enumerable.Contains translates into IN in SQL, so you can use:
if (criteria.AmenityIDs.Count > 0)
{
listings = listings.Where(x => x.ListingAmenities
.Any(y => criteria.AmenityIDs.Contains(y.AmenityID)));
}
Be careful if the AmenityIDs collection is very long because Contains has performance problems for large collections. But 100 elements or so shouldn't be a problem.

one to many join - taking only the last one on the many part

I'm quite a newbie in EF, so I'm sorry if my question has been answered before.. I just can't figure out the syntax..
I have two entities, Category & Product, where one category has many products.
I want to get all categories, with only their latest product (it has a date property named timestamp)
I have no idea how to do that. :-/
If possible I'd like to know the syntax of the two ways to write it, both the sql-like syntax, and the C# like syntax, e.g.:
ctx.Categories.Include("Products").ToList()
from c in ctx.Categories.Include("Products")
Thanks!
Here's the SQL-like way:
var categories =
from p in products
group p by p.Category into g
select new { Category = g.TheKey, LatestProduct = g.Max(p => p.TimeStamp) };
This is the Lambda-way (warning, untested):
var categories = products.GroupBy(p => p.Category)
.Select(g => new { Category = g.TheKey,
LatestProduct = g.Max(p => p.TimeStamp)});
A note on Categories.Include("Products"), you don't need this in your example. You use "Include" for eager-loading, so that for example if you had a list of Categories returned from EF, when you do Categories.Product you will get the associated product.
But all you require is a list of categories, and a single product for each one - which is already returned in the above LINQ query, so no need for Include.