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
Related
I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.
So i'm fairly new to rails and ActiveRecord and I have a need for a scope to filter between Client entities. Basically the scope should return all Client records where the client's current state is equal to a certain state object.
This is calculated by getting a client's last state_change and then pulling that state_change's from_state which is a State object.
I have defined a method to return the current_state however in rails console when I test it with Client.current_state(Client.last) I get this error:
NameError: undefined local variable or method 'state_changes for #<Class:0x0000000685eb88> but when running Client.last.state_changes in console it works fine.
My client.rb
class Client < ActiveRecord::Base
has_and_belongs_to_many :users
belongs_to :industry
belongs_to :account
has_many :contacts
has_many :state_changes
belongs_to :head, class_name: "Client"
has_many :branches, class_name: "Client", foreign_key: "head_id"
has_many :meetings, through: :contacts
has_many :sales, through: :meetings
scope :prospects, -> (client) { where(Client.current_state(client): State.PROSPECT_STATE) }
def self.has_at_least_one_sale? (client)
return client.sales.empty?
end
def self.has_account_number? (client)
return client.account_number.present?
end
def self.current_state (client)
state_changes.last.to_state
end
end
state_change.rb
class StateChange < ActiveRecord::Base
belongs_to :client
belongs_to :from_state, class_name: "State", foreign_key: :to_state_id
belongs_to :to_state, class_name: "State", foreign_key: :from_state_id
end
state.rb
class State < ActiveRecord::Base
has_many :from_states, class_name: "StateChange", foreign_key: :to_state_id
has_many :to_states, class_name: "StateChange", foreign_key: :from_state_id
def self.PROSPECT_STATE
return State.find_by name: 'Prospect'
end
def self.CLIENT_STATE
return State.find_by name: 'Client'
end
def self.SUSPECT_STATE
return State.find_by name: 'Suspect'
end
end
I also get syntax errors regarding the scope I defined in client.rb. I have followed the ActiveRecord guide but they don't explain how to have chained methods in the actualy scope query.
The reason you get the error NameError: undefined local variable or method 'state_changes for #<Class:0x0000000685eb88> is because you define current_state as a class method and pass the client as a parameter. That's why state_changes is called on the class and not the instance. In this case you would need to use the client to get the state_changes.
def self.current_state (client)
client.state_changes.last.to_state
end
Also scopes are meant to just chain query logic. I'm not sure if it is possible to just use queries to get your wanted result. And I hope I understood your logic correctly. Alternatively you could use a class method.
def self.prospects (client)
Client.all.select { |c| c.current_state(c) == State.PROSPECT_STATE }
end
As pointed out by Зелёный in the comment, maybe you also want to just change the methods to instance methods, in which case reading the resource he linked would be very helpful.
Update based on comment:
I think what you actually want is using an instance method for current_state like this:
def current_state
state_changes.last.to_state
end
And then you can get prospects like this:
def self.prospects
Client.all.select { |c| c.current_state == State.PROSPECT_STATE }
end
Please tell me the way how to implement dynamic associative link, which is itself determined by the attribute model.
I have two engines(Tbitcoin, Tstripe) each of them have a table payment. The model User has pay_currency attribute, which is the managing.
class User < ActiveRecord::Base
has_many :payments, ~> { where "pay_currency = 'real'" } , class_name: Tstripe::Payment, foreign_key: :uid
has_many :payments, ~> { where "pay_currency = 'bitcoin'" } ,class_name: Tbitcoin::Payment, foreign_key: :uid
end
What are the ways to dynamically determine the engine using User.last.payments.create ?
I think that you need a regular method instead of has_many association which will find proper payments associated with the user according to pay_currency value. Example:
class User < ActiveRecord::Base
def payments
payment_class = case pay_currency
when "real"
Tstripe::Payment
when "bitcoin"
Tbitcoin::Payment
end
payment_class.for_user(self)
end
end
class Tstripe::Payment < ActiveRecord::Base
belongs_to :user
def self.for_user(user)
where(user_id: user.uid)
end
end
I have a Lesson model which has many Completions like this:
class Lesson < ActiveRecord::Base
has_many :completions, as: :completable
belongs_to :course
end
And each Completion belongs to a User as well:
class Completion < ActiveRecord::Base
belongs_to :user
belongs_to :completable, polymorphic: true
end
From my application perspective I'm only interested in the amount of completions for a certain lesson, so I've included a counter cache. In regard to the individual Completions, I'm only interested if the Lesson is completed by the current user (I'm using Devise).
Is there some way to create a dynamic has_one relationship of some kind, that uses the information from the current_user to query the Completion table?
for instance:
has_one :completion do
def from_user current_user
Completion.where(completable: self, user: current_user)
end
end
Although this could work, I'm also having a polymorphic relationship. Rails is complaining that there's no foreign key called lesson_id. When I add a foreign_key: symbol, the do-end block stops working.
Any ideas?
Why not passing both block and options to has_many?
class Lesson < ActiveRecord::Base
has_many :completions, as: :completable do
def from_user user
if loaded?
find {|c| c.user_id = user.id}
else
find(user_id: user.id)
end
end
end
belongs_to :course
end
#lesson = Lesson.last
# Association not loaded - executing sql query
#lesson.completions.from_user(current_user)
#lesson.completions
# Association loaded - no sql query
#lesson.completions.from_user(current_user)
NOTE: You cannot treat it as an association, so it cannot be preloaded on its own.
Let's say you have the following models:
class User < ActiveRecord::Base
has_many :comments, :as => :author
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Let's say User has an attribute name, is there any way in Ruby/Rails to access it using the table name and column, similar to what you enter in a select or where query?
Something like:
Comment.includes(:author).first.send("users.name")
# or
Comment.first.send("comments.id")
Edit: What I'm trying to achieve is accessing a model object's attribute using a string. For simple cases I can just use object.send attribute_name but this does not work when accessing "nested" attributes such as Comment.author.name.
Basically I want to retrieve model attributes using the sql-like syntax used by ActiveRecord in the where() and select() methods, so for example:
c = Comment.first
c.select("users.name") # should return the same as c.author.name
Edit 2: Even more precisely, I want to solve the following problem:
obj = ANY_MODEL_OBJECT_HERE
# Extract the given columns from the object
columns = ["comments.id", "users.name"]
I don't really understand what you are trying to achieve. I see that you are using polymorphic associations, do you need to access comment.user.name while having has_many :comments, :as => :author in your User model?
For you polymorphic association, you should have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
end
And if you want to access comment.user.name, you can also have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments, :as => :author
has_many :comments
end
Please be more specific about your goal.
I think you're looking for a way to access the user from a comment.
Let #comment be the first comment:
#comment = Comment.first
To access the author, you just have to type #comment.user and If you need the name of that user you would do #comment.user.name. It's just OOP.
If you need the id of that comment, you would do #comment.id
Because user and id are just methods, you can call them like that:
comments.send('user').send('id')
Or, you can build your query anyway you like:
Comment.includes(:users).where("#{User::columns[1]} = ?", #some_name)
But it seems like you're not doing thinks really Rails Way. I guess you have your reasons.