Prevent join table being empty - ruby-on-rails

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

Related

Find or create nested attributes in a polymorphic join table, in one transaction

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?

Rails don't save if duplicate

I have a somewhat complex Rails model setup that I'll try to simplify as much as possible. The goal of this setup is to be able to have objects (Person, Pet) that are long-lived, but with relationships between them changing each year via TemporalLink. Basically, I have these models:
class Person < ActiveRecord::Base
include TemporalObj
has_many :pet_links, class_name: "PetOwnerLink"
has_many :pets, through: :pet_links
end
class Pet < ActiveRecord::Base
include TemporalObj
has_many :owner_links, class_name: "PetOwnerLink"
has_many :owners, through: :owner_links
end
class PetOwnerLink < ActiveRecord::Base
include TemporalLink
belongs_to :owner
belongs_to :pet
end
and these concerns:
module TemporalLink
extend ActiveSupport::Concern
# Everything that extends TemporalLink must have a `year` attribute.
end
module TemporalObj
extend ActiveSupport::Concern
# Everything that extends TemporalObj must have a find_existing() method.
####################
# Here be dragons! #
####################
end
The desired behavior is:
When creating a TemporalObj (Pet, Person):
1) Check to see if there is an existing one, based on certain conditions, with find_existing().
2) If an existing duplicate is found, don't perform the create but still perform necessary creations to associated objects. (This seems to be the tricky part.)
3) If no duplicate is found, perform the create.
4) [Existing magic already auto-creates the necessary TemporalLink objects.]
When destroying a TemporalObj:
1) Check to see if the object exists in more than one year. (This is simpler in actuality than in this example.)
2) If the object exists in only one year, destroy it and associated TemporalLinks.
3) If the object exists in more than one year, just destroy one of the TemporalLinks.
My problem is I have uniqueness validations on many TemporalObjs, so when I try to create a new duplicate, the validation fails before I can perform any around_create magic. Any thoughts on how I can wrangle this to work?
You can (and should) use Rails' built-in validations here. What you've described is validates_uniqueness_of, which you can scope to include multiple columns.
For example:
class TeacherSchedule < ActiveRecord::Base
validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
end
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of
In response to JacobEvelyn's comment, this is what I did.
Created a custom validate like so
def maintain_uniqueness
matching_thing = Thing.find_by(criteria1: self.criteria1, criteria2: self.criteria2)
if !!matching_thing
self.created_at = matching_thing.created_at
matching_thing.delete
end
true
end
Added it to my validations
validate :maintain_event_uniqueness
It worked.

Referencing all of a collection of dependent objects objects

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)

How to combine duplicate rails objects and update all references

I'm working on a Rails app (Ruby 1.9.2 / Rails 3.0.3) that keeps track of people and their memberships to different teams over time. I'm having trouble coming up with a scalable way to combine duplicate Person objects. By 'combine' I mean to delete all but one of the duplicate Person objects and update all references to point to the remaining copy of that Person. Here's some code:
Models:
Person.rb
class Person < ActiveRecord::Base
has_many :rostered_people, :dependent => :destroy
has_many :rosters, :through => :rostered_people
has_many :crews, :through => :rosters
def crew(year = Time.now.year)
all_rosters = RosteredPerson.find_all_by_person_id(id).collect {|t| t.roster_id}
r = Roster.find_by_id_and_year(all_rosters, year)
r and r.crew
end
end
Crew.rb
class Crew < ActiveRecord::Base
has_many :rosters
has_many :people, :through => :rosters
end
Roster.rb
class Roster < ActiveRecord::Base
has_many :rostered_people, :dependent => :destroy
has_many :people, :through => :rostered_people
belongs_to :crew
end
RosteredPerson.rb
class RosteredPerson < ActiveRecord::Base
belongs_to :roster
belongs_to :person
end
Person objects can be created with just a first and last name, but they have one truly unique field called iqcs_num (think of it like a social security number) which can be optionally stored on either the create or update actions.
So within the create and update actions, I would like to implement a check for duplicate Person objects, delete the duplicates, then update all of the crew and roster references to point to the remaining Person.
Would it be safe to use .update_all on each model? That seems kind of brute force, especially since I will probably add more models in the future that depend on Person and I don't want to have to remember to maintain the find_duplicate function.
Thanks for the help!
The 'scalable' way to deal with this is to make the de-duplication process part of the app's normal function - whenever you save a record, make sure it's not a duplicate. You can do this by adding a callback to the Person model. Perhaps something like this:
before_save :check_for_duplicate
def check_for_duplicate
if iqcs_num
dup = Person.find_by_iqcs_num(self.iqcs_num)
if dup && dup.id != self.id
# move associated objects to existing record
dup.crews = dup.crews + self.crews
# update existing record
dup.update_attributes(:name => self.name, :other_field => self.other_field)
# delete this record
self.destroy
# return false, so that no other callbacks get triggered
return false
end
end
end
You'll want to make sure that you index the table you store Person objects in on the iqcs_num column, so that this lookup stays efficient as the number of records grows - it's going to be performed every time you update a Person record, after all.
I don't know that you can get out of keeping the callback up-to-date - it's entirely likely that different sorts of associated objects will have to be moved differently. On the other hand, it only exists in one place, and it's the same place you'd be adding the associations anyway - in the model.
Finally, to make sure your code is working, you'll probably want to add a validation on the Person model that prevents duplicates from existing. Something like:
validates :iqcs_num, :uniqueness => true, :allow_nil => true

De-Normalizing Sum of associated objects in rails

I'm working on a gift registry app using rails 3.0. The app allows multiple guests to contribute towards a gift. When displaying the gifts I need to show the amount remaining, and if the total amount has been given I needs to show the item as purchased.
For performance I want to de-normalize the sum of the total contributions, and the status of the item.
Seems simple enough, but as I tried to figure out how to put this in the model in a way that is completely encapsulated, and works in all circumstances, things got much more complicated then I expected.
I tried a few different approaches including a callback on the association between item and contribution, but ultimately ended up with a callback on the contribution object.
item.rb
class Item < ActiveRecord::Base
# decimal amount
# decimal total_contributed
# boolean purchased
has_many :contributions, :inverse_of => :item
def set_total_contributed
self.total_contributed = 0
contributions.each do |cont|
self.total_contributed += cont.amount
end
purchased = self.total_contributed >= amount
end
end
order.rb
class Order < ActiveRecord::Base
has_many :contributions, :inverse_of => :order, :dependent => :destroy
end
contribution.rb
class Contribution < ActiveRecord::Base
belongs_to :item, :inverse_of => :contributions
belongs_to :order, :inverse_of => :contributions
after_create do |cont|
item.set_total_contributed
item.save
end
after_destroy do |cont|
item.contributions.delete(cont)
item.set_total_contributed
item.save
end
end
This appears to work in the situations I need it to, but it doesn't feel right.
First, the fact that I have to manually update the contributions association in the destroy callback seems odd.
Also, the de-normalized values are only properly updated when objects are persisted.
So the question is, how can I do this better and what's the best practice for this kind of scenario?

Resources