STI has_many relationship in parent class inherited by sub classes - ruby-on-rails

say I have the following STI models
class Parent < ActiveRecord::Base
has_many :childs, foreign_key: 'parent_id' # forgive the english for sake of simplicity
#def childs
# Child.where(parent_id: id) # this works just fine BTW
#end
end
class BadParent < Parent
end
class GoodParent < Parent
end
and the following Child class
class Child
belongs_to :parent # parent_id lives on this
end
I dont care about setting the type on the Child so I don't care about creating a polymorphic association.
bad_parent = BadParent.create(name: 'Michael Jackson')
child = Child.create(name: 'Bobby', parent: bad_parent)
If I run
child.parent #=> <# BadParent > # AWESOME
bad_parent.childs #=> [] NO BUENO!!!
sql_statement = bad_parent.childs.to_sql #=> "SELECT `childs`.* FROM `childs` WHERE `childs`.`parent_id` = 1"
Child.find_by_sql(sql_statement) #=> [<# Child Object #>] BUENO!!!
Is there something I have to add to the association to make this work like find_by_sql?

As per other comments, you shouldn't have both a method and association named the same as it is very unclear what will get executed - I'll assume for here on you will get rid of def childs ... Aside from that I think your issue is to do with caching i.e. rails will only hit the DB if it knows something has changed. In your example bad_parent doesn't know that new children have been added. You could either reload like:
bad_parent.reload
bad_parent.childs #> should show child object now
or force a call to the DB like:
bad_parent.childs(true)
Check out 3.1 Controlling Caching section of the rials guides for more info: http://guides.rubyonrails.org/association_basics.html

Here's what I'd do...
#app/models/person.rb
class Person < ActiveRecord::Base
#columns id | type | parent_id | type_id | name | created_at | updated_at
end
#app/models/parent.rb
class Parent < Person
belongs_to :type
has_many :children, foreign_key: 'parent_id'
end
#app/models/child.rb
class Child < Parent
belongs_to :parent
end
#app/models/type.rb
class Type < ActiveRecord::Base
#columns id | type
#values good/bad (probably overkill to have DB for this)
has_many :parents
end
This should allow you to call the following:
#parent = Parent.find params[:id]
#parent.children #-> collection of Person model with parent_id attribute for parent
In regards your specific issue - about eager loading etc - I don't have masses of experience.
There are hierarchy gems which help this out.
We've used ClosureTree before - it creates a separate table which allows you to traverse hierarchies much easier. Another one is called ancestry which is pretty popular (easier to implement).
I'd recommend using the likes of ancestry in your "parent" model. I called the parent model Parent because I think it will give you much deeper scope to work with different types of data.
For example, your Child model is its own entirely, when it should be made up of the same data as the Parent.

Related

How to create an alias in a belongs_to declaration for a polymorphic relation in Ruby on Rails 5.2?

Several objects in my applicaton belong to a parent object with such relation:
belongs_to :parent, :class_name => "Playground", :foreign_key => "playground_id"
I use the :parent declaration because this allows me to create a helper for building the breadcrums:
def breadcrumbs(object)
way_back = [object]
while object.parent.class != Playground do # stop when comes to display the Playground
path = object.parent
way_back << path
object = path
end
puts way_back.reverse
way_back.reverse
end
It gets more complicated for the BusinessObject class which is polymorphic. A Business Object belongs either to a Business Process, or to a Business Area. The relation is then defined as follow:
# The parent relationship points to either Business Area or Business Process and relies on
# area_process_type :string
# area_process_id :integer
belongs_to :area_process, polymorphic: true
Thus, how can I define an alias to this belongs_to :area_process relation in order to use it as :parent elsewhere?
Is there a syntax to declare a aliased belongs_to clause, or shall I create a method called "parent" for the Business Objects in order to solve this?
Addendum - the parent method could be this one:
### define the parent of current business object
def parent
parent_class = self.area_process_type.constantize
parent_class.find(self.area_process_id)
end
Thanks a lot.
You could use alias_attribute by adding the following to your BusinessObject model:
alias_attribute :parent, :area_process

dependent create and access a has_many by type

I have some models that essentially look like:
class Parent < ApplicationRecord
has_many :children
end
class Child < ApplicationRecord
belongs_to :parent
belongs_to :child_type
end
class ChildType < ApplicationRecord
has_many :children
end
There are 3 entries of ChildType and when I create a new Parent object, I'd like to go ahead and create the dependent Child objects, one for each child type.
Is there a cleaner way of doing this rather than:
class Parent < ApplicationRecord
has_many :children
after_create :create_children
private
def create_children
ChildType.all.each do |ct|
children.create(child_type_id: ct.id)
end
end
end
This works but I'm wondering if rails has a cleaner way of doing this.
Secondly I'd also like shortcut methods on the parent so I can easily access the three children by type:
Parent.find(id).child_type_1 # returns children.where(child_type: { id: 1 }).first
I could create a scope on Child that takes an argument for type but if possible I'd rather have a method on the parent to load it, if there's a clean way of doing that. I can make 3 methods on the parent to find the appropriate child of each type but that's not super DRY...
you can use accepts_nested_attributes instead of callback (you should change after_create -> after_save because of deprecation for new rails ver)
If you want access children list base on child_type:
#child_type = ChildType.find(params[:child_tye_id])
#child_list = #child_type.children
It should use by association which you defined

How to structure these models associations?

I am basically trying to create an app which allows a user to track what they are working on. So each user will have many projects.
Now each project can have a certain number of types e.g. videos, websites, articles, books etc. with each of these types of projects having many objects (i.e. a videos project would list all the videos they were working on, books project may have a list of books they read).
Each of these objects would have different variables (books might have author and length and websites may have URL, rank etc.)
How would I set up these models in rails?
So basically would I separate my project model from my type model (so project has_one type and type belongs_to project) and then have 4 separate object models (i.e. objectsite) for each of the different types?
Or is there a better way to design this.
Modelling Many Rails Associations
I read this question and I think this may be similar to what I want to do but am not entirely sure as I don't full understand it.
UPDATE**
So far I have:
Projects
has_one type
Type
belongs_to projects
has_many objects
object
belongs_to type
But I am not to sure if this is the best way to go about it because like I said each object for each type will be different
From what I could bring myself to read, I'd recommend this:
Models
So each user will have many projects.
#app/models/project.rb
Class Project < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :projects
end
Vars
Each of these objects would have different variables (books might have
author and length and websites may have URL, rank etc.)
There are two ways to interpret this
The first is if you know what details your different objects will require, you could include them as attributes in their respective datatables. If you don't know, you'll have to use another table to populate them.
I'll detail both approaches for you:
--
Known Attributes
As Pavan mentioned in the comments, you'll likely benefit from an STI (Single Table Inheritance) for this:
#app/models/project.rb
Class Project < ActiveRecord::Base
belongs_to :user
has_many :books, class_name: "Project::Books"
has_many :folders, class_name: "Project::Folders"
end
#app/models/object.rb
Class Object < ActiveRecord::Base
#fields - id | type | project_id | field1 | field2 | field3 | field4 | created_at | updated_at
belongs_to :project
end
#app/models/project/book.rb
Class Book < Object
... stuff in here
end
This will allow you to call:
project = Project.find(params[:id])
project.books.each do |book|
book.field1
end
--
Unknown Attributes
#app/models/project.rb
Class Project < ActiveRecord::Base
has_many :objects
end
#app/models/object.rb
Class Object < ActiveRecord::Base
belongs_to :project
has_many :options
end
#app/models/option.rb
Class Option < ActiveRecord::Base
#fields - id | object_id | name | value | created_at | updated_at
belongs_to :object
end
This will allow you to call:
project = Project.find(params[:id])
project.objects.first.options.each do |option|
option.name #-> outputs "name" of attribute (EG "length")
option.value #-> outputs "value" of attribute (EG "144")
end
This means you can use option to populate the various attributes your objects may require.

rails - preference form

I have a parent model, a child model and a join model. How does the controller of the join model look like if I need to reference the parent_id and the child_id?
For example, I have:
def new
#join = JoinModel.new
new
def create
#join = JoinModel.new(params[:join_model])
#parent_id = current_user.id
end
My question is, how do I reference the child model object?
If I use #child = Child.find(params[:id]), it gives me the errorcan't find child without an ID`.
Help.
Thanks.
Two Tips:
1) try to use a Self-Referential model for this -- just one table instead of three tables - that's a more lean design.
2) use the "ancestry" Gem to model parents / children - it has very convenient methods to get to the parent, ancestors, or descendants.
See:
http://railscasts.com/episodes/262-trees-with-ancestry (hands-on RailsCast)
Self-referencing models in Rails 3
If you need those models to be separate, you can do something like this:
class User < ActiveRecord::Base
has_many :publications, :through => :ownership
end
class Publication < ActiveRecord::Base
has_many :users , :through => :ownership
end
class Ownership < ActiveRecord::Base # the join table
belongs_to :user
belongs_to :publication
end
and then create new publications like this:
u = User.find_by_name("John")
u.publications.new( attributes-for-new-publication )
u.save
This is not really a Parent/Child relationship, but rather "has_many through"

rails model templates (or instance inheritance) options?

I have a situation where I want to make 'parametric' models in rails; for example I'd like to define PrototypeRecipe, and then be able to make multiple DerivedRecipe's; maybe one derived recipe uses more sugar and another uses less eggs or something. The critical point is that I want all the 'derived' instances to inherit properties from a single shared PrototypeRecipe, but be able to make local modifications.
Ideally, I'd like to be able to define methods on the prototype (say, putting together a shopping list), and have these methods respond to local changes in derived instances (so if I specified 3 eggs instead of 2, i could call the prototype's make_shopping_list function and it would reflect that).
Is there an existing method for accomplishing something like this? Here's the best I can come up with so far:
class Ingredient << ActiveRecord::Base
belongs_to :recipe, :polymorphic => true
# uuid => UUID String (for grouping ingredients which change between prototype and derived instances)
end
class PrototypeRecipe << ActiveRecord::Base
has_many :ingredients
def make_ingredient_list(derived_recipe = nil)
self.ingredients.map {|i| derived_recipe.nil? ? i : derived_recipe.ingredients.where(:ingredient_uuid => i.uuid).first }
end
end
class DerivedRecipe << ActiveRecord::Base
belongs_to :prototype_recipe
has_many :ingredients
def method_missing(sym, *args)
self.prototype_recipe.send( sym, *args, self)
end
end
I know this code can be made a lot cleaner, I'm more wondering if the general approach can be improved on. The basic idea is that ingredients would each have a unique ID. To modify a prototype recipe, you simply create an instance of DerivedRecipe, link it to the prototype, and then add an ingredient with the same UUID as one of the prototype's ingredients.
I'm not 100% on what behavior you are looking to have, so here's my attempted solution.
Single-Table Inheritance (STI). Your base class will be PrototypeRecipe and your child class will be DerivedRecipe.
In your prototype_recipes table, specify a type column (text). This signals to Rails you want to use STI. If you put your make_ingredients_list method inside the base class, it will be accessible from your child classes.
# app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
belongs_to :recipe, :class_name => "PrototypeRecipe"
...
end
# app/models/prototype_recipe.rb
class PrototypeRecipe < ActiveRecord::Base
has_many :ingredients
has_many :derived_recipes
def make_ingredient_list
...
end
end
# app/models/derived_recipe.rb
class DerivedRecipe < PrototypeRecipe
belongs_to :prototype_recipe
end
Now you can do something like:
#cupcakes = PrototypeRecipe.create
#cupcakes_with_extra_eggs = #cupcakes.derived_recipes.create
print #cupcakes_with_extra_eggs.make_ingredient_list
Is this what you were looking for?

Resources