How to rewrite this each loop in ruby? - ruby-on-rails

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.)

Related

Comparing two lists and syncronizing the database based on differences

I have the following resource relationship:
Class Course < ActiveRecord::Base
has_many :track_courses
has_many :tracks, through: :track_courses
end
as well as a mirroring relationship inside the Track model. The TrackCourse table which connects these models has these rows:
id: primary key
track_id: represents the track
course_id: represents the course
position: the ordering of the course inside that track
I want to allow admin users to be able to update the courses in each track via ajax. I have a list on the front-end that is being passed to the controller as a hash:
front_end_list = { course_id => position }
which represents the object and its position on the front-end sortable.
I'm also looking up the list of existing courses in that track:
existing_courses = TrackCourse.where("track_id = ?", track_id).all
GOAL: Compare these two lists and syncronize the database entries according to the front-end list. Essentially, if the user inserts Course 15 into position 2 on the webpage, I need to either insert that entry into TrackCourse table (if it doesn't exist) or update its position (if it exists). And vice versa for remove.
What is the best way of doing this? Do ActiveRecord/ActiveRelation provide methods for it? Or do I have to write something myself?
UPDATE: I found a gem called acts_as_list, but it seems to be designed for ActiveRecord tables as opposed to ActiveRelation. It essentially expects position values to be unique, whereas in TrackCourse there can be multiple course with same position (in different tracks).
I figured out a solution. I'll post my code here in case it helps anyone else down the line.
I have this method in my controller that processes the ajax request from the front-end:
def sort
track_id = params[:track_id]
courses_in_list = {}
params[:course].each do |courseid|
position = params[:course].index(courseid)
courses_in_list[courseid.to_i] = position
end
existing_courses_in_track = {}
TrackCourse.where("track_id = ?", track_id).to_a.each do |track_course|
existing_courses_in_track[track_course.course_id] = track_course.position
end
if courses_in_list.length < existing_courses_in_track.length
existing_courses_in_track.each do |courseid, position|
if courses_in_list[courseid].nil?
track_course = TrackCourse.where(track_id: track_id, course_id: courseid).first
track_course.remove_from_list
track_course.destroy!
end
end
else
if existing_courses_in_track.empty?
track_course = TrackCourse.new(track_id: track_id,
course_id: courses_in_list.keys[0])
track_course.insert_at(courses_in_list.values[0])
p "first track!"
else
courses_in_list.each do |courseid, position|
track_exists = false
if !existing_courses_in_track[courseid].nil?
track_course_position = existing_courses_in_track[courseid]
track_exists = true
end
if !track_exists
TrackCourse.new(track_id: track_id, course_id: courseid).insert_at(position)
else
p "else statement"
track_course = TrackCourse.where(track_id: track_id, course_id: courseid).first
track_course.update_attribute(:position, position)
end
end
end
end
render :nothing => true
end
Essentially, I'm building two hashes, one based on the list of front-end items and their position, and one based on the database courses and their position. I then compare them. If the front-end list is shorter, that means the user removed an item, so I iterate through the backend list, find the extra item, and remove it. Then I employ a similar mechanism for adding items to the list and resorting the list. The acts_as_list gem really helps with keeping things in the correct position. However, I did have to limit its scope when I included it in my model to ensure it runs only on relationships (TrackCourses) with a specific track_id.

ActiveRecord query array intersection?

I'm trying to figure out the count of certain types of articles. I have a very inefficient query:
Article.where(status: 'Finished').select{|x| x.tags & Article::EXPERT_TAGS}.size
In my quest to be a better programmer, I'm wondering how to make this a faster query. tags is an array of strings in Article, and Article::EXPERT_TAGS is another array of strings. I want to find the intersection of the arrays, and get the resulting record count.
EDIT: Article::EXPERT_TAGS and article.tags are defined as Mongo arrays. These arrays hold strings, and I believe they are serialized strings. For example: Article.first.tags = ["Guest Writer", "News Article", "Press Release"]. Unfortunately this is not set up properly as a separate table of Tags.
2nd EDIT: I'm using MongoDB, so actually it is using a MongoWrapper like MongoMapper or mongoid, not ActiveRecord. This is an error on my part, sorry! Because of this error, it screws up the analysis of this question. Thanks PinnyM for pointing out the error!
Since you are using MongoDB, you could also consider a MongoDB-specific solution (aggregation framework) for the array intersection, so that you could get the database to do all the work before fetching the final result.
See this SO thread How to check if an array field is a part of another array in MongoDB?
Assuming that the entire tags list is stored in a single database field and that you want to keep it that way, I don't see much scope of improvement, since you need to get all the data into Ruby for processing.
However, there is one problem with your database query
Article.where(status: 'Finished')
# This translates into the following query
SELECT * FROM articles WHERE status = 'Finished'
Essentially, you are fetching all the columns whereas you only need the tags column for your process. So, you can use pluck like this:
Article.where(status: 'Finished').pluck(:tags)
# This translates into the following query
SELECT tags FROM articles WHERE status = 'Finished'
I answered a question regarding general intersection like queries in ActiveRecord here.
Extracted below:
The following is a general approach I use for constructing intersection like queries in ActiveRecord:
class Service < ActiveRecord::Base
belongs_to :person
def self.with_types(*types)
where(service_type: types)
end
end
class City < ActiveRecord::Base
has_and_belongs_to_many :services
has_many :people, inverse_of: :city
end
class Person < ActiveRecord::Base
belongs_to :city, inverse_of: :people
def self.with_cities(cities)
where(city_id: cities)
end
# intersection like query
def self.with_all_service_types(*types)
types.map { |t|
joins(:services).merge(Service.with_types t).select(:id)
}.reduce(scoped) { |scope, subquery|
scope.where(id: subquery)
}
end
end
Person.with_all_service_types(1, 2)
Person.with_all_service_types(1, 2).with_cities(City.where(name: 'Gold Coast'))
It will generate SQL of the form:
SELECT "people".*
FROM "people"
WHERE "people"."id" in (SELECT "people"."id" FROM ...)
AND "people"."id" in (SELECT ...)
AND ...
You can create as many subqueries as required with the above approach based on any conditions/joins etc so long as each subquery returns the id of a matching person in its result set.
Each subquery result set will be AND'ed together thus restricting the matching set to the intersection of all of the subqueries.

See if one person is before another in the alphabet, ruby, rails

I'm doing an app for a membership database.
Each person may have a partner. When it comes to displaying the list, I only want to have one row for each family, so at the moment I'm comparing first names and not displaying the row if the person's name is second. Like this
person.first_name != [person.first_name, person.partner.first_name].sort[0]
This means each family only gets displayed once, not twice - once for each partner.
And I'm doing this in the view.
There must be a better way of doing this, and it'd be really great if I could do it at the database level. I'm using postgresql if that makes a difference.
Edit
Sorry if it was unclear.
Say Person 1 has the first_name "Edward" and Person 2 has the first_name "Fay". Edward and Fay are married.
I only want to show them once in my list - I want a row to look like this
Surname First name Address etc
Mysurname Edward ....
Fay
I don't want to display it again with Fay first because I've got both Fay and Edward in list of people, so I use the ruby in the first part of the question to check if I should display the row - it compares their first names and only does the row if the person has a fist name that's before his/her partner's first name.
Here's the relevant part of my person model
class Person < ActiveRecord::Base
has_one :relationship_link, :foreign_key => :person_id, :dependent => :destroy, :include => :partner
has_one :partner, :through => :relationship_link, :source => :person_b, :class_name => "Person"
I hope that's clearer
You need to use DISTINCT ON or GROUP BY. In postgres you need to be careful to group by everything that you are selecting. If you only need to get the last names you can select("DISTINCT ON(last_name) last_name").pluck("last_name"). You will only get an array of last names though.
Maybe you can get records if you order by every other fields in your table, like this:
select("DISTINCT ON(people.last_name) people.*").order("people.last_name ASC, people.first_name ASC, people.field2 DESC, people.field3 ASC...")
You need to order by every attribute so the result is not ambigious.
For this case, i would create a data structure (a Hash) to store people instances given a specific surname. Something like this:
def build_surnames_hash(people_array)
surnames_hash = {}
people_array.each do |person|
last_name = person.last_name
surnames_hash[last_name] ||= []
surnames_hash[last_name] << person
end
surnames_hash
end
That way, you can iterate over the hash and display people using their surnames stored as hash's keys:
surnames_hash = build_surnames_hash(Person.all)
surnames_hash.each do |surname, person_instances_array|
# display the surname once
# iterate over person_instances_array displaying their properties
end

Rails active record query

How would i do a query like this.
i have
#model = Model.near([latitude, longitude], 6.8)
Now i want to filter another model, which is associated with the one above.
(help me with getting the right way to do this)
model2 = Model2.where("model_id == :one_of_the_models_filtered_above", {:one_of_the_models_filtered_above => only_from_the_models_filtered_above})
the model.rb would be like this
has_many :model2s
the model2.rb
belongs_to :model
Right now it is like this (after #model = Model.near([latitude, longitude], 6.8)
model2s =[]
models.each do |model|
model.model2s.each do |model2|
model2.push(model2)
end
end
I want to accomplish the same thing, but with an active record query instead
i think i found something, why does this fail
Model2.where("model.distance_from([:latitude,:longitude]) < :dist", {:latitude => latitude, :longitude => longitude, :dist => 6.8})
this query throws this error
SQLite3::SQLException: near "(": syntax error: SELECT "tags".* FROM "tags" WHERE (model.distance_from([43.45101666666667,-80.49773333333333]) < 6.8)
, why
use includes. It will eager-load associated models (only two SQL queries instead of N+1).
#models = Model.near( [latitude, longitude], 6.8 ).includes( :model2s )
so when you will do #models.first.model2s, associated model2s will already be loaded (see RoR guides for more info).
If you want to get an array of all model2s belonging to your collection of models, you can do :
#models.collect( &:model2s )
# add .flatten at the end of the chain if you want a one level deep array
# add .uniq at the end of the chain if you don't want duplicates
collect (also called map) will gather in an array the result of any block passed to each of the caller's elements (this does exactly the same as your code, see Enumerable's doc for more info). The & before the symbol converts it into a Proc passed to each element of the collection, so this is the same as writing
#models.collect {|model| model.model2s }
one more thing : #mu is right, seems SQLite does not know about your distance_from stored procedure. As i suspect this is a GIS related question, you may ask about this particular issue on gis.stackexchange.com

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Resources