Rails after_initialize only on "new" - ruby-on-rails

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.

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

How to hide records, rather than delete them (soft delete from scratch)

Let's keep this simple. Let's say I have a User model and a Post model:
class User < ActiveRecord::Base
# id:integer name:string deleted:boolean
has_many :posts
end
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
end
Now, let's say an admin wants to "delete" (hide) a post. So basically he, through the system, sets a post's deleted attribute to 1. How should I now display this post in the view? Should I create a virtual attribute on the post like this:
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
def administrated_content
if !self.deleted
self.content
else
"This post has been removed"
end
end
end
While that would work, I want to implement the above in a large number of models, and I can't help feeling that copy+pasting the above comparative into all of my models could be DRYer. A lot dryer.
I also think putting a deleted column in every single deletable model in my app feels a bit cumbersome too. I feel I should have a 'state' table. What are your thoughts on this:
class State
#id:integer #deleted:boolean #deleted_by:integer
belongs_to :user
belongs_to :post
end
and then querying self.state.deleted in the comparator? Would this require a polymorphic table? I've only attempted polymorphic once and I couldn't get it to work. (it was on a pretty complex self-referential model, mind). And this still doesn't address the problem of having a very, very similar class method in my models to check if an instance is deleted or not before displaying content.
In the deleted_by attribute, I'm thinking of placing the admin's id who deleted it. But what about when an admin undelete a post? Maybe I should just have an edited_by id.
How do I set up a dependent: :destroy type relationship between the user and his posts? Because now I want to do this: dependent: :set_deleted_to_0 and I'm not sure how to do this.
Also, we don't simply want to set the post's deleted attributes to 1, because we actually want to change the message our administrated_content gives out. We now want it to say, This post has been removed because of its user has been deleted. I'm sure I could jump in and do something hacky, but I want to do it properly from the start.
I also try to avoid gems when I can because I feel I'm missing out on learning.
I usually use a field named deleted_at for this case:
class Post < ActiveRecord::Base
scope :not_deleted, lambda { where(deleted_at: nil) }
scope :deleted, lambda { where("#{self.table_name}.deleted_at IS NOT NULL") }
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
destroy
end
def deleted?
self.deleted_at.present?
end
# ...
Want to share this functionnality between multiple models?
=> Make an extension of it!
# lib/extensions/act_as_fake_deletable.rb
module ActAsFakeDeletable
# override the model actions
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
self.destroy
end
def undestroy # to "restore" the file
self.update(deleted_at: nil)
end
def undelete
self.undestroy
end
# define new scopes
def self.included(base)
base.class_eval do
scope :destroyed, where("#{self.table_name}.deleted_at IS NOT NULL")
scope :not_destroyed, where(deleted_at: nil)
scope :deleted, lambda { destroyed }
scope :not_deleted, lambda { not_destroyed }
end
end
end
class ActiveRecord::Base
def self.act_as_fake_deletable(options = {})
alias_method :destroy!, :destroy
alias_method :delete!, :delete
include ActAsFakeDeletable
options = { field_to_hide: :content, message_to_show_instead: "This content has been deleted" }.merge!(options)
define_method options[:field_to_hide].to_sym do
return options[:message_to_show_instead] if self.deleted_at.present?
self.read_attribute options[:field_to_hide].to_sym
end
end
end
Usage:
class Post < ActiveRecord::Base
act_as_fake_deletable
Overwriting the defaults:
class Book < ActiveRecord::Base
act_as_fake_deletable field_to_hide: :title, message_to_show_instead: "This book has been deleted man, sorry!"
Boom! Done.
Warning: This module overwrite the ActiveRecord's destroy and delete methods, which means you won't be able to destroy your record using those methods anymore. Instead of overwriting you could create a new method, named soft_destroy for example. So in your app (or console), you would use soft_destroy when relevant and use the destroy/delete methods when you really want to "hard destroy" the record.

Skipping :touch associations when saving an ActiveRecord object

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.

Memoize current user object before_create

Using Rails 3.2. Let's say I have 10 new Photos to be uploaded, I need to associate my current_user.id to each new record. For some reasons, the photos_controller.rb is blank because it's a nested with another model Shop. Here is my code:
class Photo < ActiveRecord::Base
belongs_to :attachable, :polymorphic => true, :counter_cache => true
belongs_to :user, :counter_cache => true
before_create :current_user_id
before_create :associate_current_user
def current_user_id
#current_user_id ||= UserSession.find.user.id
end
private
def associate_current_user
self.user_id = #current_user_id
end
end
It is clear that if there are 10 new records to be created, I want the model to find the current_user once, and then take it from the cache (a memoization technique), but because I am using before_create, the current_user is queried 10 times instead of getting it from cache.
What can I do to cache the #current_user_id?
Thanks.
The answer is simple : you should not do anything related to the session in your model, it breaks the MVC pattern.
Instead, do this in your controller, so you only have to get current_user.id once, and assign it to your records.
This sort of logic belongs in the controller. Move your current_user_id method to your PhotosController (or to ApplicationController if you plan to use this logic in other controllers too). That way it will assign #current_user only once per upload action.
Be sure to make it private too.

Child not being created from Parent model?

I have a checkbox that if checked allows my child resource called Engineer to be created. I'm trying to create it through my model since that's where I can call the after_save method.
Here is my code:
models/user.rb
class User < ActiveRecord::Base
has_many :armies
has_many :engineers
end
models/army.rb
class Army < ActiveRecord::Base
has_many :engineers
attr_reader :siege
after_save :if_siege
private
def if_siege
if self.siege
Engineer.create!( :user_id => current_user.id, :army_id => self.id )
end
end
end
models/engineer.rb
class Engineer < ActiveRecord::Base
belongs_to :user
belongs_to :army
end
controllers/armies_controller.rb
def new
#army = Army.new
end
def create
#army = current_user.armies.build(params[:army])
if #army.save
redirect_to new_army_path
else
render :new
end
end
end
This gives me an error though for my if_siege method:
undefined local variable or method `current_user'
How can I fix this or is there another way to do this? Not sure if this should go in the controller or model but I only can wrap my head around putting this in the model.
Thanks.
Add belongs_to :user to the Army model
In Army#if_siege, update Engineer.create! as follows
Engineer.create!( :user_id => self.user.id, :army_id => self.id )
First, the current_user object won't exist within the context of the Model layer unless your authentication is doing something to make it available. This is usually a non Threadsafe approach though. Maybe for you this isn't the issue.
Current User Instantiation
Having said that, one way (perhaps not the ideal way) to address this is by creating an attr_accessor in the model on the object called Army. Then set the current_user to this in the Army new action in the controller where the current_user instance is available.
# in the Army model
attr_accessor :the_user
# in the Army Controller
#army = Army.new(:the_user => current_user.id)
You will also have to add a hidden field to store this value in your view to carry this through to the create action.
Just an observation, but I'm fairly sure in the "if_seige" method the self calls are redundant. self should already be scoped to the Army object in that method.

Resources