Is it possible to use NH QueryOver to fetch joined entities in one query? - sql

SQL query is:
select B.* from A inner join B on A.b_id = B.id where A.x in (1,2,3)
A <-> B relation is many-to-one
I need to filter by A but fetch related B.
UPDATE:
I tried this NH QueryOver
Session.QueryOver<A>.Where(a => a.x.IsIn(array)).JoinQueryOver(a => a.B).Select(a => a.B).List<B>()
but it results in a N+1 sequence of queries: the first one fetches IDs of related Bs, and others fetch related Bs one by one by ID (analyzed via NHProf). I want it to fetch a list of Bs in one go.
UPDATE 2:
for now I worked around this with subquery
Session.QueryOver(() => b).WithSubquery.WhereExists(QueryOver.Of<A>().Where(a => a.x.IsIn(array)).And(a => a.b_id == b.id).Select(a => a.id)).List<B>()
but I still hope to see an example of QueryOver without subquery as I tend to think subquery is less efficient.

This works (at least in my test application):
var list = session.QueryOver<A>()
.Where(a => a.X.IsIn(array))
.Fetch(x => x.B).Eager
.List<A>()
.Select(x => x.B);
Note that the .Select() statement is a normal LINQ statement, not part of NHibernate.
Generated SQL:
SELECT
this_.Id as Id0_1_,
this_.B as B0_1_,
this_.X as X0_1_,
b2_.Id as Id1_0_,
b2_.SomeValue as SomeValue1_0_
FROM A this_ left outer join B b2_ on this_.B=b2_.Id
WHERE this_.X in (?, ?, ?)
It's not optimal if A is a very large class, of course.
An NHibernate-only solution with a subquery:
var candidates = QueryOver.Of<A>()
.Where(a => a.X.IsIn(array))
.Select(x => x.B.Id);
var list = session.QueryOver<B>()
.WithSubquery.WhereProperty(x => x.Id).In(candidates).List();
I'll try to find the reason why the most obvious solution (just adding Fetch().Eager) doesn't work as expected. Stay tuned!

Related

Sequel -- can I alias subqueries in a join?

Using Sequel I'd like to join two subqueries together that share some column names, and then table-qualify those columns in the select.
I understand how to do this if the two datasets are just tables. E.g. if I have a users table and an items table, with items belonging to users, and I want to list the items' names and their owners' names:
#db[:items].join(:users, :id => :user_id).
select{[items__name, users__name.as(user_name)]}
produces
SELECT "items"."name", "users"."name" AS "user_name"
FROM "items"
INNER JOIN "users" ON ("users"."id" = "items"."user_id")
as desired.
However, I'm unsure how to do this if I'm joining two arbitrary datasets representing subqueries (call them my_items and my_users)
The syntax would presumably take the form
my_items.join(my_users, :id => :user_id).
select{[ ... , ... ]}
where I would supply qualified column names to access my_users.name and my_items.name. What's the appropriate syntax to do this?
A partial solution is to use t1__name for the first argument, as it seems that the dataset supplied to a join is aliased with t1, t2, etc. But that doesn't help me qualify the item name, which I need to supply to the second argument.
I think the most desirable solution would enable me to provide aliases for the datasets in a join, e.g. like the following (though of course this doesn't work for a number of reasons)
my_items.as(alias1).join(my_users.as(alias2), :id => :user_id).
select{[alias1__name, alias2__name ]}
Is there any way to do this?
Thanks!
Update
I think from_self gets me part of the way there, e.g.
my_items.from_self(:alias => :alias1).join(my_users, :id => :user_id).
select{[alias1__name, t1__name]}
seems to do the right thing.
OK, thanks to Ronald Holshausen's hint, got it. The key is to use .from_self on the first dataset, and provide the :table_alias option in the join:
my_items.from_self(:alias => :alias1).
join(my_users, {:id => :user_id}, :table_alias => :alias2).
select(:alias1__name, :alias2__name)
yields the SQL
SELECT "alias1"."name", "alias2"."name"
FROM ( <my_items dataset> ) AS "alias1"
INNER JOIN ( <my_users dataset> ) AS "alias2"
ON ("alias2"."id" = "alias1"."user_id")
Note that the join hash (the second argument of join) needs explicit curly braces to distinguish it from the option hash that includes :table_alias.
The only way I found was to use the from method on the DB, and the :table_alias on the join method, but these don't work with models so I had to use the table_name from the model class. I.e.,
1.9.3p125 :018 > #db.from(Dw::Models::Contract.table_name => 'C1')
=> #<Sequel::SQLite::Dataset: "SELECT * FROM `vDimContract` AS 'C1'">
1.9.3p125 :019 > #db.from(Dw::Models::Contract.table_name => 'C1').join(Dw::Models::Contract.table_name, {:c1__id => :c2__id}, :table_alias => 'C2')
=> #<Sequel::SQLite::Dataset: "SELECT * FROM `vDimContract` AS 'C1' INNER JOIN `vDimContract` AS 'C2' ON (`C1`.`Id` = `C2`.`Id`)">
1.9.3p125 :020 > #db.from(Dw::Models::Contract.table_name => 'C1').join(Dw::Models::Product.table_name, {:product_id => :c1__product_id}, :table_alias => 'P1')
=> #<Sequel::SQLite::Dataset: "SELECT * FROM `vDimContract` AS 'C1' INNER JOIN `vDimProduct` AS 'P1' ON (`P1`.`ProductId` = `C1`.`ProductId`)">
The only thing I don't like about from_self is it uses a subquery:
1.9.3p125 :021 > Dw::Models::Contract.from_self(:alias => 'C1')
=> #<Sequel::SQLite::Dataset: "SELECT * FROM (SELECT * FROM `vDimContract`) AS 'C1'">

How can I create a Fluent NHibernate query that uses FetchMode.Select

I am simply trying to write a query that will look like this and be eager loaded:
Select * from Users where Id IN (1,2,3)
Select * from Bids where UserId in (1,2,3)
Right now it's causing problems because the join is returning too many results.
Try using multicriteria (or Future as in this example), should issue 2 queries in 1 batch and give you the eager loading:
var bids = Session.QueryOver<Bid>()
.JoinQueryOver(b => b.User)
.WhereRestrictionOn(u => u.Id).IsIn(ids)
.Future<Bid>();
var users = Session.QueryOver<User>()
.WhereRestrictionOn(u => u.Id).IsIn(ids)
.List<User>();
The only problem is it will join Bid -> User, to avoid that you could use HQL:
var bids = Session.CreateQuery("from Bid b where b.User.id in (:userIds)")
.SetParameterList("userIds", ids)
.Future<Bid>();
var users = Session.QueryOver<User>()
.WhereRestrictionOn(u => u.Id).IsIn(ids)
.List<User>();

Using a subquery for a column with QueryOver

I'm trying to get something similar to the SQL below via QueryOver:
SELECT
docs.*,
(SELECT TOP 1 eventDate from events WHERE id=docs.id
AND type=4 ORDER BY eventDate DESC) as eventDate
FROM documents as docs
WHERE doc.accountId = ...
I've got close with a projection, however I'm not sure how to get the entire documents table back. Documents has a one-to-many relationship with Events, I don't want to outer join as it will bring multiple results, and an inner join may not bring back a row:
var query = QueryOver<Document>
.Where(d => d.Account == account)
.SelectList(list => list
.Select(d => d)
.Select(d => d.Events.OrderByDescending(e => e.EventDate).FirstOrDefault(e => e.Type == 4))
)
.List<object[]>()
.Select(d => return new DocumentSummary(d[0],d[1]) etc.);
Is there an easier way of performing subqueries for columns? I'm reluctant to replace this with the property performing a query in its get.
After some research it looks like HQL (which QueryOver is converted into) does not support TOP inside subqueries.
My solution: create a view which includes the computed properties, and then mark these properties in the mappings files as insert="false" and update="false"

queryover and transformusing loses the ability to lazy load

I want to try and introduce the DISTINCT keyword into SQL, basically I require the following SQL:-
SELECT distinct this_.Id as y0_,
this_.Name as y1_,
this_.Description as y2_,
this_.UnitPrice as y3_,
this_.Director as y4_
FROM Product this_
inner join ActorRole actor1_
on this_.Id = actor1_.MovieId
WHERE this_.ProductType = 'Movie'
AND actor1_.Name like 'm%' /* #p0 */
The QueryOver code looks like this, however I can't use the DISTINCT keyword without using a projection:-
var movie = Session.QueryOver<Movie>()
.JoinQueryOver<Actor>(m => m.ActorList).Where(a => a.Name.IsLike("m%"))
.Select(
Projections.Distinct(
Projections.ProjectionList()
.Add(Projections.Property<Movie>(w => w.Id))
.Add(Projections.Property<Movie>(w => w.Name))
.Add(Projections.Property<Movie>(w => w.Description))
.Add(Projections.Property<Movie>(w => w.UnitPrice))
.Add(Projections.Property<Movie>(w => w.Director))
)
)
.TransformUsing(Transformers.AliasToBean<Movie>());
return movie.List<Movie>();
This works returns me distinct movies where actors begin with the letter 'm'. Now the problem comes as the projection is meant for DTO's and when I iterate over the results and want to lazy load the children. For example:-
#foreach (var item in Model.ActorList)
{
<li>#(item.Name) <em>plays</em> #item.Role</li>
}
Model.ActorList is always NULL, it appears that projecting and using a transformer loses the lazy loading as this method is designed for DTO's. What are my options?
I know I can use a sub query or HQL rather than a select distinct
Transformers.AliasToBean<Movie>() just creates a new Movie and fills in the properties. It is therefor a new Movie and not loaded from DB and therefor doesnt inherit the collection of the original Movie. AFAIK AliasToBean is to fill ViewModels etc with projected data.
Can't you just use:
Session.QueryOver<Movie>()
.JoinQueryOver<Actor>(m => m.ActorList).Where(a => a.Name.IsLike("m%"))
.List();
If anyone else is interested in this then please read the blog post that explains this behaviour

Pull back rows from multiple tables with a sub-select?

I have a script which generates queries in the following fashion (based on user input):
SELECT * FROM articles
WHERE (articles.skeywords_auto ilike '%pm2%')
AND spubid IN (
SELECT people.spubid FROM people
WHERE (people.slast ilike 'chow')
GROUP BY people.spubid)
LIMIT 1;
The resulting data set:
Array ( [0] =>
Array (
[spubid] => A00603
[bactive] => t
[bbatch_import] => t
[bincomplete] => t
[scitation_vis] => I,X
[dentered] => 2009-07-24 17:07:27.241975
[sentered_by] => pubs_batchadd.php
[drev] => 2009-07-24 17:07:27.241975
[srev_by] => pubs_batchadd.php
[bpeer_reviewed] => t
[sarticle] => Errata: PM2.5 and PM10 concentrations from the Qalabotjha low-smoke fuels macro-scale experiment in South Africa (vol 69, pg 1, 2001)
[spublication] => Environmental Monitoring and Assessment
[ipublisher] =>
[svolume] => 71
[sissue] =>
[spage_start] => 207
[spage_end] => 210
[bon_cover] => f
[scover_location] =>
[scover_vis] => I,X
[sabstract] =>
[sabstract_vis] => I,X
[sarticle_url] =>
[sdoi] =>
[sfile_location] =>
[sfile_name] =>
[sfile_vis] => I
[sscience_codes] =>
[skeywords_manual] =>
[skeywords_auto] => 1,5,69,2001,africa,assessment,concentrations,environmental,errata,experiment,fuels,low-smoke,macro-scale,monitoring,pg,pm10,pm2,qalabotjha,south,vol
[saward_number] =>
[snotes] =>
)
The problem is that I also need all the columns from the 'people' table (as referenced in the sub select) to come back as part of the data set. I haven't (obviously) done much with sub selects in the past so this approach is very new to me. How do I pull back all the matching rows/columns from the articles table AS WELL as the rows/column from the people table?
Are you familiar with joins? Using ANSI syntax:
SELECT DISTINCT *
FROM ARTICLES t
JOIN PEOPLE p ON p.spubid = t.spudid AND p.slast ILIKE 'chow'
WHERE t.skeywords_auto ILIKE'%pm2%'
LIMIT 1;
The DISTINCT saves from having to define a GROUP BY for every column returned from both tables. I included it because you had the GROUP BY on your subquery; I don't know if it was actually necessary.
Could you not use a join instead of a sub-select in this case?
SELECT a.*, p.*
FROM articles as a
INNER JOIN people as p ON a.spubid = p.spubid
WHERE a.skeywords_auto ilike '%pm2%'
AND p.slast ilike 'chow'
LIMIT 1;
Lets start from the beginning
You shouldn't need a group by. Use distinct instead (you aren't doing any aggregating in the inner query).
To see the contents of the inner table, you actually have to join it. The contents are not exposed unless it shows up in the from section. A left outer join from the people table to the articles table should be equivalent to an IN query :
SELECT *
FROM people
LEFT OUTER JOIN articles ON articles.spubid = people.spubid
WHERE people.slast ilike 'chow'
AND articles.skeywords_auto ilike '%pm2%'
LIMIT 1