I'm trying to build a search query using NHibernate that will filter on parameters from several different tables and result in somewhat-reasonable SQL that can take advantage of NHibernate's lazy-loading.
Fromm reading various tips online, it seems that the latest-and-greatest way to do that is to use the QueryOver object to conditionally add in the parameters being used, as in the following snippet:
Hibernate.Criterion.QueryOver<Models.Site, Models.Site> query = NHibernate.Criterion.QueryOver.Of<Models.Site>();
if (!string.IsNullOrEmpty(state))
query = query.WhereRestrictionOn(r => r.State.StateName).IsInsensitiveLike("%" + state + "%");
if (startDate.HasValue)
query = query.Where(r => r.Events
.Where(e=>e.EventDate >= startDate.Value)
.Count() > 0
);
return query.GetExecutableQueryOver(currentSession).Cacheable().List();
(an event has a foreign-key to site)
I have two questions:
How do I filter the child objects, instead of just the parent? The sample code above gives me all the sites with a matching events, but within that site, I only want matching events. If I'm supposed to use a join or a subquery, how? I'm confused about maintaining my tree-like hierarchy with lazy-loading through a join or subquery.
Edit: this has been answered. Thanks psousa!
How do I add an or clause? I found reference to a Disjunction object, but it doesn't seem like that's available using the QueryOver method.
Edit:
I want to result in a list of sites (top level object) filtered by the site criteria, and each site should have its list of events filtered by the event criteria.
I expect it to generate SQL like the following:
SELECT *
FROM [site] s
LEFT JOIN [event] e ON s.siteID = e.siteID
WHERE e.eventDate > #eventDate
AND (s.stateCd = #state OR s.stateName LIKE #state)
I would do that query as such:
//use aliases. Optional but more practical IMHO
Site siteAlias = null;
Event eventAlias = null;
//use JoinAlias instead of JoinQueryOver to keep the condition at the "Site" level
var results = Session.QueryOver(() => siteAlias)
.JoinAlias(m => m.Event, () => eventAlias)
.Where(() => eventAlias.EventDate > eventDate)
.Where(() => siteAlias.StateCd == state || Restrictions.On(() => siteAlias.StateName).IsLike(state))
.List();
You mentioned the Disjunction class, and it may in fact be used with QueryOver, like:
var disjunction= new Disjunction();
disjunction.Add(() => siteAlias.StateCD == state);
disjunction.Add(Restrictions.On(() => siteAlias.StateName).IsLike(state));
The QueryOver query would be:
var results = Session.QueryOver(() => siteAlias)
.JoinAlias(m => m.Event, () => eventAlias)
.Where(() => eventAlias.EventDate > eventDate)
.Where(disjunction)
.List();
When using a join alias as suggested by psousa, you will get results in a strange combination of an object structure and a row structure, with the top-level objects being duplicated by the child objects that are attached to them. In order to get the results I was looking for, you can use TransformUsing and a DistinctRootEntityResultTransformer as shown in the following code:
Site siteAlias = null;
Event eventAlias = null;
var results = currentSession.QueryOver<Site>(() => siteAlias)
.JoinAlias(m => m.Event, () => eventAlias)
.Where(() => eventAlias.EventDate > eventDate)
.Where(() => siteAlias.StateCd == state || Restrictions.On(() => siteAlias.StateName).IsLike(state))
.TransformUsing(new NHibernate.Transform.DistinctRootEntityResultTransformer())
.List();
Related
i have an online store with dynamic fields (specifications) and filters. All specifications are stored in table e.g
product_specifications:
-id
-product_id
-spec_id
-spec_val_id
Filters are connected with specifications, so when i send filter value i send specification values. For example
$_GET['Filters']['filter_id']['spec_val_id']
When i loop filters, i want to add left joins to product query. Something like that
$joinString = "";
foreach($filters){
$joinString .= "LEFT JOIN product_specifications AS prod_'.filter.' ON .....";
}
I have query to ActiveDataProvider like that:
$prodQuery = Product::find()->joinWith('translation')->joinWith('cats')->[HERE FILTERS JOINS]->where($whereString)->groupBy(['id']);
But if i have 5 filters, i need 5 joins to table product_specifications.
Can i add in joinWith an array with all joins or add 5 joins to query chain?
To one category my filters are also dynamically, so the page can have 5 or 10 filters, i can't add static number of joinWith('specs').
Thanks in advance.
I'm agree with different desicion, too.
EDIT:
I change query with findBySql like this
$prodQuery = Product::findBySql('
SELECT `product`.*
FROM `product`
LEFT JOIN `productLang` ON `product`.`id` = `productLang`.`product_id`
LEFT JOIN `product_cat` ON `product`.`id` = `product_cat`.`product_id`
LEFT JOIN `page` ON `product_cat`.`page_id` = `page`.`id`
LEFT JOIN `page` `parent` ON `page`.`id_in` = `parent`.`id`'
.$joinString.
' WHERE ( '
.$whereString.
' AND (`language`=\''.$lang->url.'\')) GROUP BY `product`.`id` ');
And my dataprovider:
$dataProvider = new ActiveDataProvider([
'query' => $prodQuery,
'pagination' => [
'totalCount' => count($prodQuery->all()),
'pageSize' => $pageSize,
'route' => Yii::$app->getRequest()->getQueryParam('first_step'),
],
'sort' => [
'defaultOrder' => $orderArr,
],
]);
Now my problem is pagination and sorting. In my test site i have 4 products, when i set $pageSize = 1;, I have 4 pages in pagination, but in every page i have all 4 products.
Where is my mistake?
Could be you can use andFilterWhere (if the value is null no where codntion is added otherwise the and Where is added)
$prodQuery = Product::find()->joinWith('translation')->joinWith('cats')->where($whereString)->groupBy(['id']);
$prodQuery->andFilterWhere(['attribute1' => $value1])
->andFilterWhere(['attribute2' => $value2])
->andFilterWhere(['attribute3' => $value3])
->andFilterWhere(['attribute4' => $value4])
->andFilterWhere(['attribute5' => $value5])
Lets say I have two models with a many to many relationship: Item and Property
Now I have an array of properties and I want to filter all items which properties match a given value (lets say a boolean value: property.value = true)
When I try
#items = Item.includes(:properties).where(:properties => {:id => [1,2,3].to_a, :value => true})
I would like to get all items where property(1) is true AND property(2) is true and so on. But with the code above I get all items related to the property id's and where any property is true. How should I change my code?
I would appreciate not to use a gem for this.
Looks like you are almost there:
property_ids = [1,2,3]
Item.joins(:properties).
where(:properties => { :id => property_ids, :value => true }).
group('items.id').
having('COUNT(properties.id) >= ?', property_ids.size)
joins does an INNER JOIN and is preferred over includes when you really need to join tables.
where is basically the conditions you already had, the only change is that there is not need to call to_a on the array.
Than you have to group to make that COUNT in SQL work.
having extracts the lines that have at least the expected number of property lines matching the condition.
Hi I have a linq query which I use to get data from the DB.
And I have join two tables in the query.
Here is my database structure..
I need to get Customer with primary telephone number and default shipping address.
And this is my query..
var customer=UnitOfWork.DbContext.Set<Domain.BoundedContext.ScreenPop.Entities.Customer>()
.Include(x => x.CustomerPhoneNumbers.Select(p => p.IsPrimary == true))
.Include(x => x.ShippingAddresses.Select(s => s.IsDefault == true))
.Where(c => c.CustomerId == customerQuery.CustomerId).FirstOrDefault();
But it gives me this error..
The Include path expression must refer to a navigation property defined on the type.
Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
How can I get these information by using those three tables
The includes are just a way to say what navigation properties/tables you want included. Try this
var customer=UnitOfWork.DbContext.Set<Domain.BoundedContext.ScreenPop.Entities.Customer>()
.Include(x => x.CustomerPhoneNumbers)
.Include(x => x.ShippingAddresses)
.Where(c => c.CustomerId == customerQuery.CustomerId).FirstOrDefault();
and then just get the phone number and address you want
var phonenumber = customer.CustomerPhoneNumbers.FirstOrDefault(x=>x.IsPrimary);
var address = customer.ShippingAddresses.FirstOrDefault(x=>x.IsDefault);
I'm using will_paginate 2.3.x gem and I'm trying to get an array of all the ids of the objects that are in the WillPaginate::Collection. Is there a simple way to iterate all the items in the collection? With ActiveRecord collection this is simple:
all_ids = MyObject.find(:all).map { |o| o.id }
However, when paginated, the collection returns only the N elements that are in the first page:
not_all_ids = MyObject.paginate(:page => 1).map { |o| o.id }
What I want is to go through all the pages in the collection. Basically, I'm looking for a method that retrieves the next page. Any thoughts? thanks.
EDIT - I'm using rails 2.3. Also, I'm using pagination with some conditions but dropped them to simplify:
MyObject.paginate(:conditions => [...], :include => [...], :page => 1)
You could do something like that :
ids = MyObject.select("id").paginate(:page => 1)
if ids.total_pages > 1
(2..ids.total_pages).each do |page|
ids << MyObject.select("id").paginate(:page => page)
end
end
ids = ids.map(&:id)
But why using paginate in this case ?
ids = MyObject.select("id").map(&:id)
will be faster, and will use less resources ... if you're db contains 10000 elements and you're iterating 10 at a times you'll make 1000 cals to your db vs 1 :-)
ps: I'm using .select("id") so the request generated is :
SELECT id from users;
VS
SELECT * FROM users;
I'm also using a nice shortcut map(&:id), this is the equivalent of .map { |o| o.id }
Facing problem for merging multiple having clause ... As in my model i had two scopes written
1.)
scope :vacant_list, lambda {|daterange|
where("vacancies.vacancy_date" => daterange , "vacancies.availability" => ["Y" ,"Q"]).joins(:vacancies).group("vacancies.property_id").having("count(vacancies.vacancy_date) >= #{daterange.count}") unless daterange.blank?
}
2.)
scope :amenity_type, lambda {|term|
where("amenities.name" => term).joins(:amenities).group("amenities.property_id").having("count(amenities.name) >= #{term.size}") unless term.blank?
}
I need to do something like this
#model = Model.vacant_list(daterange).amenity_type(term)
But i always get wrong number of arguments (2 for 1)
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/arel-2.0.10/lib/arel/select_manager.rb:100:in `having'
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.0.6/lib/active_record/relation/query_methods.rb:180:in `build_arel'
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.0.6/lib/active_record/relation/query_methods.rb:149:in `arel'.
If i remove any one of scopes having clause then the above action works perfectly .
Is there any way to like-
#model = Model.vacant_list(daterange) and then remove the active record relation and then apply #model.amenity_type(term).
I tried lots of things but didnt find any solution for this.....
I think you're doing this wrong - it took me quite a while to dig out what the actual intent of the 'having' clauses above was. From what I can tell, it looks like the idea is that you pass in an array of dates or amenities and want to find properties that match all of them.
The underlying issue is that (AFAIK) code like this will NOT do the right thing:
# NOTE: WILL NOT WORK
scope :vacant_on, lambda { |date| where('vacancies.vacancy_date' => date, "vacancies.availability" => ["Y" ,"Q"]).joins(:vacancies) }
scope :vacant_list, lambda {|daterange|
daterange.inject(self) { |rel, date| rel.vacant_on(date) }
}
Unless this has changed in Arel (haven't poked it much) then this fails because you end up with exactly one join to the vacancies table, but multiple where clauses that specify incompatible values. The solution is to do multiple joins and alias them individually. Here's how I used to do it in Rails 2:
named_scope :vacant_on, lambda { |date|
n = self.connection.quote_table_name("vacancies_vacant_on_#{date}")
{ :joins => "INNER JOIN vacancies AS #{n} ON #{n}.property_id = properties.id",
:conditions => ["#{n}.vacancy_date = ? AND #{n}.availability IN (?)", date, ["Y","Q"]] }
}
Explicitly specifying an 'AS' here lets multiple versions of this scope coexist in one query, and you get the results you'd expect.
Here's a rough translation of this to modern Arel syntax:
scope :vacant_on, lambda { |date|
our_vacancies = Vacancy.arel_table.alias
joins(our_vacancies).on(our_vacancies[:property_id].eq(Property.arel_table[:id])).
where(our_vacancies[:vacancy_date].eq(date),
our_vacancies[:availability].in(["Y" ,"Q"]))
}
Haven't tried it, but this post:
http://www.ruby-forum.com/topic/828516
and the documentation seem to imply it would do the right thing...