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.
Related
In my Rails app I have Clients and Users. And Users can have many Clients.
The models are setup as so:
class Client < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :users, through: :client_users
end
class User < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :clients, through: :client_users
end
class ClientUser < ApplicationRecord
belongs_to :user
belongs_to :client
end
So if I wanted to create a new client that had the first two users associated with it how would I do it?
e.g.
Client.create!(name: 'Client1', client_users: [User.first, User.second])
Trying that gives me the error:
ActiveRecord::AssociationTypeMismatch: ClientUser(#70142396623360) expected, got #<User id: 1,...
I also want to do this for an RSpec test. e.g.
user1 = create(:user)
user2 = create(:user)
client1 = create(:client, client_users: [user1, user2])
How do I create a client with associated users for in both the Rails console and in an RSpec test?
If you do not want to accept_nested_attributes for anything, as documented here you can also pass block to create.
Client.create!(name: 'Client1') do |client1|
client1.users << [User.find(1), User.find(2), User.find(3)]
end
Try this. It should work
Client.create!(name: 'Client1').client_users.new([{user_id:
User.first},{user_id: User.second}])
You can do this with the following code:
user1 = create(:user)
user2 = create(:user)
client1 = create(:client, users: [user1, user2])
See ClassMethods/has_many for the documentation
collection=objects
Replaces the collections content by deleting and adding objects as
appropriate. If the :through option is true callbacks in the join
models are triggered except destroy callbacks, since deletion is
direct.
If you are using factory_girl you can add trait :with_users like this:
FactoryGirl.define do
factory :client do
trait :with_two_users do
after(:create) do |client|
client.users = create_list :user, 2
end
end
end
end
Now you can create a client with users in test like this:
client = create :client, :with_two_users
accepts_nested_attributes_for :users
and do as so:
Client.create!(name: 'Client1', users_attributes: { ........ })
hope this would work for you.
You can make use of after_create callback
class Client < ApplicationRecord
has_many :client_users, dependent: :destroy
has_many :users, through: :client_users
after_create :add_users
private def add_users
sef.users << [User.first, User.second]
end
end
Alternatively, A simpler approach would be
Client.create!(name: 'Client1', user_ids: [User.first.id, User.second.id])
The reason you're getting a mismatch is because you're specifying the client_users association that expects ClientUser instances, but you're passing in User instances:
# this won't work
Client.create!(client_users: [User.first, User.second])
Instead, since you already specified a users association, you can do this:
Client.create!(users: [User.first, User.second])
There's a simpler way to handle this, though: ditch the join model and use a has_and_belongs_to_many relationship. You still need a clients_users join table in the database, but you don't need a ClientUser model. Rails will handle this automatically under the covers.
class Client < ApplicationRecord
has_and_belongs_to_many :users
end
class User
has_and_belongs_to_many :clients
end
# Any of these work:
client = Client.new(name: "Kung Fu")
user = client.users.new(name: "Panda")
client.users << User.new(name: "Nemo")
client.save # => this will create two users and a client, and add two records to the `clients_users` join table
I am running into a weird issue, and reading the callbacks RoR guide didn't provide me an answer.
class User < ActiveRecord::Base
...
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
has_many :user_teams, dependent: :destroy
has_many :teams, through: :user_teams
before_create :check_company!
after_create :check_team
def check_company!
return if self.companies.present?
domain = self.email_domain
company = Company.find_using_domain(domain)
if company.present?
assign_company(company)
else
create_and_assign_company(domain)
end
end
def check_team
self.companies.each do |company|
#do stuff
end
end
...
end
The after_create :check_team callback is facing issues because the line
self.companies.each do |company|
Here, self.companies is returning an empty array [] even though the Company and User were created and the User was associated with it. I know I can solve it by making it a before_create callback instead. But I am puzzled!
Why does the after_create callback not have access to self's associations after the commit?
Solution: Please read my comments in the accepted answer to see the cause of the problem and the solution.
inside before_create callbacks, the id of the record is not yet available, because it is before... create... So it is not yet persisting in the database to have an id. This means that the associated company_user record doesn't have a user_id value yet, precisely because the user.id is still nil at that point. However, Rails makes this easy for you to not worry about this "chicken-and-egg" problem, provided that you do it correctly:
I recreated your setup (Company, User, and CompanyUser models), and the following is what should work on your case (tested working):
class User < ApplicationRecord
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
before_create :check_company!
after_create :check_team
def check_company!
# use `exists?` instead of `present?` because `exists?` is a lot faster and efficient as it generates with a `LIMIT 1` SQL.
return if companies.exists?
## when assigning an already persisted Company record:
example_company = Company.first
# 1) WORKS
companies << example_company
# 2) WORKS
company_users.build(company: example_company)
## when assigning and creating a new Company record:
# 1) WORKS (this company record will be automatically saved/created after this user record is saved in the DB)
companies.build(name: 'ahaasdfwer') # or... self.companies.new(name: 'ahaasdfwer')
# 2) DOES NOT WORK, because you'll receive an error `ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved`
companies.create(name: 'ahaasdfwer')
end
def check_team
puts companies.count
# => 1 if "worked"
puts companies.first.persisted?
# => true if "worked"
end
end
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
I'm trying to create an object and adding an existing object to a "has_many through" association, but after saving my object the reference to my newly created object is set to nil in the join model.
To be specific, I'm creating a Notification object and adding a pre-existing Member object to the Notification.members association. I'm using nested resources and I'm invoking the notification controller's new function using the following relative URL:
/members/1/notifications/new
After filling out the form and submitting, the create function is called, and from what I understand from the Rails Associations guide, section 4.3.3 "When are Objects Saved?", the members associations should be created in the database when the new notification object is saved:
"If the parent object (the one declaring the has_many association) is unsaved (that is, new_record? returns true) then the child objects are not saved when they are added. All unsaved members of the association will automatically be saved when the parent is saved."
After creating the notification object, the following record was created in the database:
select id, notification_id, notifiable_type, notifiable_id from deliveries;
1|<NULL>|Member|1
I worked around the problem by saving the notification object before adding the member object to the association. At first this seemed to be an ok solution for now, but I soon discovered that this has it's downsides. I don't want to save the notification without it's member association since I then have to write workarounds for my callbacks so that they don't start performing tasks on the not yet valid notification object.
What am I doing wrong here? All tips are appreciated. :D
Models
class Notification < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :members, :through => :deliveries, :source => :notifiable, :source_type => "Member"
has_many :groups, :through => :deliveries, :source => :notifiable, :source_type => "Group"
end
class Member < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
class Delivery < ActiveRecord::Base
belongs_to :notification
belongs_to :notifiable, :polymorphic => true
end
# Group is not really relevant in this example.
class Group < ActiveRecord::Base
has_many :deliveries, :as => :notifiable
has_many :notifications, :through => :deliveries
end
Controller
class NotificationsController < ApplicationController
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
#notification.members << #member
respond_to do |format|
if #notification.save
...
end
end
end
end
After posting a bug report, I got som help from one of the Rails gurus. In short, it's impossible to get this working the way I thought.
I decided to proceed with slightly more controller code, seems to be working just fine:
def create
#notification = Notification.new(params[:notification])
#member = Member.find(params[:member_id])
respond_to do |format|
if #notification.save
#member.notifications << #notification
#member.save
...
I have two Models: Campaign and Contact.
A Campaign has_many Contacts.
A Contact has_many Campaigns.
Currently, each Contact has a contact.date_entered attribute. A Campaign uses that date as the ate to count down to the different Events that belong_to the Campaign.
However, there are situations where a Campaign for a specific Contact may need to be delayed by X number of days. In this instance, the campaigncontact.delaydays = 10.
In some cases, the Campaign must be stopped altogether for the specific Contact, so for now I set campaigncontact.delaydays = 1. (Are there major problems with that?)
By default, I am assuming that no campaigncontact exists (but not sure how that works?)
So here's what I've tried to do:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
end
class Campaign < ActiveRecord::Base
has_many :campaigncontacts
has_many :contacts, :through => :campaigncontacts
end
script/generate model campaigncontact campaign_id:integer contact_id:integer delaydays:integer
class Campaigncontact < ActiveRecord::Base
belongs_to :campaign
belongs_to :contact
end
So, here's the question: Is the above correct? If so, how do I allow a user to edit the delay of a campaign for a specific Contact.
For now, I want to do so from the Contact View.
This is what I tried:
In the Contact controller (?)
in_place_edit_for :campaigncontact, column.delaydays
And in the View
<%= in_place_editor_field :campaigncontact, :delaydays %>
How can I get it right?
I would add an integer field to your Campaigncontacts resource called days_to_delay_communication_by, since this information relates to the association of a campaign and a contact rather than a contact itself.
in your migration:
def self.up
add_column(:campaigncontacts, :days_to_delay_communication_by, :integer)
end
def self.down
remove_column(:campaigncontacts, :days_to_delay_communication_by)
end
Now you can set that value by:
campaigncontact = Campaigncontacts.find(:first, :conditions => { :campaign_id => campaign_id, :contact_id => contact_id })
campaigncontact.days_to_delay_communication_by = 10
Then in the admin side of your application you can have a controller and a view for campaign communications that lets you set the days_to_delay_communication_by field for campaigncontacts. I can expand on this further for you if you're interested, but I think you get the idea.
Then you'll need to run a background process of some sort (probably a cron job, or use the delayed_job plugin), to find communications that haven't happened yet, and make them happen when the date has passed. You could do this in a rake task like so:
namespace :communications do
namespace :monitor do
desc 'Monitor and send communications for campaigns'
task :erma => :environment do
Rails.logger.info "-----BEGIN COMMUNICATION MONITORING-----"
unsent_communications = Communication.all(:conditions => { :date_sent => nil})
unsent_communications.each do |communication|
Rails.logger.info "**sending communication**"
communication.send if communication.time_to_send < Time.now
Rails.logger.info "**communication sent**"
end
Rails.logger.info "-----END COMMUNICATION MONITORING-----"
end #end erma task
end #end sync namespace
end #end db namespace
Then your cron job would do something like:
cd /path/to/application && rake communications:monitor RAILS_ENV=production
Also, I'd consider changing the name of your join model to something more descriptive of it's purpose, for instance memberships, a campaign has many memberships and a contact has many memberships. Then a membership has a days_to_delay_communication field.
A good way to do this is use a "fake" attribute on your Contact model like so:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
attr_accessor :delay
def delay #edit
self.campaigncontacts.last.delaydays
end
def delay=(val)
self.campaigncontacts.each do |c|
c.delaydays = val
end
end
end
Then just set the in_place_editor for this fake field:
in_place_edit_for :contact, :delay
and
<%= in_place_editor_field :contact, :delay %>
I'm not sure I understood exactly what you wanted to accomplish, but I hope this at least points you into the right direction.