Ransack no implicit conversion of Ransack::Search into Array - sql

I am currently implementing Ransack for searching functionality.
I have a model Campaigns which collaborates campaigns that the user directly created as well as others so long as the user belongs to the same vendor.
I can combine the results as such:
#search = current_user.campaigns + current_user.vendor.campaigns.where.not(:user_id => current_user.id)
Problem with this is that Ransack will not accept this combination and spits out
no implicit conversion of Ransack::Search into Array
Can someone point me in the direction on how to refactor this code?
TIA
Adding Addition Data
When looking at my console I can see *current_user.campaigns*:
Campaign Load (0.3ms)
SELECT DISTINCT "campaigns".* FROM "campaigns"
WHERE "campaigns"."user_id" = ? [["user_id", 2]]
Running *current_user.vendor.campaigns* give me:
Campaign Load (0.4ms)
SELECT DISTINCT "campaigns".* FROM "campaigns"
INNER JOIN "weeks" ON "campaigns"."id" = "weeks"."campaign_id"
INNER JOIN "products" ON "weeks"."product_id" = "products"."id"
INNER JOIN "locations" ON "products"."location_id" = "locations"."id"
WHERE "locations"."vendor_id" = ? [["vendor_id", 2]]
I can get the first filter of current_user achieved with:
#search = Campaign.where("campaigns.user_id" => current_user.id).search(params[:q])
But I am lost of how I go about building the rest of the join tables to include both elements of data

Solved
#search = Campaign.includes(:weeks).where('(campaigns.user_id LIKE ?) OR (weeks.vendor_id LIKE ?)', current_user.id, current_user.vendor.id).search(params[:q])

Related

rails 4 complex SQL scope

I have a model Users which has_many EventLogs.
I would like create a scope which will order Users by those with the most occurrences of EventLogs they have.
scope :highest_completed_events, .....
How can I count the number of EventLogs with a status of 2, and then order the users with the highest occurrence of that type of event.
User.joins(:event_logs).where("event_logs.status_id = 2")#... COUNT, then ORDER BY
Hope that makes sense.
Here's a query you can execute to get your users ordered by the number of events they have:
#users = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
group('users.id').
order('event_logs_count DESC')
You should use a LEFT JOIN since you'll want to include users who don't have any events.
If you were to write it as a scope:
scope(:highest_completed_events, {
select: 'users.*, COUNT(event_logs.id) as event_logs_count',
joins: 'LEFT JOIN event_logs ON event_logs.user_id = users.id',
group: 'users.id',
order: 'event_logs_count DESC'
})
#users = User.highest_completed_events
In order to filter the events by a particular status, simply use a where().
#users = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
where('event_logs.status = ?', STATUS_COMPLETE).
group('users.id').
order('event_logs_count DESC')
As an aside, sometimes you'll run into issues with ActiveRecord stripping out your custom select() statement when doing something like #users.count. What I normally do is nest this kind of thing in a custom from() statement.
_from = User.
select("users.*, COUNT(event_logs.id) as event_logs_count").
joins('LEFT JOIN event_logs ON event_logs.user_id = users.id').
group('users.id').
order('event_logs_count DESC').to_sql
#users = User.from("(#{_from}) as users")
#users.count # will work
Try:
User.all.sort_by{|u| u.event_logs.select{|l| l.status_id = 2}.count}.reverse
Or is it 'eventlogs'? Schouldn't your line be has_many :event_logs ?
BTW, my solution is not very efficient but DB-agnostic.

Rails ActiveRecord Join Query With conditions

I have following SQL Query:
SELECT campaigns.* , campaign_countries.points, offers.image
FROM campaigns
JOIN campaign_countries ON campaigns.id = campaign_countries.campaign_id
JOIN countries ON campaign_countries.country_id = countries.id
JOIN offers ON campaigns.offer_id = offers.id
WHERE countries.code = 'US'
This works perfectly well. I want its rails active record version some thing like:
Campaign.includes(campaign_countries: :country).where(countries: {code: "US"})
Above code runs more or less correct query (did not try to include offers table), issue is returned result is collection of Campaign objects so obviously it does not include Points
My tables are:
campaigns --HAS_MANY--< campaign_countries --BELONGS_TO--< countries
campaigns --BELONGS_TO--> offers
Any suggestions to write AR version of this SQL? I don't want to use SQL statement in my code.
I some how got this working without SQL but surely its poor man's solution:
in my controller I have:
campaigns = Campaign.includes(campaign_countries: :country).where(countries: {code: country.to_s})
render :json => campaigns.to_json(:country => country)
in campaign model:
def points_for_country country
CampaignCountry.joins(:campaign, :country).where(countries: {code: country}, campaigns: {id: self.id}).first
end
def as_json options={}
json = {
id: id,
cid: cid,
name: name,
offer: offer,
points_details: options[:country] ? points_for_country(options[:country]) : ""
}
end
and in campaign_countries model:
def as_json options={}
json = {
face_value: face_value,
actual_value: actual_value,
points: points
}
end
Why this is not good solution? because it invokes too many queries:
1. It invokes query when first join is performed to get list of campaigns specific to country
2. For each campaign found in first query it will invoke one more query on campaign_countries table to get Points for that campaign and country.
This is bad, Bad and BAD solution. Any suggestions to improve this?
If You have campaign, You can use campaign.campaign_countries to get associated campaign_countries and just get points from them.
> campaign.campaign_countries.map(&:points)
=> [1,2,3,4,5]
Similarly You will be able to get image from offers relation.
EDIT:
Ok, I guess now I know what's going on. You can use joins with select to get object with attached fields from join tables.
cs = Campaign.joins(campaign_countries: :country).joins(:offers).select('campaigns.*, campaign_countries.points, offers.image').where(countries: {code: "US"})
You can than reference additional fields by their name on Campaign object
cs.first.points
cs.first.image
But be sure, that additional column names do not overlap with some primary table fields or object methods.
EDIT 2:
After some more research I came to conclusion that my first version was actually correct for this case. I will use my own console as example.
> u = User.includes(:orders => :cart).where(:carts => { :id => [5168, 5167] }).first
> u.orders.length # no query is performed
=> 2
> u.orders.count # count query is performed
=> 5
So when You use includes with condition on country, in campaign_countries are stored only campaign_countries that fulfill Your condition.
Try this:
Campaign.joins( [{ :campaign_countries => :countries}, :offers]).where('`countries`.`code` = ?', "US")

Rails .joins query over multiple associations

I have this query that works as expected:
#dog.listings.joins(:address_country).merge(Country.where(permalink: 'uk'))
This query gives me the Listings where the country matches 'uk' (Listing has_one :address_country, which is a country from the Country model)
But when I add another association to the chain in between cat and listing (litter), it doesn't work (a litter belongs to a listing, as well as to a cat):
#dog.litters.joins(:listing) & Listing.joins(:address_country) & Country.where(permalink: 'uk')
In this query I'd like it to fetch the Litters where the country (of the associated listing) matches. But it just returns an empty array. The first query works, and I guess I just need to bolt that on to #cat.litters?)
In Rails C, I'm getting this:
d.litters.joins(:listing) & Listing.joins(:address_country).merge(Country.where(permalink: 'uk'))
Litter Load (0.6ms) SELECT "litters".* FROM "litters" INNER JOIN "listings" ON "listings"."id" = "litters"."listing_id" WHERE "litters"."litterable_id" = 11 AND "litters"."litterable_type" = 'Dog'
Listing Load (0.4ms) SELECT "listings".* FROM "listings" INNER JOIN "countries" ON "countries"."id" = "listings"."address_country_id" WHERE "countries"."permalink" = 'uk'
=> []
Any ideas what I'm doing wrong?
One thing that's definitely wrong is to assume that & is the same as merge. It used to be but was removed in fbd917 - now it's just ruby's array intersection and that's not what you want.
I am not sure I follow the database schema from the brief description you gave but just rewriting it to merge is worth the shot:
#dog.litters.joins(:listing).merge(Listing.joins(:address_country)).merge(Country.where(permalink: 'uk'))
and again without actually running the code I would guess that this is equivalent:
#dog.litters.joins(listing: :address_country).where(countries: {permalink: "uk"})

Complex query, use :includes and :joins at the same time?

(Using Rails 3.1.3)
I have an app that manages products. I import the products from several resellers and they all name their categories different. Because of this I have resellercategories that are mapped to my own subcategories.
Categories
Subcategories (belongs_to Category)
Resellercategories (belongs_to Subcategory)
Products (belongs_to Resellercategory)
You can see the models and how the relations are defined here:
http://snipt.net/Linuus/category-and-subcategory?key=38ba590408ac4233927a06046eeca30d
On my site I want to display the categories and their subcategories, easy.
If a user filters the products for, say, only 'female' products I want to filter also the categories and subcategories so that only categories and subcategories that have 'female' products are displayed. The gender is stored in the products.
So, how can I do this?
I tried to create a query like this:
http://snipt.net/Linuus/categories-1/?key=2d5d54fd573f0afe60eaa3c47a23fd4d
which (I think) filters the correct Categories. However, when I do something like:
#menu_categories.each do |c|
c.subcategories.each do |sc|
# do something...
end
end
It still queries all the subcategories whether or not they have female products. So, I got a suggestion over at the Ruby on Rails Google Group to eagerly load the :subcategories using .includes(). So, something like this:
Category.includes(:subcategories)
.joins("INNER JOIN resellercategories AS r ON subcategories.id = r.subcategory_id")
.joins("INNER JOIN products AS p ON r.id = p.resellercategory_id")
.group("categories.id")
.order("categories.name ASC")
.where("p.gender = 'unisex' OR p.gender = 'female'")
.where("subcategories.id > 0") # Dummy to trigger eager loading
However, when mixing .includes() and .joins() the includes seems to fail to eager load anything. Thus throwing the error below:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: subcategories.id:
SELECT "categories".* FROM "categories"
INNER JOIN resellercategories AS r ON subcategories.id = r.subcategory_id
INNER JOIN products AS p ON r.id = p.resellercategory_id
WHERE (p.gender = 'unisex' OR p.gender = 'female')
GROUP BY categories.id
ORDER BY categories.name ASC
Is this behavior expected? Is it a bug?
Am I trying to do this the right way or is there a better way to do it?
Any help is very appreciated.
(The discussion on RoR Google Group: https://groups.google.com/forum/?pli=1#!topic/rubyonrails-talk/UkCF7jbehHk)
Solution:
Ok, so the solution is to use eager_load() instead of includes(). I also had to remove group()
This seems to work for me:
Category.eager_load(:subcategories)
.joins("INNER JOIN resellercategories AS r ON subcategories.id = r.subcategory_id")
.joins("INNER JOIN products AS p ON r.id = p.resellercategory_id")
.order("categories.name ASC")
.where("p.gender = 'unisex' OR p.gender = 'female'")
Rails does not always use joins to realise an include. You can force it too by doing eager_load rather than includes.
This AR chain looks a lot cleaner.
Category.joins({:subcategories =>
{:resellercategories =>
:products}})
.includes(:subcategories)
.where('products.gender = unisex OR
products.gender = ?', gender)
BUT I don't think it will solve your original problem of getting all the subcategories. To solve that you'll actually have to query the association.
#menu_categories.each do |c|
c.subcategories.joins({:resellercategories =>
:products}})
.where('products.gender = unisex OR
products.gender = ?', gender)
.each do |sc|
# do something...
end
end

Nested sql queries in rails when :has_and_belongst_to_many

In my application I the next task that has not already been done by a user. I have Three models, A Book that has many Tasks and then I have a User that has has and belongs to many tasks. The table tasks_users table contains all completed tasks so I need to write a complex query to find the next task to perform.
I have came up with two solutions in pure SQL that works, but I cant translate them to rails, thats what I need help with
SELECT * FROM `tasks`
WHERE `tasks`.`book_id` = #book_id
AND `tasks`.`id` NOT IN (
SELECT `tasks_users`.`task_id`
FROM `tasks_users`
WHERE `tasks_users`.`user_id` = #user_id)
ORDER BY `task`.`date` ASC
LIMIT 1;
and equally without nested select
SELECT *
FROM tasks
LEFT JOIN tasks_users
ON tasks_users.tasks_id = task.id
AND tasks_users.user_id = #user_id
WHERE tasks_users.task_id IS NULL
AND tasks.book_id = #book_id
LIMIT 1;
This is what I Have done in rails with the MetaWhere plugin
book.tasks.joins(:users.outer).where(:users => {:id => nil})
but I cant figure out how to get the current user there too,
Thanks for any help!
I think this will duplicate the second form with the LEFT JOIN:
class Task < ActiveRecord::Base
scope :next_task, lambda { |book,user| book.tasks.\
joins("LEFT JOIN task_users ON task_users.task_id=tasks.id AND task_users.user_id=#{user.id}").\
where(:tasks=>{:task_users=>{:task_id=>nil}}).\
order("date DESC").limit(1) }
end
Note that instead of tasks_users this uses the table name task_user, which is more typical for a join model. Also, it needs to be called with:
Task.next_task(#book_id,#user_id)
book.tasks.where("tasks.id not in (select task_id from tasks_users where user_id=?)", #user_id).first
That would give you the first task that doesn't already have an entry in tasks_users for the current user.