Selective ActiveRecord - ruby-on-rails

Say I'm having a table 'Users'.
A user can exist 3 times (records) in my table, in 3 different states (state1, state2, state3).
First state1 will be created, then state2, ...
If state3 exists, I don't want to look at state1 and state2 anymore, but I'll have to keep them in my table, for later purposes.
All 3 records have the same uuid.
If I want to collect all users, I can't use User.all (because he will give me all 3 states for the same user).
Is there a short solution for this in my model? Now I'm collecting all uuid's and for each uuid I'll check which is the latest state, then I put those records in an array.
Problem with this array is that it is just 'an array', instead of an ActiveRecord object.
#uuid = []
#users = [] #will contain only the latest states at the end
User.all.each do |u|
#uuid << u.uuid unless #uuid.includes?(u.uuid)
end
#uuid.each do |u|
if user = User.find_by_state_and_uuid(3, u)
#users << user
elsif user = User.find_by_state_and_uuid(2, u)
#users << user
elsif user = User.find_by_state_and_uuid(1, u)
#users << user
end
end
Any ideas how I can translate this logic to an ActiveRecord object?
In short: User.magic_function to return only the latest state of each uuid
Thanks in advance!
Wouter

If you plan ahead, you can always sort on your state and return the "highest" one. This works well if you have a linear progression from one to the next. As an example:
user = User.where(:uuid => u).order('users.state DESC').first
For more complicated transitions you're not going to be able to use this trick. You could try using a different column for ordering, such as fetching the last by created_at time.
From a design perspective it seems highly unusual to have several user records in different states. A better plan might be to split out the state-driven part of the user record into a separate table and do the state tracking there, everything linked back to a singular user record.

Have you looked into using scopes? You should be able to create a scope for each User state, and then use those for querying.

Try :
User.get_user(state, uuid)
And make the scope in your user model :
scope :get_user, lambda { |*args| { :conditions => ["state = ? AND uuid = ?",args.first , args.second ] }}

Related

Rails best way to get previous and next active record object

I need to get the previous and next active record objects with Rails. I did it, but don't know if it's the right way to do that.
What I've got:
Controller:
#product = Product.friendly.find(params[:id])
order_list = Product.select(:id).all.map(&:id)
current_position = order_list.index(#product.id)
#previous_product = #collection.products.find(order_list[current_position - 1]) if order_list[current_position - 1]
#next_product = #collection.products.find(order_list[current_position + 1]) if order_list[current_position + 1]
#previous_product ||= Product.last
#next_product ||= Product.first
product_model.rb
default_scope -> {order(:product_sub_group_id => :asc, :id => :asc)}
So, the problem here is that I need to go to my database and get all this ids to know who is the previous and the next.
Tried to use the gem order_query, but it did not work for me and I noted that it goes to the database and fetch all the records in that order, so, that's why I did the same but getting only the ids.
All the solutions that I found was with simple order querys. Order by id or something like a priority field.
Write these methods in your Product model:
class Product
def next
self.class.where("id > ?", id).first
end
def previous
self.class.where("id < ?", id).last
end
end
Now you can do in your controller:
#product = Product.friendly.find(params[:id])
#previous_product = #product.next
#next_product = #product.previous
Please try it, but its not tested.
Thanks
I think it would be faster to do it with only two SQL requests, that only select two rows (and not the entire table). Considering that your default order is sorted by id (otherwise, force the sorting by id) :
#previous_product = Product.where('id < ?', params[:id]).last
#next_product = Product.where('id > ?', params[:id]).first
If the product is the last, then #next_product will be nil, and if it is the first, then, #previous_product will be nil.
There's no easy out-of-the-box solution.
A little dirty, but working way is carefully sorting out what conditions are there for finding next and previous items. With id it's quite easy, since all ids are different, and Rails Guy's answer describes just that: in next for a known id pick a first entry with a larger id (if results are ordered by id, as per defaults). More than that - his answer hints to place next and previous into the model class. Do so.
If there are multiple order criteria, things get complicated. Say, we have a set of rows sorted by group parameter first (which can possibly have equal values on different rows) and then by id (which id different everywhere, guaranteed). Results are ordered by group and then by id (both ascending), so we can possibly encounter two situations of getting the next element, it's the first from the list that has elements, that (so many that):
have the same group and a larger id
have a larger group
Same with previous element: you need the last one from the list
have the same group and a smaller id
have a smaller group
Those fetch all next and previous entries respectively. If you need only one, use Rails' first and last (as suggested by Rails Guy) or limit(1) (and be wary of the asc/desc ordering).
This is what order_query does. Please try the latest version, I can help if it doesn't work for you:
class Product < ActiveRecord::Base
order_query :my_order,
[:product_sub_group_id, :asc],
[:id, :asc]
default_scope -> { my_order }
end
#product.my_order(#collection.products).next
#collection.products.my_order_at(#product).next
This runs one query loading only the next record. Read more on Github.

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.

Rails sort users by method

I'm trying to rank my user's in order of an integer. The integer I'm getting is in my User Model.
def rating_number
Impression.where("impressionable_id = ?", self).count
end
This gives each User on the site a number (in integer form). Now, on the homepage, I want to show an ordered list that places these user's in order with the user with the highest number first and lowest number second. How can I accomplish this in the controller???
#users = User....???
Any help would be appreciated!
UPDATE
Using this in the controller
#users = User.all.map(&:rating_number)
and this for the view
<% #users.each do |user| %>
<li><%= user %></li>
<% end %>
shows the user's count. Unfortunately, the variable user is acting as the integer not the user, so attaching user.name doesn't work. Also, the list isn't in order based on the integer..
The advice here is still all kinds of wrong; all other answers will perform terribly. Trying to do this via a nested select count(*) is almost as bad an idea as using User.all and sorting in memory.
The correct way to do this if you want it to work on a reasonably large data set is to use counter caches and stop trying to order by the count of a related record.
Add a rating_number column to the users table, and make sure it has an index defined on it
Add a counter cache to your belongs_to:
class Impression < ActiveRecord::Base
belongs_to :user, counter_cache: :rating_number
end
Now creating/deleting impressions will modify the associated user's rating_number.
Order your results by rating_number, dead simple:
User.order(:rating_number)
The advice here is just all kinds of wrong. First of model your associations correctly. Secondly you dont ever want to do User.all and then sort it in-memory based on anything. How do you think it will perform with lots of records?
What you want to do is query your user rows and sort them based on a subquery that counts impressions for that user.
User.order("(SELECT COUNT(impressions.id) FROM impressions WHERE impressionable_id = users.id) DESC")
While this is not terribly efficient, it is still much more efficient than operating with data sets in memory. The next step is to cache the impressions count on the user itself (a la counter cache), and then use that for sorting.
It just pains me that doing User.all is the first suggestion...
If impressions is a column in your users table, you can do
User.order('impressions desc')
Edit
Since it's not a column in your users table, you can do this:
User.all.each(&:rating_number).sort {|x,y| y <=> x }
Edit
Sorry, you want this:
User.all.sort { |x, y| y.rating_number <=> x.rating_number }

Rails: how and where to add this method

I have an app where I retrieve a list of users from a specific country.
I did this in the UsersController:
#fromcanada = User.find(:all, :conditions => { :country => 'canada' })
and then turned it into a scope on the User model
scope :canada, where(:country => 'Canada').order('created_at DESC')
but I also want to be able to retrieve a random person or multiple persons from the country. I found this method that's supposed to be an efficient way to retrieve a random user from the database.
module ActiveRecord
class Base
def self.random
if (c = count) != 0
find(:first, :offset =>rand(c))
end
end
end
end
However, I have a few questions about how to add it, and how the syntax works.
Where would I put that code? Direct in the User model?
Syntax: so that I don't use code that I don't understand, can you explain how the syntax is working? I don't get (c = count). What is count counting? What is rand(c) doing? Is it finding the first one starting at the offset? If rand is an expensive method (hence the need to create a different more efficient random method), why use the expensive 'rand' in this new more efficient random method?
How could I add the call to random on my find method in the UsersController? How to add it to the scope in the model?
Building on question 3, is there a way to get two or three random users?
I wouldn't monkey patch that (or anything else!) into ActiveRecord, putting that into your User would make more sense.
The count is counting how many elements there are in your table and storing that number in c. Then rand(c) gives you a random integer in the interval [0,c) (i.e. 0 <= rand(c) < c). The :offset works the way you think it does.
rand isn't terribly expensive but doing order by random() inside the database can be very expensive. The random method that you're looking at is just a convenient way to get a random record/object from the database.
Adding it to your own User would look something like this:
def self.random
n = scoped.count
scoped.offset(rand(n)).first
end
That would allow you to chain random after a bunch of scopes:
u = User.canadians_eh.some_other_scope.random
but the result of random would be a single user so your chaining would stop there.
If you wanted multiple users you'd want to call random multiple times until you got the number of users you wanted. You could try this:
def self.random
n = scoped.count
scoped.offset(rand(n))
end
us = User.canadians_eh.random.limit(3)
to get three random users but the users would be clustered together in whatever order the database ended up with after your other scopes and that's probably not what you're after. If you want three you'd be better off with something like this:
# In User...
def self.random
n = scoped.count
scoped.offset(rand(n)).first
end
# Somewhere else...
scopes = User.canadians_eh.some_other_scope
users = 3.times.each_with_object([]) do |_, users|
users << scopes.random
scopes = scopes.where('id != :latest', :latest => users.last.id)
end
You'd just grab a random user, update your scope chain to exclude them, and repeat until you're done. You would, of course, want to make sure you had three users first.
You might want to move the ordering out of your canada scope: one scope, one task.
That code is injecting a new method into ActiveRecord::Base. I would put it in lib/ext/activerecord/base.rb. But you can put it anywhere you want.
count is a method being called on self. self will be some class inheriting from ActiveRecord::Base, eg. User. User.count returns the number of user records (sql: SELECT count(*) from users;). rand is a ruby stdlib method Kernel#rand. rand(c) returns a random integer in the Range 0...c and c was previously computed by calling #count. rand is not expensive.
You don't call random with find, User#random is a find, it returns one random record from all User records. In your controller you say User.random and it returns a single random record (or nil if there are no user records at all).
modify the AR::Base::random method like so:
module ActiveRecord
class Base
def self.random( how_many = 1 )
if (c = count) != 0
res = (0..how_many).inject([]) do |m,i|
m << find(:first, :offset =>rand(c))
end
how_many == 1 ? res.first : res
end
end
end
end
User.random(3) # => [<User Rand1>,<User Rand2>,<User Rand3>]

Ruby: Compare two arrays for matches, and order results in DESC order

I have a user model. Each user has restaurant names associated with them. I have a view (index.html.erb) which shows all users.
I want to order the users in this view based on how many restaurants the current_user and some other user have in common in descending order... (it does the opposite!)
ex.
User1 (current_user) has been to McDonalds, Burger King, Arby's
User2 has been to Ivar's
User3 has been to McDonalds, Burger King
When User1 loads the index view, the order the users should be displayed is:
User1 (3/3 restaurants match)
User3 (2/3 restaurants match)
User2 (0/3 restaurants match)
my User.rb file
def compare_restaurants
self.restaurants.collect
end
my users_controller.rb
def index
#users = User.all.sort_by {|el| (el.compare_resturants & current_user.compare_resturants).length }
end
If you're dead set on using sort_by then you can just negate the numbers:
def index
#users = User.all.sort_by { |el|
-(el.compare_resturants & current_user.compare_resturants).length
}
end
This trick only works because you're sorting by numeric values. If you were sorting on strings then you'd have to use something like this:
reverse_sorted = a.sort_by { |x| something(x) }.reverse
but that would involve an extra copy of the array and the extra work that reverse would do. In such cases you should use a full sort with reversed comparison logic.
If you're were trying to use sort_by to avoid computing something expensive for each comparison and you were sorting on something non-numeric, then you could always use sort with an explicit Schwartzian Transform to compute the expensive things only once.
Just sort them in the controller.
my = User.find_by_id this_user # Or however you're getting "the current user"
#users = User.all.-([my]).sort do |a, b|
common_with_a = (my.restaurants & a.restaurants).length
common_with_b = (my.restaurants & b.restaurants).length
common_with_a <=> common_with_b
end
... but if you're planning on letting the user start sorting various columns or reversing the sorting a lot, you should implement your sort in Javascript so you can cut down on the number of page reloads and subsequent database hits you have to make.

Resources