RubyOnRails multiple scopes composition - ruby-on-rails

I got a Product model with has_many Types table and several scopes:
class Product < ActiveRecord::Base
has_many :product_types
has_many :types, through: :product_types
scope :type1, -> { joins(:types).where(types: { name: "Type1" }) }
scope :type2, -> { joins(:types).where(types: { name: "Type2" }) }
end
When I try to use one scope (Product.type1 for example) all goes well, but two scopes at a time (Product.type1.type2) returns an empty query. Yes, one product may have multiple types.
Final goal is to create a filter of products by type with checkboxes form. When I check type1 and type2 I want to see all my products that have Type1 and Type1 at the same time.
UPD 1
So I've tried to do several queries and then & them as #aaron.v suggested. I wanted to do the logic inside of the function so:
def products_by_type_name(types)
all_types = types.each { |type| Product.joins(:types).where(types: { name: type }).distinct }
...
end
My point was to iterate through each type, collect all products and then & them inside the function.
The problem is when I'm iterating, each loop returns string instead of array of hashes.
Product.joins(:types).where(types: { name: types }).distinct # returns array of hashes, it's okay.
types.each { |type| Product.joins(:types).where(types: { name: type }).distinct } # each loop returns string (Type name) instead of array of hashes.
What am I doing wrong?
SOLUTION 1
Suggested by #aaron.v, explained below
def self.by_type_name(types)
product_ids = []
types.each do |t|
product_ids << (joins(:types).where(types: { name: t }).distinct.select(:id).map(&:id))
end
find(product_ids.inject(:&))
end
SOLUTION 2
Found on reddit.
In this function you are fetching all products with at least one required type and than grouping only ones that have required count of types. Thus, you get only those products that belongs to every type at the same time.
def self.by_type_name(types)
joins(:types).where(types: { name: types }).distinct.group('products.id').having('count(*) = ?', types.each.count)
end

If you have a database background, this would be pretty obvious as to why you wouldn't be able to find products with multiple types based off how you are writing your scopes.
Database queries that are written from these scopes will multiply the rows to ensure that a product that has many types, will have a distinct row for each type. Your query when you combine both scopes is writing
where `types`.name = "Type1" AND `types`.name = "Type2"
This will never happen since columns aren't added with multiples of the same row on a join.
What you want to do is have a method that you can pass an array of type names to find it
def self.by_type_name(types)
joins(:types).where(types: { name: types }).distinct
end
This method can accept one name by itself or an array of names for the types you want to find
If you pass an array of type names like ["type1", "type2"], this will result in a query like
where `types`.name in ("type1", "type2")
And then you should see the results you expect
UPDATE
To revise what you need in finding products that have all types given to your method, I decided to do this
def self.by_type_name(types)
product_ids = []
types.each do |t|
product_ids << (joins(:types).where(types: { name: t }).distinct.select(:id).map(&:id))
end
find(product_ids.inject(:&))
end
This method requires an array of type names(even if it is one type, pass an array with one type in it). What it will do is find all products with one specific type name, only select the product id(which will be lighter on your DB) and map it into an array which will get dumped into the product_ids array. after it has looped over each type name, it will find all products that intersect in each of the arrays resulting with products that have all types passed in.

Related

Rails create scope of duplicates records

If you need to pull all duplicates by name in the class you can achieve it by:
Company.select(:name).group(:name).having("count(*) > 1")
By what to do if you want it in the scope
scope :duplicates, -> { where (...?)}
Also in return I need few fields not only name. Did anyone had the same problem to create a scope?
You need to run this in two queries. The first query selects the duplicate names, the second one selects the records with those duplicate names and uses the current_scope so that it can be chained with more scopes if needed (unfortunately current_scope seems to be a very useful but undocumented method):
scope :duplicates,
-> {
dup_names = Company.group(:name).having("count(*) > 1").pluck(:name)
current_scope.where(name: dup_names)
}
(The dup_names variable will contain an array of duplicate names found among the companies.)
Then you can easily add further conditions on the duplicate records, for example:
Company.duplicates.where("name like 'a%'").limit(2)
will select just two companies with the name starting with 'a' (and with duplicate names).
Since
scope :red, -> { where(color: 'red') }
is simply 'syntactic sugar' for defining an actual class method:
class Shirt < ActiveRecord::Base
def self.red
where(color: 'red')
end
end
you could define scope like this:
scope :duplicates, -> { ids = select(:id).group(:name).having("count(name) > 1"); where(id: ids) }

How to sort the ActiveAdmin index based on a filter and a column

I have a Rails application with a Movie model. The Movie model has 'name' and 'release_date' as regular attributes, as well as a scope used to search for movie names with elasticsearch.
class Movie < ActiveRecord::Base
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
Movie.where(id: movie_ids).reorder('').order_by_ids(movie_ids) unless movie_ids.nil?
}
end
I then set up my active admin to show this data
ActiveAdmin.register Promotion do
filter :movie_name_search, as: :string, label: "Movie Name"
index do
actions
column :name
column :release date, sortable: :release_date
end
end
Putting in a movie name into the search bar works perfectly, and sorting against release_date works perfectly, but I can't do both at the same time. Once I'm using the filter for movie names, the sort by date doesn't work. It only works when I remove the reorder and new order.
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
Movie.where(id: movie_ids) unless movie_ids.nil?
}
It would appear that the ordering I enforce in the scope takes precedence over the sort of the column but I have no idea why. Any ideas?
You're resetting the scope chain when you call Movie.where in movie_search_name. You want to send where to self instead (i.e. just delete the Movie. part), so that prior conditions are preserved.
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
where(id: movie_ids) unless movie_ids.nil?
}
Edit: I understand the issue now
Like you say, Elastic Search is returning an array of ids in sorted order, but where does not respect that order. It just pulls records from the database as it finds them, so long as their ids are in the array. It's no good for us to sort the records afterwards as an array, because then ActiveRecord can't apply additional clauses to the query. It has to be done as part of the query.
SQL does provide a way to enforce an arbitrary order: ORDER BY CASE, but it's not built in to Rails. Fortunately, it's not hard to add.
Let's suppose your search results are [2, 1, 3]. In SQL, we can retrieve them in order like this:
SELECT * FROM movies
WHERE id IN (2, 1, 3)
ORDER BY CASE id
WHEN 2 THEN 0
WHEN 1 THEN 1
WHEN 3 THEN 2
ELSE 3 END;
To make this compatible with ActiveRecord, we can add a class method to Movie:
app/models/movie.rb
class Movie < ActiveRecord::Base
# ...
def self.order_by_ids_array(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
end
order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
order(order_clause)
end
end
Now your ActiveAdmin scope uses both where and order_by_ids_array:
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
where(id: movie_ids).order_by_ids_array(movie_ids) unless movie_ids.nil?
}
Reference:
http://www.justinweiss.com/articles/how-to-select-database-records-in-an-arbitrary-order/
Edit II: A real hack
Note: This requires a recent version of ActiveAdmin that uses Ransack.
The issue we're having is that filters don't play well with sorting. So here's
the new plan: let's add another column to our index table that shows the search
rank of each movie. This column will only appear when we've filtered by movie
name, and it will be sortable. This way there will be no "default" sort, but
you can sort by anything you want, including search ranking.
The trick is to insert a computed column into the query using a CASE like
above, but in the SELECT clause. We'll call it search_rank and it can be
accessed on any returned movie as movie.attributes['search_rank'].
app/admin/movies.rb
ActiveAdmin.register Movie do
filter :movie_search, as: string, label: 'Movie Name'
index do
# only show this column when a search term is present
if params[:q] && params[:q][:movie_search]
# we'll alias this column to `search_rank` in our scope so it can be sorted by
column :search_rank, sortable: 'search_rank' do |movie|
movie.attributes['search_rank']
end
end
end
end
Now in our model, we need to define the movie_search scope (as a class method)
and mark it as ransackable.
app/models/movie.rb
class Movie < ActiveRecord::Base
def self.ransackable_scopes(opts)
[:movie_search]
end
def self.movie_search(term)
# do search here
ids = elasticSearch(term, :name).map(&id)
# build extra column
rank_col = "(CASE movies.id "
ids.each_with_index do |id, index|
rank_col << "WHEN #{ id } THEN #{ index } "
end
rank_col << 'ELSE NULL END) AS search_rank'
select("movies.*, #{ rank_col }").where(id: ids)
end
end

How to rewrite this each loop in ruby?

I have this loop:
stations = Station.where(...)
stations.all.each do |s|
if s.city_id == city.id
show_stations << s
end
end
This works well, but because of looping the all the data, I think it's kinda slow. I've tried to rewrite it with using select, like this:
show_stations << stations.select { |station| station.city_id == city.id}
But the amount of saved data into show_stations is different compared to the each version and then, the data are in different format (array/object).
Is there any better/faster way to rewrite the loop version?
The fastest version of this maybe the built-in rails ActiveRecord method for finding associated objects.
So provided your Station model contains this:
class Station < ActiveRecord::Base
belongs_to :city
And your City model contains this:
class City < ActiveRecord::Base
has_many :stations
Then rails automatically generates the method city.stations which automatically fetches the stations which contain that city's id from the database. It should be pretty optimized.
If you want to make it even faster then you can add add_index :stations, :city_id to your table in a migration and it will retrieve faster. Note that this only saves time when you have a lot of stations to search through.
If you need to make it an array you can just convert it after with city.stations.to_a. And if you wanted to narrow it further, just use the select method and add the conditions that you wanted to previously add in your Station.where(...) statement.
(e.g. city.stations.to_a.select { |item| your_filter })
You should also cache the query results like
stations ||= Station.where("your where").where(:city_id => city.id)
Maybe you need to include into the where clause the city parameter:
stations = Station.where("your where").where(:city_id => city.id)
or the same
stations = Station.where("your where").where('city_id = ?', city.id)
Station appears to be an active record model. If that is the case, and you don't need all the stations, you can add the city.id filter to your where statement.
The issue you're having now is that you're adding the array returned from select as the last item of show_stations. If you want show_stations to only contain stations that match city.id then use show_stations = ... rather than show_stations << .... If you want show_stations to contain what it already contains plus the stations that match city.id then use show_stations + stations.select { |station| station.city_id == city.id }. (There are a number of other approaches for adding two arrays together.)

Return values were not extracted in Ruby

def get_dept_class_type
departments, classifications, types = [["-- select one --"]] * 3
Department.all.each do |d|
departments << d.name
end
Classification.all.each do |c|
classifications << c.name
end
Type.all.each do |t|
types << t.name
end
return departments, classifications, types
end
def new
#departments, #classifications, #types = get_dept_class_type
end
Hello guys,
above is my code in Ruby to assign the return values from "get_dept_class_type" function to "def new" instance variables. The problem is, the return values from the "get_dept_class_type" function were not extracted, So all instance variables have the same values. Each instance variable is the containing value of a select tag in html form.
The values of department, classification, type select tags have these the same content:
Information Technology
Administrative Department
Internal Use
Top Secret
Confidential
Public
Policy
Procedure Manual
Please it help me to figure this out. Thank you in advanced.
Your main problem is -
departments, classifications, types = [["-- select one --"]] * 3
changed it to -
departments, classifications, types = Array.new(3) { ["-- select one --"] }
Lets debug it :-
([[]] * 3).map(&:object_id) # => [72506330, 72506330, 72506330]
but
Array.new(3) { [] }.map(&:object_id) # => [76642680, 76642670, 76642520]
You can see, that all the inner objects are basically same object. Basically you have created an array of array, say a, where all the element arrays are same object. Thus if you have modified, say a[0], you can see the same change when you would inspect a[1] or a[2]. But if you do create the same array of array, a as , Array.new(3) { [] }, then each inner element array of a, will be different object. Thus if you say modify a[0], then a[1] and a[2] will be intact.
Worth to read Common gotchas.

building a dynamic where clause in model with ruby, iterating over activerecord object

I have a model with an has_many association: Charts has_many ChartConditions
charts model has fields for:
name (title)
table_name (model)
The chart_conditions model has fields for
assoc_name (to .joins)
name (column)
value (value)
operator (operator
Basically my Chart tells us which model (using the table_name field) I want to run a dynamic query on. Then the chart_conditions for the Chart will tell us which fields in that model to sort on.
So In my models that will be queried, i need to dynamically build a where clause using multiple chart_conditions.
Below you can see that i do a joins first based on all the object's assoc_name fields
Example of what I came up with. This works, but not with a dynamic operator for the name/value and also throws a deprecation warning.
def self.dynamic_query(object)
s = joins(object.map{|o| o.assoc_name.to_sym})
#WORKS BUT GIVES DEPRECATED WARNING (RAILS4)
object.each do |cond|
s = s.where(cond.assoc_name.pluralize.to_sym => {cond.name.to_sym => cond.value})
end
end
How can i then add in my dynamic operator value to this query? Also why can I not say:
s = s.where(cond.assoc_name.pluralize : {cond.name : cond.value})
I have to use the => and .to_sym to get it to work. The above syntax errors: syntax error, unexpected ':' ...ere(cond.assoc_name.pluralize : {cond.name : cond.value}) ... ^
What if you store the query in a variable and append to that?
def self.dynamic_query(object)
q = joins(object.map{|o| o.assoc_name.to_sym})
object.each do |cond|
q = q.where(cond.assoc_name.pluralize : {cond.name : cond.value})
end
q # returns the full query
end
Another approach might be the merge(other) method. From the API Docs:
Merges in the conditions from other, if other is an ActiveRecord::Relation. Returns an array representing the intersection of the resulting records with other, if other is an array.
Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
# Performs a single join query with both where conditions.
That could be useful to knot all the conditions together.

Resources