This has been a 2 week battle for me so far with no luck. :(
Let me first state my objective. To be able to search entities which are tagged "foo" and "bar". Wouldn't think that was too hard right?
I know this can be done easily with HQL but since this is a dynamically built search query that is not an option. First some code:
public class Foo
{
public virtual int Id { get;set; }
public virtual IList<Tag> Tags { get;set; }
}
public class Tag
{
public virtual int Id { get;set; }
public virtual string Text { get;set; }
}
Mapped as a many-to-many because the Tag class is used on many different types. Hence no bidirectional reference.
So I build my detached criteria up using an abstract filter class. Lets assume for simplicity I am just searching for Foos with tags "Apples"(TagId1) && "Oranges"(TagId3) this would look something like.
SQL:
SELECT ft.FooId
FROM Foo_Tags ft
WHERE ft.TagId IN (1, 3)
GROUP BY ft.FooId
HAVING COUNT(DISTINCT ft.TagId) = 2; /*Number of items we are looking for*/
Criteria
var idsIn = new List<int>() {1, 3};
var dc = DetachedCriteria.For(typeof(Foo), "f").
.CreateCriteria("Tags", "t")
.Add(Restrictions.InG("t.Id", idsIn))
.SetProjection( Projections.ProjectionList()
.Add(Projections.Property("f.Id"))
.Add(Projections.RowCount(), "RowCount")
.Add(Projections.GroupProperty("f.Id")))
.ProjectionCriteria.Add(Restrictions.Eq("RowCount", idsIn.Count));
}
var c = Session.CreateCriteria(typeof(Foo)).Add(Subqueries.PropertyIn("Id", dc))
Basically this is creating a DC that projects a list of Foo Ids which have all the tags specified.
This compiled in NH 2.0.1 but didn't work as it complained it couldn't find Property "RowCount" on class Foo.
After reading this post I was hopeful that this might be fixed in 2.1.0 so I upgraded. To my extreme disappointment I discovered that ProjectionCriteria has been removed from DetachedCriteria and I cannot figure out how to make the dynamic query building work without DetachedCriteria.
So I tried to think how to write the same query without needing the infamous Having clause. It can be done with multiple joins on the tag table. Hooray I thought that's pretty simple. So I rewrote it to look like this.
var idsIn = new List<int>() {1, 3};
var dc = DetachedCriteria.For(typeof(Foo), "f").
.CreateCriteria("Tags", "t1").Add(Restrictions.Eq("t1.Id", idsIn[0]))
.CreateCriteria("Tags", "t2").Add(Restrictions.Eq("t2.Id", idsIn[1]))
In a vain attempt to produce the below sql which would do the job (I realise its not quite correct).
SELECT f.Id
FROM Foo f
JOIN Foo_Tags ft1
ON ft1.FooId = f.Id
AND ft1.TagId = 1
JOIN Foo_Tags ft2
ON ft2.FooId = f.Id
AND ft2.TagId = 3
Unfortunately I fell at the first hurdle with this attempt, receiving the exception "Duplicate Association Path". Reading around this seems to be an ancient and still very real bug/limitation.
What am I missing?
I am starting to curse NHibernates name at making what is you would think so simple and common a query, so difficult. Please help anyone who has done this before. How did you get around NHibernates limitations.
Forget reputation and a bounty. If someone does me a solid on this I will send you a 6 pack for your trouble.
I managed to get it working like this :
var dc = DetachedCriteria.For<Foo>( "f")
.CreateCriteria("Tags", "t")
.Add(Restrictions.InG("t.Id", idsIn))
.SetProjection(Projections.SqlGroupProjection("{alias}.FooId", "{alias}.FooId having count(distinct t1_.TagId) = " + idsIn.Count,
new[] { "Id" },
new IType[] { NHibernateUtil.Int32 }));
The only problem here is the count(t1_.TagId) - but I think that the alias should be generated the same every time in this DetachedCriteria - so you should be on the safe side hard coding that.
Ian,
Since I'm not sure what db backend you are using, can you do some sort of a trace against the produced SQL query and take a look at the SQL to figure out what went wrong?
I know I've done this in the past to understand how Linq-2-SQL and Linq-2-Entities have worked, and been able to tweak certain cases to improve the data access, as well as to understand why something wasn't working as initially expected.
Related
We are having som performance issue with EF and I want to try rewrite a query to inline sql. However I am having some difficulties, it is probably just a noob issue.
Lets say I have 3 classes: License, GroupLicense and Product
public class License
{
//stuff
Product MyProduct{get;set}
}
public class GroupLicense:License
{
// more stuff
}
public class Product
{
//product info stuff
}
Now I need to fetch some Grouplicenses depending on some requirements. However doing it with the datacontext and linq takes 2 minutes.
Something similar to this
var institutionLicenses = db.GroupLicenses
.Include(lic => lic.Product).Where(x => productIds.Contains(x.Product.Id) && x.LicenseStatus == StatusEnum.Active).ToList();
I want to do the same query using inline sql similar to this: I join the tables so all the primitive fields are okay.
var gl = db.Database.SqlQuery<GroupLicense>("select * from GroupLicense as g left join Licenses on g.Id =Licenses.Id").ToList();
(It is just example code - I know it is not working :))
However when executing, The base product property on License is null, all the primitive fields are there.
What do I need to change in my sql statement to make it work?
All,
I have a requirement to hide my EF implementation behind a Repository. My simple question: Is there a way to execute a 'find' across both a DbSet AND the DbSet.Local without having to deal with them both.
For example - I have standard repository implementation with Add/Update/Remove/FindById. I break the generic pattern by adding a FindByName method (for demo purposes only :). This gives me the following code:
Client App:
ProductCategoryRepository categoryRepository = new ProductCategoryRepository();
categoryRepository.Add(new ProductCategory { Name = "N" });
var category1 = categoryRepository.FindByName("N");
Implementation
public ProductCategory FindByName(string s)
{
// Assume name is unique for demo
return _legoContext.Categories.Where(c => c.Name == s).SingleOrDefault();
}
In this example, category1 is null.
However, if I implement the FindByName method as:
public ProductCategory FindByName(string s)
{
var t = _legoContext.Categories.Local.Where(c => c.Name == s).SingleOrDefault();
if (t == null)
{
t = _legoContext.Categories.Where(c => c.Name == s).SingleOrDefault();
}
return t;
}
In this case, I get what I expect when querying against both a new entry and one that is only in the database. But this presents a few issues that I am confused over:
1) I would assume (as a user of the repository) that cat2 below is not found. But it is found, and the great part is that cat2.Name is "Goober".
ProductCategoryRepository categoryRepository = new ProductCategoryRepository();
var cat = categoryRepository.FindByName("Technic");
cat.Name = "Goober";
var cat2 = categoryRepository.FindByName("Technic");
2) I would like to return a generic IQueryable from my repository.
It just seems like a lot of work to wrap the calls to the DbSet in a repository. Typically, this means that I've screwed something up. I'd appreciate any insight.
With older versions of EF you had very complicated situations that could arise quite fast due to the required references. In this version I would recomend not exposing IQueryable but ICollections or ILists. This will contain EF in your repository and create a good seperation.
Edit: furthermore, by sending back ICollection IEnumerable or IList you are restraining and controlling the queries being sent to the database. This will also allow you to fine tune and maintain the system with greater ease. By exposing IQueriable, you are exposing yourself to side affects which occur when people add more to the query, .Take() or .Where ... .SelectMany, EF will see these additions and will generate sql to reflect these uncontrolled queries. Not confining the queries can result in queries getting executed from the UI and is more complicated tests and maintenance issues in the long run.
since the point of the repository pattern is to be able to swap them out at will. the details of DbSets should be completly hidden.
I think that you're on a good path. The only thing I probaly ask my self is :
Is the context long lived? if not then do not worry about querying Local. An object that has been Inserted / Deleted should only be accessible once it has been comitted.
if this is a long lived context and you need access to deleted and inserted objects then querying the Local is a good idea, but as you've pointed out, you may run into difficulties at some point.
My current project is using NHibernate 3.0b1 and the NHibernate.Linq.Query<T>() API. I'm pretty fluent in LINQ, but I have absolutely no experience with HQL or the ICriteria API. One of my queries isn't supported by the IQueryable API, so I presume I need to use one of the previous APIs -- but I have no idea where to start.
I've tried searching the web for a good "getting started" guide to ICriteria, but the only examples I've found are either far too simplistic to apply here or far too advanced for me to understand. If anyone has some good learning materials to pass along, it would be greatly appreciated.
In any case, the object model I'm querying against looks like this (greatly simplified, non-relevant properties omitted):
class Ticket {
IEnumerable<TicketAction> Actions { get; set; }
}
abstract class TicketAction {
Person TakenBy { get; set; }
DateTime Timestamp { get; set; }
}
class CreateAction : TicketAction {}
class Person {
string Name { get; set; }
}
A Ticket has a collection of TicketAction describing its history. TicketAction subtypes include CreateAction, ReassignAction, CloseAction, etc. All tickets have a CreateAction added to this collection when created.
This LINQ query is searching for tickets created by someone with the given name.
var createdByName = "john".ToUpper();
var tickets = _session.Query<Ticket>()
.Where(t => t.Actions
.OfType<CreateAction>()
.Any(a => a.TakenBy.Name.ToUpper().Contains(createdByName));
The OfType<T>() method causes a NotSupportedException to be thrown. Can I do this using ICriteria instead?
try something like this. It's uncompiled, but it should work as long as IEnumerable<TicketAction> Actions and Person TakenBy is never null. If you set it to an empty list in the ticket constructor, that will solve a problem with nulls.
If you add a reference to the Ticket object in the TicketAction, you could do something like this:
ICriteria criteria = _session.CreateCriteria(typeof(CreateAction))
.Add(Expression.Eq("TakenBy.Name", createdByName));
var actions = criteria.List<CreateAction>();
var results = from a in criteria.List<>()
select a.Ticket;
In my experience, nhibernate has trouble with criteria when it comes to lists when the list is on the object side - such as is your case. When it is a list of values on the input side, you can use Expression.Eq. I've always had to find ways around this limitation through linq, where I get an initial result set filtered down as best as I can, then filter again with linq to get what I need.
OfType is supported. I'm not sure ToUpper is though, but as SQL ignores case it does not matter (as long as you are not also running the query in memory...). Here is a working unit test from the nHibernate.LINQ project:
var animals = (from animal in session.Linq<Animal>()
where animal.Children.OfType<Mammal>().Any(m => m.Pregnant)
select animal).ToArray();
Assert.AreEqual("789", animals.Single().SerialNumber);
Perhaps your query should look more like the following:
var animals = (from ticket in session.Linq<Ticket>()
where ticket.Actions.OfType<CreateAction>().Any(m => m.TakenBy.Name.Contains("john"))
select ticket).ToArray();
I hope someone can help with this please.
I am trying to query an OLAP Fact table with NHibernate, but am struggling to get it to work. Its seems a simple requirement but I just cant see what the problem could be.
I have a central Fact table with several Dimension tables, one of the Dimensions has a secondary Dimension.
So ERD is. Fact >---1 Factor_Dim >---1 Target_Dim
My NHibernate query is.
facts = session.CreateCriteria(typeof(Fact), "facts")
.CreateAlias("facts.FactorDimension", "factDim", JoinType.InnerJoin)
.CreateAlias("factDim.TargetDimension", "targetDim",JoinType.InnerJoin)
.Add(Restrictions.Eq("targetDim.TargetID", targetId))
.List();
The error is "The multi-part identifier "targetdim2_.TargetID" could not be bound.". The generated SQL does not have the Factor_DIM or Target_DIM tables in the From clause.
Are there any alternative techniques to get this query to work? Id like to stick to this style as opposed to CreateSQLQuery() if possible.
Please help. Thanks.
Linq or QueryOver will be your cleanest solutions. If you are determined to stay with ICriteria you probably would want to wrap each of your entities with a class with common crud methods, it also makes your code access common, so code corrections are done in one place, not over hundres of files or classes.
Theres plenty of projects at http://nhforge.org/wikis/general/open-source-project-ecosystem.aspx which can help you out. I know NhGen ( http://sourceforge.net/projects/nhgen/ ) creates a CRUD class for each entity based on the NHibernate.Burrows GenericDao class with a few CRUD methods. It takes care of all the aliases and joins so queries become as simple as
IMessageDao messageDao = new MessageDao();
// Get All
IList<IMessage> messageList1 dao.FindAll();
// Find using QueryByExample
IList<IMessage> messageList2 = dao.FindByExample(messageDetails, orderBy)).ToList();
// Find using a simple entity query
IList<IMessage> messageList3 = messageDao.Find( new [] { Restrictions.Le(MessageHelper.Columns.Date, dateLastChecked) } );
// Find using a join and a query on said joined entities
IList<IMessage> messageList4 = messageDao.Find
( new []
{
Restrictions.Le(MessageHelper.Columns.Date, dateLastChecked),
Restrictions.Eq(MessageHelper.Columns.IsActive, true))
}, new[]
{
Restrictions.Eq(CategoryHelper.KeyColumns.Rsn, categoryRsn),
Restrictions.Eq(CategoryHelper.Columns.IsActive, true))
}, new []
{
Restrictions.Eq(ChannelHelper.KeyColumns.Rsn, channelRsn),
Restrictions.Eq(ChannelHelper.Columns.IsActive, true))
}
);
Theres plenty of overrides so you can specify your join type or it naturally assumes inner join.
Im having a problem creating a projection for my nhibernate detachedcriteria object.
I have a class Spa which is linked to table Address.
Address has a field called City which is a string.
public class Spa : IAggregateRoot
{
[BelongsTo("AddressID", Cascade = CascadeEnum.All)]
public Address Address { get; set; }
}
My ultimate goal is to get a distinct list of City names.
If i could get all spas with distinct cities i would be happy too.
All my attempts have been for naught and havent found any helpful posts.
So far i've tried:
DetachedCriteria query = DetachedCriteria.For<Spa>()
.CreateAlias("Address", "A")
query.SetProjection(
Projections.Distinct(Projections.ProjectionList()
.Add(Projections.Alias(Projections.Property("Address"), "A"))));
var Spas = ActiveRecordMediator<Spa>.FindAll(query);
I know the above is not correct, just trying to find somewhere to start.
Any help would be appreciated.
Also any simple projections tutorials would be appreciated, cant seem to find anything straight forward out there.
I also tried, but got cast error, looking into it:
DetachedCriteria query = DetachedCriteria.For<Spa>()
.CreateAlias("Address", "A")
.SetProjection(Projections.Distinct(Projections.Property("A.City")));
It seems to me there are two parts to your question.
1. What should my DetachedCriteria look like?
If you are not performing any other aggregations, GROUP BY should provide the same results as DISTINCT. This is the query I would use:
var query = DetachedCriteria.For<Spa>()
.CreateAlias("Address", "A")
.SetProjection(Projections.GroupProperty("A.City"));
2. How do I execute it with Castle ActiveRecord?
I have never used ActiveRecord, but based on the method signatures, I would expect something like this to work:
var cities = ActiveRecordMediator<string>.FindAll(query);
If you have access to the NHibernate session, you could also execute it this way:
var cities = query.GetExecutableCriteria(session).List<string>();