ActiveRecord models interdependencies and correlations - ruby-on-rails

I have a bunch of AR models that depend one to each other and I'm not able to figure out how to make everything work together...
First of all a Pet and a Dog model:
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true, dependent: :destroy
belongs_to :facility
has_many :pet_pictures, dependent: :destroy
validates :name, :birth, :facility, :pet_pictures, presence: true
end
class Dog < ActiveRecord::Base
has_one :pet, as: :animal
has_many :mixtures, as: :blended, dependent: :destroy
validates :mixtures, presence: true
end
Then both PetPictures and Mixtures:
class PetPicture < ActiveRecord::Base
belongs_to :pet
validates :photo_file, presence: true
end
class Mixture < ActiveRecord::Base
belongs_to :blended, polymorphic: true
validates :breed, presence: true
validates :breed, uniqueness: { scope: [:blended_id, :blended_type] }
end
The problem is that this is too complex and I'm having huge problem coordinating all the dependencies, I also had to remove some validations (a pic MUST have a pet related to it, but it's so difficult that I ended up removing it). It appears to me that there's no correct order of creation to end up with a valid object.
For example, take this spec:
RSpec.describe PetPicture, type: :model do
let(:dog) do
FactoryGirl.build(:marley,
mixtures: [mixture_yorkshire, mixture_dachsund],
pet: FactoryGirl.build(
:dog,
pet_pictures: [FactoryGirl.build(:marleys_pic),
FactoryGirl.build(:second_marleys_pic)],
facility: lida))
end
context 'when creating' do
it 'should be valid' do
# dog.save && dog.pet.save
dog.pet.pictures.each do |picture|
expect(picture).to be_valid
end
end
end
end
This spec passes only after saving by hand (if I FactoryGirl.create nothing is created due to validation errors related to the order of creation) BUT, and I seriously see no reason for this behavior, picture.pet_id is NULL.
Can you help me debug this? Any suggestion or link on how to improve/refactor/cleanup this mess is very welcome - just keep in mind that I adopted this for a reason, so a Pet has many pictures and can be a Dog, a Cat or whatever, which have all a different set of attributes very specific to their class. Also, a Dog can be 50% dachsund and 50% yorkshire, to explain the mixtures relation.
Thanks in advance.

I think if you are trying to check for every validations, you should do it one at a time, don't try create a bunch of them. For example:
RSpec.describe PetPicture, type: :model do
let(:pet) { FactoryGirl.create(:pet) }
let(:picture) do
FactoryGirl.build(:marleys_pic, pet: pet)
end
context 'when creating' do
it 'should be valid' do
expect(picture).to be_valid
end
end
end
And if you want something really clean and clear, try shoulda-matchers. Every associations and validations could be tested by only 1 line of code.

Related

FactoryBot ActiveRecord::AssociationTypeMismatch error with wrong class

Good morning,
I am working on a proof of concept Rails application after a long break from using Rails. I set up RSpec tests, as well as FactoryBot and Faker to generate test data. In my app, I have two models:
class Admin::Tenant < ApplicationRecord
has_rich_text :description
has_and_belongs_to_many :users,
association_foreign_key: :admin_user_id,
foreign_key: :admin_tenant_id
has_many :tenant_groups,
inverse_of: :tenant,
dependent: :destroy,
class_name: 'Tenant::Group'
validates :name,
presence: true,
length: { maximum: 255 }
end
class Tenant::Group < ApplicationRecord
has_rich_text :description
belongs_to :tenant,
class_name: 'Admin::Tenant',
inverse_of: :tenant_groups
validates :name,
presence: true,
length: { maximum: 255 }
acts_as_tenant :tenant
end
I also have the two factories defined:
FactoryBot.define do
factory :admin_tenant, class: 'Admin::Tenant' do
name { Faker::Lorem.sentence }
end
factory :tenant_group, class: 'Tenant::Group' do
association :tenant, factory: :admin_tenant
name { Faker::Lorem.sentence }
end
end
When utilizing the factory :admin_tenant on its own, it seems to work fine, but when I attempt to generate a :tenant_group (using create(:tenant_group)) I receive an error in rspec:
Failure/Error: let(:tenant_group) { create(:tenant_group) }
ActiveRecord::AssociationTypeMismatch:
Tenant(#52813860) expected, got #<Admin::Tenant id: 295, name: "Perspiciatis sit numquam fugit.", created_at: "2020-01-03 15:08:13", updated_at: "2020-01-03 15:08:13"> which is an instance of Admin::Tenant(#55283820)
It seems that for some reason, it is assuming the class of the factory should be something else. Since I specify class_name in the association, I'd assume it would work (it does when I'm using the application itself). I saw that Spring might cause issues, so I followed the advice of the FactoryBot README and placed config.before(:suite) { FactoryBot.reload } in my rails_helper.rb file.
Update 1
Now finding out that the problem lies with acts_as_tenant. The stack trace was too short in RSpec output to realize what the issue was, but now it's showing up in regular usage, as well.
Update 2
I'm going to go ahead and mark this as solved. It doesn't appear to be an issue with FactoryBot as I initially thought, but rather an issue with my understanding of acts_as_tenant. When the class name cannot be easily inferred by the association name, you must specify the :class_name option. This became clear after browsing the source code for a bit. In retrospect, it seems obvious, since all associations seem to behave the same way...
The error is most likely caused by ActsAsTenant and not FactoryBot which is doing the right thing.
When you create multiple associations with the same name the later overwrite the former. And acts_as_tenant :tenant does just that and clobbers the association you already set up. Its not very well documented but the acts_as_tenant macro takes roughly the same options as belongs_to.
class Tenant::Group < ApplicationRecord
has_rich_text :description
acts_as_tenant :tenant,
class_name: 'Admin::Tenant',
inverse_of: :tenant_groups
validates :name,
presence: true,
length: { maximum: 255 }
end

Prevent from raising ActiveRecord::RecordInvalid or adding twice on has_many association

I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.

How to validate presence of associated records in case of has_many, through: association?

There are models with has has_many through association:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
validates :categories, presence: true
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
validates_presence_of :event, :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
The issue is with assigning event.categories = [] - it immediately deletes rows from event_categories. Thus, previous associations are irreversibly destroyed and an event becomes invalid.
How to validate a presence of records in case of has_many, through:?
UPD: please carefully read sentence marked in bold before answering.
Rails 4.2.1
You have to create a custom validation, like so:
validate :has_categories
def has_categories
unless categories.size > 0
errors.add(:base, "There are no categories")
end
end
This shows you the general idea, you can adapt this to your needs.
UPDATE
This post has come up once more, and I found a way to fill in the blanks.
The validations can remain as above. All I have to add to that, is the case of direct assignment of an empty set of categories. So, how do I do that?
The idea is simple: override the setter method to not accept the empty array:
def categories=(value)
if value.empty?
puts "Categories cannot be blank"
else
super(value)
end
end
This will work for every assignment, except when assigning an empty set. Then, simply nothing will happen. No error will be recorded and no action will be performed.
If you want to also add an error message, you will have to improvise. Add an attribute to the class which will be populated when the bell rings.
So, to cut a long story short, this model worked for me:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
attr_accessor :categories_validator # The bell
validates :categories, presence: true
validate :check_for_categories_validator # Has the bell rung?
def categories=(value)
if value.empty?
self.categories_validator = true # Ring that bell!!!
else
super(value) # No bell, just do what you have to do
end
end
private
def check_for_categories_validator
self.errors.add(:categories, "can't be blank") if self.categories_validator == true
end
end
Having added this last validation, the instance will be invalid if you do:
event.categories = []
Although, no action will have been fulfilled (the update is skipped).
use validates_associated, official documentaion is Here
If you are using RSpec as your testing framework, take a look at Shoulda Matcher. Here is an example:
describe Event do
it { should have_many(:categories).through(:event_categories) }
end

ActiveRecord association not being set in the database?

I’m sure this is a “duh" newbie-type question, but I’ve been at it for days and cannot figure out why my code isn’t setting a relationship in the database correctly. I have a simple belongs_to relationship between two models.
class Pod < ActiveRecord::Base
belongs_to :instigator, :class_name => “User"
attr_accessor :instigator, :instigator_id, :title
validates_presence_of :instigator, :title
validates_associated :instigator
end
and
class User < ActiveRecord::Base
has_many :instigated_pods, inverse_of: :instigator, :class_name => "Pod", as: "instigator"
end
Then I want to test them with rspec and Factory Girl using (what I think) are, again, pretty simple factories
FactoryGirl.define do
factory :pod do
title "Test pod"
instigator
end
factory :user, aliases: [:instigator] do
username
end
end
With this setup, most tests pass, but my PodsController update test kept failing, and I finally found a test that shows why.
require 'rails_helper'
RSpec.describe Pod, type: :model do
it "saves the relationship to the database" do
pod = FactoryGirl.create(:pod)
expect(pod.save).to be_truthy
expect(pod.reload.instigator).to_not be_nil # passes - cached?
pod_from_database = Pod.find(pod.id)
expect(pod_from_database.instigator).to_not be_nil # <- fails
end
end
It seems that something is preventing the pod.instigator_id from being set in the database, so the relationship isn’t persisting. And I have no clue why!!!
I tried setting validates_presence_of :instigator_id, but that makes most of the standard rspec tests fail, and I saw this from the Rails Guides:
If you want to be sure that an association is present, you'll need to test
whether the associated object itself is present, and not the foreign key used
to map the association.
class LineItem < ActiveRecord::Base
belongs_to :order
validates :order, presence: true
end
In order to validate associated records whose presence is required, you must
specify the :inverse_of option for the association:
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
Any help straightening this out would be appreciated!
While I’m not altogether clear why, it appears that removing the line
attr_accessor :instigator, :instigator_id, :title
solves the problem. It appears to be blocking writing these attributes. Any indicator why would be appreciated!
attr_accessor is a ruby method that makes a getter and a setter(make them able to be read and to be written), this is not Database fields which called attr_accessible in Rails 3.
what you did is declare an ruby methods this is why it doesn't worked.
read more here:
http://rubyinrails.com/2014/03/17/what-is-attr_accessor-in-rails/

Running custom validations with gem specific methods

I setup a custom validation that checks if a user has voted for an album before submitting a review. The validation works fine on the client side but when it comes to running my Rspec tests I seem to run into some problems.
The validation makes use of the Acts As Votable gem's voted_for? method. Unfortunately this is where things go bad. For my non-custom validations (that do work regularly btw) I get an error like this:
3) Review validations should ensure body has a length of at least 40
Failure/Error: it { should validate_length_of(:body).is_at_least(40) }
NoMethodError:
undefined method `voted_for' for nil:NilClass
What do I need to do in order for this method to be recognized?
Review Model
class Review < ActiveRecord::Base
belongs_to :album
belongs_to :owner, class_name: "User", foreign_key: :user_id
validates :body, presence: true, length: { minimum: 40 }
def album_vote
if !owner.voted_for?(album)
errors.add(:review, "requires an album vote before submitting")
end
end
end
Review Factory
FactoryGirl.define do
factory :review do
body { Faker::Lorem.paragraph(2) }
album
association :owner, factory: :user
end
end
You need to ensure that there actually is an owner. The error is simply because you are calling on nil and does not really have anything to do with ActsAsVotable.
class Review < ActiveRecord::Base
belongs_to :album
belongs_to :owner, class_name: "User", foreign_key: :user_id
validates :body, presence: true, length: { minimum: 40 }
validates :owner, presence: true
def album_vote
# consider raising an error if there is no owner.
if owner && !owner.voted_for(album)
errors.add(:review, "requires an album vote before submitting")
end
end
end
And then change your factory definition to create the owner:
FactoryGirl.define do
factory :user, aliases: [:owner] do
# ...
end
end
FactoryGirl.define do
factory :review do
body { Faker::Lorem.paragraph(2) }
album
owner
end
end

Resources