Alias find_by queries of often used objects - ruby-on-rails

I have a Mood model which contains several moods which are seeded to the database, then they are never changed.
So, I was wondering, instead of always doing Mood.find_by(name: 'happy'), can't I somehow alias this to something like happy, and have it available everywhere where I would have the model available? Is there something in terms of Rails/ActiveRecord that allows this?

You can use Ruby's define_singleton_method in moods.rb. I didn't load ActiveRecord, so just replace the puts "Call Mood..." with the actual call:
class Mood
##moods = ['angry', 'calm', 'happy']
##moods.each { |mood| define_singleton_method(mood.to_sym) { puts "Call Mood.find_by(name: #{mood}) here" } }
end
Here's the output:
2.2.1 :003 > load 'moods.rb'
Call Mood.find_by(name: calm) here
Call Mood.find_by(name: angry) here
=> true

Defining a [] method is done sometimes for this.
class Mood < ActiveRecord::Base
def self.[](mood_name)
find_by_name(mood_name)
end
end
Then you can refer to it as Mood[:happy].

Related

How to map using constants in Ruby

I'm trying to map an array of custom values using a constant that's already defined. I'm running into some problems.
This works perfectly:
Car.where(brand: car_brands.map(&:Car_honda))
Although I have all the car brands already defined in my file, so I would prefer to use the constants over rewriting the names. For example:
HONDA = "Car_honda"
When I try and map this constant to the array it doesn't seem to work properly:
Car.where(brand: car_brands.map(&:HONDA))
I tried to use a block with map, but I still got the same result:
Car.where(brand: car_brands.map {|c| c.HONDA}))
Are we able to use constants with map?
Just use send:
Car.where(brand: car_brands.map { |c| c.send(HONDA) })
I'm not sure where you're going with this, or precisely where you're coming from, but here's an example that follows Rails conventions:
class Brand < ActiveRecord::Base
has_many :cars
end
class Car < ActiveRecord::Base
belongs_to :brand
end
Then you can find all cars associated with the "Honda" brand:
Brand.find_by(name: 'Honda').cars
Or find all cars matching one or more arbitrary brand names using a JOIN operation:
Car.joins(:brand).where(brand: { name: %w[ Honda Ford ] })
If you go with the flow in Rails things are a lot easier.
Are you able to use constants with map?
Nope. Not like this, anyhow.
car_brands.map { |c| c.HONDA }
This means, for every thing in car_brands call method HONDA on it and return results of the invocations. So, unless you have method HONDA defined (which you absolutely shouldn't), this has no chance to work.
Constants are defined on the class, not on the object. You can invoke them through .class.
:005 > Integer.const_set('ABC', 1)
=> 1
:006 > Integer::ABC
=> 1
:007 > [1,2,3].map {|i| i.class::ABC}
=> [1, 1, 1]
This will not work for your use case unless car_brands contains an array of different Car classes.
First off, you probably don't want to things this way. Perhaps it's the way your example is worded, but the only way it makes sense, as I'm reading it, is if Car_brands is an array of classes. And if that's the case, if doesn't make sense to have a constant called HONDA. If anything, you would have a constant called BRAND that might equal "Honda" for a given class. I strongly recommend you rethink your data structures before moving forward.
All that said, you can use const_get to access constants using map. e.g.
class CarBrand1
BRAND = 'Honda'
end
class CarBrand2
BRAND = 'Toyota'
end
car_brands = [CarBrand1, CarBrand2]
car_brands.map{|car_brand| car_brand.const_get("BRAND")}
car_brands.map{|car_brand| car_brand::BRAND} # Alternatively
# => ["Honda", "Toyota"]

How do I get back an ActiveRecord instead of an ActiveRecord relation?

Using Rails 5. How do I get an ActiveRecord instead of an ActiveRecord relation? I ahve the below model
class State < ActiveRecord::Base
belongs_to :country
...
def self.cached_find_us_state_by_name(name)
Rails.cache.fetch("#{name.upcase} US") do
find_us_state_by_name(name)
end
end
# Look up a US state by its full name
def self.find_us_state_by_name(name)
search_criteria = ["upper(states.name) = ? "]
search_criteria.push( "states.country_id = countries.id " )
search_criteria.push( "countries.iso = 'US' " )
results = State.joins(:country).where( search_criteria.join(' and '),
name.upcase)
end
but when I lookup an item using the methods, waht I get back is an ActiveReocrd relation ...
2.4.0 :004 > State.cached_find_us_state_by_name('Ohio')
=> #<ActiveRecord::Relation [#<State id: 3538, name: "Ohio", country_id: 233, iso: "OH">]>
This is cauisng problems later on, specifically ...
ActiveRecord::AssociationTypeMismatch: State(#70288305660760) expected, got State::ActiveRecord_Relation(#70288290686360)
from /Users/davea/.rvm/gems/ruby-2.4.0#global/gems/activerecord-5.0.1/lib/active_record/associations/association.rb:221:in `raise_on_type_mismatch!'
from /Users/davea/.rvm/gems/ruby-2.4.0#global/gems/activerecord-5.0.1/lib/active_record/associations/belongs_to_association.rb:12:in `replace'
Edit: Per the suggestion given, I changed my methods to
def self.cached_find_us_state_by_name(name)
Rails.cache.fetch("#{name.upcase} US") do
find_us_state_by_name(name)
end
end
# Look up a US state by its full name
def self.find_us_state_by_name(name)
search_criteria = ["upper(states.name) = ? "]
search_criteria.push( "states.country_id = countries.id " )
search_criteria.push( "countries.iso = 'US' " )
results = State.joins(:country).where( search_criteria.join(' and '),
name.upcase).take
end
but alas, I still get an ActiveRecord::Relation
2.4.0 :011 > State.cached_find_us_state_by_name('Ohio')
=> #<ActiveRecord::Relation [#<State id: 3538, name: "Ohio", country_id: 233, iso: "OH">]>
You can use find_by instead of where - the source code for this is simple:
def find_by(*args)
where(*args).take
end
Another way is to call .first or [0] but if your where has a lot of records this will be slow since it's loading them all into memory before selecting the first. If use limit(1) then these methods will be acceptably fast.
There are a variety of ways to get the ActiveRecord object from an ActiveRecord::Relation.The result of the call to
results = State.joins(:country).where( search_criteria.join(' and '),
name.upcase)
returns an ActiveRecord::Relation. This is always the case when you use the where clause in ActiveRecord. In order to get a specific ActiveRecord object, you could:
use the find_by method which returns the first instance in the database that it finds. Note that this does not return all of the records matching the criteria. * For example, User.find_by(first_name: "Mark") will return an ActiveRecord object containing the first record with a first name of "Mark"
Use the first method on the ActiveRecord::Relation collection. For example, User.where(first_name: "Mark").first does the same thing as the previous example.
Lastly, you could write something like this:
User.where(first_name: "Mark") do |u|
puts u.last_name
end
This allows you to iterate through the ActiveRecord::Relation collection one record at a time. So for your example, it would look something like this:
results.each do |res|
# do stuff
end
Most of ActiveRecord methods returns ActiveRecord::Relations in order to provide a fluent API ( syntactically pleasing method chaining if you will ). I think this is a design decision. There are a few method that breaks this, such as first (which really is a call to find_nth!(0) ), last to_a, to_sql and some others.
So, the way I found "around" this is to use .first where I really intended to get one and only one result back, as Mark and maxple are suggesting. I also think its probably one of the only way to do so.
This : https://github.com/rails/rails/blob/603ebae2687a0b50fcf003cb830c57f82fb795b9/activerecord/lib/active_record/querying.rb
And this (to which Querying delegates most of it's methods): https://github.com/rails/rails/blob/603ebae2687a0b50fcf003cb830c57f82fb795b9/activerecord/lib/active_record/querying.rb
are probably worth skimming through.

convert string to model in Rails rake task

I'm trying to run a quick rake task on all my Rails models but haven't been able to call them because this piece of code tells me that I can't call the method columns on a string.
I tried classify instead of camelize and it hasn't worked either, tried inserting a class_eval in there as well, but that dosen't seem to work here / don't know too much about it.
task :collect_models_and_field_names => :environment do
models = Dir.glob("#{models_path}/*").map do |m|
m.capitalize.camelize.columns.each { |n| puts n.name }
end
I do know that this worked so I would have manual access to the model if I needed, but I don't really want to do that...
Model.columns.each { |c| puts c.name }
Try
Kernel.const_get(m.classify).columns
classify just changes the string to look like a class -- i.e. with a capital letter and in camelcase, singular.
after using classify to make the string look like a class/model, you need to use constantize, which actually takes the string and converts it into a class.
See:
http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize
You can use something like this:
models = Dir[Rails.root.join("app", "models", "*.rb")].map do |m|
model = File.basename(m, ".rb").classify.constantize
model.columns.each { |n| puts n.name }
end

How do I find the .max of an attribute value among a group of different Models?

everyone: I am also open to just straight-up refactoring what I'm finding to be pretty repetitive, but to give a baseline of how it's working....
I have for every contact a Campaign, which has_many of three types of Models: Email, Call, and Letter.
When an Email (Call or Letter) has been executed for a specific contact, I have a Contact_Email(_or_Call_or_Letter) which belongs to both the Contact and the Model (Email_or_Call_or_Letter).
Each Contact_Email for example pairing has a :date_sent attribute. So does each Contact_Call and Contact_Letter.
How do I find the latest of all of them?
Here is the code I wrote that can find the latest Email and my finding retyping similar code for Call and Letter, but then stuck on how to do a .max on all of them:
def last_email(contact)
#get campaign the contact belongs to
#campaign = Campaign.find_by_id(contact.campaign_id)
#last_email = ContactEmail.find(:last,
:conditions => "contact_id = #{contact.id}",
:order => "date_sent DESC")
#last_call = ContactCall.find(:last,
:conditions => "contact_id = #{contact.id}",
:order => "date_sent DESC")
#last_letter = ContactLetter.find(:last,
:conditions => "contact_id = #{contact.id}",
:order => "date_sent DESC")
# how do I get the latest of all of these to display?
#email_template = Email.find_by_id(#last_email.email_id)
if #last_email.nil?
return "no email sent"
else
return #last_email.date_sent.to_s(:long) + link_to('email was sent', #email_template)
end
end
Question 1: With what I have, how can I find effectively #last_event given I can find the last Email, last Call, and last Letter for every contact?
Question 2: How can I remove the repetitive code that I have to write for each Model?
Do you have has_many associations setup in Contact referring to the other models? Something like:
class Contact < ActiveRecord::Base
has_many :contact_emails
has_many :contact_calls
has_many :contact_letters
end
If so, you can then create a last_event method on the Contact model:
def latest_event
[contact_emails, contact_calls, contact_letters].map do |assoc|
assoc.first(:order => 'date_sent DESC')
end.compact.sort_by { |e| e.date_sent }.last
end
Handling nil
When using the latest_event method you will get nil if there are no associated records. There are a couple of ways you can workaround this. The first is to check for nil first with something like:
contact.latest_event && contact.latest_event.date_sent
On late versions of Rails/Ruby you can also use Object#try which will call the method if it exists:
contact.latest_event.try(:date_sent)
I prefer not to use this as it doesn't check for nil but only if the object can respond to a method. This has cause some interesting errors if you expect nil if the object is nil but are calling a method which nil itself responds to.
Finally, my preferred method for the simple case is to use the andand gem which provides Object#andand. This greatly shortens the safe case above and saves calling of latest_event multiple times:
contact.latest_event.andand.date_sent
date_sent, nil and You.
For your example usage of calling to_s(:long), you could either use && or andand:
contact.latest_event.andand.date_sent.andand.to_s(:long)
or
contact.latest_event && contact.latest_event.date_sent.to_s(:long)
The first is safer if date_sent itself may be nil. Without using andand this could be written as:
contact.latest_event &&
contact.latest_event.date_sent &&
contact.latest_event.date_sent.to_s(:long)
which is rather complex and unwieldily in my opinion. I would recommend looking into andand
For question 1:
Just do
#last_event = [#last_letter, #last_email, #last_call].sort_by{|m| m.date_sent}.first
For question 2:
Well this is more interesting. This kind of depends on how exactly do your models look, but you might want to consider Single Table Inheritance for this type of scenario.

How to apply named_scopes incrementally in Rails

named_scope :with_country, lambad { |country_id| ...}
named_scope :with_language, lambad { |language_id| ...}
named_scope :with_gender, lambad { |gender_id| ...}
if params[:country_id]
Event.with_country(params[:country_id])
elsif params[:langauge_id]
Event.with_state(params[:language_id])
else
......
#so many combinations
end
If I get both country and language then I need to apply both of them. In my real application I have 8 different named_scopes that could be applied depending on the case. How to apply named_scopes incrementally or hold on to named_scopes somewhere and then later apply in one shot.
I tried holding on to values like this
tmp = Event.with_country(1)
but that fires the sql instantly.
I guess I can write something like
if !params[:country_id].blank? && !params[:language_id].blank? && !params[:gender_id].blank?
Event.with_country(params[:country_id]).with_language(..).with_gender
elsif country && language
elsif country && gender
elsif country && gender
.. you see the problem
Actually, the SQL does not fire instantly. Though I haven't bothered to look up how Rails pulls off this magic (though now I'm curious), the query isn't fired until you actually inspect the result set's contents.
So if you run the following in the console:
wc = Event.with_country(Country.first.id);nil # line returns nil, so wc remains uninspected
wc.with_state(State.first.id)
you'll note that no Event query is fired for the first line, whereas one large Event query is fired for the second. As such, you can safely store Event.with_country(params[:country_id]) as a variable and add more scopes to it later, since the query will only be fired at the end.
To confirm that this is true, try the approach I'm describing, and check your server logs to confirm that only one query is being fired on the page itself for events.
Check Anonymous Scopes.
I had to do something similar, having many filters applied in a view. What I did was create named_scopes with conditions:
named_scope :with_filter, lambda{|filter| { :conditions => {:field => filter}} unless filter.blank?}
In the same class there is a method which receives the params from the action and returns the filtered records:
def self.filter(params)
ClassObject
.with_filter(params[:filter1])
.with_filter2(params[:filter2])
end
Like that you can add all the filters using named_scopes and they are used depending on the params that are sent.
I took the idea from here: http://www.idolhands.com/ruby-on-rails/guides-tips-and-tutorials/add-filters-to-views-using-named-scopes-in-rails
Event.with_country(params[:country_id]).with_state(params[:language_id])
will work and won't fire the SQL until the end (if you try it in the console, it'll happen right away because the console will call to_s on the results. IRL the SQL won't fire until the end).
I suspect you also need to be sure each named_scope tests the existence of what is passed in:
named_scope :with_country, lambda { |country_id| country_id.nil? ? {} : {:conditions=>...} }
This will be easy with Rails 3:
products = Product.where("price = 100").limit(5) # No query executed yet
products = products.order("created_at DESC") # Adding to the query, still no execution
products.each { |product| puts product.price } # That's when the SQL query is actually fired
class Product < ActiveRecord::Base
named_scope :pricey, where("price > 100")
named_scope :latest, order("created_at DESC").limit(10)
end
The short answer is to simply shift the scope as required, narrowing it down depending on what parameters are present:
scope = Example
# Only apply to parameters that are present and not empty
if (!params[:foo].blank?)
scope = scope.with_foo(params[:foo])
end
if (!params[:bar].blank?)
scope = scope.with_bar(params[:bar])
end
results = scope.all
A better approach would be to use something like Searchlogic (http://github.com/binarylogic/searchlogic) which encapsulates all of this for you.

Resources