tags:

views:

83

answers:

4

I have a sql query that performs the type of select I'm after:

select * from Products p where p.ProductId in (
    select distinct ProductId from ProductFacets 
    where ProductId in (select ProductId from ProductFacets where FacetTypeId = 1)
    and ProductId in (select ProductId from ProductFacets where FacetTypeId = 4)
)

There can be multiple FacetTypeIds passed into this query.

This query is constructed in a method based on a parameter argument of type int[].

public IEnumerable<Product> GetProductsByFacetTypes(string productTypeSysName, int[] facetTypeIds)

I'm trying to work out how to achieve this in LINQ. So far I've come up with something like this:

var products = from p in sc.Products
where p.ProductType.SysName == productTypeSysName
where p.FacetTypes.Any(x => x.FacetTypeId == 1)
where p.FacetTypes.Any(x => x.FacetTypeId == 4)
select p;

This returns the correct result set.

However I'm not sure how I can build this query using the int[] facetTypeIds parameter.

EDIT:

ProductFacets contains the following data:

ProductId, FacetTypeId
1, 1
1, 2
2, 1
2, 3
3, 4
3, 5
4, 1
4, 2

As an example, I'd like to be able to select only Products which have a FacetTypeId of 1 AND 2. The result set should contain ProductIds 1 and 4

+1  A: 

EDIT: Sorry I misread the question. I would suggest you use a PredicateBuilder and build up the Where clause dynamically if you need all of the types to be present. This would use extension methods directly.

var facetTypeIds = new [] { 1, 4, ... };
var predicate = PredicateBuilder.True<Product>();
foreach (int id in facetTypeIds)
{
    int facetId = id; // avoid capturing loop variable
    predicate = predicate.And( p => p.FacetTypes.Any( x => x.FacetTypeId == facetId );
}

var products = sc.Products
                 .Where( p => p.ProductType.SysName == productTypeSysName )
                 .Where( predicate );

Original (wrong, but left for context):

You want to use Contains. Note also you can use the logical and to replace multiple Where clauses with a single Where clause.

var facetTypeIds = new [] { 1, 4, ... };

var products = from p in sc.Products
where p.ProductType.SysName == productTypeSysName
      &&  p.FacetTypes.Any( x => facetTypeIds.Contains( x.FacetTypeId ) )
select p;
tvanfosson
This won't check it has them all...
ck
Unfortunately this doesn't produce the results I'm after. This shows all products with either FacetTypeId 1 or 4. The Linq query I have shows only products with BOTH FacetTypeId 1 and 4
sf
@sf - I misread your question (actually, just skimmed it -- sorry). I've updated my answer based on your actual requirements.
tvanfosson
Post edit answer looks good. A bit more intensive than a straightforward call, but the only viable option if the dataset being queries is large.
ck
Cheers for the update. I misread the last update and only just saw the Predicate style code. The query runs but I can't loop thru the products var. I'm getting an error: "The LINQ expression node type 'Invoke' is not supported in LINQ to Entities."
sf
@sf - didn't know you were working with LINQ to Entities. The PredicateBuilder must rely on some LINQ to SQL implementation details. I suppose that it could be modified to work with LINQ to Entities, but I haven't used it with them. It is essentially doing what the code in your answer is doing, but building up just the where clause then applying it rather than simply further filtering it. It's more powerful than what I've shown since you can create arbitrarily complex conditions. If it doesn't work with Entities, though, that doesn't help much.
tvanfosson
A: 

This is based on tvanfosson's code. I have doubts about the performance of this approach though.

var facetTypeIds = new [] { 1, 4, ... };

var products = from p in sc.Products
where p.ProductType.SysName == productTypeSysName
      && facetTypeIds.All(ft => p.FacetTypes.Any(x => x.FacetTypeId == ft))
select p;
recursive
Yeah, anything where you cross between the database and IEnumerables in memory tends to be be "brute forced" by the SQL generator. It's good for small "n", though! :)
Dave Markle
hmm, this is proving to be trickier than I thought. The data set is going to be fairly large and hit quite often too.
sf
This won't work. You'll get an exception "Local sequence cannot be used in LINQ to SQL implementations of query operators except the Contains operator". Which means that you can only pass the facetTypeIds int array if you use it in a Contains expression.
Ronald Wildenberg
This query is still the one that pulls the correct result set. I am however seeing an issue when trying to apply a .Skip to the end result.
sf
+1  A: 

A local collection may be transmitted to the database by calling Contains:

from ft in facetTypes
where facetTypeIds.Contains(ft.FacetTypeId)
select ft;

The local collection is translated into sql parameters. Sql Server has a limit of ~2100 parameters, so beware.


Products that have any of the facets

from p in sc.Products
where p.ProductType.SysName == productTypeSysName
where 
(
  from ft in p.FacetTypes
  where facetTypeIds.Contains(ft.FacetTypeId)
  select ft
).Any()
select p;

Products that have all facets.

int facetCount = facetTypeIds.Count();

from p in sc.Products
where p.ProductType.SysName == productTypeSysName
where 
(
  from ft in p.FacetTypes
  where facetTypeIds.Contains(ft.FacetTypeId)
  select ft.FacetTypeId
).Distinct().Count() == facetCount
select p;
David B
Thanks for this. I've just tested out and it's pulling the wrong result set. I'm after products that have all the FacetTypeIds. eg: products that have both FacetTypeIds and 4 but not products which only have FacetTypeId 1
sf
Of course: I have now added that query.
David B
A: 

I stumbled onto this post

The following code works correct but there are a few things in it that I don't quite fully understand:

var facetTypeIds = new[] { 1, 4 };

var products6 = from p in sc.Products
                where p.ProductType.SysName == productTypeSysName
                where p.FacetTypes.Any(x => facetTypeIds.Contains(x.FacetTypeId))
                select p;

foreach (var facetTypeId in facetTypeIds)
{
    // This assignment is needed?!?
    var tempFacetTypeId = facetTypeId; 

    products6 = from p in products6 where p.FacetTypes.Any(x => x.FacetTypeId == tempFacetTypeId) select p;
}

First, what are the performance implications of this type of query? Is this even a good way to do something?

Second, why is that assignement needed in the loop?

sf
@sf - the assignment is needed to avoid capturing the loop variable. Since the expression isn't actually evaluated until later, you need to capture a fresh variable, holding the value of the loop variable at the time of that iteration, in the subexpression for that iteration. If you captured the loop variable itself, then when the expression is evaluated, each subexpression will use the value that the captured variable has at the time of evaluation, i.e., the last value of the loop variable.
tvanfosson