Skipping :touch associations when saving an ActiveRecord object - ruby-on-rails

Is there a way to skip updating associations with a :touch association when saving?
Setup:
class School < ActiveRecord::Base
has_many :students
end
class Student < ActiveRecord::Base
belongs_to :school, touch: true
end
I would like to be able to do something like the following where the touch is skipped.
#school = School.create
#student = Student.create(school_id: #school.id)
#student.name = "Trevor"
#student.save # Can I do this without touching the #school record?
Can you do this? Something like #student.save(skip_touch: true) would be fantastic but I haven't found anything like that.
I don't want to use something like update_column because I don't want to skip the AR callbacks.

As of Rails v4.1.0.beta1, the proper way to do this would be:
#school = School.create
#student = Student.create(school_id: #school.id)
ActiveRecord::Base.no_touching do
#student.name = "Trevor"
#student.save
end

One option that avoids directly monkey patching is to override the method that gets created when you have a relation with a :touch attribute.
Given the setup from the OP:
class Student < ActiveRecord::Base
belongs_to :school, touch: true
attr_accessor :skip_touch
def belongs_to_touch_after_save_or_destroy_for_school
super unless skip_touch
end
after_commit :reset_skip_touch
def reset_skip_touch
skip_touch = false
end
end
#student.skip_touch = true
#student.save # touch will be skipped for this save
This is obviously pretty hacky and depends on really specific internal implementation details in AR.

Unfortunately, no. save doesn't provide such option.
Work around this would be to have another time stamp attribute that functions like updated_at but unlike updated_at, it updates only on certain situations for your liking.

Related

Custom belongs_to attribute writer

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

Rails after_initialize only on "new"

I have the following 2 models
class Sport < ActiveRecord::Base
has_many :charts, order: "sortWeight ASC"
has_one :product, :as => :productable
accepts_nested_attributes_for :product, :allow_destroy => true
end
class Product < ActiveRecord::Base
belongs_to :category
belongs_to :productable, :polymorphic => true
end
A sport can't exist without the product, so in my sports_controller.rb I had:
def new
#sport = Sport.new
#sport.product = Product.new
...
end
I tried to move the creation of the product to the sport model, using after_initialize:
after_initialize :create_product
def create_product
self.product = Product.new
end
I quickly learned that after_initialize is called whenever a model is instantiated (i.e., from a find call). So that wasn't the behavior I was looking for.
Whats the way I should be modeling the requirement that all sport have a product?
Thanks
Putting the logic in the controller could be the best answer as you stated, but you could get the after_initialize to work by doing the following:
after_initialize :add_product
def add_product
self.product ||= Product.new
end
That way, it only sets product if no product exists. It may not be worth the overhead and/or be less clear than having the logic in the controller.
Edit: Per Ryan's answer, performance-wise the following would likely be better:
after_initialize :add_product
def add_product
self.product ||= Product.new if self.new_record?
end
Surely after_initialize :add_product, if: :new_record? is the cleanest way here.
Keep the conditional out of the add_product function
If you do self.product ||= Product.new it will still search for a product every time you do a find because it needs to check to see if it is nil or not. As a result it will not do any eager loading. In order to do this only when a new record is created you could simply check if it is a new record before setting the product.
after_initialize :add_product
def add_product
self.product ||= Product.new if self.new_record?
end
I did some basic benchmarking and checking if self.new_record? doesn't seem to affect performance in any noticeable way.
Instead of using after_initialize, how about after_create?
after_create :create_product
def create_product
self.product = Product.new
save
end
Does that look like it would solve your issue?
It looks like you are very close. You should be able to do away with the after_initialize call altogether, but first I believe if your Sport model has a "has_one" relationship with :product as you've indicated, then your Product model should also "belong_to" sport. Add this to your Product model
belongs_to: :sport
Next step, you should now be able to instantiate a Sport model like so
#sport = #product.sport.create( ... )
This is based off the information from Association Basics from Ruby on Rails Guides, which you could have a read through if I am not exactly correct
after_initialize :add_product, unless: :persisted?
You should just override initialize method like
class Sport < ActiveRecord::Base
# ...
def initialize(attributes = {})
super
self.build_product
self.attributes = attributes
end
# ...
end
Initialize method is never called when record is loaded from database.
Notice that in the code above attributes are assigned after product is build.
In such setting attribute assignment can affect created product instance.

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Struct Objects with ActiveRecord methods? (Ruby on Rails)

I have a limited set of objects (20 - 30) which I need to be able to combine with ActiveRecord Objects. Putting them into the DB just seems awful because I already have two other join models hooked up to the model.
So let's say i have a class
class Thing < ActiveRecord::Base
has_many :other_things, :class_name => 'OtherThing'
end
with an existing table. How would I be able to combine this with a class not inheriting from ActiveRecord (here's my best guess)
class OtherThing < ActiveRecord::Base
OtherThing = Struct.new(:id, :name, :age, :monkey_fighting_ability)
belongs_to :thing, :class_name => 'Thing'
validate :something
def self.search_for(something)
MY_GLOBAL_HASH[something].map do |hash|
instance = OtherThing.new
hash.each_pair do |k,v|
instance.send(:"#{k}=", v)
end
instance
end
end
#if AR wants to call save
def save
return true
end
alias save save!
protected
def something
self.errors.add(:monkey_fighting_ability, 'must be unlimited') if self.class.search_for(something).empty?
end
end
Point being that I want to use ActiveRecord methods and so on without ever hitting the db. Help is greatly appreciated.
I'd suggest reading the post on "Make any Ruby Object Feel Like An Active Record" by Yehuda Katz. It goes over how to convert any object into a model-like class, without the database backing.
Good Luck!

Using accepts_nested_attributes_for + mass assignment protection in Rails

Say you have this structure:
class House < ActiveRecord::Base
has_many :rooms
accepts_nested_attributes_for :rooms
attr_accessible :rooms_attributes
end
class Room < ActiveRecord::Base
has_one :tv
accepts_nested_attributes_for :tv
attr_accessible :tv_attributes
end
class Tv
belongs_to :user
attr_accessible :manufacturer
validates_presence_of :user
end
Notice that Tv's user is not accessible on purpose. So you have a tripple-nested form that allows you to enter house, rooms, and tvs on one page.
Here's the controller's create method:
def create
#house = House.new(params[:house])
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
Question: How in the world would you populate user_id for each tv (it should come from current_user.id)? What's the good practice?
Here's the catch22 I see in this.
Populate user_ids directly into params hash (they're pretty deeply nested)
Save will fail because user_ids are not mass-assignable
Populate user for every tv after #save is finished
Save will fail because user_id must be present
Even if we bypass the above, tvs will be without ids for a moment of time - sucks
Any decent way to do this?
Anything wrong with this?
def create
#house = House.new(params[:house])
#house.rooms.map {|room| room.tv }.each {|tv| tv.user = current_user }
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
I haven't tried this out, but it seems like the objects should be built and accessible at this point, even if not saved.

Resources