class Topic < ActiveRecord::Base
belongs_to :success_criterion
end
class SuccessCriterion < ActiveRecord::Base
has_many :topics, dependent: :restrict_with_error
end
SuccessCriterion manages a topics_counter counter, but it doesn't seem to be updated when using the following factory:
FactoryGirl.define do
factory :topic do
success_criterion { create(:success_criterion) }
title 'Topic test title'
intro 'Topic test intro'
outro 'Topic test outro'
end
end
Take a look at the following:
#topic = create :topic
#success_criterion = #topic.success_criterion
#success_criterion.topics.any?
=> false
#success_criterion.topics_count
=> 0
#success_criterion.topics.count
=> 1
What's happening here? Without the topics_counter field, it works as expected, so it seems the FactoryGirl doesn't seem to update the counter when doing success_criterion { create(:success_criterion) }.
You have to enable counter_cache in your model.
Add the following to your Topic model:
class Topic < ActiveRecord::Base
belongs_to :success_criterion, counter_cache: true
end
Be sure that the topics_count column is present in your SuccessCriterion model.
Ryan Bates described this feature very well in one of his RailsCasts.
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.
I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.
There are models with has has_many through association:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
validates :categories, presence: true
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
validates_presence_of :event, :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
The issue is with assigning event.categories = [] - it immediately deletes rows from event_categories. Thus, previous associations are irreversibly destroyed and an event becomes invalid.
How to validate a presence of records in case of has_many, through:?
UPD: please carefully read sentence marked in bold before answering.
Rails 4.2.1
You have to create a custom validation, like so:
validate :has_categories
def has_categories
unless categories.size > 0
errors.add(:base, "There are no categories")
end
end
This shows you the general idea, you can adapt this to your needs.
UPDATE
This post has come up once more, and I found a way to fill in the blanks.
The validations can remain as above. All I have to add to that, is the case of direct assignment of an empty set of categories. So, how do I do that?
The idea is simple: override the setter method to not accept the empty array:
def categories=(value)
if value.empty?
puts "Categories cannot be blank"
else
super(value)
end
end
This will work for every assignment, except when assigning an empty set. Then, simply nothing will happen. No error will be recorded and no action will be performed.
If you want to also add an error message, you will have to improvise. Add an attribute to the class which will be populated when the bell rings.
So, to cut a long story short, this model worked for me:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
attr_accessor :categories_validator # The bell
validates :categories, presence: true
validate :check_for_categories_validator # Has the bell rung?
def categories=(value)
if value.empty?
self.categories_validator = true # Ring that bell!!!
else
super(value) # No bell, just do what you have to do
end
end
private
def check_for_categories_validator
self.errors.add(:categories, "can't be blank") if self.categories_validator == true
end
end
Having added this last validation, the instance will be invalid if you do:
event.categories = []
Although, no action will have been fulfilled (the update is skipped).
use validates_associated, official documentaion is Here
If you are using RSpec as your testing framework, take a look at Shoulda Matcher. Here is an example:
describe Event do
it { should have_many(:categories).through(:event_categories) }
end
I read this interesting article about Using Polymorphism to Make a Better Activity Feed in Rails.
We end up with something like
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
end
Now, if two of those subjects are for example:
class Event < ActiveRecord::Base
has_many :guests
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
class Image < ActiveRecord::Base
has_many :tags
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
With create_activities defined as
def create_activities
Activity.create(subject: self)
end
And with guests and tags defined as:
class Guest < ActiveRecord::Base
belongs_to :event
end
class Tag < ActiveRecord::Base
belongs_to :image
end
If we query the last 20 activities logged, we can do:
Activity.order(created_at: :desc).limit(20)
We have a first N+1 query issue that we can solve with:
Activity.includes(:subject).order(created_at: :desc).limit(20)
But then, when we call guests or tags, we have another N+1 query problem.
What's the proper way to solve that in order to be able to use pagination ?
Edit 2: I'm now using rails 4.2 and eager loading polymorphism is now a feature :)
Edit: This seemed to work in the console, but for some reason, my suggestion of use with the partials below still generates N+1 Query Stack warnings with the bullet gem. I need to investigate...
Ok, I found the solution ([edit] or did I ?), but it assumes that you know all subjects types.
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
belongs_to :event, -> { includes(:activities).where(activities: { subject_type: 'Event' }) }, foreign_key: :subject_id
belongs_to :image, -> { includes(:activities).where(activities: { subject_type: 'Image' }) }, foreign_key: :subject_id
end
And now you can do
Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)
But for eager loading to work, you must use for example
activity.event.guests.first
and not
activity.part.guests.first
So you can probably define a method to use instead of subject
def eager_loaded_subject
public_send(subject.class.to_s.underscore)
end
So now you can have a view with
render partial: :subject, collection: activity
A partial with
# _activity.html.erb
render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject
And two (dummy) partials
# _event.html.erb
<p><%= event.guests.map(&:name).join(', ') %></p>
# _image.html.erb
<p><%= image.tags.first.map(&:name).join(', ') %></p>
This will hopefully be fixed in rails 5.0. There is already an issue and a pull request for it.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
I have forked rails and applied the patch to 4.2-stable and it works for me. Feel free to use my fork, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
You can use ActiveRecord::Associations::Preloader to preload guests and tags linked, respectively, to each of the event and image objects that are associated as a subject with the collection of activities.
class ActivitiesController < ApplicationController
def index
activities = current_user.activities.page(:page)
#activities = Activities::PreloadForIndex.new(activities).run
end
end
class Activities::PreloadForIndex
def initialize(activities)
#activities = activities
end
def run
preload_for event(activities), subject: :guests
preload_for image(activities), subject: :tags
activities
end
private
def preload_for(activities, associations)
ActiveRecord::Associations::Preloader.new.preload(activities, associations)
end
def event(activities)
activities.select &:event?
end
def image(activities)
activities.select &:image?
end
end
image_activities = Activity.where(:subject_type => 'Image').includes(:subject => :tags).order(created_at: :desc).limit(20)
event_activities = Activity.where(:subject_type => 'Event').includes(:subject => :guests).order(created_at: :desc).limit(20)
activities = (image_activities + event_activities).sort_by(&:created_at).reverse.first(20)
I would suggest adding the polymorphic association to your Event and Guest models.
polymorphic doc
class Event < ActiveRecord::Base
has_many :guests
has_many :subjects
after_create :create_activities
end
class Image < ActiveRecord::Base
has_many :tags
has_many :subjects
after_create :create_activities
end
and then try doing
Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
Does this generate a valid SQL query or does it fail because events can't be JOINed with tags and images can't be JOINed with guests?
class Activity < ActiveRecord::Base
self.per_page = 10
def self.feed
includes(subject: [:guests, :tags]).order(created_at: :desc)
end
end
# in the controller
Activity.feed.paginate(page: params[:page])
This would use will_paginate.
I've got the following two classes
class Car < Vehicle
has_one :steering_wheel, as: :attached
end
class SteeringWheel < ActiveRecord::Base
belongs_to :attached
has_many :components
has_one :rim, class_name: 'Components', order: 'id DESC'
attr_accessible :components
end
class Component < ActiveRecord::Base
include SpecificationFileService::Client
attr_accessible :created_by
belongs_to :steering_wheel
end
Then in my specs:
context "given an attachment", :js do
before do
#car = create(:car, make: "honda")
#steering_wheel = SteeringWheel.create(attached: #car)
#steering_wheel.save
#car.save
#car.reload
end
specify "test the setup", :js do
puts #car.steering_wheel
end
end
Which prints: nil
A way that I have found fixes this is explicitly setting steering_wheel on car like so:
#car.steering_wheel = #steering_wheel
just before the save.
EDIT:
As suggested in the comments below, I have tried adding polymorphic: true, which did not resolve the issue. Also, I've fleshed out more of the SteeringWheel model above
My question is why, and how can I add this to the callback chain implicitly
Like #abraham-p mentioned in a comment, you need to declare the belongs_to relation as:
belongs_to :attached, polymorphic: true
Otherwise it will attempt to look for an Attached model, and be sure to include these fields in your SteeringWheel model:
attached_type
attached_id
The rest is worked out by Rails :)