Ruby: Parent-child association only returns "Object" - ruby-on-rails

I'm trying the list my parent and children of my Model "Performance Indicators", in my model I have this for parent/children:
belongs_to :parent, class_name: "PerformanceIndicator", :foreign_key => 'parent_id2'
has_many :children, class_name: "PerformanceIndicator", :foreign_key => 'parent_id2'
and in my performance indicators table I have the column parent_id2: t.integer "parent_id2"
But if I try to call the parent like this for example: PerformanceIndicator.parent, its returning "Object", and if I try for the children it says #<NoMethodError: undefined method children' for
What am I doing wrong?
I want to list the performance indicators something like this:
Parent1
Children1
Children2
Parent2
Children1
EDIT:
this is my performance indicators model:
class PerformanceIndicator < ActiveRecord::Base
has_many :improvement_actions
has_ancestry
belongs_to :parent, class_name: "PerformanceIndicator"#, :foreign_key => :parent_id2
has_many :children, class_name: "PerformanceIndicator", :foreign_key => 'parent_id2'
scope :parents, -> { where('parent_id2=''') }
def self.search(search)
where("name iLIKE ? OR description iLIKE ?", "%#{search}%", "%#{search}%")
# where("description LIKE ?", "%#{search}%")
end
end

I think you should consider changing your terminology. A child-parent relationship generally infers the child class inheriting from the parent class.
But if I try to call the parent like this for example: PerformanceIndicator.parent, its returning "Object", and if I try for the children it says #
This is happening because you are actually calling the class method parent on your class PerformanceIndicator, which inherits from Object. thus, the parent class of PerformanceIndicator is Object. There is no class method children for your class because it doesn't exist. What you have defined in your class is an instance method.
You must have an instance of your class to call these methods. You can instantiate a new instance of this object like so:
pi = PerformanceIndicator.new
pi.parent
pi.children

The associations of a model are always per instance.
Therefore, you can't access PerformanceIndicator.parent as it is a call to the model itself, but not an instance.
to create proper associations, you have to create different instances, like:
indi1 = PerformanceIndicator.create
indi2 = PerformanceIndicator.create(parent: indi1)
Now you can access them like
indi1.children # => ActiveRecord::Relation
indi1.children.to_a # => [<PerformanceIndicator: id: 2, ....>]
indi2.parent # => <PerformanceIndicator: id: 1> alias indi1
If you already have a parent_id, then you should take away parent_id2 and change the relations as follows:
belongs_to :parent, class_name: "PerformanceIndicator"
has_many :children, class_name: "PerformanceIndicator", :foreign_key => 'parent_id'
Afterwards, you can create a parent scope:
scope :parents, -> { where(parent_id: nil) }
And you can iterate through them:
PerformanceIndicator.parents.each do |parent|
# do stuff with parent
parent.children.each do |child|
# do stuff with child
end
end

Related

ActiveRecord Joins get child objects instead of parent

My problem:
I have Three models: Company, Parent, and Child.
Child belongs_to Parent which belongs_to Company
I need to get all children where a certain attribute is set to false, within one company. (The Parent model has a company_id while the Child model does not.)
What I'm trying:
I have the following join:
#objects = Parent.joins(:childs).where('parents.company_id' => current_user.company_id, 'childs.foo' => false)
In my view:
<!-- This should be a list of child objects -->
<% #objects.each do |obj| %>
<%= obj.foo %>
<% end %>
(foo being the attribute of the child object)
Models:
class Company < ActiveRecord::Base
has_many :parents, :dependent => :destroy
...
end
class Parent < ActiveRecord::Base
has_many :childs, :dependent => :destroy
belongs_to :company
...
end
class Child < ActiveRecord::Base
belongs_to :parent
...
end
However, writing the Parent.joins(:childs)... returns an ActiveRecord relation of Parent objects. (Throwing an error when I try to access the child attributes) I need the end list to be of child objects. But I am finding it difficult to do so.
A good answer to this question would be one that:
Solved this problem in another way that made more sense while not being too computationally intensive.
Or something that shows how to get a list/relation of child objects instead of the parent objects.
Simple, start with the Child class:
Child.joins(:parent).where(parents: {company_id: current_user.company_id}, foo: false)
I would probably advise using scopes/class methods to accomplish this in a cleaner fashion:
class Parent < ActiveRecord::Base
def self.for_user(user)
where(company_id: user.company_id)
end
end
class Child < ActiveRecord::Base
scope :fooless, ->{where foo: false}
end
Now you can do this:
Child.joins(:parent).merge(Parent.for_user(current_user)).fooless

Validation to prevent parent referencing self as children

I'm looking for a solution to prevent "parents" to add his self as "children".
My Model looks like this:
class Category < ActiveRecord::Base
belongs_to :parent, :class_name => 'Category'
has_many :children, :class_name => 'Category', :foreign_key => 'parent_id'
end
Now I look for a solution to prevent things like this
parent = Category.create(name: "Parent")
Category.new(name: "Children", parent_id: parent.id).valid? # should be => false
You can add a custom validation for that.
Something like
class ParentValidator < ActiveModel::Validator
def validate(record)
if record.parent_id == record.id
record.errors[:parent_id] << 'A record\'s parent cannot be the record itself'
end
end
end
class Category
include ActiveModel::Validations
validates_with ParentValidator
end
or even simpler (if it is a one off thing)
class Category < ActiveRecord::Base
validate :parent_not_self, on: :save
def parent_not_self
if parent_id == id
errors.add(:parent_id, 'A record\'s parent cannot be the record itself')
end
end
end
Both cases will generate a validation error when you try to assign the record itself as the parent's record

Rails association with multiple foreign keys

I want to be able to use two columns on one table to define a relationship. So using a task app as an example.
Attempt 1:
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
So then Task.create(owner_id:1, assignee_id: 2)
This allows me to perform Task.first.owner which returns user one and Task.first.assignee which returns user two but User.first.task returns nothing. Which is because task doesn't belong to a user, they belong to owner and assignee. So,
Attempt 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
That just fails altogether as two foreign keys don't seem to be supported.
So what I want is to be able to say User.tasks and get both the users owned and assigned tasks.
Basically somehow build a relationship that would equal a query of Task.where(owner_id || assignee_id == 1)
Is that possible?
Update
I'm not looking to use finder_sql, but this issue's unaccepted answer looks to be close to what I want: Rails - Multiple Index Key Association
So this method would look like this,
Attempt 3:
class Task < ActiveRecord::Base
def self.by_person(person)
where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
def tasks
Task.by_person(self)
end
end
Though I can get it to work in Rails 4, I keep getting the following error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
TL;DR
class User < ActiveRecord::Base
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
end
Remove has_many :tasks in User class.
Using has_many :tasks doesn't make sense at all as we do not have any column named user_id in table tasks.
What I did to solve the issue in my case is:
class User < ActiveRecord::Base
has_many :owned_tasks, class_name: "Task", foreign_key: "owner_id"
has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User"
belongs_to :assignee, class_name: "User"
# Mentioning `foreign_keys` is not necessary in this class, since
# we've already mentioned `belongs_to :owner`, and Rails will anticipate
# foreign_keys automatically. Thanks to #jeffdill2 for mentioning this thing
# in the comment.
end
This way, you can call User.first.assigned_tasks as well as User.first.owned_tasks.
Now, you can define a method called tasks that returns the combination of assigned_tasks and owned_tasks.
That could be a good solution as far the readability goes, but from performance point of view, it wouldn't be that much good as now, in order to get the tasks, two queries will be issued instead of once, and then, the result of those two queries need to be joined as well.
So in order to get the tasks that belong to a user, we would define a custom tasks method in User class in the following way:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.
Extending upon #dre-hh's answer above, which I found no longer works as expected in Rails 5. It appears Rails 5 now includes a default where clause to the effect of WHERE tasks.user_id = ?, which fails as there is no user_id column in this scenario.
I've found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) {
unscope(:where).where(owner: user).or(where(assignee: user)
}
end
Rails 5:
you need to unscope the default where clause
see #Dwight answer if you still want a has_many associaiton.
Though User.joins(:tasks) gives me
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
As it is no longer possible you can use #Arslan Ali solution as well.
Rails 4:
class User < ActiveRecord::Base
has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end
Update1:
Regarding #JonathanSimmons comment
Having to pass the user object into the scope on the User model seems like a backwards approach
You don't have to pass the user model to this scope.
The current user instance is passed automatically to this lambda.
Call it like this:
user = User.find(9001)
user.tasks
Update2:
if possible could you expand this answer to explain what's happening? I'd like to understand it better so I can implement something similar. thanks
Calling has_many :tasks on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate a tasks method on its object, which will call this lambda. The generated method would look similar to following pseudocode:
class User
def tasks
#define join query
query = self.class.joins('tasks ON ...')
#execute tasks_lambda on the query instance and pass self to the lambda
query.instance_exec(self, self.class.tasks_lambda)
end
end
I worked out a solution for this. I'm open to any pointers on how I can make this better.
class User < ActiveRecord::Base
def tasks
Task.by_person(self.id)
end
end
class Task < ActiveRecord::Base
scope :completed, -> { where(completed: true) }
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
def self.by_person(user_id)
where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
end
end
This basically overrides the has_many association but still returns the ActiveRecord::Relation object I was looking for.
So now I can do something like this:
User.first.tasks.completed and the result is all completed task owned or assigned to the first user.
Since Rails 5 you can also do that which is the ActiveRecord safer way:
def tasks
Task.where(owner: self).or(Task.where(assignee: self))
end
My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
class User < ActiveRecord::Base
has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
Warning:
If you are using RailsAdmin and need to create new record or edit existing record,please don't do what I've suggested.Because this hack will cause problem when you do something like this:
current_user.tasks.build(params)
The reason is that rails will try to use current_user.id to fill task.user_id,only to find that there is nothing like user_id.
So,consider my hack method as an way outside the box,but don't do that.
Better way is using polymorphic association:
task.rb
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
assigned_task.rb
class AssignedTask < Task
end
owned_task.rb
class OwnedTask < Task
end
user.rb
class User < ActiveRecord::Base
has_many :assigned_tasks, as: :taskable, dependent: :destroy
has_many :owned_tasks, as: :taskable, dependent: :destroy
end
In result, we can use it so:
new_user = User.create(...)
AssignedTask.create(taskable: new_user, ...)
OwnedTask.create(taskable: new_user, ...)
pp user.assigned_tasks
pp user.owned_tasks

Using ActiveRelation with :joins on polymorphic has_one to implicitly create relation

We can use ActiveRelation like this:
MyModel.where(:field => "test").create => #<Message ... field:"test">
But it doesnt work for joins with polymorphic has_one associations:
class RelatedModel < AR::Base
# has :some_field
belongs_to :subject, :polymorphic => true
end
class MyModel < AR::Base
# need some dirty magic here
# to build default related_model with params from active_relation
has_one :related_model, :as => :subject, :dependent => :destroy
end
describe MyModel do
it "should auto-create has_one association with joins" do
test = MyModel.joins(:related_model).where("related_models.subject_type" => "MyModel", "related_models.some_field" => "chachacha").create
test.related_model.should_not be_nil
test.related_model.some_field.should == "chachacha"
test.related_model.subject_type.should == "MyModel"
test.related_model.subject_id.should == test.id
# fails =)
end
end
Is it possible to extract active_relation params, pass them to MyModel for use in before_create and build RelatedModel with them?
Diving into ActiveRecord sources i found that
ActiveRecord::Relation covers 'create' with 'scoping' method.
ActiveRecord::Persistance 'create' calls 'initialize' from ActiveRecord::Core.
ActiveRecord::Core 'initialize' calls 'populate_with_current_scope_attributes'
This method declared in ActiveRecord::Scoping uses 'scope_attributes' declared in ActiveRecord::Scoping::Named.
scope_attributes creating relation 'all' and calls 'scope_for_create' on it.
'ActiveRecord::Relation's 'scope_for_create' uses only 'where_values_hash' from current_scope that does not contain rules like 'related_models.subject_type' (this values are contained in where_clauses). So we need to have simple key-value wheres to be used with 'create' on ActiveRecord::Relation. But ActiveRecord not clever enough to know that 'some_field' in where clause should be used with join table.
I found it can be implemented only by accessing where options with self.class.current_scope.where_clauses in 'before_create' on MyModel, parsing them and setting up attributes.
class MyModel < AR::Base
before_create :create_default_node
def create_default_node
clause = self.class.current_scope.where_clauses.detect{|clause| clause =~ /\`related_models\`.\`some_field\`/}
value = clause.scan(/\=.+\`([[:word:]]+)\`/).flatten.first
self.create_node(:some_field => value)
end
end
But it is so dirty, then i decided to find simpler solution and inverted dependency as described in Railscast Pro #394, moved RelatedModel functionality to MyModel with STI. Actually i needed such complicated relation creation because RelatedModel had some functionality common for all models (acts as tree). I decided to delegate 'ancestors' and 'children' to RelatedModel. Inverting dependency solved this problem.
class MyModel < AR::Base
acts_as_tree
belongs_to :subject, :polymorphic => true
end
class MyModel2 < MyModel
end
class RelatedModel < AR::Base
# has :some_field
has_one :my_model, :as => :subject, :dependent => :destroy
end
MyModel.create{|m| m.subject = RelatedModel.create(:some_field => "chachacha")}
MyModel.ancestors # no need to proxy relations

Creating a model that has a tree structure

I have categories that are in a tree structure. I am trying to link them together by defining a parent for each one. (I couldn't figure out how to call the property parent so it's just category for now, but it means the parent).
class Category < ActiveRecord::Base
has_one :category # the parent category
end
But the relationship ends up the wrong way around.
The getter function is on the child category (correctly) but the category_id is stored on the parent:
parent = Category.create(:name => "parent")
child = Category.create(:name => "child", :category => parent)
parent.id # 1
child.id # 2
child.category_id # nil
parent.category_id # 2
child.category.name # "parent" (!!)
The parent needs to be able to have multiple children so this isn't going to work.
What you're looking for is self joins. Check this section of the Rails guide out: http://guides.rubyonrails.org/association_basics.html#self-joins
class Category < ActiveRecord::Base
  has_many :children, class_name: "Category", foreign_key: "parent_id"
  belongs_to :parent, class_name: "Category"
end
Every Category will belong_to a parent, even your parent categories. You can create a single category parent that your highest level categories all belong to, then you can disregard that information in your application.
You can use acts_as_tree gem to achieve this, find below example and link.
https://github.com/amerine/acts_as_tree/tree/master
class Category < ActiveRecord::Base
include ActsAsTree
acts_as_tree order: "name"
end
root = Category.create("name" => "root")
child1 = root.children.create("name" => "child1")
subchild1 = child1.children.create("name" => "subchild1")
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1
You should take a look at the ancestry gem: https://github.com/stefankroes/ancestry
It provides all the functionality you need and is able to get all descendants, siblings, parents, etc with a single SQL query by using a variant of materialized paths so it'll have better performance than the self-joins and acts_as_tree answers above.
Category should have many categories, and the foreign key of each category should be the parent_id. So, when you do parent.children it lists all the categories which have parent_id=parent.id.
Have you read on Single Table Inheritance?
Full Article - https://blog.francium.tech/best-practices-for-handling-hierarchical-data-structure-in-ruby-on-rails-b5830c5ea64d
A Simple table
Table Emp
id: Integer
name: String
parent_id: Integer
Associations
app/models/emp.rb
class Emp < ApplicationRecord
has_many :subs, class_name: 'Emp', foreign_key: :parent_id
belongs_to :superior, class_name: 'Emp', foreign_key: :parent_id
end
Scope Definition
class Emp < ApplicationRecord
----
----
scope :roots, -> { where(parent_id: nil) }
end
Fetching data
def tree_data
output = []
Emp.roots.each do |emp|
output << data(emp)
end
output.to_json
end
def data(employee)
subordinates = []
unless employee.subs.blank?
employee.subs.each do |emp|
subordinates << data(emp)
end
end
{name: employee.name, subordinates: subordinates}
end
Eager Loading
def tree_data
output = []
Emp.roots.includes(subs: {subs: {subs: subs}}}.each do |emp|
output << data(emp)
end
output.to_json
end

Resources