When a conversation is created, I want that conversation to have its creator automatically following it:
class Conversation < ActiveRecord::Base
belongs_to :user
has_many :followers
has_many :users, through: :followers
alias_method :user, :creator
before_create { add_follower(self.creator) }
def add_follower(user)
unless self.followers.exists?(user_id: user.id)
self.transaction do
self.update_attributes(follower_count: follower_count + 1)
self.followers.create(user_id: user.id)
end
end
end
end
However, when a user attempts to create a conversation I get a stack level too deep
. I'm creating an infinite loop, and I think this is because the before_create callback is being triggered by the self.update_attributes call.
So how should I efficiently update attributes before creation to stop this loop happening?
Option 1 (preferred)
Rename your column follower_count to followers_count and add:
class Follower
belongs_to :user, counter_cache: true
# you can avoid renaming the column with "counter_cache: :follower_count"
# rest of your code
end
Rails will handle updating followers_count for you.
Then change your add_follower method to:
def add_follower(user)
return if followers.exists?(user_id: user.id)
followers.build(user_id: user.id)
end
Option 2
If you don't want to use counter_cache, use update_column(:follower_count, follower_count + 1). update_column does not trigger any validations or callbacks.
Option 3
Finally you don't need to save anything at this point, just update the values and they will be saved when callback finishes:
def add_follower(user)
return if followers.exists?(user_id: user.id)
followers.build(user_id: user.id)
self.follower_count = follower_count + 1
end
Related
I have 2 models: User and Favorite. In model Favorite:
class Favorite < ApplicationRecord
belongs_to :user, foreign_key: :user_id
def self.add_favorite(options)
create!(options)
end
def self.unfavorite(options)
where(options).delete_all
end
Now, I want to limit number of records saved to Favorite is 10. It mean that users are only liked 10 products. I researched google, someone said that I try to use callback and I think it's right way, but it raise 2 questions:
1. Can I use query in method for callback?
2. Callback can be pass argument?
It is sample code I think:
class Favorite < ApplicationRecord
after_create :limit_records(user_id)
belongs_to :user, foreign_key: :user_id
def self.add_favorite(options)
create!(options)
end
def self.unfavorite(options)
where(options).delete_all
end
def limit_records(user_id)
count = self.where(user_id: user_id).count
self.where(used_id: user_id).last.delete if count > 10
end
If user had 10 favorite, when they like any products, callback will be called after Favorite is created and will be delete if it's 11th record.
You have:
belongs_to :user, foreign_key: :user_id
in your Favorite model and limit_records is an instance method on Favorite. So you have access to the user as self.user_id (or just user_id since self is implied) inside limit_records and there is no need for an argument:
after_create :limit_records
def limit_records
# same as what you have now, `user_id` will be `self.user_id`
# now that there is no `user_id` argument...
count = self.where(user_id: user_id).count
self.where(used_id: user_id).last.delete if count > 10
end
An application I'm working on, is trying to use the concept of polymorphism without using polymorphism.
class User
has_many :notes
end
class Customer
has_many :notes
end
class Note
belongs_to :user
belongs_to :customer
end
Inherently we have two columns on notes: user_id and customer_id, now the bad thing here is it's possible for a note to now have a customer_id and a user_id at the same time, which I don't want.
I know a simple/better approach out of this is to make the notes table polymorphic, but there are some restrictions, preventing me from doing that right now.
I'd like to know if there are some custom ways of overriding these associations to ensure that when one is assigned, the other is unassigned.
Here are the ones I've tried:
def user_id=(id)
super
write_attribute('customer_id', nil)
end
def customer_id=(id)
super
write_attribute('user_id', nil)
end
This doesn't work when using:
note.customer=customer or
note.update(customer: customer)
but works when using:
note.update(customer_id: 12)
I basically need one that would work for both cases, without having to write 4 methods:
def user_id=(id)
end
def customer_id=(id)
end
def customer=(id)
end
def user=(id)
end
I would rather use ActiveRecord callbacks to achieve such results.
class Note
belongs_to :user
belongs_to :customer
before_save :correct_assignment
# ... your code ...
private
def correct_assignment
if user_changed?
self.customer = nil
elsif customer_changed?
self.user = nil
end
end
end
Does counter_cache increments and decrements fire active_record callbacks ?
User << AR
has_many :cups
after_update :do_something
def do_something
"Will I be called when number of cups updated ?"
end
end
Cup << AR
belongs_to :user, counter_cache: true
end
In the above code will the function do_something be called when a new cup is added and it belongs to a user, update will be called on that user to update the cups_count, but from what I have tried it seems like the counter_cache updates don't fire callbacks, may be because they are themselves inside callbacks ?
Thanks
From the source for counter cache, it seems that ActiveRecord is doing a direct database update, which will skip callbacks.
update_all(updates.join(', '), primary_key => id )
According to the documentation for update_all, it does skip callbacks.
As #davogones mentions using callbacks is out, but you can still do something similar by overriding the update_counters method in your parent object.
In my case I needed to do something if the counter_cache count exceeded a certain value:
class Cups < ApplicationRecord
belongs_to :user, :counter_cache => true
end
class User < ApplicationRecord
has_many :cups
# This will be called every time there is a counter_cache update, + or -
def self.update_counters(id, counters)
user = User.find(id)
if user.cups_count + counters['cups_count'] >= some_value
user.do_something!
end
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I am trying to set up an attribute of a model as soon as it is initialised. I have tried before_initialise, after_initialise, before_create, after_create.
What am I doing wrong?
describe "turn based system" do
before(:each) do
#user1 = Factory :user
#user2 = Factory :user
#rota = #user1.rotas.create
#rota.users << #user2
end
it "should be user1's turn at the beginning" do
#rota.current_turn.should == #user1.id --> fails
end
Implementation:
class Rota < ActiveRecord::Base
has_many :rotazations
has_many :users, :through => :rotazations
has_many :invitations
after_create :set_current_turn
private
def set_current_turn
if self.users.count > 0
self.current_turn = self.users[0].id
end
end
end
EDIT
If I construct the rota model using:
#rota = #user1.rotas.create(:current_turn => #user1.id)
it works - however, I feel like im duplicating code - I always want the current_turn field to be the object creator's id.
Thanks!
You have a typo: change before_filter to after_create
1) before_filter is the controller callback function.
2) You want to use after_create so the user has already been associated and you can use the association.
On destruction of a restful resource, I want to guarantee a few things before I allow a destroy operation to continue? Basically, I want the ability to stop the destroy operation if I note that doing so would place the database in a invalid state? There are no validation callbacks on a destroy operation, so how does one "validate" whether a destroy operation should be accepted?
You can raise an exception which you then catch. Rails wraps deletes in a transaction, which helps matters.
For example:
class Booking < ActiveRecord::Base
has_many :booking_payments
....
def destroy
raise "Cannot delete booking with payments" unless booking_payments.count == 0
# ... ok, go ahead and destroy
super
end
end
Alternatively you can use the before_destroy callback. This callback is normally used to destroy dependent records, but you can throw an exception or add an error instead.
def before_destroy
return true if booking_payments.count == 0
errors.add :base, "Cannot delete booking with payments"
# or errors.add_to_base in Rails 2
false
# Rails 5
throw(:abort)
end
myBooking.destroy will now return false, and myBooking.errors will be populated on return.
just a note:
For rails 3
class Booking < ActiveRecord::Base
before_destroy :booking_with_payments?
private
def booking_with_payments?
errors.add(:base, "Cannot delete booking with payments") unless booking_payments.count == 0
errors.blank? #return false, to not destroy the element, otherwise, it will delete.
end
It is what I did with Rails 5:
before_destroy do
cannot_delete_with_qrcodes
throw(:abort) if errors.present?
end
def cannot_delete_with_qrcodes
errors.add(:base, 'Cannot delete shop with qrcodes') if qrcodes.any?
end
State of affairs as of Rails 6:
This works:
before_destroy :ensure_something, prepend: true do
throw(:abort) if errors.present?
end
private
def ensure_something
errors.add(:field, "This isn't a good idea..") if something_bad
end
validate :validate_test, on: :destroy doesn't work: https://github.com/rails/rails/issues/32376
Since Rails 5 throw(:abort) is required to cancel execution: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain
prepend: true is required so that dependent: :destroy doesn't run before the validations are executed: https://github.com/rails/rails/issues/3458
You can fish this together from other answers and comments, but I found none of them to be complete.
As a sidenote, many used a has_many relation as an example where they want to make sure not to delete any records if it would create orphaned records. This can be solved much more easily:
has_many :entities, dependent: :restrict_with_error
The ActiveRecord associations has_many and has_one allows for a dependent option that will make sure related table rows are deleted on delete, but this is usually to keep your database clean rather than preventing it from being invalid.
You can wrap the destroy action in an "if" statement in the controller:
def destroy # in controller context
if (model.valid_destroy?)
model.destroy # if in model context, use `super`
end
end
Where valid_destroy? is a method on your model class that returns true if the conditions for destroying a record are met.
Having a method like this will also let you prevent the display of the delete option to the user - which will improve the user experience as the user won't be able to perform an illegal operation.
I ended up using code from here to create a can_destroy override on activerecord:
https://gist.github.com/andhapp/1761098
class ActiveRecord::Base
def can_destroy?
self.class.reflect_on_all_associations.all? do |assoc|
assoc.options[:dependent] != :restrict || (assoc.macro == :has_one && self.send(assoc.name).nil?) || (assoc.macro == :has_many && self.send(assoc.name).empty?)
end
end
end
This has the added benefit of making it trivial to hide/show a delete button on the ui
You can also use the before_destroy callback to raise an exception.
I have these classes or models
class Enterprise < AR::Base
has_many :products
before_destroy :enterprise_with_products?
private
def empresas_with_portafolios?
self.portafolios.empty?
end
end
class Product < AR::Base
belongs_to :enterprises
end
Now when you delete an enterprise this process validates if there are products associated with enterprises
Note: You have to write this in the top of the class in order to validate it first.
Use ActiveRecord context validation in Rails 5.
class ApplicationRecord < ActiveRecord::Base
before_destroy do
throw :abort if invalid?(:destroy)
end
end
class Ticket < ApplicationRecord
validate :validate_expires_on, on: :destroy
def validate_expires_on
errors.add :expires_on if expires_on > Time.now
end
end
I was hoping this would be supported so I opened a rails issue to get it added:
https://github.com/rails/rails/issues/32376