I have 3 models as follows:
class Document < ActiveRecord::Base
has_many :documents_tasks, inverse_of: :document
has_many :tasks, through: :documents_tasks, dependent: :destroy
end
class Task < ActiveRecord::Base
has_many :documents_tasks, inverse_of: :task
has_many :documents, through: :documents_tasks, dependent: :destroy
end
class DocumentsTask < ActiveRecord::Base
belongs_to :task, inverse_of: :documents_tasks
belongs_to :document, inverse_of: :documents_tasks
validates_uniqueness_of :document_id, scope: :task_id
end
In the above when I try to update the record for a Task it throws a validation error for duplicate entries on the DocumentsTask model if I keep the validation or directly inserts duplicates if remove the validation.
My code to update the Task record is:
def update
#task = #coach.tasks.find(params[:id])
#task.update(:name => task_params[:name], :description => task_params[:description] )
#task.documents << Document.find(task_params[:documents])
if #task.save
render 'show'
else
render status: 500, json: {
error: true,
reason: #task.errors.full_messages.to_sentence
}
end
end
I know I can add unique index to the db to automatically prevent duplicate entries but is there some way I can prevent the controller from updating the join table values when they're the same?
So when I attempt to update the associated documents, ex:
I had document 5 initially
Now I add document 6 and call the update function
It attempts to re-add both documents 5 and 6 to the db so I get the error:
Completed 422 Unprocessable Entity in 9176ms
ActiveRecord::RecordInvalid (Validation failed: Document has already been taken)
This is because I added the following validation:
validates_uniqueness_of :document_id, scope: :task_id
in my DocumentsTask model as shown above. The issue is how can I prevent it from attempting to re-add existing records
Assuming that task_params[:documents] is an array of document ids (based on how you're using it with find now), you should be able to do something like this as a quick fix:
#task.documents << Document.where(id: task_params[:documents]).where.not(task_id: #task.id)
Basically this just filters out the documents that are already associated to the given task before assigning them to the task.
That said, I'd suggest something more robust as a long term solution. A couple of options (among many) would be extracting the responsibility of task creation out into it's own class (so you can more easily test it and make that functionality more portable), or you could look into overriding the setter method(s) for documents in your task model similar to what this answer describes: https://stackoverflow.com/a/2891245/456673
Related
Background
I want to save skills on different types of objects. Typically, a user can have a project or experience, and tag it with different skills etc. Ruby , python or Excel. These skills are global and used across the various possible objects that are "skillable".
The problem consists of three models:
Project - contains info about the project
Skill - contains the name of the skill
SkillObject - join table between skill and projects
(polymorphic)
Please look at this ER-diagram for a more detailed picture.
The problem
When I create or update a project, I also want the skills to be added in the same transaction, just by sending the names of the skills from the front-end.
I want a kind of find_or_create_by to avoid duplicate skills. At the same time, validations must ensure that two identical skills cannot be put on the same project (skill_object.rb) and that the skill name cannot be zero (skill.rb)
The wanted behavior is if the validation of skills is not successful, then either the project nor the skills are stored in the database. If you enter two different skill names and one is already in the database, the one that exists should be found and connected to the project, and the other created and so connected through the join table, if not validations fails.
project.rb
class Project < ApplicationRecord
include Skillable
[...]
end
skill_object.rb
class SkillObject < ApplicationRecord
belongs_to :skill, inverse_of: :skill_objects
belongs_to :skillable, polymorphic: true
delegate :delete_if_empty_skill_objects, to: :skill
after_destroy :delete_if_empty_skill_objects
# Avoiding duplicates of the same skill
validates :skill, uniqueness: { scope: :skillable }
end
skill.rb
class Skill < ApplicationRecord
has_many :skill_objects, inverse_of: :skill
has_many :projects, through: :skill_objects, source: :sectorable, source_type: 'Project'
has_many :experiences, through: :skill_objects, source: :sectorable, source_type: 'Experience'
validates :name, uniqueness: true, presence: true
def delete_if_empty_skill_objects
self.destroy if self.skill_objects.empty? and not self.is_global
end
end
I use a concern that is included on the different types of skillable objects:
concercns/skillable.rb
module Skillable
extend ActiveSupport::Concern
included do
attr_accessor :skills_attributes
after_save :set_skills
# Adding needed relations for skillable objects
has_many :skill_objects, -> { order(created_at: :asc) }, as: :skillable, dependent: :destroy
has_many :skills, through: :skill_objects
private
def set_skills
# THIS CODE IS THE ONE I AM STRUGGLING WITH
# Parsing out all the skill names
skill_names = skills_attributes.pluck(:name)
# Removing existing skills
self.skills = []
# Building new skills
skill_names.each do |name|
existing = Skill.find_by(name: name)
if existing
self.skills << existing
else
self.skills.new(name: name)
end
end
raise ActiveRecord::Rollback unless self.valid?
self.skills.each(&:save)
end
end
end
Does anyone know how I can write the set_skill function in skillable.rb to be able to save and update skills, validate the parent object and the skills, do a rollback if not validated and add the appropriate errors to the project object if it failes?
I have an issue creating a new record with nested associations in a clean way. Here's the controller code:
#listing = current_user.listings.build(params[:listing].permit(ATTRIBUTES_FOR_CREATE))
This builds an entity with several nested associations, like this:
class ListingDataField < ActiveRecord::Base
belongs_to :listing
validates_presence_of :listing
end
However, when I do #listing.save in controller, I get validation errors on those nested ListingDataField entities that 'listing can't be blank'. If I understand correctly, AutosaveAssociation first validates and saves nested associations, and eventually saves top-level entity. Thus, it fails validating ListingDataField, because Listing is not yet saved.
But I believe it's right having :listing validation in ListingDataField, so I wouldn't consider removing this. I can see 2 solutions:
in transaction - save Listing record, then build nested associations
one by one
#listing.save(:validate => false) but this is too ugly
Both aren't as much elegant as current_user.listings.build(...), so my question is - what is the proper Rails way for this?
P.S. I searched SO for similar question but I couldn't find any, hopefully this is not a duplicate :)
Have you tried adding:
class ListingDataField < ActiveRecord::Base
belongs_to :listing, inverse_of: :listing_data_fields
validates :listing, presence: true
end
and
class Listing < ActiveRecord::Base
has_many :listing_data_fields, inverse_of: :listing
end
This should make validation of presence work.
I have three activerecord classes: Klass, Reservation and Certificate
A Klass can have many reservations, and each reservation may have one Certificate
The definitions are as follows...
class Klass < ActiveRecord::Base
has_many :reservations, dependent: :destroy, :autosave => true
has_many :certificates, through: :reservations
attr_accessible :name
def kill_certs
begin
p "In Kill_certs"
self.certificates.destroy_all
p "After Destroy"
rescue Exception => e
p "In RESCUE!"
p e.message
end
end
end
class Reservation < ActiveRecord::Base
belongs_to :klass
has_one :certificate, dependent: :destroy, autosave: true
attr_accessible :klass_id, :name
end
class Certificate < ActiveRecord::Base
belongs_to :reservation
attr_accessible :name
end
I would like to be able to delete/destroy all the certificates for a particular klass within the klass controller with a call to Klass#kill_certs (above)
However, I get an exception with the message:
"In RESCUE!"
"Cannot modify association 'Klass#certificates' because the source
reflection class 'Certificate' is associated to 'Reservation' via :has_one."
I('ve also tried changing the reservation class to "has_many :certificates", and then the error is...
"In RESCUE!"
"Cannot modify association 'Klass#certificates' because the source reflection
class 'Certificate' is associated to 'Reservation' via :has_many."
It's strange that I can do Klass.first.certificates from the console and the certs from the first class are retrieved, but I can't do Klass.first.certificates.delete_all with out creating an error. Am I missing something?
Is the only way to do this..
Klass.first.reservations.each do |res|
res.certificate.destroy
end
Thanks for any help.
RoR docs have clear explanation for this (read bold only for TLDR):
Deleting from associations
What gets deleted?
There is a potential pitfall here: has_and_belongs_to_many and
has_many :through associations have records in join tables, as well as
the associated records. So when we call one of these deletion methods,
what exactly should be deleted?
The answer is that it is assumed that deletion on an association is
about removing the link between the owner and the associated
object(s), rather than necessarily the associated objects themselves.
So with has_and_belongs_to_many and has_many :through, the join
records will be deleted, but the associated records won’t.
This makes sense if you think about it: if you were to call
post.tags.delete(Tag.find_by(name: 'food')) you would want the ‘food’
tag to be unlinked from the post, rather than for the tag itself to be
removed from the database.
However, there are examples where this strategy doesn’t make sense.
For example, suppose a person has many projects, and each project has
many tasks. If we deleted one of a person’s tasks, we would probably
not want the project to be deleted. In this scenario, the delete
method won’t actually work: it can only be used if the association on
the join model is a belongs_to. In other situations you are expected
to perform operations directly on either the associated records or the
:through association.
With a regular has_many there is no distinction between the
“associated records” and the “link”, so there is only one choice for
what gets deleted.
With has_and_belongs_to_many and has_many :through, if you want to
delete the associated records themselves, you can always do something
along the lines of person.tasks.each(&:destroy).
So you can do this:
self.certificates.each(&:destroy)
I have tried to find a solution for this but most of the literature around involves how to create the form rather than how to save the stuff in the DB. The problem I am having is that the accepts_nested_attributes_for seems to work ok when saving modifications to existing DB entities, but fails when trying to create a new object tree.
Some background. My classes are as follows:
class UserGroup < ActiveRecord::Base
has_many :permissions
has_many :users
accepts_nested_attributes_for :users
accepts_nested_attributes_for :permissions
end
class User < ActiveRecord::Base
has_many :user_permissions
has_many :permissions, :through => :user_permissions
belongs_to :user_group
accepts_nested_attributes_for :user_permissions
end
class Permission < ActiveRecord::Base
has_many :user_permissions
has_many :users, :through => :user_permissions
belongs_to :user_group
end
class UserPermission < ActiveRecord::Base
belongs_to :user
belongs_to :permission
validates_associated :user
validates_associated :permission
validates_numericality_of :threshold_value
validates_presence_of :threshold_value
default_scope order("permission_id ASC")
end
The permission seem strange but each of them has a threshold_value which is different for each user, that's why it is needed like this.
Anyway, as I said, when I PUT an update, for example to the threshold values, everything works ok. This is the controller code (UserGroupController, I am posting whole user groups rather than one user at a time):
def update
#ug = UserGroup.find(params[:id])
#ug.update_attributes!(params[:user_group])
respond_with #ug
end
A typical data coming in would be:
{"user_group":
{"id":3,
"permissions":[
{"id":14,"name":"Perm1"},
{"id":15,"name":"Perm2"}],
"users":[
{"id":7,"name":"Tallmaris",
"user_permissions":[
{"id":1,"permission_id":14,"threshold_value":"0.1"},
{"id":2,"permission_id":15,"threshold_value":0.3}]
},
{"name":"New User",
"user_permissions":[
{"permission_id":14,"threshold_value":0.4},
{"permission_id":15,"threshold_value":0.2}]
}]
}
}
As you can see, the "New User" has no ID and his permission records have no ID either, because I want everything to be created. The "Tallmaris" user works ok and the changed values are updated no problem (I can see the UPDATE sql getting run by the server); on the contrary, the new user gives me this nasty log:
[...]
User Exists (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."name" = 'New User' LIMIT 1
ModelSector Load (8.7ms) SELECT "user_permissions".* FROM "user_permissions" WHERE (user_id = ) ORDER BY permission_id ASC
PG::Error: ERROR: syntax error at or near ")"
The error is obviously the (user_id = ) with nothing, since of course the user does not exists, there are no user_permissions set already and I wanted them to be created on the spot.
Thanks to looking around to this other question I realised it was a problem with the validation on the user.
Basically I was validating that the threshold_values summed up within certain constraints but to do that I was probably doing something wrong and Rails was loading data from the DB, which was ok for existing values but of course there was nothing for new values.
I fixed that and now it's working. I'll leave this here just as a reminder that often a problem in one spot has solutions coming from other places. :)
Relationships
class Promotion < ActiveRecord::Base
has_many :promotion_sweepstakes,
has_many :sweepstakes,
:through => :promotion_sweepstakes
end
class PromotionSweepstake < ActiveRecord::Base
belongs_to :promotion
belongs_to :sweepstake
end
class Sweepstake < ActiveRecord::Base
# Not relevant in this question, but I included the class
end
So a Promotion has_many Sweepstake through join table PromotionSweepstake. This is a legacy db schema so the naming might seem a bit odd and there are some self.table_name == and foreign_key stuff left out.
The nature of this app demands that at least one entry in the join table is present for a promotionId, because not having a sweepstake would break the app.
First question
How can I guarantee that there is always one entry in PromotionSweepstake for a Promotion? At least one Sweepstake.id has to be included upon creation, and once an entry in the join table is created there has to be a minimum of one for each Promotion/promotion_id.
Second question (other option)
If the previous suggestion would not be possible, which I doubt is true, there's another way the problem can be worked around. There's a sort of "default Sweepstake" with a certain id. If through a form all the sweepstake_ids would be removed (so that all entries for the Promotion in the join table would be deleted), can I create a new entry in PromotionSweepstake?
pseudo_code (sort of)
delete promotion_sweepstake with ids [1, 4, 5] where promotion_id = 1
if promotion with id=1 has no promotion_sweepstakes
add promotion_sweepstake with promotion_id 1 and sweepstake_id 100
end
Thank you for your help.
A presence validation should solve the problem in case of creation and modification of Promotions.
class Promotion < ActiveRecord::Base
has_many :promotion_sweepstakes
has_many :sweepstakes,
:through => :promotion_sweepstakes
validates :sweepstakes, :presence => true
end
In order to assure consistency when there's an attempt to delete or update a Sweepstake or a PromotionSweepstake you'd have to write your own validations for those two classes. They would have to check whether previously referenced Promotions are still valid, i.e. still have some Sweepstakes.
A simple solution would take and advantage of validates :sweepstakes, :presence => true in Promotion. After updating referenced PromotionSweepstakes or Sweepstakes in a transaction you would have to call Promotion#valid? on previously referenced Promotions. If they're not valid you roll back the transaction as the modification broke the consistency.
Alternatively you could use before_destroy in both PromotionSweepstake and Sweepstake in order to prevent changes violating your consistency requirements.
class PromotionSweepstake < ActiveRecord::Base
belongs_to :promotion
belongs_to :sweepstake
before_destroy :check_for_promotion_on_destroy
private
def check_for_promotion_on_destroy
raise 'deleting the last sweepstake' if promotion.sweepstakes.count == 1
end
end
class Sweepstake < ActiveRecord::Base
has_many :promotion_sweepstakes
has_many :promotions, :through => :promotion_sweepstakes
before_destroy :check_for_promotions_on_destroy
private
def check_for_promotions_on_destroy
promotions.each do |prom|
raise 'deleting the last sweepstake' if prom.sweepstakes.count == 1
end
end
end