Rails association collection build if not exist - ruby-on-rails

I have three models Exam, User and ExamResult. ExamResult contains the records for all the students (User) for exams (Exam). For one particular Exam record, there should be one record in ExamResult for each student. In the edit method of ExamController, depending if the ExamResult record has been created for one student, I need to build one new record or just skip it. Not sure if this is idiomatic Rails way of doing it.
# ExamController
def edit
User.students.each do |s|
#exam.exam_results.build(user_id: s.id) unless #exam.exam_results.find_by(user_id: s.id)
end
end
or this way:
def edit
newIds = User.students.map(&:id) - #exam.exam_results.map(&:user_id)
newIds.each do |id|
#exam.exam_results.build(user_id: id)
end
end
Maybe neither is idiomatic Rails. Any suggestions are welcome.
Edit
Bring find_or_initialize_by (recommended by #user3334690) on the table. If I understand the doc correctly, this should do the same as previous two implementations.
def edit
User.students.each do |s|
#exam.exam_results.find_or_initialize_by(user_id: s.id)
end
end

Instead of build you can use find_or_create_by_user_id in above case.
def edit
User.students.each do |s|
ExamResult.find_or_create_by_exam_id_and_user_id(exam_id, user_id)
end
end

there is this way:
def edit
User.students.each do |s|
ExamResult.where(exam_id: #exam.id, user_id: s.id).first_or_create
end
end

Use your second example:
def edit
newIds = User.students.map(&:id) - #exam.exam_results.map(&:user_id)
newIds.each do |id|
#exam.exam_results.build(user_id: id)
end
end
As I said in comments above this will query the database twice regardless of the number of students which will scale much better. If you're in a really extreme environment you can write it to use only one query along with "pluck" to pull only the "id" column (and avoid the object creation overhead) like this:
newIds = User.students.where("users.id not in (select user_id from exam_results where exam_id=?)", #exam.id).pluck(:id)
However, the readability is reduced for that. Your original would also benefit from using "pluck" instead of "map".
One other style note - I would use "new_ids" which is the standard idiomatic way of doing it with Rails.

Related

How to display an specific item of database

My rails app has a database set.
def index
#clubs = Club.all
end
This is my controller.
If i type in my Index.html.erb
<% #clubs.each do |club| %>
<%= club.name %>
<% end %>
I get all the names of my database show in my index view.
What if I just want to pick one or just a couple?
Thru the rails console i can by typing c=Club.find(1) 1 by default takes id=1.
So how can i just display several ID's and not all one the database in the same index.html.erb.
thanks anyway!
Try this:
Let us consider that params[:ids] contains all the ids that belong to the records you want to get.
def index
#clubs = Club.where(id: params[:ids])
end
Fix
The straightforward answer here is to recommend you look at the ActiveRecord methods you can call in your controller; specifically .where:
#app/controllers/clubs_controller.rb
Class ClubsController < ApplicationController
def index
#clubs = Club.where column: "value"
end
end
This will populate the #clubs instance variable with only the records which match that particular condition. Remember, it's your Rails app, so you can do what you want with it.
Of course, it's recommended you stick with convention, but there's nothing stopping you populating specific data into your #clubs variable
--
RESTful
As someone mentioned, you shouldn't be including "filtered" records in an index action. Although I don't agree with this idea personally, the fact remains that Rails is designed to favour convention over configuration - meaning you should really leave the index action as showing all the records
You may wish to create a collection-specific action:
#config/routes.rb
resources :clubs do
collection do
get :best #-> domain.com/clubs/best
end
end
#app/controllers/clubs_controller.rb
Class ClubsController < ApplicationController
def best
#clubs = Club.where attribute: "value"
render "index"
end
end
There are several ways to select a specific record or group of records from the database. For example, you can get a single club with:
#club = Club.find(x)
where x is the id of the club. Then in your view (the .html.erb file), you can simply access the #club object's attributes.
You can also cast a wider net:
#disco_clubs = Club.where(type: "disco") # returns an ActiveRecord Relation
#disco_clubs = Club.where(type: "disco").to_a # returns an array
And then you can iterate over them in the same manner you do in your index.html.erb. Rails has a rich interface for querying the database. Check it out here.
Also note that individual records - such as those selected with the find method - are more commonly used with the show action, which is for displaying a single record. Of course, that's for generic CRUD applications. It't not a hard rule.
change
def index
#clubs = Club.all
end
to this
def index
#clubs = Club.find(insert_a_number_that_is_the_id_of_the_club_you_want)
end
Querying your database is a complex thing and gives you a ton of options so that you can get EXACTLY what you want and put it into your #clubs variable. I suggest reading this part of the rails guide
It should also be noted that if you're only going to query your database for one record then change #clubs to #club so you know what to expect.

How to fetch a random record in rails?

I am trying to fetch a random record in rails, to render in my home page.
I have a post model with content and title attributes. Lets say i wanted to fetch a random post(content and title) for some reason, How can i go about it in ruby. Thanks in advance.
You might find this gem handy : Faker
It allows to generate random strings with some meaning.
For example, a name :
Faker::Name.name => “Bob Hope”
Or an e-mail
Faker::Internet.email
In addition to this gem, if you want to be able to generate mock models very easily, I recommend the gem Factory Girl
It allows you to create factories for your model, sou you can generate a model with random attributes quickly.
Posting another answer since the first one answered to an unclear question.
As #m_x said, you can use RANDOM() for SQL.
If you don't mind loading all the dataset, you can do it in ruby as well :
Post.all.sample
This will select one random record from all Posts.
I know this is an old question, but since no answer was chosen, answering it might be helpful for other users.
I think the best way to go would be generating a random offset in Ruby and using it in your Active Record statement, like so:
Thing.limit(1).offset(rand(Thing.count)).first
This solution is also performant and portable.
In your post controller,
def create
#post = Post.new(params[:post])
if you_want_some_random_title_and_content
title_length = 80 #choose your own
content_length = 140 #choose your own
#post.title = (0...title_length).map{(65+rand(26)).chr}.join
#post.content = (0...content_length).map{(65+rand(26)).chr}.join
end
if #post.save
redirect_to #post
else
render 'new'
end
end
Using Kent Fedric's way to generate random string
unfortunately, there is no database-agnostic method for fetching a random record, so ActiveRecord does not implement any.
For postgresql you can use :
Post.order( 'RANDOM()' ).first
To fetch one random post.
Additionnally, i usually create a scope for this:
class Post < ActiveRecord::Base
scope :random_order, ->{ order 'RANDOM()' }
end
so if you change your RDBMS, you just have to change the scope.

Can I make Rails update_attributes with nested form find existing records and add to collections instead of creating new ones?

Scenario: I have a has_many association (Post has many Authors), and I have a nested Post form to accept attributes for Authors.
What I found is that when I call post.update_attributes(params[:post]) where params[:post] is a hash with post and all author attributes to add, there doesn't seem to be a way to ask Rails to only create Authors if certain criteria is met, e.g. the username for the Author already exists. What Rails would do is just failing and rollback update_attributes routine if username has uniqueness validation in the model. If not, then Rails would add a new record Author if one that does not have an id is in the hash.
Now my code for the update action in the Post controller becomes this:
def update
#post = Post.find(params[:id])
# custom code to work around by inspecting the author attributes
# and pre-inserting the association of existing authors into the testrun's author
# collection
params[:post][:authors_attributes].values.each do |author_attribute|
if author_attribute[:id].nil? and author_attribute[:username].present?
existing_author = Author.find_by_username(author_attribute[:username])
if existing_author.present?
author_attribute[:id] = existing_author.id
#testrun.authors << existing_author
end
end
end
if #post.update_attributes(params[:post])
flash[:success] = 'great!'
else
flash[:error] = 'Urgg!'
end
redirect_to ...
end
Are there better ways to handle this that I missed?
EDIT: Thanks for #Robd'Apice who lead me to look into overriding the default authors_attributes= function that accepts_nested_attributes_for inserts into the model on my behalf, I was able to come up with something that is better:
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
if author_attributes[:id].nil? and author_attributes[:username].present?
author = Radar.find_by_username(radar_attributes[:username])
if author.present?
author_attributes[:id] = author.id
self.authors << author
end
end
end
assign_nested_attributes_for_collection_association(:authors, authors_attributes, mass_assignment_options)
end
But I'm not completely satisfied with it, for one, I'm still mucking the attribute hashes from the caller directly which requires understanding of how the logic works for these hashes (:id set or not set, for instance), and two, I'm calling a function that is not trivial to fit here. It would be nice if there are ways to tell 'accepts_nested_attributes_for' to only create new record when certain condition is not met. The one-to-one association has a :update_only flag that does something similar but this is lacking for one-to-many relationship.
Are there better solutions out there?
This kind of logic probably belongs in your model, not your controller. I'd consider re-writing the author_attributes= method that is created by default for your association.
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
author_to_update = Author.find_by_id(author_attributes[:id]) || Author.find_by_username(author_attributes[:username]) || self.authors.build
author_to_update.update_attributes(author_attributes)
end
end
I haven't tested that code, but I think that should work.
EDIT: To retain the other functionality of accepts_nested_Attributes_for, you could use super:
def authors_attributes=(authors_attributes)
authors_attributes.each do |key, author_attributes|
authors_attributes[key][:id] = Author.find_by_username(author_attributes[:username]).id if author_attributes[:username] && !author_attributes[:username].present?
end
super(authors_attributes)
end
If that implementation with super doesn't work, you probably have two options: continue with the 'processing' of the attributes hash in the controller (but turn it into a private method of your controller to clean it up a bit), or continue with my first solution by adding in the functionality you've lost from :destroy => true and reject_if with your own code (which wouldn't be too hard to do). I'd probably go with the first option.
I'd suggest using a form object instead of trying to get accepts_nested_attributes to work. I find that form object are often much cleaner and much more flexible. Check out this railscast

random loop with conditions in rails

I have a feature called "Browse" that allows users to browse through random profiles. When a user clicks on "browse" they are immediately taken to a users profile that they are NOT already friends with. What should my controller look like?
Right now I've got:
def browse
#users = User.all.offset(rand(current_user.matches.count))
#users.each do |user|
if !current_user.friends.include?(user)
#user = user
return
end
end
end
However that doesn't seem to be working. Any advice? I am admittedly bad with blocks, it seems!
You could try something like this
def browse
#user = (User.all - current_user.friends).sample
end
A better version would be
def browse
#user = User.where('id not in (?)', current_user.friends.map(&:id))
.offset(rand(current_user.matches.count)).limit(1)
end
Also, if you are too concerned about performance, instead of using the offset technique, better use the randumb gem to fetch the random record. It uses database specific functions for selecting random records, if available.
Add an extra method to your User, something like this:
def random_stranger
self.class.where(%Q{
id not in (
select friend_id
from friends
where user_id = ?
}, self.id).
order('random()').
limit(1).
first
end
Then in your controller:
def browse
#user = current_user.random_stranger
end
If your database doesn't know how to optimize that not in then you could replace it with a LEFT OUTER JOIN combined with WHERE friend_id is null.

I want to map my database lookup tables to a hash, good idea?

I am developing a Rails web application and am confused about how to utilize the lookup table values in my models. Here is an example model from my app:
table name: donations
id
amount
note
user_id
appeal_id
donation_status_id
donation_type_id
is_anonymous
created_at
updated_at
The fields *donation_status_id* and *donation_type_id* refer to lookup tables. So in my code I have several random places where I make calls like this:
my_donation = Donation.find(params[:id])
if my_donation.donation_status_id == DonationStatus.find_by_name("completed").id
#do something
end
To my inexperienced eyes, a one-off query to the DonationStatus table seems incredibly wasteful here, but I don't see any other good way to do it. The first idea I thought of was to read all my lookup tables into a hash at application startup and then just query against that when I need to.
But is there a better way to do what I am trying to do? Should I not worry about queries like this?
Thanks!
Since you have two models, you should use ActiveRecord Model Associations when building the models.
class Donation
has_one :donation_status
end
class DonationStatus
belongs_to :donation
end
Then when you do
my_donation = Donation.find(params[:id])
if my_donation.donation_status.status_name == 'complete'
#do something
end
For more information, you may want to read up how rails is doing the model associations http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html Don't worry about performance, rails has taken care of that for you if you follow how the way it should be done
How about putting it in a constant? For example, something like this:
class DonationStatus < ActiveRecord::Base
COMPLETED_DONATION_ID = DonationStatus.find_by_name("completed").id
PENDING_DONATION_ID = DonationStatus.find_by_name("pending").id
# ...
end
class DonationsController < ApplicationController
def some_action
my_donation = Donation.find(params[:id])
if my_donation.donation_status_id == DonationStatus::COMPLETED_DONATION_ID
#do something
end
end
This way, DonationStatus.find_by_name("pending").id gets executed exactly one. I'm assuming, of course, that this table won't change often.
BTW, I learned this trick in Dan Chak's book, Enterprise Rails.
EDIT: I forgot to mention: in practice, I declare constants like this:
COMPLETED_DONATION_ID = DonationStatus.find_by_name("completed").id rescue "Can't find 'completed' in donation_statuses table"
What you could do is add this method to Donation:
# Donation.rb
def completed?
self.donation_status.name == 'completed' ? true : false
end
And then just do my_donation.completed?. If this is called a second time, Rails will look to cache instead of going to the DB.
You could add memcached if you want, or use Rails' caching further, and do:
def completed?
return Rails.cache.fetch("status_#{self.donation_status_id}_complete") do
self.donation_status.name == 'completed' ? true : false
end
end
What that will do is make a hash key called (for example) "status_1_complete" and if it's not defined the first time, will evaluate the block and set the value. Otherwise, it will just return the value. That way, if you had 1,000,000,000 donations and each of them had donation_status 1, it would go directly to the cache. memcached is quite fast and popular.

Resources