Overriding ActiveRecord collection (association) methods - ruby-on-rails

I'm new to Ruby and I'm trying to create a model where collections are assembled if they do not exist.
I already overloaded individual attributes like so:
class My_class < ActiveRecord::Base
def an_attribute
tmp = super
if tmp
tmp
else
#calculate this and some similar, associated attributes, for example:
self.an_attribute = "default" #{This does work as it is calling an_attribute=() }
self.an_attribute
end
end
end
test = My_class.new
p test.an_attribute # => "default"
This works great and basically gives me ||= functionality.
So, without thinking, I went and wrote myself a pretty big function to do the same with a collection (i.e. if there are no objects in the collection, then go and work out what they should be)
class My_class < ActiveRecord::Base
def things
thngs = super
if thngs.empty? #we've got to assemble the this collection!
self.other_things.each do |other_thing| #loop through the other things
#analysis of the other_things and creation and addition of things
self.things << a_thing_i_want
end
else #just return them
thngs
end
end
end
test = My_class.new
test.other_things << other_thing1
test.other_things << other_thing2
p test.things # => METHOD ERROR ON THE 'super' ARGH FAIL :(
This fails unfortunately. Any ideas for solutions? I don't think extensions are the right solution here
Attempted so far:
super --> 'method_missing': super: no superclass method
self[:things] --> test.things.each generates a nilClass error
Potential solution:
Would be rename the table column and has_many association from things to priv_things. This would allow me to simply create the things method and use self.priv_things instead of super

Related

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

Ruby/Rails: Is it possible to execute a default method when calling an instance (#instance == #instance.all IF "all" is the default method)?

I understand my question is a bit vague but I don't know how else to describe it. I've asked in numerous places and no one seems to understand why I want to do this. But please bear with me, and I'll explain why I want something like this.
I'm using Liquid Templates to allow users to make some dynamic pages on my site. And for those that don't know, Liquid uses a class of theirs called LiquidDrop to expose certain items to the user. Any method in the drop can be called by the Liquid template.
class PageDrop < Liquid::Drop
def initialize(page)
#page = page
end
def name
#page.name
end
def children
PagesDrop.new(#page.children)
end
end
class PagesDrop < Liquid::Drop
def initialize(pages)
#pages = pages
end
def group_by
GroupByDrop.new(#pages)
end
def all
#pages.all
end
def size
#pages.size
end
end
For example, I want to be able to do this:
#page_drop = PageDrop.new(#page)
#page_drop.children # to get an array of children
instead of
#page_drop.children.all
Why do I have a pages drop?
Because I want to be able to cleanly split up the methods I can do to an array of pages, and methods I can do to a single page. This allows me to group pages like so:
#page_drop.children.group_by.some_method_here_that_the_group_drop_contains
To make it simpler for my users, I don't want them to have to think about adding "all" or not to a drop instance to get the "default" object/s that it contains. To reiterate:
#pages_drop = PagesDrop.new(Page.all)
#pages_drop == #pages_drop.pages #I want this to be true, as well as
#pages_drop == #pages_drop.all
Where did I get this idea?
In Rails, a scope (association object) (#person.friends) seems to return the array when you do certain things to it: #person.friends.each, for person in #person.friends
This isn't really possible. When you write #instance you aren't really calling an instance as you describe, you're getting a reference to the object that #instance refers to.
The reason it seems to work with the collections for Rails' associations is that the the association objects are instances of Array that have had some of their methods overridden.
I would consider removing PagesDrop and using the group_by(&:method) syntax if you want a concise way to express groupings. If you do want to keep it then you can get some way towards what you want by implementing each and [] on PagesDrop and having them delegate to #pages. That will let you use #page_drop.children in for loops, for instance.
It looks like you want to implement has_many outside of rails. Will the following work?
class PageDrop < Liquid::Drop
attr_accessor :children
def initialize(page)
#page = page
#children = []
end
def name
#page.name
end
end
This allows you to do the following:
#page_drop = PageDrop.new(#page)
#page_drop.children.size # => 0
#page_drop.children # => []
This also gives you all the standard array functions (group_by, size, each, etc). If you want to add your own methods, create a class that inherits from Array and add your methods there.
class PageArray < Array
def my_method
self.each{|a| puts a}
end
end
class PageDrop < Liquid::Drop
attr_accessor :children
def initialize(page)
#page = page
#children = PageArray.new
end
[...]
end
#page_drop = PageDrop.new(#page)
#page_drop.children.size # => 0
#page_drop.children # => []
#page_drop.children.my_method # Prints all the children
Then any functions you don't define in PageArray fall through to the Ruby Array methods.

Executing Rails virtual attribute setters in order

I have an ActiveRecord model with several virtual attribute setters. I want to build an object but not save it to the database. One setter must execute before the others. How to do?
As a workaround, I build the object in two steps
#other_model = #some_model.build_other_model
#other_model.setup(params[:other_model)
Where setup is:
class OtherModel < ActiveRecord::Base
def setup(other_params)
# execute the important_attribute= setter first
important_attribute = other_params.delete(:important_attribute)
# set the other attributes in whatever order they occur in the params hash
other_params.each { |k,v| self.send("#{k}=",v) }
end
end
This seems to work, but looks kludgy. Is there a better way?
EDIT
per neutrino's suggestion, I added a method to SomeModel:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = OtherModel.new(:some_model=>self)
other_model.setup(other_params)
other_model
end
end
It's a good thing that you have this manipulations done in an OtherModel's method, because you can just call this method and not worry about the order of assignments. So I would leave this part but just call it from a SomeModel's method:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = build_other_model
other_model.setup(other_params)
other_model
end
end
So then you would have
#other_model = #some_model.build_other_model(params[:other_model])
I took your idea of deleting the important attribute first in your setup method, but used alias_chain_method instead to make it more of a transparent process:
def attributes_with_set_important_attribute_first=(attributes = {})
# Make sure not to accidentally blank out the important_attribute when none is passed in
if attributes.symbolize_keys!.include?(:important_attribute)
self.important_attribute = attributes.delete(:important_attribute)
end
self.attributes_without_set_important_attribute_first = attributes
end
alias_method_chain :attributes=, :set_important_attribute_first
This way none of your code should change from the normal Rails style
#other_model = #some_model.other_models.build(params[:other_model])

Is there a way to get a collection of all the Models in your Rails app?

Is there a way that you can get a collection of all of the Models in your Rails app?
Basically, can I do the likes of: -
Models.each do |model|
puts model.class.name
end
The whole answer for Rails 3, 4 and 5 is:
If cache_classes is off (by default it's off in development, but on in production):
Rails.application.eager_load!
Then:
ActiveRecord::Base.descendants
This makes sure all models in your application, regardless of where they are, are loaded, and any gems you are using which provide models are also loaded.
This should also work on classes that inherit from ActiveRecord::Base, like ApplicationRecord in Rails 5, and return only that subtree of descendants:
ApplicationRecord.descendants
If you'd like to know more about how this is done, check out ActiveSupport::DescendantsTracker.
Just in case anyone stumbles on this one, I've got another solution, not relying on dir reading or extending the Class class...
ActiveRecord::Base.send :subclasses
This will return an array of classes. So you can then do
ActiveRecord::Base.send(:subclasses).map(&:name)
EDIT: Look at the comments and other answers. There are smarter answers than this one! Or try to improve this one as community wiki.
Models do not register themselves to a master object, so no, Rails does not have the list of models.
But you could still look in the content of the models directory of your application...
Dir.foreach("#{RAILS_ROOT}/app/models") do |model_path|
# ...
end
EDIT: Another (wild) idea would be to use Ruby reflection to search for every classes that extends ActiveRecord::Base. Don't know how you can list all the classes though...
EDIT: Just for fun, I found a way to list all classes
Module.constants.select { |c| (eval c).is_a? Class }
EDIT: Finally succeeded in listing all models without looking at directories
Module.constants.select do |constant_name|
constant = eval constant_name
if not constant.nil? and constant.is_a? Class and constant.superclass == ActiveRecord::Base
constant
end
end
If you want to handle derived class too, then you will need to test the whole superclass chain. I did it by adding a method to the Class class:
class Class
def extend?(klass)
not superclass.nil? and ( superclass == klass or superclass.extend? klass )
end
end
def models
Module.constants.select do |constant_name|
constant = eval constant_name
if not constant.nil? and constant.is_a? Class and constant.extend? ActiveRecord::Base
constant
end
end
end
ActiveRecord::Base.connection.tables.map do |model|
model.capitalize.singularize.camelize
end
will return
["Article", "MenuItem", "Post", "ZebraStripePerson"]
Additional information If you want to call a method on the object name without model:string unknown method or variable errors use this
model.classify.constantize.attribute_names
For Rails5 models are now subclasses of ApplicationRecord so to get list of all models in your app you do:
ApplicationRecord.descendants.collect { |type| type.name }
Or shorter:
ApplicationRecord.descendants.collect(&:name)
If you are in dev mode, you will need to eager load models before:
Rails.application.eager_load!
I looked for ways to do this and ended up choosing this way:
in the controller:
#data_tables = ActiveRecord::Base.connection.tables
in the view:
<% #data_tables.each do |dt| %>
<br>
<%= dt %>
<% end %>
<br>
source: http://portfo.li/rails/348561-how-can-one-list-all-database-tables-from-one-project
I think #hnovick's solution is a cool one if you dont have table-less models. This solution would work in development mode as well
My approach is subtly different though -
ActiveRecord::Base.connection.tables.map{|x|x.classify.safe_constantize}.compact
classify is well supposed to give you the name of the class from a string properly. safe_constantize ensures that you can turn it into a class safely without throwing an exception. This is needed in case you have database tables which are not models. compact so that any nils in the enumeration are removed.
If you want just the Class names:
ActiveRecord::Base.descendants.map {|f| puts f}
Just run it in Rails console, nothing more. Good luck!
EDIT: #sj26 is right, you need to run this first before you can call descendants:
Rails.application.eager_load!
With Rails 6, Zetiwerk became the default code loader.
For eager loading, try:
Zeitwerk::Loader.eager_load_all
Then
ApplicationRecord.descendants
This seems to work for me:
Dir.glob(RAILS_ROOT + '/app/models/*.rb').each { |file| require file }
#models = Object.subclasses_of(ActiveRecord::Base)
Rails only loads models when they are used, so the Dir.glob line "requires" all the files in the models directory.
Once you have the models in an array, you can do what you were thinking (e.g. in view code):
<% #models.each do |v| %>
<li><%= h v.to_s %></li>
<% end %>
On one line: Dir['app/models/\*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
ActiveRecord::Base.connection.tables
Yes there are many ways you can find all model names but what I did in my gem model_info is , it will give you all the models even included in the gems.
array=[], #model_array=[]
Rails.application.eager_load!
array=ActiveRecord::Base.descendants.collect{|x| x.to_s if x.table_exists?}.compact
array.each do |x|
if x.split('::').last.split('_').first != "HABTM"
#model_array.push(x)
end
#model_array.delete('ActiveRecord::SchemaMigration')
end
then simply print this
#model_array
In just one line:
ActiveRecord::Base.subclasses.map(&:name)
I can't comment yet, but I think sj26 answer should be the top answer. Just a hint:
Rails.application.eager_load! unless Rails.configuration.cache_classes
ActiveRecord::Base.descendants
To avoid pre-load all Rails, you can do this:
Dir.glob("#{Rails.root}/app/models/**/*.rb").each {|f| require_dependency(f) }
require_dependency(f) is the same that Rails.application.eager_load! uses. This should avoid already required file errors.
Then you can use all kind of solutions to list AR models, like ActiveRecord::Base.descendants
This works for Rails 3.2.18
Rails.application.eager_load!
def all_models
models = Dir["#{Rails.root}/app/models/**/*.rb"].map do |m|
m.chomp('.rb').camelize.split("::").last
end
end
Dir.foreach("#{Rails.root.to_s}/app/models") do |model_path|
next unless model_path.match(/.rb$/)
model_class = model_path.gsub(/.rb$/, '').classify.constantize
puts model_class
end
This will give to you all the model classes you have on your project.
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }
Here's a solution that has been vetted with a complex Rails app (the one powering Square)
def all_models
# must eager load all the classes...
Dir.glob("#{RAILS_ROOT}/app/models/**/*.rb") do |model_path|
begin
require model_path
rescue
# ignore
end
end
# simply return them
ActiveRecord::Base.send(:subclasses)
end
It takes the best parts of the answers in this thread and combines them in the simplest and most thorough solution. This handle cases where your models are in subdirectories, use set_table_name etc.
Just came across this one, as I need to print all models with their attributes(built on #Aditya Sanghi's comment):
ActiveRecord::Base.connection.tables.map{|x|x.classify.safe_constantize}.compact.each{ |model| print "\n\n"+model.name; model.new.attributes.each{|a,b| print "\n#{a}"}}
This worked for me. Special thanks to all the posts above. This should return a collection of all your models.
models = []
Dir.glob("#{Rails.root}/app/models/**/*.rb") do |model_path|
temp = model_path.split(/\/models\//)
models.push temp.last.gsub(/\.rb$/, '').camelize.constantize rescue nil
end
I've tried so many of these answers unsuccessfully in Rails 4 (wow they changed a thing or two for god sakes) I decided to add my own. The ones that called ActiveRecord::Base.connection and pulled the table names worked but didn't get the result I wanted because I've hidden some models (in a folder inside of app/models/) that I didn't want to delete:
def list_models
Dir.glob("#{Rails.root}/app/models/*.rb").map{|x| x.split("/").last.split(".").first.camelize}
end
I put that in an initializer and can call it from anywhere. Prevents unnecessary mouse-usage.
The Rails implements the method descendants, but models not necessarily ever inherits from ActiveRecord::Base, for example, the class that includes the module ActiveModel::Model will have the same behavior as a model, just doesn't will be linked to a table.
So complementing what says the colleagues above, the slightest effort would do this:
Monkey Patch of class Class of the Ruby:
class Class
def extends? constant
ancestors.include?(constant) if constant != self
end
end
and the method models, including ancestors, as this:
The method Module.constants returns (superficially) a collection of symbols, instead of constants, so, the method Array#select can be substituted like this monkey patch of the Module:
class Module
def demodulize
splitted_trail = self.to_s.split("::")
constant = splitted_trail.last
const_get(constant) if defines?(constant)
end
private :demodulize
def defines? constant, verbose=false
splitted_trail = constant.split("::")
trail_name = splitted_trail.first
begin
trail = const_get(trail_name) if Object.send(:const_defined?, trail_name)
splitted_trail.slice(1, splitted_trail.length - 1).each do |constant_name|
trail = trail.send(:const_defined?, constant_name) ? trail.const_get(constant_name) : nil
end
true if trail
rescue Exception => e
$stderr.puts "Exception recovered when trying to check if the constant \"#{constant}\" is defined: #{e}" if verbose
end unless constant.empty?
end
def has_constants?
true if constants.any?
end
def nestings counted=[], &block
trail = self.to_s
collected = []
recursivityQueue = []
constants.each do |const_name|
const_name = const_name.to_s
const_for_try = "#{trail}::#{const_name}"
constant = const_for_try.constantize
begin
constant_sym = constant.to_s.to_sym
if constant && !counted.include?(constant_sym)
counted << constant_sym
if (constant.is_a?(Module) || constant.is_a?(Class))
value = block_given? ? block.call(constant) : constant
collected << value if value
recursivityQueue.push({
constant: constant,
counted: counted,
block: block
}) if constant.has_constants?
end
end
rescue Exception
end
end
recursivityQueue.each do |data|
collected.concat data[:constant].nestings(data[:counted], &data[:block])
end
collected
end
end
Monkey patch of String.
class String
def constantize
if Module.defines?(self)
Module.const_get self
else
demodulized = self.split("::").last
Module.const_get(demodulized) if Module.defines?(demodulized)
end
end
end
And, finally, the models method
def models
# preload only models
application.config.eager_load_paths = model_eager_load_paths
application.eager_load!
models = Module.nestings do |const|
const if const.is_a?(Class) && const != ActiveRecord::SchemaMigration && (const.extends?(ActiveRecord::Base) || const.include?(ActiveModel::Model))
end
end
private
def application
::Rails.application
end
def model_eager_load_paths
eager_load_paths = application.config.eager_load_paths.collect do |eager_load_path|
model_paths = application.config.paths["app/models"].collect do |model_path|
eager_load_path if Regexp.new("(#{model_path})$").match(eager_load_path)
end
end.flatten.compact
end
Make sure to eager load your app before calling descendants so that all the classes are loaded:
Rails.application.eager_load! unless Rails.application.config.eager_load
ApplicationRecord.descendants.each do |clazz|
# do something with clazz, e.g. User, Event, Attendance, etc.
end
def load_models_in_development
if Rails.env == "development"
load_models_for(Rails.root)
Rails.application.railties.engines.each do |r|
load_models_for(r.root)
end
end
end
def load_models_for(root)
Dir.glob("#{root}/app/models/**/*.rb") do |model_path|
begin
require model_path
rescue
# ignore
end
end
end
can check this
#models = ActiveRecord::Base.connection.tables.collect{|t| t.underscore.singularize.camelize}
Assuming all models are in app/models and you have grep & awk on your server (majority of the cases),
# extract lines that match specific string, and print 2nd word of each line
results = `grep -r "< ActiveRecord::Base" app/models/ | awk '{print $2}'`
model_names = results.split("\n")
It it faster than Rails.application.eager_load! or looping through each file with Dir.
EDIT:
The disadvantage of this method is that it misses models that indirectly inherit from ActiveRecord (e.g. FictionalBook < Book). The surest way is Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name), even though it's kinda slow.
I'm just throwing this example here if anyone finds it useful. Solution is based on this answer https://stackoverflow.com/a/10712838/473040.
Let say you have a column public_uid that is used as a primary ID to outside world (you can findjreasons why you would want to do that here)
Now let say you've introduced this field on bunch of existing Models and now you want to regenerate all the records that are not yet set. You can do that like this
# lib/tasks/data_integirity.rake
namespace :di do
namespace :public_uids do
desc "Data Integrity: genereate public_uid for any model record that doesn't have value of public_uid"
task generate: :environment do
Rails.application.eager_load!
ActiveRecord::Base
.descendants
.select {|f| f.attribute_names.include?("public_uid") }
.each do |m|
m.where(public_uid: nil).each { |mi| puts "Generating public_uid for #{m}#id #{mi.id}"; mi.generate_public_uid; mi.save }
end
end
end
end
you can now run rake di:public_uids:generate

Resources