Rails: handling object creation via candidate key - ruby-on-rails

I have a table foo which belongs_to :bar (bar has_many :foos).
There are too many foos for it to be sensible to offer the user a list when they're creating a bar, so I'm going to get the user to enter the name of the bar that the foo belongs to. The name is a candidate key -- guaranteed to be present and unique.
I think I can see the steps I need to take: make foo validate that bar_id is present, and somewhere put a find_where to look up the bar that has the given name (if it's not present I'll get a nil, so the validation should fail -- good).
What I want to know is where's the best place to put that find_where in order to check all creations and edits of foo whilst maintaining DRY principles? How to I join it up to the new and edit forms? (Or is there a more Rails way?)
Rails 4, by the way.
EDIT:
To make it more concrete, suppose I have a list of every airport in the world, in Airport, which contains the airport name (name), the 3-character identifier that gets put on your luggage tag (code), and the Rails-generated airport_id. And suppose I have Departure which belongs_to :airport. When I create/edit a departure, the user enters the airport code, which I have to map to an actual entry in Airport to create/edit the entry. What's the best place/way to do that?

You don't actually need to do all of that, Rails has a way! When you specify an association between two models in your database, it suffices to simply add the dependent model name to a validation:
class Foo < ActiveRecord::Base
belongs_to :bar, validate: true
end
This will cause ActiveRecord to validate the presence of the bar object in question. And, because this is Rails, there are other ways of writing this type of validation:
With validates_associated
class Foo < ActiveRecord::Base
belongs_to :bar
validates_associated :bar
end
or with validates presence
class Foo < ActiveRecord::Base
belongs_to :bar, inverse_of :foo
validates :bar, presence: true
end
class Bar < ActiveRecord::Base
has_many :foo, inverse_of :bar
end
Beware a couple gotchas:
In the validates_associated construction, you must make sure to only specify the validation on one of your two related models. If you specify the validates_associated attribute on both, your validations will call each other infinitely.
In the validates presence version, you must specify the inverse_of relationship in order to validate the presence of the associated AR model.
EDIT
In response to your edited question, I've updated my answer:
You'll want to query the Airport by its code attribute in your DeparturesController, under the #create and #update actions:
class DeparturesController < ApplicationController
def create
airport = Airport.find_by(code: params[:departure][:code])
if airport
departure = airport.departures.build(params[:departure])
if departure.save
...
else
...
end
else
...
end
end
end
The upshot of using the find_by method in ActiveRecord is that it returns nil if the search fails, making it easy for you to write error-handling logic in the event that your user enters an invalid code.

Related

Automatically creating missing join models

Lately I've grown weary of littering my app/models directory with pointless boilerplate models such as:
Join models that always contain a couple belongs_tos and nothing else.
Status log models that just include SomeConcern and make a couple macro calls.
Revision tracking models that again, just include a concern and call a macro.
These models only exist to support has_many and has_many ... through: associations.
Adding model concerns that generate these models as needed clears simplifies the app/models directory. So instead of:
has_many :model_things
has_many :things, through: :model_things
and a trivial app/models/model_thing.rb that says:
class ModelThing < ApplicationRecord
belongs_to :model
belongs_to :thing
end
I can have a ThingSupport concern with a has_things macro that:
Creates the has_many :model_things association based on the class name and some options to has_things.
Creates the has_many :things, through: :model_things association.
Find or create the Model::Thing (see below for why this name is used) class with a call like:
ModuleUtil.find_or_create(join_model_name) do
Class.new(ApplicationRecord) do
# Set the table name, call belongs_to as needed, call concern methods, ...
end
end
where ModuleUtil.find_or_create is a simple method that uses String#constantize to find the desired module (if it exists) or create it using the block and Object#const_set if it can't be found.
All the model and association names can be built using the usual Rails conventions from the caller's class name and some options to has_things for special cases.
The question is am I playing with fire here? What can go wrong with this sort of chicanery?
One problem that I've already come across is that the model classes that are generated don't exist on their own so they cannot be directly referenced from an ActiveJob (such as a deliver_later mailer). For example, if loading Model creates the ModelThing association model then you can't reference a ModelThing in a mailer argument because ActiveJob won't know that you have to load the Model class before ModelThing exists. However, this can be solved by using Model::Thing instead so that constantize will look for Model (and find it in app/models/model.rb) before trying to find Model::Thing (which will exist because constantize will have just loaded Model which creates Model::Thing). Am I missing something else?
I have no idea if I'm following you or not. So, if this is way off target, please say so and I'll delete.
Focusing in on the join model bit, I also got tired of that flim flam. So, I created a model like:
module ActsAsHaving
class HasA < ActiveRecord::Base
validates :haser_type, :haser_id, :hased_type, :hased_id, presence: true
belongs_to :hased, polymorphic: true
belongs_to :haser, polymorphic: true
acts_as_taggable
def haser=(thing)
self.haser_type = thing.class.name
self.haser_id = thing.id
end
def haser
haser_type.constantize.find_by(id: haser_id)
end
def hased=(thing)
self.hased_type = thing.class.name
self.hased_id = thing.id
end
def hased
hased_type.constantize.find_by(id: hased_id)
end
end
end
I didn't use the built-in accessors and validations because I sometimes use this to join non-AR records (which I grab from remote API services some of which belong to me and some of which don't but that's a longer story).
Anyway, I then wrote an acts_as_having macro that let me do stuff like:
class Person < ActiveRecord::Base
acts_as_having :health_events, class_name: "Foo::Event", tag_with: "health_event", remote: true
acts_as_having :program_events, class_name: "Foo::Event", tag_with: "program_event", remote: true
acts_as_having :email_addresses, :phone_numbers, :physical_addresses
end
Which gives me stuff like:
#person.email_addresses
#person.email_addresses << #email_address
etc...
I can do the inverse like:
class EmailAddress < ActiveRecord::Base
acts_as_had_by :person
end
Which gives me stuff like:
#email_address.person
etc...
Then, I wrapped all that junk up into a gem. Now I rarely create join models unless they have some specific requirements that I can't shoe horn into my acts_as_having bit.
Anyway, I don't know if it's playing with fire or not. I don't even know if I'm making sense or addressing your concept. But, I started my gem about three years ago and I haven't regretted it. So, there's that.

Ruby on Rails - Get object association values with array of symbols

For example, have the models:
class Activity < ActiveRecord::Base
belongs_to :event
end
class Event < ActiveRecord::Base
has_many :activities
attr_accessible :foo
end
I can get the activity's event foo by using activity.event.foo (simple enough).
But I want to make a generic function that finds out first if an object has a belongs_to association and then get that object's foo through the belongs_to association (pretend that all objects have a foo through the belongs_to association)?
So far I have the following with gives me a reflection:
def get_foo(object)
object.class.reflect_on_all_associations(:belongs_to).each do |belongs_to|
return object.??????
end
end
I can either get an array of the reflection's class name via belongs_to.klass (e.g. [Event]) or an array of symbols for the belongs_to association via belongs_to.name (e.g. [:event]).
How do I get the object's belongs_to's foo given what I get from the reflection?
Is there an easier way to do this without using the reflection?
I'm hoping this is something simple and I'm just spacing out on how to solve this. I also hope I am being somewhat clear. This is my first Stack Overflow question.
You can do this but its not exactly pretty:
def get_foo(object)
object.class.reflect_on_all_associations(:belongs_to).map do |reflection|
object.send(reflection.name).try(:foo)
end
end
That will give you an array of all the foos associated with the object. You can change it to not do map and do a first or something.

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.

Factory Girl with polymorphic association for has_many and has_one

I am currently working on a project and I wanted to create tests using factory girl but I'm unable to make it work with polymorphic has_many association. I've tried many different possibilities mentioned in other articles but it still doesn't work. My model looks like this:
class Restaurant < ActiveRecord::Base
has_one :address, as: :addressable, dependent: :destroy
has_many :contacts, as: :contactable, dependent: :destroy
accepts_nested_attributes_for :contacts, allow_destroy: true
accepts_nested_attributes_for :address, allow_destroy: true
validates :name, presence: true
#validates :address, presence: true
#validates :contacts, presence: true
end
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
# other unimportant validations, address is created valid, the problem is not here
end
class Contact < ActiveRecord::Base
belongs_to :contactable, polymorphic: true
# validations ommitted, contacts are created valid
end
So bassically I want to create factory for Restaurant with address and contacts (with validations on Restaurant for presence, but if it's not possible, even without them) but I'm unable to do so. Final syntax should be like:
let(:restaurant) { FactoryGirl.create(:restaurant) }
Which should also create associated address and contacts. I have read many articles but I always get some sort of error. Currently my factories (sequences are defined correctly) are like this:
factory :restaurant do
name
# address {FactoryGirl.create(:address, addressable: aaa)}
# contacts {FactoryGirl.create_list(:contact,4, contactable: aaa)}
# Validations are off, so this callback is possible
after(:create) do |rest|
# Rest has ID here
rest.address = create(:restaurant_address, addressable: rest)
rest.contacts = create_list(:contact,4, contactable: rest)
end
end
factory :restaurant_address, class: Address do
# other attributes filled from sequences...
association :addressable, factory: :restaurant
# addressable factory: restaurant
# association(:restaurant)
end
factory :contact do
contact_type
value
association :contactable, :factory => :restaurant
end
Is there a way to create restaurant with one command in test with addresses and contacts set? Do I really need to get rid off my validations because of after(:create) callback?
Current state is as fllowing:
Restaurant is created with name and id.
Than the address is being created - all is correcct, it has all the values including addressable_id and addressable_type
After that all contacts are being creaed, again everything is fine, cntacts has the right values.
After that, restaurant doesn't have any ids from associated objects, no association to adddress or contacts
After than, for some reason restaurant is build again (maybe to add those associations?) and it fails: I get ActiveRecord:RecordInvalid.
I'm using factory_girl_rails 4.3.0, ruby 1.9.3 and rails 4.0.1. I will be glad for any help.
UPDATE 1:
Just for clarification of my goal, I want to be able to create restaurant in my spec using one command and be able to access associated address and contacts, which should be created upon creation of restaurant. Let's ignore all validations (I had them commented out in my example from the beginning). When I use after(:build), first restaurant is created, than address is created with restaurant's ID as addressable_id and class name as addressable_type. Same goes to contacts, all are correct. The problem is, that restaurant doesn't know about them (it has no IDs of address or contacts), I can't access them from restaurant which I want to.
After really thorough search I have found an answer here - stackoverflow question.This answer also point to this gist. The main thing is to build associations in after(:build) callback and then save them in after(:create) callback. So it looks like this:
factory :restaurant do
name
trait :confirmed do
state 1
end
after(:build) do |restaurant|
restaurant.address = build(:restaurant_address, addressable: restaurant)
restaurant.contacts = build_list(:contact,4, contactable: restaurant)
end
after(:create) do |restaurant|
restaurant.contacts.each { |contact| contact.save! }
restaurant.address.save!
end
end
I had also a bug in my rspec, because I was using before(:each) callback instead of before(:all). I hope that this solution helps someone.
The Problem
Validating the length of a related list of rows is a difficult problem to frame in SQL, so it's a difficult problem to frame in ActiveRecord as well.
If you're storing a restaurant foreign key on the addresses table, you can't ever actually create a restaurant that has addresses by the time it's saved, because you need to save the restaurant to get its primary key. You can get around this problem in ActiveRecord by building up the associated objects in memory, validating against those, and then committing the entire object graph in one SQL transaction.
How to do what you're asking
You can generally get around this by moving things into an after(:build) hook instead of after(:create). ActiveRecord will save its dependent has_one and has_many associations once it saves itself.
You're getting errors now because you can't modify an object to satisfy validations in an after(:create) block, because validations have already run by the time the callback runs.
You can change your restaurant factory to look something like this:
factory :restaurant do
name
after(:build) do |restaurant|
restaurant.address = build(:restaurant_address, addressable: nil)
restaurant.contacts = build(:contact, 4, contactable: nil)
end
end
The nils there are to break the cyclic relationship between the factories. If you do it this way, you can't have a validation on the addressable_id or contactable_id keys, because they won't be available until the restaurant is saved.
Alternatives
Although you can get both ActiveRecord and FactoryGirl to do what you're asking, it sets up a precarious list of dependencies which are difficult to understand and are likely to result in leaky validations or unexpected errors like the ones you're seeing now.
If you're validating contacts this way from the restaurant model because of a form in which you create both a restaurant and its corresponding contacts, you can save yourself a lot of pain by creating a new ActiveModel object to represent that form. You can collect the attributes you need for each object there, move some of the validations (especially the ones which validate the length of the contacts list), and then create the object graph on that form in a way that's much clearer and less likely to break.
This has the added benefit of making it easy to create lightweight restaurant objects in other tests which don't need to worry about contacts or addresses. If you force your factories to create these dependent objects every time, you'll quickly run into two problems:
Your tests will be painfully slow. Creating five dependent records every time you want to work with a restaurant won't scale very far.
If you ever want to specify different contacts or addresses in your tests, you'll constantly be fighting with your factories.

Best practice about empty belongs_to association

Imagine the following situation:
I have a dog model and a house model. A dog can belong to a house, and a house can have many dogs, so:
Class Dog < ActiveRecord::Base
belongs_to :house
end
Class House < ActiveRecord::Base
has_many :dogs
end
Now, imagine that I also want to create dogs that don't have a house. They don't belong to house. Can I still use that relationship structure and simply don't inform a :house_id when creating it?
Is there a better practice?
Obs.: I used this analogy to simplify my problem, but my real situation is: I have a model a user can generate instances of it. He can also create collections of those instances, but he can leave an instance outside a collection.
Be careful with this in Rails 5...
#belongs_to is required by default
From now on every Rails application will have a new configuration
option config.active_record.belongs_to_required_by_default = true, it
will trigger a validation error when trying to save a model where
belongs_to associations are not present.
config.active_record.belongs_to_required_by_default can be changed to
false and with this keep old Rails behavior or we can disable this
validation on each belongs_to definition, just passing an additional
option optional: true as follows:
class Book < ActiveRecord::Base
belongs_to :author, optional: true
end
from: https://sipsandbits.com/2015/09/21/whats-new-in-rails-5/#belongs_toisrequiredbydefault
I think it is absolutely normal approach.
You can just leave house_id with null value in database for the models which don't belong to other.

Resources