translating a query on collections from SQL to nhibernate - sql

I have a database schema, where I have a Product, Category, CategoryFeature, and an ProductCategoryFeatureValue.
The Model is mapped using Fluent NHibernate, but basically is as follows.
Product
-------
ID
Title
Category
--------
ID
Title
CategoryFeature
---------------
ID
CategoryID
Title
ProductCategoryFeatureValue
---------------
ID
ProductID
CategoryFeatureID
_______________________
Category [one] <-> [many] CategoryFeature
Product [many] <-> [many] ProductCategoryFeatureValue
Basically, the features available to a product are listed in the ProductCategoryFeatureValue table, which is the 'middle-link' for the many-to-many collection.
I need to create a query, where i can find all products, which have ALL the features selected by the user.
Example, doing a search for two features with ids 643229 & 667811 in SQL terms, I would do something like this:
SELECT * FROM Product
JOIN ProductCategoryFeatureValue AS feature1 ON Product.id = feature1.ProductID AND feature1.categoryfeatureid = 643229
JOIN productcategoryfeaturevalue AS feature2 ON Product.id = feature2.ProductID AND feature2.categoryfeatureid = 667811
Another query which I could do is this:
SELECT * FROM product WHERE
((SELECT id FROM productcategoryfeaturevalue AS feature1 WHERE feature1.ItemGroupID = product.id AND feature1.categoryFeatureID = 643229 LIMIT 1) IS NOT NULL)
AND
((SELECT id FROM productcategoryfeaturevalue AS feature2 WHERE feature2.ItemGroupID = product.id AND feature2.categoryFeatureID = 667811 LIMIT 1) IS NOT NULL)
Both have been tested and work well. However, I cannot seem to reproduce them using NHibernate. Any ideas?
Thanks!

I believe you want something like this in SQL
Select *
from Products p
where p.id in (
select fv.ProductId
from ProductCategoryFeatureValue fv
where fv.CategoryFeatureID in (643229,643230)
group by fv.ProductId
having count(*)=#NumberOfDistinctFeaturesSelected
)
That will stop you having to JOIN to the ProductCategoryFeatureValue table multiple times for every feature selected by the user. At the very least your going to get a nicer query plan. If you don't like the IN clause you could also use a temp table instead.
In terms of translating this into NHibernate it doesn't support any HAVING clause logic in the Criteria API but it is supported using HQL.
HQL Examples
var results = session.CreateQuery("from Product p where p.Id in (
select fv.Product.Id
from ProductCategoryFeatureValue fv
where fv.CategoryFeature.Id in :featureids
group by fv.Product.Id
having count(fv)=:features
)")
.SetParameter("featureids", arrayOfFeatureIds)
.SetParameter("features", arrayOfFeatureIds.Count)
.List<Product>();

Not 100% sure from the question exactly what your mappings are but this may be close to what you need
object[] featureIds = new object[2];
featureIds[0] = 643229;
featureIds[1] = 667811;
ICriteria criteria = base.CreateCriteria(typeof(Product));
criteria.CreateAlias("ProductCategoryFeatureValueList",
"ProductCategoryFeatureValue", JoinType.InnerJoin);
criteria.CreateAlias("ProductCategoryFeatureValue.CategoryFeatureID",
"CategoryFeature", JoinType.InnerJoin);
criteria.Add(Expression.In("CategoryFeature.ID", featureIds));
If the "Expression.In" doesn;t quite do what you are after you could just do a quick loop adding
criteria.Add(Expression.Eq("CategoryFeature.ID", featureIds[i]));

I think your biggest problem is adding a condition on a join. I haven't tried it yet, but have been looking forward to the feature of NH 3.+ that lets you add a criteria join.
CreateAlias(string associationPath, string alias, JoinType joinType, ICriterion withClause) CreateCriteria(string associationPath, string alias, JoinType joinType, ICriterion withClause)

Related

Optimizing a request doing tons of exclusions

I have a huge and dirty SQL request doing many exclusions and I feel bad about it. Perhaps, you know a better way to proceed.
Here's a part of my request:
select name, version, iteration, score
from article, articlemaster
where article.idmaster = articlemaster.id
and article.id not in (select article.id
from article, spsarticlemaster
where article.idmaster = articlemaster.id
and articlemaster.name = 'nameOfMyArticle'
and article.version = 'A'
and article.state = 'CONTINUE')
and article.id not in....
and article.id not in....
You think it doesn't look that bad ? Actually, this is only a portion of the request, the "and spsarticle.id not in ...." exclude one article, and i got more than 1000 to exclude, so i'm using a java program to append the other 999.
Any idea how could i make a light version of this abomination ?
You might be better off loading all of the articles to exclude into a temporary table, then joining that table in to your query.
For example, create exclude_articles:
name version state
---- ------- -----
nameOfMyArticle A CONTINUE
Then exclude its results from the query:
select
article.name,
article.version,
article.iteration,
article.score
from
article
join articlemaster
on article.idmaster = articlemaster.id
where
not exists (
select 1
from article article2
join articlemaster articlemaster2
on article2.idmaster = articlemaster2.id
join exclude_articles
on articlemaster2.name = exclude_articles.name
and article2.version = exclude_articles.version
and article2.state = exclude_articles.state
where article.id = article2.id)
This is all assuming that the version and state are actually necessary for the exclusion logic. It would be a much easier case if the name is unique.
If you're using Java to create the query and process the results, then why not do all the complicated logic in Java? Just ask the database for all the articles matching some basic criterion (or maybe you really do want to read through all of them) and then filter the results:
select am.name, a.version, a.iteration, a.score, a.state
from article a, articlemaster am
where a.idmaster = am.id
and <some other basic criteria>
Then in Java loop over all the results (sorry, my Java is super rusty) and filter out the ones you don't want:
ArrayList recordList = ArrayList();
ArrayList finalList = ArrayList();
for (record in recordList) {
if (! filterThisRecord(record)) {
finalList.append(record);
}
}

FULL OUTER JOIN on a Many-to-Many with LINQ Entity Framework

I have a many-to-many relationship of products (p) and materials (m) and the products2materials table (p2m) as the many-to-many link.
I need to get
- all products that have materials assigned,
- all products with no materials assigned,
- and all materials with no products assigned.
Basically a union of what is there.
However, since this will be a data filter, I need to filter out products and/or materials that do not match the search criteria (e.g. all products that start with "A", etc.).
How do I do this in LINQ-to-EF 4.1?
Many thanks!
The following should do the job:
from m in context.Materials //m has to be the first
from p in context.Products
where !p.Select(p1 => p1.Material).Contains(m) || p.Material == null || p.Material == m
For performance it would probably be better the following:
var a = from p in context.Products select p.Material;
var b = from m in context.Materials //m has to be the first
from p in context.Products
where a.Contains(m) || p.Material == null || p.Material == m
Linq doesn't offer full outer join operation directly so your best choice is to try separate left and right join L2E queries and union them to single result set.
I would try something like (not tested):
var query = (from p in context.Products
from m in p.Materials
select new { p, m })
.Union(
from m in context.Materials
from p in m.Products
select new { p, m })
...
Perhaps you will have to use DefaultIfEmpty to enforce outer joins.
From yours description it looks like you actually need:
All Products
All unused Materials
Do you need it as a single IQueryable (if yes which fields do you need) or as 2 IQueryables?

VB.NET LINQ group join followed by another group

I have two collections of objects in VB.NET that I want to link together using a join, and possibly group together. Basically my objects look like this:
Institution
ID
Name
Visit
ID
InstitutionID
Date
IsFollowUp
IsSelfScheduled
A visit has to be to an institution, but an institution can also have no visits. I can link them together using LINQ, but I can't quite get the institutions that do not have any visits to appear in the list also (which is what I am looking for). I know I have to use a group join, but I can't work out how to incorporate the existing join as well.
From p In visitList Where p.IsFollowUp = False AndAlso p.IsSelfScheduled = False
Group p By Key = p.InstitutionID Into grp = Group
Select InstitutionID = Key, Visits = grp, LastVisitDate = grp.FirstOrDefault().ProvisionalDate
If the worst comes to the worst, I can implement a private class to do what I want, but it seems like it should be something simple in LINQ.
Edit:
Okay, using the link below that Tim posted, I managed to come up with something like this:
From i In institutionList
Select InstitutionID = i.ID, Name = i.Name, Inspections =
(
From p In visitList Where p.InstitutionID = i.ID Select p
)
It uses a sub-query to pull out the relevant information, but whether it is efficient or not, I have no idea. It seems to pull out the relevant information.

NHibernate Return Values

I am currently working on a project using NHiberate as the DAL with .NET 2.0 and NHibernate 2.2.
Today I came to a point where I had to join a bunch of entities/collections to get what I want. That is fine.
What got me was that I do not want the query to return a list of objects of a certain entity type but rather the result would include various properties from different entities.
The following query is not what I am doing but it is kind of query that I am talking about here.
select order.id, sum(price.amount), count(item)
from Order as order
join order.lineItems as item
join item.product as product,
Catalog as catalog
join catalog.prices as price
where order.paid = false
and order.customer = :customer
and price.product = product
and catalog.effectiveDate < sysdate
and catalog.effectiveDate >= all (
select cat.effectiveDate
from Catalog as cat
where cat.effectiveDate < sysdate
)
group by order
having sum(price.amount) > :minAmount
order by sum(price.amount) desc
My question is, in this case what type result is supposed to be returned? It is certainly not of type Order, neither is of type LineItems.
Thanks for your help!
John
you can always use List of object[] for returning data and it will work fine.
This is called a projection, and it happens any time you specify an explicit select clause that contains rows from various tables (or even aggregate / summary data from a single table).
Using LINQ you can create anonymous objects to store these rows of data, like this:
var crunchies = (from foo in bar
where foo.baz == quux
select new { foo.corge, foo.grault }).ToList();
Then you can do crunchies[0].corge for example to pull out the rows & columns.
If you are using NHibernate.Linq this will "just work".
If you're using HQL or Criteria API, then what Fahad mentioned will work. You'll get a List<object[]> as a result, and the index of the array references the order of the columns that you returned in your select clause.

LINQ To SQL Paging

I've been using .Skip() and .Take() extension methods with LINQ To SQL for a while now with no problems, but in all the situations I've used them it has always been for a single table - such as:
database.Users.Select(c => c).Skip(10).Take(10);
My problem is that I am now projecting a set of results from multiple tables and I want to page on the overall set (and still get the benefit of paging at the DB).
My entity model looks like this:
A campaign [has many] groups, a group [has many] contacts
this is modelled through a relationship in the database like
Campaign -> CampaignToGroupMapping -> Group -> GroupToContactMapping -> Contact
I need to generate a data structure holding the details of a campaign and also a list of each contact associated to the campaign through the CampaignToGroupMapping, i.e.
Campaign
CampaignName
CampaignFrom
CampaignDate
Recipients
Recipient 1
Recipient 2
Recipient n...
I had tried to write a LINQ query using .SelectMany to project the set of contacts from each group into one linear data set, in the hope I could .Skip() .Take() from that.
My attempt was:
var schedule = (from c in database.Campaigns
where c.ID == highestPriority.CampaignID
select new PieceOfCampaignSchedule
{
ID = c.ID,
UserID = c.UserID,
Name = c.Name,
Recipients = c.CampaignGroupsMappings.SelectMany(d => d.ContactGroup.ContactGroupMappings.Select(e => new ContactData() { /*Contact Data*/ }).Skip(c.TotalSent).Take(totalRequired)).ToList()
}).SingleOrDefault();
The problem is that the paging (with regards to Skip() and Take()) is happening for each group, not the entire data set.
This means if I use the value 200 for the parameter totalRequired (passed to .Take()) and I have 3 groups associated with this campaign, it will take 200 from each group - not 200 from the total data from each group associated with the campaign.
In SQL, I could achieve this with a query such as:
select * from
(
select [t1].EmailAddress, ROW_NUMBER() over(order by CampaignID desc) as [RowNumber] from contacts as [t1]
inner join contactgroupmapping as [t2] on [t1].ID = [t2].ContactID
inner join campaigngroupsmapping as [t3] on [t3].ContactGroupID = [t2].GroupID
where [t3].CampaignID = #HighestPriorityCampaignID
) as [Results] where [Results].[RowNumber] between 500 and 3000
With this query, I'm paging over the combined set of contacts from each group associated with the particular campaign. So my question is, how can I achieve this using LINQ To SQL syntax instead?
To mimic the SQL query you provided you would do this:
var schedule = (from t1 in contacts
join t2 in contactgroupmapping on t1.ID equals t2.GroupID
join t3 in campaigngroupsmapping on t3.ContactGroupID = t2.GroupID
where t3.CampaignID = highestPriority.CampaignID
select new PieceOfCampaignSchedule
{
Email = t1.EmailAddress
}).Skip(500).Take(2500).ToList()
Are you trying to page over campaigns, recipients, or both?
Use a view to aggregate the results from the multiple tables and then use LINQ over the view
I think your attempt is really close; Maybe I'm missing something, but I think you just need to close your SelectMany() before the Skip/Take:
Recipients = c.CampaignGroupsMappings.SelectMany(d => d.ContactGroup.ContactGroupMappings.Select(e => new ContactData() { /*Contact Data*/ })).Skip(c.TotalSent).Take(totalRequired).ToList()
Note: added ")" after "/* Contact Data */ })" and removed ")" from after ".Take(totalRequired)"