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();
Related
I have an application that uses a few data warehousing principles such as dimensional modeling to do reporting on a fairly simple database.
An example (simplified) entity named Call looks like this:
public virtual long Id { get; set; }
public virtual string OriginatorNumber { get; set; }
public virtual string DestinationNumber { get; set; }
public virtual DateDimension DateDimension { get; set; }
A few of the properties of the real model have been removed as they are irrelevant. The simplified DateDimension looks like this:
public virtual long Id { get; set; }
public virtual DateTime Date { get; set; }
public virtual int DayOfMonth { get; set; }
public virtual int Weekday { get; set; }
There are a LOT more columns like this - they are prepopulated for the current decade by application setup. So each date in the entire decade has a row in this table, and each Call has a link to the date that it occured. This is all mapped in Fluent NHibernate and working fine.
If I want to do some reporting, I can do this easily with the improved NHibernate LINQ provider in 3.0. We would like to use LINQ for the improved maintainability it gives us, but if we really MUST, we'll consider HQL, ICriteria or even plain SQL.
So say I want to build a report that shows the number of calls from a certain number, divided by the day of the week they occur. I can do that easily this way:
var query = Calls
.Where(c => c.OriginatorNumber == "402")
.GroupBy(c => c.DateDimension.Weekday)
.Select(g => new { Day = g.Key, Calls = g.Count() } );
In this example, "Calls" is basically an IQueryable returned from NHibernates LINQ provider (Query) through a repository interface. The query above gives me the correct results, NHibernate Profiler shows me that the SQL is pretty optimal, all is well.
However, if I want to do something slightly more advanced, I get stuck. Say I want the average number of calls per weekday. Not too far from the above, right? I just need to figure out the number of unique dates each weekday has in the result set, divide the total number of calls by it, and we're all set - right? Well, no, this is where I start to hit the limitations of the NHibernate LINQ provider. With LINQ to objects I could construct a query to do it - something along the lines of
.Select(g => g.Count() / g.GroupBy(c => c.DateDimension.Date).Count());
However, this does not convert into the correct query when using it in NHibernate. Rather, it turns both .Count() calls in the above to the same count(*) of call records, so the result is always 1.
I COULD of course just query for each call, weekday and date as a new anonymous object, then do the math on the application side, but according to conventional wisdom, That's Just Wrong (tm). I could end up doing it in desperation, tho, even tho it means pain when the table grows to a million++ calls.
The below is an SQL query that gives me the result I am looking for.
select ss.Weekday, AVG(cast(ss.Count as decimal))
from
(
select dd.Weekday, dd.Date, COUNT(*) as Count
from Call c
left outer join DateDimension dd
on c.DateDimension_id = dd.Id
where c.OriginatorNumber = '402'
group by dd.Weekday, dd.Date
) ss
group by ss.Weekday
order by ss.Weekday
Is it possible to do this with the NHibernate LINQ provider? Or, if that is not possible, how close can I get before I have to let the application fetch the intermediary result and do the rest?
There are a lot of things you can't do with the LINQ provider. Using HQL or CreateCriteria is just something you'll have to accept with NHibernate.
I haven't tried it, but it looks like you should be able to do what you want to do using HQL or CreateCriteria (with DetatchedCriteria).
If you are desperate you can also fall back to plain SQL using CreateSqlQuery.
I'm just trying to get my head around nHibernate and have a query. When setting up the mappings file (with Fluent or regular .hbm.xml files) you specify relationships (bags; one-to-many, etc) and sub-types - the idea being (I believe) is that when you fetch an object it also fetches and matching data. My question is can I programmatically tell my query to ignore that relationship?
So, below, there is a Foo class with a list of Bar objects. Within the mappings file this would be a one-to-many relationship and sometimes I want to retrieve a Foo with all Bars BUT sometimes I want to just retrieve the Foo object without the Bar, for performance reasons. How can I do this?
public class Foo { public int Id { get; set; } public List<Bar> { get; set; } }
public class Bar { public int Id { get; set; }
Cheers
The relationship shouldn't be loaded automatically unless you turn off Lazy Loading or specify it to be eager loaded in the query.
Edit:
To answer your questions in the comment below.
1) It's done as part of the query. An basic example using QueryOver in NHibernate 3.0 would look something like:
var result = Session.QueryOver()
.Fetch(x => x.Category).Eager
.Where(x => x.Price > 10)
.List();
I think with ICriteria it's "SetFetchMode("Category", FetchMode.Eager)"
2) If you turn off lazy-loading on the mapping for an object, it will effectively always be eager loaded. Tho I suggest you eager load on a query-by-query basis to avoid the possibility of having a massive chain of data loaded, or loading data you don't actually need.
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.
If i want to use Linq-SQL i also have to drag the DB Table unto the designer surface to create the entity classes.
I always like full control in my application and do not like the classes created by dotnet.
Is it possible to provide this connection between Linq and the DB using my own Data Access Layer Entity classes?
How can i get it done?
You can write your own classes very easily using Linq-to-SQL - just involves painting your classes with some Attributes.
For Example, this is a very simple table I have in one of my projects, and it works with Linq-to-SQL just fine:
[Table(Name = "Categories")]
public class Category : IDataErrorInfo
{
[Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int Id { get; set; }
[Column] public string Name { get; set; }
[Column] public string ExtensionString { get; set; }
}
The code was very easy, especially if you make your property names line up with your table names (you don't have to).
Then you just need a Repository to connect to the DB:
class CategoryRepository : ICategoryRepository
{
private Table<Category> categoryTable;
public CategoryRepository(string connectionString)
{
categoryTable = (new DataContext(connectionString)).GetTable<Category>();
}
}
Of course there is more to it, but this shows you the very basics and it is not hard to do once you understand it. This way you have 100% control over your classes and you can still take advantage of Linq-to-SQL.
I learned this approach from Pro ASP.NET MVC Framework, an awesome book.
If you want to see more, all of my Linq-to-SQL classes were written from scratch on one of my projects you can browse here.
To avoid drag & drop you can take a look at SqlMetal.exe.
However, it sounds like you really are requesting Persistence Ignorance, and I'm not sure that this is possible with L2S - it certainly isn't possible with LINQ to Entities until .NET 4...
I once wrote a blog post on using SqlMetal.exe and subsequently modifying the generated schema - perhaps you will find it useful, although it has a different underlying motivation.
I've got a couple tutorials up on CodeProject that walk through how to do this, including how to handle the relationships (M:M, 1:M, M:1) in an OO way and keep them in synch as you make updates:
A LINQ Tutorial: Mapping Tables to Objects
A LINQ Tutorial: Adding/Updating/Deleting Data
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>();