I have a the following models:
class Part
belongs_to :note, inverse_of: :part, dependent: :destroy
class Note
has_many :attachments, as: :attachable, dependent: :destroy
has_one :part, inverse_of: :note
end
class Attachment
belongs_to :attachable, polymorphic: true, touch: true
end
When I add attachments to a note, I do it via the part
def update
#part = current_user.parts.find params[:id]
#part.note.update note_params
…
end
What I find strange is the following:
def update
#part = current_user.parts.find params[:id]
#part.note.update note_params
#part.note.attachments.any? # => false
#part.note.attachments.any? # => true
end
Why does the first call return false? Since I need this in my view, I'm left with calling #part.note.reload, but I can't understand what is going on.
Thanks
Associations are cached for performance reasons. Use association(true) to bypass the cache and force Rails to refetch the current state after you have done something to change it:
#part.note(true).attachments.any?
See Association Basic: Controlling Caching.
Related
In my Game model, I have
class Game < ApplicationRecord
has_many :assignments
has_many :users, through: :assignments
accepts_nested_attributes_for :assignments
after_create :create_assignments
def create_assignments
3.times { Assignment.create!(game_id: id, user_id: 1) }
end
end
This works but I need the user_id to be blank when it's created (it will be edited at a later time). It won't create assignments unless there is a user_id with a value.
I've tried leaving it blank and omitting it, but that doesn't work. I think I need to put optional: true somewhere but I'm pretty stumped on this one.
Needed to add :optional => true on Assignment Model
class Assignment < ApplicationRecord
belongs_to :game
belongs_to :user, :optional => true
end
I'm trying to solve validation of associated object with condition.
User doesn't need to have filled author_bio until he is author. So app needs to ensure, that author can't create post without author_bio and author_bio can't be deleted if user already created any post.
class User < ApplicationRecord
has_many :posts, foreign_key: 'author_id', inverse_of: :author
validates :author_bio, presence: { if: :author? }
def author?
posts.any?
end
end
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', inverse_of: :posts, required: true
end
Unfortunately this doesn't validate author on creation of new post:
user = User.first
user.author_bio
=> nil
post = Post.new(author: user)
post.valid?
=> true
post.save
=> true
post.save
=> false
post.valid?
=> false
So how can I prevent creating of new post by user without author_bio? I can add second validation to Post model, but this is not DRY. Is there any better solution?
The answer here seems to be use of validates_associated once you have your associations correctly set up (including inverse_of which you have, but stating for others, rails in many cases misses them or creates them incorrectly)
so to adjust the classes here:
class User < ApplicationRecord
has_many :posts, foreign_key: 'author_id', inverse_of: :author
validates :author_bio, presence: { if: :author? }
def author?
posts.any?
end
end
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', inverse_of: :posts
validates :author, presence: true
validates_associated :author
end
Now when you try running what you did before:
user = User.first
user.author_bio
=> nil
post = Post.new(author: user)
post.valid?
=> false
post.save
=> false
Does not allow you to save since author_bio is empty
Only thing to watch out there was to set up correct associations, otherwise rails was confused and skipping validation on User class since it thought the relationship is not yet in existence.
NOTE: I removed required: true from belongs_to since in rails 5 is default, therefore you won't need validates :author, presence: true either only in rails 5.
If you want to go straight to the question, just go to the last paragraph.
A Pack has many items included, Item is polymorphic and one of the linked tables is Access (so Access is an Item that can be added to the Pack)
Here you are the models and controller.
class Pack < ActiveRecord::Base
has_many :pack_items, dependent: :destroy
has_many :items_included, through: :pack_items, source: :item
accepts_nested_attributes_for :pack_items, allow_destroy: true
validate :valid_max_value, if: :infinite_item?
end
class Item < ActiveRecord::Base
has_many :pack_items, dependent: :restrict_with_error
has_many :packs, through: :pack_items
end
class Access < ActiveRecord::Base
has_one :item, as: :itemable, dependent: :destroy
has_one :entitlement, as: :entitlementable, dependent: :destroy
accepts_nested_attributes_for :item, allow_destroy: true
accepts_nested_attributes_for :entitlement, allow_destroy: true
validate :valid_max_value, if: :infinite?
private
def infinite?
entitlement.infinite
end
end
class PacksController < BaseController
def update
#pack = Pack.find(params[:id])
if #pack.update(permitted_params)
...
end
end
private
def permitted_params
params.require(:pack).permit(item_attributes: [:id, :name, :max_purchasable],
pack_items_attributes: [:id, :item_id, :amount, :_destroy])
end
end
There is an importan validation in pack "valid_max_value. If a pack has an infinite Access inside, the max_value of the Pack should never be higher than 1.
It works perfectly when I create a pack and I add some Accesses, but the problem is this:
I have a Pack with two Items. An Access that's infinite and a common Access(not infinite). So the Pack's max_value should be 1 because it has an infinite Access inside.
Now I edit that Pack and I delete the infinite Access, so now I can select a higher max_value, 5 in example, because the pack doesn't have an Access with restriction inside.
When I click update there is a rollback because the valid_max_value validation runs before the deletion of the infinite Access, so it says the max_value is invalid because the validation depends on a child field.
In short, my question is: How can I delete the nested items before run the parent validation?
You do not have to actually delete the items before validation, check marked_for_destruction? instead in your validations, so that items that are to be deleted will be ignored
I have many associations in my model. Every record associated with parent object is saved every time parent is saved. It is behavious i want, just not every time. Is there a special method to save JUST the 'mother' object?
The ability to autosave associated models can be turned on or off on a per-association basis, like so:
class Post < ActiveRecord::Base
has_one :author, autosave: false
has_many :things, autosave: false
end
Read all about it here: https://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
Here is sample not sure this is what you are looking for??
class Survey < ActiveRecord::Base
has_many :questions, :dependent => :destroy
accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
end
class Question < ActiveRecord::Base
belongs_to :survey
has_many :answers, :dependent => :destroy
accepts_nested_attributes_for :answers, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
end
Here Survey has many Questions, So I assume from your question that when survey is build up Question should not be saved?? Lets now have a look to controller of Survey.
surveys_controller.rb
def new
#survey = Survey.new
#survey.build # here I have commented below lines which create the object for questions also
# 3.times do
# question = #survey.questions.build
# 4.times { question.answers.build }
# end
end
def create
#survey = Survey.new(params[:survey])
if #survey.save
flash[:notice] = "Successfully created survey."
redirect_to #survey
else
render :action => 'new'
end
end
As far as I know, by default a belongs_to association won't get saved if you save the parent class (model).
To enable this feature you need autosave.
Eg.
class Post < ActiveRecord::Base
has_one :author, autosave: true
end
However, looking further I found out this default behavior varies for different associations. Have a look at this answer: When will ActiveRecord save associations?
Maybe accepts_nested_attributes_for in your model. I have the same problem. I set it as save: false, but it doesn't make sense. I remove the accepts_nested_attributes_for, and then it makes sense. Hope it will help you.
Unfortunately there's no such functionality.
In case of has_many association, the child is saved on saving its parent if child is #new_record?.
I'd avoid saving it by not permitting params, if only it's modified in controller.
For example, I have three model user, question and answer, and the relationship between them are:
class User < ActiveRecord::Base
has_many :answers
has_many :questions
end
class Question < ActiveRecord::Base
has_many :answers, :dependent => :destroy
belongs_to :user, :counter_cache => true
end
class Answer < ActiveRecord::Base
belongs_to :user, :counter_cache => true
belongs_to :question, :counter_cache => true
end
Then when i want to destroy a question(with 1000 answers), these will happen:
The answers will destroy one by one and will update the counter in the user model, even the counter in the question i want to destroy, and which will take a long time to do the counter update.
my question is how to make it faster?
I got my own solution like this:
step 1:
remove the dependent destroy, which will call for counter update before destroy itself.
step 2:
add my own before_destroy, like this
before_destroy :delete_dependents
and use delete_all function to delete without call any before_destroy, then call reset_counters function to reset the counter in User model.
full codes of class question:
class Question < ActiveRecord::Base
has_many :answers
has_many :answer_users, :through => :answers, :source => :user, :uniq => true
belongs_to :user, :counter_cache => true
before_destroy :delete_dependents
private
def delete_dependents
answer_user_ids = self.answer_user_ids # eager loading for store the answer users id
Answer.delete_all(:question_id => self.id)
answer_user_ids.each do |u_id|
User.reset_counters u_id, :answers
end
end
end
PS: If there are too many counters need to be reset, you might need a background job to work it out.