Rails metaprogramming: singleton_class and associations - ruby

I'm trying to understand metaprogramming in rails, creating validations and associations dynamically on a class.
Let's say I have the following models:
class House < ActiveRecord::Base
belongs_to :owner
end
class Owner < ActiveRecord::Base
end
Now let's say my House model has a boolean attribute is_ownable, and I only want the house to have the owner association if is_ownable==true.
I thought this would work:
class House < ActiveRecord::Base
after_initialize :create_associations
after_find :create_associations
def create_associations
if self.is_ownable
self.singleton_class.belongs_to :owner
end
end
end
Now when I build or find a record of House, the create_associations function gets called with no errors, but then when I try to access the House.first.owner it throws ActiveRecord::AssociationNotFoundError.
Am I misunderstanding something about how AR associations work?

I hate to say it but this is probably a bad idea. Models should have consistent relationships even if they're not utilized on every model. This is not only against the spirit of ActiveRecord or Ruby, but object oriented programming in general. In most cases objects of a particular class are expected to have an identical interface for the sake of consistency and clarity. Adding methods to individual objects is permitted, but there should be exceptional circumstances to justify such a thing.
That's not to say you can't get the effect you want in a more idiomatic way:
class House < ActiveRecord::Base
belongs_to :owner
validates :validate_owner_assignment
protected
def validate_owner_assignment
if (self.ownable? and !self.owner)
self.errors.add(:owner, "is required if ownable")
elsif (!self.ownable? and self.owner)
self.errors.add(:owner, "cannot be assigned if not ownable")
end
end
end
Now assigning owner will trigger a save failure of type ActiveRecord::RecordInvalid if the expectations aren't met.
I'd advocate calling your booleans x and not is_x to reduce verbosity. The vast majority of the time the is_ part is redundant.

Related

return results from active record relation many_to_many assocation

I currently have a many_to_many association between templates and types.
I have an active record relation of templates.
I want to return all of the types that are linked to those templates.
For example, in an ideal world i would be able to do templates.types.
I have tried templates.joins(:types), however this returns templates rather then types.
So i'm not sure on another way to do this.
In pure ruby, without a DB you would want to flat_map
types = templates.flat_map do |template|
template.types
end
types.uniq # probably you only want unique types
This works but is not efficient as soon as you got many Templates/Types since it triggers more queries and loads more objects than needed.
When you have ActiveRecord you can add a scope or self method (either or, not borth as in my example) to Type
class Type < ApplicationRecord
has_many :template_types
has_many :templates, through: :template_types
scope :for_templates, -> (templates) { joins(:template_types).where(template_types: {template: templates}).distinct } # either this or the method below
def self.for_templates(templates)
Type.joins(:template_types).where(template_types: {template: templates}).distinct
end
end
(i assumed that the join model is TemplateType)
and then do
templates = Template.some_complicated_query
Type.for_templates(template)
I'd recommend you rename Type since type already has a special meaning with ActiveRecord (Single Table Inheritance).

Rails overriding active record setter in a relation

I want to override the << setter in my relation. For example, given:
class Library < ActiveRecord::Base
has_many :groups
def readers
groups.find_by(name: 'readers').users
end
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, through: :group_memberships
end
class GroupMembership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class User < ActiveRecord::Base
has_many :groups, through :group_membership
end
I want to do something like
someLibrary.readers << user1
and some additional things to happen after this.
The code should look something like:
def <<(objects)
super objects
#do other things here
end
Where should it be? I guess in Group, like:
class Group
...
def users<<(objects)
super objects
#do stuff
end
end
but I only want to do it when I'm invoking << on readers.
I want to know if there is a way to know whether I'm invoking << on a group users relationship, or whether I have access to group object when I'm invoking << method on group users through the relationship.
I want to do it because it looks nice. The easiest way would be to define separate method to set readers (and be more explicit), but I want to know if it is possible in activerecord or in ruby.
edit:
Yeah I know that overriding core methods is bad thing and people go to hell for that, yada yada yada.
I'm just curious how it's done. Like, for learning purposes.
Besides the aim is just to override the << method on that particular relation so probable there might be some justification why someone might want to do it.
Obligatory disclaimer:
I do not recommend that you do this, in 'important' code. Changing the behaviour of methods like this will confuse the hell out of other developers (as well as your future self), and lead to all sorts of unintended behavioural changes!
But assuming that this is 'just for fun'...
Based on the information above, someLibrary.readers returns a collection of User records. So all we need to do is add the desired behaviour to that class.
Normally you can do this by just defining a class method, in one of two ways:
class User
def self.foo
puts 'this works!'
end
class << self
def bar
puts 'this works too!'
end
end
end
With the above in place, you can call the methods like:
someLibrary.readers.foo
someLibrary.readers.bar
...However, there is some rails black magic going on under the hood here. someLibrary.readers is actually an instance of User::ActiveRecord_Associations_CollectionProxy, and the above methods are being picked up dynamically and appended to ActiveRecord::Associations::CollectionProxy.
Because of this dynamic method definition, it is not possible to override existing Rails methods (such as <<) in this manner. Instead, we'll need to monkey-patch the User::ActiveRecord_Associations_CollectionProxy class directly:
class User
class ActiveRecord_Associations_CollectionProxy
def <<(objects)
super(objects)
# do stuff
end
end
end
If you're looking for a better way of doing this however, I'd recommend using a service object design pattern. You can then encapsulate any more complex/custom logic relating to creating/updating/deleting users, libraries, etc. in a clean and isolated abstraction.
The more established way to do this...
class Library < ActiveRecord::Base
has_many :groups
has_one :reader_group -> {groups.find_by(name: 'readers')}
has_many :readers, through: :reader_group, class_name: 'User', foreign_key: 'user_id'
end
And that's it. You can now do
my_library.readers << another_user

ActiveRecord Scope: Strange behaviour with select on relation

I have two different models with a 1:N relation.
Let's name them 'myobject' and 'related'
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, includes(:related).select{ |o| o.related.size == 0 }
end
class Related < ActiveRecord::Base
end
The defined scope seems to work great as long as I don't create new assignments from Myobjects to Related:
Direct rails c command "Myobject.includes(:related).select ... (as defined in Scope) works as expected
Calls to scope "Myobject.without_related" still return objects that have been assigned in the meantime
It seems that this can be fixed by restarting the rails console or restarting Webrick.
But I can't always restart a webapplication only because a relation between objects has been changed ;)
Is there any way to fix this problem or to write the scope in a better way?
PS: I need this query as scope to pass its name as group_method to a grouped_select in the form of the Myobject model
Your problem is that in fact your scope is not scope :)
Scopes must return relations, but your scope returns array.
Though it can work as you expect, if you wrap it in lambda
scope :without_related, lambda{ includes(:related).select{ |o| o.related.size == 0 } }
But I recommend to rewrite this code as usual class method to not mislead those who'll work with this code in future
def self.without_related
includes(:related).select{ |o| o.related.size == 0 }
end
or use counter cache, as advised in other answer.
I would recommend you to use counter_cache for this, you need to add column *related_count* of type int to Myobject, make migration and then you will be able to do so:
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, where(related_count: 0)
end
class Related < ActiveRecord::Base
belongs_to :myobject, counter_cache: true
end
After that you will have super fast scope for getting all objects with no related records and an a count of that objects as well
Or if you know column name that should be present in related table, use this definition:
class Myobject < ActiveRecord::Base
has_many :related
scope :without_related, includes(:related).where('related.id', true)
end

Clean association definitions with Ruby Sequel

I am using Jeremy Evan's Sequel to populate an (SQLite) database with data I scrape from web pages.
The database involves a number of many_to_many relationships that I express with Associations.
The associations are created in class definitions, which are always evaluated when the script is run.
Importantly, the association class definitions need to have the necessary tables in place.
Thus the table creation methods should be in the top level with the association definitions.
Here is an example:
module Thing
db = Sequel.Sqlite('data.sqlite')
db.create_table(:clients)
String :client_id, :primary_key => true
String :client_data
end
db.create_table(:orders)
String :order_id, :primary_key => true
String :order_data
end
db.create_table(:orders_clients)
String :order_id
String :client_id
primary_key [:order_id,:client_id]
end
class Person < Sequel::Model
unrestrict_primary_key
many_to_many :orders
end
class Order < Sequel::Model
unrestrict_primary_key
many_to_many :orders
end
end
First of all, I think that this is a rather dirty solution, since my method calls and class definitions sit in the same namespace.
If I try to separate the class definitions, I get No database associated with Sequel::Model error (which makes sense, but I want to defer the evaluation of the association definitions, having those after the table calls, whenever they might happen).
I want to be able to create the tables and associations in a method call. Thus, I could for example pass the name of the new database file:
def create_tables_and_schema (database_name)
db = Sequel.Sqlite(database_name)
db.create_table... #three of those, as above
class Person < Sequel::Model
unrestrict_primary_key
many_to_many :orders
end
class Order < Sequel::Model
unrestrict_primary_key
many_to_many :orders
end
end
What I think is needed is a different way to express table relations.
Any suggestions on approach and style are appreciated. Please ask for clarifications if the explanation is confusing.
Your method calls and class definitions do not need to sit in the same namespace, it's just that the tables need to be created before the model classes. An easy way to separate them is to move the table creation to a separate file. Also, usually you assign the database object to a constant.
create_tables.rb:
DB.create_table(:clients)
String :client_id, :primary_key => true
String :client_data
end
DB.create_table(:orders)
String :order_id, :primary_key => true
String :order_data
end
DB.create_table(:orders_clients)
String :order_id
String :client_id
primary_key [:order_id,:client_id]
end
models.rb:
DB = Sequel.sqlite('data.sqlite')
require 'create_tables'
class Person < Sequel::Model
unrestrict_primary_key
many_to_many :orders
end
class Order < Sequel::Model
unrestrict_primary_key
many_to_many :orders
end
You mentioned that you want to create the tables and associations in a method call, but that doesn't make sense if you are creating classes with constants. The main reason to create them via a method call is to allow for multiple databases at runtime, but that wouldn't work with your model classes since they are defined with constant names.
If you don't need multiple databases at runtime, the example above should work if you just want to separate the table creation from the model creation.
If you do need multiple databases at runtime, then creating the tables and models via a method call makes sense, but you need to create anonymous model classes, as otherwise you will have problems.

Rails nested form on many-to-many: how to prevent duplicates?

I've setup a nested form in my rails 3.2.3 app, it's working fine, my models are:
class Recipe < ActiveRecord::Base
attr_accessible :title, :description, :excerpt, :date, :ingredient_lines_attributes
has_and_belongs_to_many :ingredient_lines
accepts_nested_attributes_for :ingredient_lines
end
and:
class IngredientLine < ActiveRecord::Base
attr_accessible :ingredient_id, :measurement_unit_id, :quantity
has_and_belongs_to_many :recipes
belongs_to :measurement_unit
belongs_to :ingredient
end
As above, a Recipe can have multiple IngredientLines and vice versa.
What I'm trying to avoid is record duplication on IngredienLine table.
For example imagine that for recipe_1 an IngredientLine with {"measurement_unit_id" => 1, "ingredient_id" => 1, "quantity" => 3.5} is associated, if for recipe_5 the IngredientLine child form is compiled by the user with the same values, I don't want a new record on IngredientLine table, but only a new association record in the join table ingredient_lines_recipes.
Note that currently I dont't have any IngredientLine controller as saving and updating IngredientLines is handled by nested form routines. Even my Recipe controller is plain and standard:
class RecipesController < ApplicationController
respond_to :html
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new(params[:recipe])
flash[:notice] = 'Recipe saved.' if #recipe.save
respond_with(#recipe)
end
def destroy
#recipe = Recipe.find(params[:id])
#recipe.destroy
respond_with(:recipes)
end
def edit
respond_with(#recipe = Recipe.find(params[:id]))
end
def update
#recipe = Recipe.find(params[:id])
flash[:notice] = 'Recipe updated.' if #recipe.update_attributes(params[:recipe])
respond_with(#recipe)
end
end
My guess is that should be enough to override the standard create behavior for IngredientLine with find_or_create, but I don't know how to achieve it.
But there's another important point to take care, imagine the edit of a child form where some IngredientLines are present, if I add another IngredientLine, which is already stored in IngredientLine table, rails of course should not write anything on IngredientLine table, but should also distinguish between child records already associated to the parent, and the new child record for which needs to create the relation, writing a new record on the join table.
Thanks!
in Recipe model redefine method
def ingredient_lines_attributes=(attributes)
self.ingredient_lines << IngredientLine.where(attributes).first_or_initialize
end
Old question but I had the same problem. Forgot to add :id to white list with rails 4 strong_parameters.
For example:
widgets_controller.rb
def widget_params
params.require(:widget).permit(:name, :foos_attributes => [:id, :name, :_destroy],)
end
widget.rb
class Widget < ActiveRecord::Base
has_many :foos, dependent: :destroy
accepts_nested_attributes_for :foos, allow_destroy: true
end
foo.rb
class Foo < ActiveRecord::Base
belongs_to :widget
end
I have run into a similar situation and found inspiration in this answer. In short, I don't worry about the duplication of nested models until save time.
Translated to your example, I added autosave_associated_records_for_ingredient_lines to Recipe. It iterates through ingredient_lines and performs a find_or_create as your intuition said. If ingredient_lines are complex, Yuri's first_or_initialize approach may be cleaner.
I believe this has the behavior you're looking for: nested models are never duplicated, but editing one causes a new record rather than updating a shared one. There is the strong possibility of orphaned ingredient_lines but if that's a serious concern you could choose to update if that model has only one recipe with an id that matches the current one.

Resources