In Type member support in LINQ-to-Entities? I was attempting to declare a class property to be queried in LINQ which ran into some issues. Here I will lay out the code inside the implementation in hopes of some help for converting it to a query.
I have a class Quiz which contains a collection of Questions, each of which is classified according to a QuestionLevel... I need to determine whether a quiz is "open" or "closed", which is accomplished via an outer join on the question levels and a count of the questions in each level, as compared with a table of maximum values. Here's the code, verbatim:
public partial class Quiz
{
public bool IsClosed
{
get
{
// if quiz has no questions, it's open
if (this.Questions.Count() == 0) return false;
// get a new handle to the EF container to do a query for max values
using (EFContainer db = new EFContainer())
{
// we get a dictionary of LevelName/number
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
// count the number of questions in each level, comparing to the maxima
// if any of them are less, the quiz is "open"
foreach (QuestionLevel ql in db.QuestionLevels)
{
if (this.Questions.Where(x => x.Level == ql).Count() < max["Q:Max:" + ql.Name])
return false;
}
}
// the quiz is closed
return true;
}
}
}
so here's my not-yet-working attempt at it:
public static IQueryable<Quiz> WhereIsOpen(this IQueryable<Quiz> query)
{
EFContainer db = new EFContainer();
return from ql in db.QuestionLevels
join q in query on ql equals q.Questions.Select(x => x.Level)
into qs
from q in qs.DefaultIfEmpty()
where q.Questions.Count() < db.Registry
.Where(x => x.Domain == "Quiz")
.Where(x => x.Key == "Q:Max" + ql.Name)
.Select(x => Convert.ToInt32(x.Value))
select q;
}
it fails on account on the join, complaining:
The type of one of the expressions in the join clause is incorrect.
The type inference failed in the call to 'GroupJoin'
I'm still trying to figure that out.
* update I *
ah. silly me.
join q in query on ql equals q.Questions.Select(x => x.Level).Single()
one more roadblock:
The specified LINQ expression contains references to queries that are
associated with different contexts.
this is because of the new container I create for the maximum lookups; so I thought to re-factor like this:
public static IQueryable<Quiz> WhereIsOpen(this IQueryable<Quiz> query)
{
EFContainer db = new EFContainer();
IEnumerable<QuestionLevel> QuestionLevels = db.QuestionLevels.ToList();
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
return from ql in QuestionLevels
join q in query on ql equals q.Questions.Select(x => x.Level).Single()
into qs
from q in qs.DefaultIfEmpty()
where q.Questions.Count() < max["Q:Max:" + ql.Name]
select q;
}
but I can't get the expression to compile... it needs me to cast QuestionLevels to an IQueryable (but casting doesn't work, producing runtime exceptions).
* update II *
I found a solution to the casting problem but now I'm back to the "different contexts" exception. grr...
return from ql in QuestionLevels.AsQueryable()
* update (Kirk's suggestion) *
so I now have this, which compiles but generates a run-time exception:
public static IQueryable<Quiz> WhereIsOpen(this IQueryable<Quiz> query)
{
EFContainer db = new EFContainer();
IEnumerable<string> QuestionLevels = db.QuestionLevels.Select(x => x.Name).ToList();
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
return from ql in QuestionLevels.AsQueryable()
join q in query on ql equals q.Questions.Select(x => x.Level.Name).Single()
into qs
from q in qs.DefaultIfEmpty()
where q.Questions.Count() < max["Q:Max:" + ql]
select q;
}
which I then call like this:
List<Product> p = db.Quizes.WhereIsOpen().Select(x => x.Component.Product).ToList();
with the resulting exception:
This method supports the LINQ to Entities infrastructure and is not
intended to be used directly from your code.
The issues you're coming across are common when you couple your database objects to your domain objects. It's for this exact reason that it's good to have a separate set of classes that represent your domain and a separate set of classes that represent your database and are used for database CRUD. Overlap in properties is to be expected, but this approach offers more control of your application and decouples your database from your business logic.
The idea that a quiz is closed belongs to your domain (the business logic). Your DAL (data access layer) should be responsible for joining all the necessary tables so that when you return a Quiz, all the information needed to determine whether or not it's closed is available. Your domain/service/business layer should then create the domain object with the IsClosed property properly populated so that in your UI layer (MVC) you can easily access it.
I see that you're access the database context directly, I'd warn against that and encourage you to look into using DI/IoC framework (Ninject is great), however, I'm going to access the database context directly also
Use this class in your views/controllers:
public class QuizDomainObject
{
public int Id {get; set;}
public bool IsClosed {get; set;}
// all other properties
}
Controller:
public class QuizController : Controller
{
public ActionResult View(int id)
{
// using a DI/IoC container is the
// preferred method instead of
// manually creating a service
var quizService = new QuizService();
QuizDomainObject quiz = quizService.GetQuiz(id);
return View(quiz);
}
}
Service/business layer:
public class QuizService
{
public QuizDomainObject GetQuiz(int id)
{
// using a DI/IoC container is the
// preferred method instead of
// access the datacontext directly
using (EFContainer db = new EFContainer())
{
Dictionary<string, int> max = db.Registry
.Where(x => x.Domain == "Quiz")
.ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value));
var quiz = from q in db.Quizes
where q.Id equals id
select new QuizDomainObject()
{
Id = q.Id,
// all other propeties,
// I'm still unclear about the structure of your
// database and how it interlates, you'll need
// to figure out the query correctly here
IsClosed = from q in ....
};
return quiz;
}
}
}
Re: your comment
The join to QuestionLevels is making it think there are two contexts... but really there shouldn't be because the QuestionLevels should contain in-memory objects
I believe that if you join on simple types rather than objects you'll avoid this problem. The following might work for you:
return from ql in QuestionLevels
join q in query
on ql.LevelId equals q.Questions.Select(x => x.Level).Single().LevelId
into qs
(and if this doesn't work then construct some anonymous types and join on the Id)
The problem is that joining on the Level objects causes EF to do some under-the-covers magic - find the objects in the database and perform a join there. If you tell it to join on a simple type then it should send the values to the database for a SELECT, retrieve the objects and stitch them together back in your application layer.
Related
I have an Entity Framework v5 model created from a database. The table Season has a corresponding entity called Season. I need to calculate the Season's minimum start date and maximum end date for each year for a Project_Group. I then need to be able to JOIN those yearly min/max Season values in other LINQ queries. To do so, I have created a SeasonLimits class in my Data Access Layer project. (A SeasonLimits table does not exist in the database.)
public partial class SeasonLimits : EntityObject
{
public int Year { get; set; }
public DateTime Min_Start_Date { get; set; }
public int Min_Start_Date_ID { get; set; }
public DateTime Max_End_Date { get; set; }
public int Max_End_Date_ID { get; set; }
public static IQueryable<SeasonLimits> QuerySeasonLimits(MyEntities context, int project_Group_ID)
{
return context
.Season
.Where(s => s.Locations.Project.Project_Group.Any(pg => pg.Project_Group_ID == project_Group_ID))
.GroupBy(x => x.Year)
.Select(sl => new SeasonLimits
{
Year = sl.Key,
Min_Start_Date = sl.Min(d => d.Start_Date),
Min_Start_Date_ID = sl.Min(d => d.Start_Date_ID),
Max_End_Date = sl.Max(d => d.End_Date),
Max_End_Date_ID = sl.Max(d => d.End_Date_ID)
});
}
}
// MVC Project
var seasonHoursByYear =
from d in context.AuxiliaryDateHours
from sl in SeasonLimits.QuerySeasonLimits(context, pg.Project_Group_ID)
where d.Date_ID >= sl.Min_Start_Date_ID
&& d.Date_ID < sl.Max_End_Date_ID
group d by new
{
d.Year
} into grp4
orderby grp4.Key.Year
select new
{
Year = grp4.Key.Year,
HoursInYear = grp4.Count()
};
In my MVC project, whenever I attempt to use the QuerySeasonLimits method in a LINQ query JOIN, I receive the message,
"LINQ to Entities does not recognize the method
'System.Linq.IQueryable`1[MyDAL.SeasonLimits]
QuerySeasonLimits(MyDAL.MyEntities, MyDAL.Project_Group)' method, and
this method cannot be translated into a store expression."
Is this error being generated because SeasonLimits is not an entity that exists in the database? If this can't be done this way, is there another way to reference the logic so that it can be used in other LINQ queries?
EF is trying to translate your query to SQL and as there is no direct mapping between your method and the generated SQL you're getting the error.
First option would be not to use the method and instead write the contents of the method directly in the original query (I'm not sure at the moment if this would work, as I don't have a VS running). In the case this would work, you'll most likely end up with a very complicated SQL with a poor performance.
So here comes the second option: don't be afraid to use multiple queries to get what you need. Sometimes it also makes sense to send a simpler query to the DB and continue with modifications (aggregation, selection, ...) in the C# code. The query gets translated to SQL everytime you try to enumerate over it or if you use one of the ToList, ToDictionary, ToArray, ToLookup methods or if you're using a First, FirstOrDefault, Single or SingleOrDefault calls (see the LINQ documentation for the specifics).
One possible example that could fix your query (but most likely is not the best solution) is to start your query with:
var seasonHoursByYear =
from d in context.AuxiliaryDateHours.ToList()
[...]
and continue with all the rest. This minor change has fundamental impact:
by calling ToList the DB will be immediately queried and the whole
AuxiliaryDateHours table will be loaded into the application (this will be a performance problem if the table has too many rows)
a second query will be generated when calling your QuerySeasonLimits method (you could/should also include a ToList call for that)
the rest of the seasonHoursByYear query: where, grouping, ... will happen in memory
There are a couple of other points that might be unrelated at this point.
I haven't really investigated the intent of your code - as this could lead to further optimizations - even total reworks that could bring you more gains in the end...
I eliminated the SeasonLimits object and the QuerySeasonLimits method, and wrote the contents of the method directly in the original query.
// MVC Project
var seasonLimits =
from s in context.Season
.Where(s => s.Locations.Project.Project_Group.Any(pg => pg.Project_Group_ID == Project_Group_ID))
group s by new
{
s.Year
} into grp
select new
{
grp.Key.Year,
Min_Start_Date = grp.Min(x => x.Start_Date),
Min_Start_Date_ID = grp.Min(x => x.Start_Date_ID),
Max_End_Date = grp.Max(x => x.End_Date),
Max_End_Date_ID = grp.Max(x => x.End_Date_ID)
};
var seasonHoursByYear =
from d in context.AuxiliaryDateHours
from sl in seasonLimits
where d.Date_ID >= sl.Min_Start_Date_ID
&& d.Date_ID < sl.Max_End_Date_ID
group d by new
{
d.Year
} into grp4
orderby grp4.Key.Year
select new
{
Year = grp4.Key.Year,
HoursInYear = grp4.Count()
};
I would like to get a list of all organisations within x miles of a location entered by the user. This is converted to a long/lat location.
Organisations are stored in the database with long and lat.
I am using MVC with entity framework and a Unit of Work with the respository pattern to access the dataset.
Here is my EntityRepository:
public IQueryable<T> All
{
get
{
return dbSet;
}
}
public IQueryable<T> AllIncluding(params System.Linq.Expressions.Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = dbSet;
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query;
}
public IEnumerable<T> Where(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return dbSet.Where(predicate).AsEnumerable();
}
To query the data in my datacontext I use a service class for each entity, a UOW is injected into each service. The service call for organisations is:
public class OrgService :IOrgService
{
private IUnitOfWork _UoW;
public OrgService(IUnitOfWork UoW)
{
_UoW = UoW;
}
public Organisation GetOrgByID(int OrgID)
{
return _UoW.OrganisationRepo.Find(OrgID);
}
public IList<Organisation> GetAllOrgs()
{
return _UoW.OrganisationRepo.All.ToList();
}
public IList<Organisation> GetOrgsByLocation(double lat, double lng, int range)
{
/// I need to return a list of all organisations within X miles
}
}
All other queries are working as the they should however I am no trying to write the method GetOrgsByLocation(). This is the query I think I need to get my results:
var userLocation = new GeoCoordinate(lat, lng);
var result = _UoW.OrganisationRepo.Where(x => new GeoCoordinate(x.Latitude, x.Longitude))
.Where(x => x.GetDistanceTo(userLocation) < radius).ToList();
When I try to run this query I get:
"cannot implicitly convert type system.device.location.geoCoordinate to bool"
Can anyone help?
** Update - Working solution **
var userLocation = new GeoCoordinate(lat, lng);
var nearbyOrganizations = _UoW.OrganisationRepo.All.ToList()
.Select(x => new
{ //use an anonymous type or any type you want
Org = x,
Distance = new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation)
})
.Where(x => x.Distance < 50000)
.ToList();
foreach (var organisation in nearbyOrganizations)
{
Console.WriteLine("{0} ({1:n0} meters)", organisation.Org, organisation.Distance);
}
Thanks to the help below for this solution, though it seems all objects have to be queried in orfer for this to work, it seems that query would be better suited to run on the database, I'll have to look into this more.
The Where method has to return a boolean value.
_UoW.OrganisationRepo.Where(x => new GeoCoordinate(x.Latitude, x.Longitude))
Maybe you meant to use a .Select there? Would the following code work?
_UoW.OrganisationRepo.Select(x => new GeoCoordinate(x.Latitude, x.Longitude))
.Where(x => x.GetDistanceTo(userLocation) < radius).ToList();
Be aware that Entity Framework tries to generate some SQL from the expression provided. I'm afraid that x.GetDistanceTo(userLocation) might not work inside the .Where expression, unless you cast it to an IEnumerable, or call .AsEnumerable() or .ToList() or .ToArray() before calling .Where. Or maybe EF is smart enough to see that GeoCoordinate is not mapped to a table, and then stop generating the SQL right there.
Edit
The code you commented won't work:
_UoW.OrganisationRepo.All.Select(x => new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation) < radius).ToList()
Notice that you're selecting a list of bools because you're selecting the results instead of filtering by them. You won't know which organizations are within the radius. That's why we use .Select and .Where separately.
Try something like this:
_UoW.OrganisationRepo.All
.Select(x => new GeoCoordinate(x.Latitude, x.Longitude))
.ToEnumerable() //or .ToList() or .ToArray(), make sure it's outside of EF's reach (prevent SQL generation for this)
.Where(x=> x.GetDistanceTo(userLocation) < radius).ToList()
However, if you want to know which organizations are within the radius, you'll need to carry more information along the path.
var nearbyOrganizations = _UoW.OrganisationRepo.All.ToList()
.Select(x => new
{ //use an anonymous type or any type you want
Org = x,
Distance = new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation)
}) //it's probably outside EF's SQL generation step by now, but you could add one more .Select here that does the math if it fails (return the GeoCoordinate object)
.Where(x=> x.Distance < radius)
.ToList();
Seems like it would be useful for you to know more about .Where and .Select. They're super useful. .Where is essentialy a filter, and .Select transforms objects. And due to anonymous types, you can do whatever you want.
Notice that this will fetch all objects from the database. You probably want to use native features of the database to work with geographical data, and EF probably supports it.
I think your problem is with your first Where as you are trying to create a new GeoCoordinate class and Where is expecting a bool output from the lambda not a GeoCoordinate instance.
I would suggest making the following change:
var result = _UoW.OrganisationRepo.Where(x => new GeoCoordinate(x.Latitude, x.Longitude).GetDistanceTo(userLocation) < radius).ToList();
This will give you a list of Organisations back within the radius you are interested in.
Update
The prior won't work with IQueryable as the provider will not be able to create an SQL query because the .GetDistanceTo function is not known within SQL.
As an alternative could you not use the Spatial Data types in EF 5 onwards?
This blog post by Rick Strahl gives an example of querying geography data stored in SQL by distance from a given location
https://weblog.west-wind.com/posts/2012/jun/21/basic-spatial-data-with-sql-server-and-entity-framework-50
I have a basic IQueryable,
private static IQueryable<TestObject> GetFilteredQuery(Guid componentId, Guid productId)
{
IQueryable<TestObject> query = from t in ModelQuery.Query<TestObject>()
where t.ComponentId == componentId && t.ProductId == productId
select t;
return query;
}
This is trivial if I have to compare single componentId and productId.
My problem is how can I handle when I have a list of value pairs,
Guid[] componentIds, Guid[] productIds
where, its kind of a keyValue pair.
something like,
private static IQueryable<TestObject> GetFilteredQuery(Guid[] componentIds, Guid[] productIds)
{
IQueryable<TestObject> query = from t in ModelQuery.Query<TestObject>()
where (t.ComponentId must be present in componentIds[] && t.ProductId must be present in productIds)
select t;
return query;
}
Use Contains:
private static IQueryable<TestObject> GetFilteredQuery(Guid[] componentIds, Guid[] productIds)
{
IQueryable<TestObject> query =
from t in ModelQuery.Query<TestObject>()
where (componentIds.Contains(t.ComponentId)
&& productIds.Contains(t.ProductId))
select t;
return query;
}
Edit
AFAIK there is no way Linq2Sql is going to map a sequence of Guid tuples to native Sql (you would likely need an #Table parameter for this)
So here's one approach, viz to run a query the same contains as above, but using OR on the 2 filter lists. Sql will hopefully be able to filter a significant amount of data out at the database level.
The results (candidates) then need to be materialized, and then filtered in memory against the component and product pairs. I've done this by zipping the 2 guid arrays together (assuming similar length - possibly you want to remodel the arrays as an array of Pairs to express the intention more explicitly?)
private static IQueryable<TestObject> GetFilteredQuery(Guid[] componentIds,
Guid[] productIds)
{
var candidates = ModelQuery
.Query<TestObject>()
.Where(componentIds.Contains(
t.ComponentId) || productIds.Contains(t.ProductId))
.ToList();// Need to materialize
var guidPairs = componentIds.Zip(productIds,
(c, p) => new {ComponentId = c, ProductId = p});
return candidates
.Join(guidPairs,
c => new {ComponentId = c.ComponentId, ProductId = c.ProductId},
gp => gp,
(c, gp) => c)
.AsQueryable();
}
Note that the resultant queryable isn't really suitable for further composition, given that it has already been materialized. Also, if you can do additional filtering before hitting this, it would be beneficial. And I'm afraid I haven't actually tested this.
Use Contains:
where componentIds.Contains(t.ComponentId) && productIds.Contains(t.ProductId)
I'm re-writing some of my old NHibernate code to be more database agnostic and use NHibernate queries rather than hard coded SELECT statements or database views. I'm stuck with one that's incredibly slow after being re-written. The SQL query is as such:
SELECT
r.recipeingredientid AS id,
r.ingredientid,
r.recipeid,
r.qty,
r.unit,
i.conversiontype,
i.unitweight,
f.unittype,
f.formamount,
f.formunit
FROM recipeingredients r
INNER JOIN shoppingingredients i USING (ingredientid)
LEFT JOIN ingredientforms f USING (ingredientformid)
So, it's a pretty basic query with a couple JOINs that selects a few columns from each table. This query happens to return about 400,000 rows and has roughly a 5 second execution time. My first attempt to express it as an NHibernate query was as such:
var timer = new System.Diagnostics.Stopwatch();
timer.Start();
var recIngs = session.QueryOver<Models.RecipeIngredients>()
.Fetch(prop => prop.Ingredient).Eager()
.Fetch(prop => prop.IngredientForm).Eager()
.List();
timer.Stop();
This code works and generates the desired SQL, however it takes 120,264ms to run. After that, I loop through recIngs and populate a List<T> collection, which takes under a second. So, something NHibernate is doing is extremely slow! I have a feeling this is simply the overhead of constructing instances of my model classes for each row. However, in my case, I'm only using a couple properties from each table, so maybe I can optimize this.
The first thing I tried was this:
IngredientForms joinForm = null;
Ingredients joinIng = null;
var recIngs = session.QueryOver<Models.RecipeIngredients>()
.JoinAlias(r => r.IngredientForm, () => joinForm)
.JoinAlias(r => r.Ingredient, () => joinIng)
.Select(r => joinForm.FormDisplayName)
.List<String>();
Here, I just grab a single value from one of my JOIN'ed tables. The SQL code is once again correct and this time it only grabs the FormDisplayName column in the select clause. This call takes 2498ms to run. I think we're on to something!!
However, I of course need to return several different columns, not just one. Here's where things get tricky. My first attempt is an anonymous type:
.Select(r => new { DisplayName = joinForm.FormDisplayName, IngName = joinIng.DisplayName })
Ideally, this should return a collection of anonymous types with both a DisplayName and an IngName property. However, this causes an exception in NHibernate:
Object reference not set to an instance of an object.
Plus, .List() is trying to return a list of RecipeIngredients, not anonymous types. I also tried .List<Object>() to no avail. Hmm. Well, perhaps I can create a new type and return a collection of those:
.Select(r => new TestType(r))
The TestType construction would take a RecipeIngredients object and do whatever. However, when I do this, NHibernate throws the following exception:
An unhandled exception of type 'NHibernate.MappingException' occurred
in NHibernate.dll
Additional information: No persister for: KitchenPC.Modeler.TestType
I guess NHibernate wants to generate a model matching the schema of RecipeIngredients.
How can I do what I'm trying to do? It seems that .Select() can only be used for selecting a list of a single column. Is there a way to use it to select multiple columns?
Perhaps one way would be to create a model with my exact schema, however I think that would end up being just as slow as the original attempt.
Is there any way to return this much data from the server without the massive overhead, without hard coding a SQL string into the program or depending on a VIEW in the database? I'd like to keep my code completely database agnostic. Thanks!
The QueryOver syntax for conversion of selected columns into artificial object (DTO) is a bit different. See here:
16.6. Projections for more details and nice example.
A draft of it could be like this, first the DTO
public class TestTypeDTO // the DTO
{
public string PropertyStr1 { get; set; }
...
public int PropertyNum1 { get; set; }
...
}
And this is an example of the usage
// DTO marker
TestTypeDTO dto = null;
// the query you need
var recIngs = session.QueryOver<Models.RecipeIngredients>()
.JoinAlias(r => r.IngredientForm, () => joinForm)
.JoinAlias(r => r.Ingredient, () => joinIng)
// place for projections
.SelectList(list => list
// this set is an example of string and int
.Select(x => joinForm.FormDisplayName)
.WithAlias(() => dto.PropertyStr1) // this WithAlias is essential
.Select(x => joinIng.Weight) // it will help the below transformer
.WithAlias(() => dto.PropertyNum1)) // with conversion
...
.TransformUsing(Transformers.AliasToBean<TestTypeDTO>())
.List<TestTypeDTO>();
So, I came up with my own solution that's a bit of a mix between Radim's solution (using the AliasToBean transformer with a DTO, and Jake's solution involving selecting raw properties and converting each row to a list of object[] tuples.
My code is as follows:
var recIngs = session.QueryOver<Models.RecipeIngredients>()
.JoinAlias(r => r.IngredientForm, () => joinForm)
.JoinAlias(r => r.Ingredient, () => joinIng)
.Select(
p => joinIng.IngredientId,
p => p.Recipe.RecipeId,
p => p.Qty,
p => p.Unit,
p => joinIng.ConversionType,
p => joinIng.UnitWeight,
p => joinForm.UnitType,
p => joinForm.FormAmount,
p => joinForm.FormUnit)
.TransformUsing(IngredientGraphTransformer.Create())
.List<IngredientBinding>();
I then implemented a new class called IngredientGraphTransformer which can convert that object[] array into a list of IngredientBinding objects, which is what I was ultimately doing with this list anyway. This is exactly how AliasToBeanTransformer is implemented, only it initializes a DTO based on a list of aliases.
public class IngredientGraphTransformer : IResultTransformer
{
public static IngredientGraphTransformer Create()
{
return new IngredientGraphTransformer();
}
IngredientGraphTransformer()
{
}
public IList TransformList(IList collection)
{
return collection;
}
public object TransformTuple(object[] tuple, string[] aliases)
{
Guid ingId = (Guid)tuple[0];
Guid recipeId = (Guid)tuple[1];
Single? qty = (Single?)tuple[2];
Units usageUnit = (Units)tuple[3];
UnitType convType = (UnitType)tuple[4];
Int32 unitWeight = (int)tuple[5];
Units rawUnit = Unit.GetDefaultUnitType(convType);
// Do a bunch of logic based on the data above
return new IngredientBinding
{
RecipeId = recipeId,
IngredientId = ingId,
Qty = qty,
Unit = rawUnit
};
}
}
Note, this is not as fast as doing a raw SQL query and looping through the results with an IDataReader, however it's much faster than joining in all the various models and building the full set of data.
IngredientForms joinForm = null;
Ingredients joinIng = null;
var recIngs = session.QueryOver<Models.RecipeIngredients>()
.JoinAlias(r => r.IngredientForm, () => joinForm)
.JoinAlias(r => r.Ingredient, () => joinIng)
.Select(r => r.column1, r => r.column2})
.List<object[]>();
Would this work?
For the purposes of this question, let's say I have a zoo with multiple exhibits. Each exhibit has it's own schedule with a set of activities, as well as a department that is responsible for them and a list of sub-departments.
I have a fairly large LINQ query that I’m using to get any exhibits that has an employee attached to it. When using the query with an IEnumerable (session.Query<Exhibit>().AsEnumerable()) it works fine; however, when I switch the query over to use IQueryable, NHibernate breaks down.
I’ve pinpointed the initial source of the original error to one specific condition in my query:
var filtered = session.Query<Exhibit>().Where(x =>
x.AnimalSchedule.Activities
.OfType<FeedActivity>()
.Any(g => g.ResponsibleDepartment.Manager == employee)
);
Since that condition is pretty long by itself, I went ahead and broke it out step-by-step:
var collection = session.Query<Exhibit>(); // works
var exhibitAnimalSchedules = collection.Select(x => x.AnimalSchedule); // works
var activities = exhibitAnimalSchedules.SelectMany(x => x.Activities); // could not execute query - Incorrect syntax near the keyword 'from'.
var feedActivities = activities.OfType<FeedActivity>(); // Could not find property nor field 'class' in class 'MyProject.Collections.ScheduleActivityCollection'
var filteredFeedActivities = feedActivities.Where(g => g.ResponsibleDepartment.Manager == employee); // Specified method is not supported.
The error on activities also gives the SQL that NHibernate tries to generate:
SELECT
FROM [dbo].[Exhibits] exhibit0_
INNER JOIN [dbo].[ExhibitAnimalSchedules] exhibitanima1_ ON exhibit0_.AnimalScheduleID = exhibitanima1_.ScheduleID
INNER JOIN [dbo].[Schedules] exhibitanima1_1_ ON exhibitanima1_.ScheduleID = exhibitanima1_1_.[ID]
WHERE exhibit0_.ZooID IS NULL
If you notice, NHibernate failed to list out any columns in the SELECT statement.
Am I thinking the wrong way about this query? If this is actually a bug in NHibernate with one of the LINQ lambdas, is there some workaround?
UPDATE - See answer below
It looks like NHibernate is having some trouble figuring out which table to join to for the .SelectMany(x => x.Activities)
For reference, here are all of the simplified Fluent mappings for the classes involved:
public class ExhibitMap : ClassMap<Exhibit>
{
public ExhibitMap()
{
References(p => p.AnimalSchedule).Cascade.All();
}
}
public class ScheduleMap : ClassMap<Schedule>
{
public ScheduleMap()
{
Component(x => x.Activities, m => m {
var cfg = m.HasMany<ScheduleActivity>(Reveal.Member<ScheduleActivityCollection>("_innerList")).LazyLoad();
cfg.KeyColumn("ScheduleID").Inverse();
cfg.ForeignKeyConstraintName("FK_Schedule_Activities");
cfg.Cascade.AllDeleteOrphan();
});
}
}
public class ExhibitAnimalScheduleMap : SubclassMap<ExhibitAnimalSchedule>
{
public ExhibitAnimalScheduleMap()
{
References(x => x.Exhibit).Cascade.None();
}
}
public class ScheduleActivityMap : ClassMap<ScheduleActivity>
{
public ScheduleActivityMap()
{
References(x => x.Schedule).Cascade.None().Not.LazyLoad();
}
}
public class FeedActivityMap : SubclassMap<FeedActivity>
{
public FeedActivityMap()
{
this.References(x => x.ResponsibleDepartment).Cascade.All().Not.LazyLoad();
Component(x => x.Departments, m => m {
var cfg = m.HasMany<FeedActivityDepartment>(Reveal.Member<FeedActivityDepartmentCollection>("_innerList")).LazyLoad();
cfg.KeyColumn("ScheduleID").Inverse();
cfg.ForeignKeyConstraintName("FK_Schedule_Activities");
cfg.Cascade.AllDeleteOrphan();
});
}
}
As #Firo pointed out, NHibernate was having some difficulty with the components I'm using for my custom collections. If you look at the SQL in the question, you'll see that NHibernate failed to join to the Activities table and the Departments table to look for the associated responsible department.
I corrected this by switching over to LINQ query syntax and explicitly joining to the tables. I also avoided issues with the OfType (again, most likely component issues) by checking inline with is and casting with as:
var schedules = from schedule in session.Query<Schedule>()
join activity in session.Query<ScheduleAcivity>() on schedule equals activity.Schedule
join department in session.Query<FeedActivityDepartment>() on activity equals department.FeedActivity
where (activity is FeedActivity && (activity as FeedActivity).ResponsibleDepartment.Manager == employee)
|| (department != null && department.Employee == employee)
select schedule;
var exhibits = from exhibit in session.Query<Exhibit>()
where schedules.Any(x => x == exhibit.AnimalSchedule)
select exhibit;
Note: Sometimes NHibernate is finicky about the names of aliases in the LINQ queries. If you follow this pattern and are still experiencing errors, try changing the names of your aliases (I like to add "temp" in front of them -- from tempSchedule in ..., join tempActivity in ..., etc.).