Implementing simpler find_by_[attribute] code in Rails - ruby-on-rails

I have a table of statuses, each of which have a name attribute. Currently I can do:
FooStatus.find_by_name("bar")
And that's fine. But I'm wondering if I could do:
FooStatus.bar
So I have this approach:
class FooStatus < ActiveRecord::Base
def self.method_missing(meth, *args, &block)
if self.allowed_statuses.include?(meth.to_s.titleize)
self.where("name = ?", meth.to_s.titleize).first
else
super(meth, *args, &block)
end
end
def self.allowed_statuses
self.pluck(:name)
end
end
The above code works, but it leads to the following weird behavior:
FooStatus.respond_to?(:bar) => false
FooStatus.bar => #<FooStatus name: 'bar'>
That's not great, but if I try to implement respond_to?, I get a recursion problem
class FooStatus < ActiveRecord::Base
def self.method_missing(meth, *args, &block)
if self.allowed_statuses.include?(meth.to_s.titleize)
self.where("name = ?", meth.to_s.titleize).first
else
super(meth, *args, &block)
end
end
def self.allowed_statuses
self.pluck(:name)
end
def self.respond_to?(meth, include_private = false)
if self.allowed_statuses.include?(meth.to_s.titleize)
true
else
super(meth)
end
end
end
And that gets me:
FooStatus.bar => ThreadError: deadlock; recursive locking
Any ideas on getting method_missing and respond_to to work together?

I agree with Philip Hallstrom's suggestion. If you know allowed_statuses when the class is built, then just loop through the list and define the methods explicitly:
%w(foo bar baz).each do |status|
define_singleton_method(status) do
where("name = ?", status.titleize).first
end
end
…or if you need that list of statuses elsewhere in the code:
ALLOWED_STATUSES = %w(foo bar baz).freeze
ALLOWED_STATUSES.each do |status|
define_singleton_method(status) do
where("name = ?", status.titleize).first
end
end
Clearer, shorter, and far less prone to future breakage and weird rabbit hole conflicts with ActiveRecord like the one you're in.
You can do really cool things with method_missing and friends, but it's not the first approach to go to when doing metaprogramming. Explicit is usually better when possible.
I also agree with Philip's concernt about creating conflicts with built-in methods. Having a hard-coded list of statuses prevents that from going too far, but you might consider a convention like FooStatus.named_bar instead of FooStatus.bar if that list is likely to grow or change.

I don't know if I'd recommend your approach... seems too magical to me and I worry about what happens when you have a status with a name of 'destroy' or some other method you might legitimately want to call (or that Rails' calls internally that you aren't aware of).
But... instead of mucking with method missing, I think you'd be better off extending the class and automatically defining the methods by looping through allowed_statuses and creating them. This will make respond_to? work. And you could also check to make sure it's not already defined somewhere else...

Use a scope.
class FooStatus < ActiveRecord::Base
scope :bar, where(:name => "bar")
# etc
end
Now, you can do FooStatus.bar which will return an ActiveRelation object. If you expect this to return a single instance, you could do FooStatus.bar.first or if many FooStatus.bar.all, or you could put the .first or .all on the end of the scope in which case it'll return the same thing as the finder.
You can also define a scope with a lambda if the input isn't constant (not always "bar"). Section 13.1 of this guide has an example

Related

What's a really good pattern to extract logic from models and have the results persisted to the db?

I've been experimenting with different ways of structuring my app, and particularly with ActiveRecord-based models, looking into the notion of having a separate class whose results are saved as a single field. Now composition might have worked, but there's been a lot of discussion on whether its staying or not, so I was wondering what alternatives you might have.
Example:
class Gadget < ActiveRecord::Base
attr_accessor: lights
end
Now, I would like the lights property to be 'managed' by a completely separate class:
class Lights
def doSomething
end
def doSomethingElse
end
end
What's a good way of, say, proxying or delegating this logic out of the ActiveRecord model and into the Lights class in order to transparently store the results?
e.g. using an instance of the Lights class as the lights property - but that won't work, since there's no association, right?
e.g. use method_missing to push all requests out to the lights instance - but that won't work either, because it won't be persisted to the database.
Maybe it's not possible, but I'm interested in ways of persisting the results of logical operations. All suggestions welcome.
What about something like:
class Gadget < ActiveRecord::Base
def lights
#lights ||= Lights.new(self)
end
end
class Lights
attr_reader :root
delegate :save, :save!, to: :root
def initialize(root)
#root = root
end
def doSomething
end
def doSomethingElse
end
def method_missing(method_name, *args, &block)
#delegate all setters to root
if method_name =~ /.*=$/
root.send(method_name, *args, &block)
else
super
end
end
end
Thus you can do:
gadget = Gadget.new
#say gagdet has a name column
gadget.lights.name = 'foo'
gadget.lights.save
gadget.name #=> 'foo'
Still unsure why you need it but it should work

Object does not get loaded

This is the weirdest thing ever happened to me with ruby/rails.
I have a model, Store, which has_many Balances. And I have a method that gives me the default balance based on the store's currency.
Store model.
class Store < ActiveRecord::Base
has_many :balances, as: :balanceable, dependent: :destroy
def default_balance
#puts self.inspect <- weird part.
balances.where(currency: self.currency)[0]
end
...
end
Balance model.
class Balance < ActiveRecord::Base
belongs_to :balanceable, :polymorphic => true
...
end
Ok, so then in the Balance controller I have the show action, that will give me a specific balance or the default one.
Balance controller.
class Api::Stores::BalancesController < Api::Stores::BaseController
before_filter :load_store
# Returns a specific alert
# +URL+:: GET /api/stores/:store_id/balances/:id
def show
#puts #store.inspect <- weird part.
#balance = (params[:id] == "default") ? #store.default_balance : Balance.find(params[:id])
respond_with #balance, :api_template => :default
end
...
private
# Provides a shortcut to access the current store
def load_store
#store = Store.find(params[:store_id])
authorize! :manage, #store
end
end
Now here is where the weird part comes...
If I make a call to the show action; for example:
GET /api/stores/148/balances/default
It returns null (because the currency was set as null, and there is no Balance with null currency), and the SQL query generated is:
SELECT `balances`.* FROM `balances` WHERE `balances`.`balanceable_id` = 148 AND `balances`.`balanceable_type` = 'Store' AND `balances`.`currency` IS NULL
So I DON'T know why... it is setting the currency as NULL. BUT if in any where in that process I put
puts #store.inspect
or inside the default_balance method:
puts self.inspect
it magically works!!!.
So I don't know why is that happening?... It seems like the store object is not getting loaded until I "inspect" it or something like that.
Thanks
Sam and Adrien are on the right path.
ActiveRecord overrides method_missing to add a whole bunch of dynamic methods including the accessors for the column-backed attributes like Store#currency. While I'm glossing over a lot, suffice it to say that when the logic is invoked then the dynamic class/instance methods are added to the Store class/instances so that subsequent calls no longer require the method_missing hook.
When YOU overrode method_missing without calling super, you effectively disabled this functionality. Fortunately, this functionality can be invoked by other means, one of which you tripped upon when you called store#inspect.
By adding the call to super, you simply assured that ActiveRecord's dynamic methods are always added to the class when they're needed.
OK finally after a lot of debugging, I found the reason...
In the Store model I have a method_missing method and I had it like this:
def method_missing method_name, *args
if method_name =~ /^(\w+)_togo$/
send($1, *args).where(togo: true)
elsif method_name =~ /^(\w+)_tostay$/
send($1, *args).where(tostay: true)
end
end
So when I was calling self.currency it went first to the method_missing and then returned null. What I was missing here was the super call.
def method_missing method_name, *args
if method_name =~ /^(\w+)_togo$/
send($1, *args).where(togo: true)
elsif method_name =~ /^(\w+)_tostay$/
send($1, *args).where(tostay: true)
else
super
end
end
But I continue wondering why after I had called puts #store.inspect or puts self.inspect it worked well?. I mean, why in that case that super call wasn't needed?

How do I modify Rails ActiveRecord query results before returning?

I have a table with data that needs to be updated at run-time by additional data from an external service. What I'd like to do is something like this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data.each do |object|
puts object.some_attribute_from_external_data_source
end
Even if I can't use this exact syntax, I would like the end result to respect any scopes I may use. I've tried this:
def self.enhance_with_external_data
external_data = get_external_data
Enumerator.new do |yielder|
# mimick some stuff I saw in ActiveRecord and don't quite understand:
relation.to_a.each do |obj|
update_obj_with_external_data(obj)
yielder.yield(obj)
end
end
end
This mostly works, except it doesn't respect any previous scopes that were applied, so if I do this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data
It gives back ALL MyModels, not just the ones scoped by some_custom_scope and some_other_scope.
Hopefully what I'm trying to do makes sense. Anyone know how to do it, or am I trying to put a square peg in a round hole?
I figured out a way to do this. Kind of ugly, but seems to work:
def self.merge_with_extra_info
the_scope = scoped
class << the_scope
alias :base_to_a :to_a
def to_a
MyModel.enhance(base_to_a)
end
end
the_scope
end
def self.enhance(items)
items.each do |item|
item = add_extra_item_info(item)
end
items
end
What this does is add a class method to my model - which for reasons unknown to me seems to also make it available to ActiveRecord::Relation instances. It overrides, just for the current scope object, the to_a method that gets called to get the records. That lets me add extra info to each record before returning. So now I get all the chainability and everything like:
MyModel.where(:some_attribute => some_value).merge_with_extra_info.limit(10).all
I'd have liked to be able to get at it as it enumerates versus after it's put into an array like here, but couldn't figure out how to get that deep into AR/Arel.
I achieved something similar to this by extending the relation:
class EnhancedModel < DelegateClass(Model)
def initialize(model, extra_data)
super(model)
#extra_data = extra_data
end
def use_extra_data
#extra_data.whatever
end
end
module EnhanceResults
def to_a
extra_data = get_data_from_external_source(...)
super.to_a.map do |model_obj|
EnhancedModel.new(model_obj, extra_data)
end
end
end
models = Model.where('condition')
models.extend(EnhanceResults)
models.each do |enhanced_model|
enhanced_model.use_extra_data
end

Is there a simpler way to find a record and return one of its attribute in Rails 3?

I found myself writing the following piece of code over and over again:
MyModel.find(my_id).my_field
I wonder if there is a simpler method to write this ?
If you are asking if there is more concise way of writing that.. not sure there is with the standard finders. What you have is pretty small. Just for fun I wrote this for you though :)
class ActiveRecord::Base
def self.method_missing_with_retrieve_just_a_field(method_called, *args, &block)
if(method_called.to_s=~/get_/)
self.find(args[0]).send(method_called.to_s.gsub("get_", ""))
else
method_missing_without_retrieve_just_a_field(method_called, *args, &block)
end
end
class << self
alias_method_chain :method_missing, :retrieve_just_a_field
end
end
If you put this in your config/initializers as some file like crazy_finder.rb you can then just say:
MyModel.get_my_field(my_id)
It doesnt save you much, but just thought it would be fun to write.
In addition to Jake's global solution for every model and every attribute, you can easily define explicit individual accessors:
class MyModel
def self.my_field(id)
find(id).my_field
end
end
Or an array of fields:
class MyModel
class << self
[:my_field, :other_field].each do |field|
define_method field do |id|
find(id).send(field)
end
end
end
end
Are you doing this for same resource over and over again or to many different resources? It would really help if you'd give us better example of what you're trying to do, if you're doing that many times, it would help to give us example of what you're doing many times.
If you're doing this for one resource only to get different values:
If you're doing this in same action over and over again, you're doing it wrong. First store your result in a variable like this:
#mymodel = MyModel.find(id)
and then you can use
#mymodel.my_field
and then again (without the need to find it again)
#mymodel.different_field
or if you want to do this for a collection you can do:
#mymodels = MyModel.find([my_first_id, my_second_id, my_third_id])
etc.. better example from your side would help to help you!

searchlogic with globalize2?

Given there is a model:
class MenuItem < ActiveRecord::Base
translates :title
end
and searchlogic is plugged in, I'd expect the following to work:
>> MenuItem.search(:title_like => 'tea')
Sadly, it doesn't:
Searchlogic::Search::UnknownConditionError: The title_like is not a valid condition. You may only use conditions that map to a named scope
Is there a way to make work?
P.S.
The closest I managed to get workging, was:
>> MenuItem.search(:globalize_translations_title_like => 'tea')
Which doesn't look nice.
I developed searchlogic. By default, it leverages existing named scopes and the database columns. It can't really go beyond that because ultimately it has to create the resulting SQL using valid column names. That said, there really is no way for searchlogic to cleanly understand what your :title attribute means. Even if it did, it would be specific to the logic defined in your translation library. Which is a red flag that this shouldn't be in the library itself, but instead a plugin or code that gets initialized within your app.
Why not override the method_missing method and do the mapping yourself? Searchlogic provides and easy way to alias scoped by doing alias_scope:
alias_scope :title_like, lambda { |value| globalize_translations_title_like(value) }
Here's a quick stab (this is untested):
module TranslationsMapping
def self.included(klass)
klass.class_eval do
extend ClassMethods
end
end
module ClassMethods
protected
def method_missing(name, *args, &block)
translation_attributes = ["title"].join("|")
conditions = (Searchlogic::NamedScopes::Conditions::PRIMARY_CONDITIONS +
Searchlogic::NamedScopes::Conditions::ALIAS_CONDITIONS).join("|"))
if name.to_s =~ /^(#{translation_attributes})_(#{conditions})$/
attribute_name = $1
condition_name = $2
alias_scope "#{attribute_name}_#{condition_name}", lambda { |value| send("globalize_translations_#{attribute_name}_#{condition_name}", value) }
send(name, *args, &block)
else
super
end
end
end
end
ActiveRecord::Base.send(:include, TranslationsMapping)
Hope that helps. Again, I haven't tested the code, but you should get the general idea. But I agree, the implementation of the translations should be behind the scenes, you really should never be typing "globalize_translations" anywhere in your app, that should be take care of transparently on the model level.

Resources