Here is my scenario:
A model called Course has many CourseCodes. A CourseCode belongs to a Course.
A CourseCode can't be created without Course and a Course can't be created without at least one CourseCode.
class Course < ActiveRecord::Base
has_many :course_codes
validate :existence_of_code
private
def existence_of_code
unless course_codes.any?
errors[:course_codes] << "missing course code"
end
end
end
class CourseCode < ActiveRecord::Base
belongs_to :course
validates_presence_of :course
end
The whole scenario feels a bit like catch 22.
Is there a way to create both on the same time?
I'm using Rails 3.2
Solved the problem by using accepts_nested_attributes_for.
Nested attributes allow you to save attributes on associated records through the parent. By default nested.
class Course < ActiveRecord::Base
has_many :course_codes, inverse_of: :course
validate :existence_of_code
accepts_nested_attributes_for :course_codes
private
def existence_of_code
unless course_codes.any?
errors[:course_codes] << "missing course code"
end
end
end
class CourseCode < ActiveRecord::Base
belongs_to :course, inverse_of: :course_codes
validates_presence_of :course
end
Used like this.
Course.create!({
course_codes_attributes: [{ code: "TDA123" }],
# ...
})
Looks good to me. Removing the validates_presence_of :course might make things easier on you, too, as it will tend to get in the way an not add much.
When you create a course, do it like this:
Course.create course_codes: [CourseCode.new(...), CourseCode.new(...)]
ActiveRecord will figure things out.
You could add an unless to whichever model you would plan to create first. For instance:
class CourseCode < ActiveRecord::Base
belongs_to :course
validates_presence_of :course, :unless => lambda { Course.all.empty? }
end
Related
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.
Edit: In retrospect this isn't that great of an idea. You are putting functionality that belongs to ZipWithCBSA into the models of others. The models receiving the concern act as they are supposed to, and the fact that ZipWithCBSA responds to :store_locations should be obvious in some capacity from ZipWithCBSA. It has nothing to do with the other models/concern. Props to Robert Nubel for making this obvious with his potential solutions.
Is it possible to both has_many and belongs_to relationships in a single concern?
Overview
I have a table ZipWithCBSA that essentially includes a bunch of zip code meta information.
I have two models that have zip codes: StoreLocation and PriceSheetLocation. Essentially:
class ZipWithCBSA < ActiveRecord::Base
self.primary_key = :zip
has_many :store_locations, foreign_key: :zip
has_many :price_sheet_locations, foreign_key: :zip
end
class StoreLocation< ActiveRecord::Base
belongs_to :zip_with_CBSA, foreign_key: :zip
...
end
class PriceSheetLocation < ActiveRecord::Base
belongs_to :zip_with_CBSA, foreign_key: :zip
...
end
There are two properties from ZipWithCBSA that I always want returned with my other models, including on #index pages. To prevent joining this table for each item every time I query it, I want to cache two of these fields into the models themselves -- i.e.
# quick schema overview~~~
ZipWithCBSA
- zip
- cbsa_name
- cbsa_state
- some_other_stuff
- that_I_usually_dont_want
- but_still_need_access_to_occasionally
PriceSheetLocation
- store
- zip
- cbsa_name
- cbsa_state
StoreLocation
- zip
- generic_typical
- location_address_stuff
- cbsa_name
- cbsa_state
So, I've added
after_save :store_cbsa_data_locally, if: ->(obj){ obj.zip_changed? }
private
def store_cbsa_data_locally
if zip_with_cbsa.present?
update_column(:cbsa_name, zip_with_cbsa.cbsa_name)
update_column(:cbsa_state, zip_with_cbsa.cbsa_state)
else
update_column(:cbsa_name, nil)
update_column(:cbsa_state, nil)
end
end
I'm looking to move these into concerns, so I've done:
# models/concerns/UsesCBSA.rb
module UsesCBSA
extend ActiveSupport::Concern
included do
belongs_to :zip_with_cbsa, foreign_key: 'zip'
after_save :store_cbsa_data_locally, if: ->(obj){ obj.zip_changed? }
def store_cbsa_data_locally
if zip_with_cbsa.present?
update_column(:cbsa_name, zip_with_cbsa.cbsa_name)
update_column(:cbsa_state, zip_with_cbsa.cbsa_state)
else
update_column(:cbsa_name, nil)
update_column(:cbsa_state, nil)
end
end
private :store_cbsa_data_locally
end
end
# ~~models~~
class StoreLocation < ActiveRecord::Base
include UsesCBSA
end
class PriceSheetLocation < ActiveRecord::Base
include UsesCBSA
end
This is all working great-- but I still need to manually add the has_many relationships to the ZipWithCBSA model:
class ZipWithCBSA < ActiveRecord::Base
has_many :store_locations, foreign_key: zip
has_many :price_sheet_locations, foreign_key: zip
end
Finally! The Question!
Is it possible to re-open ZipWithCBSA and add the has_many relationships from the concern? Or in any other more-automatic way that allows me to specify one single time that these particular series of models are bffs?
I tried
# models/concerns/uses_cbsa.rb
...
def ZipWithCBSA
has_many self.name.underscore.to_sym, foregin_key: :zip
end
...
and also
# models/concerns/uses_cbsa.rb
...
ZipWithCBSA.has_many self.name.underscore.to_sym, foregin_key: :zip
...
but neither worked. I'm guessing it has to do with the fact that those relationships aren't added during the models own initialization... but is it possible to define the relationship of a model in a separate file?
I'm not very fluent in metaprogramming with Ruby yet.
Your best bet is probably to add the relation onto your ZipWithCBSA model at the time your concern is included into the related models, using class_exec on the model class itself. E.g., inside the included block of your concern, add:
relation_name = self.table_name
ZipWithCBSA.class_exec do
has_many relation_name, foreign_key: :zip
end
I'm trying to build one model on top of another:
# This is a 'base' class which...
class Company < ActiveRecord::Base
has_many :managers
end
# ...has managers, but they are just...
class Manager < ActiveRecord::Base
belongs_to :company
belongs_to :person
delegate :name, :email,
to: :person
validates_presence_of :position
end
# ...a 'layer' on top of a Person
class Person < ActiveRecord::Base
has_one :manager
validates_presence_of :name, :email
end
Because when you have managers, contact persons, clients, employees and so on, it's natural to make Person model to take care of the names, phones, emails and all the attributes that logically belong to person. It's like subclassing:
class PlanetDesigner < ActiveRecord::Base
# if it wasn't AR, we would use PlanetDesigner < Person...
belongs_to :person
belongs_to :what_not
delegate :name, :email, #...but it's AR and we're using this instead
to: :person
# here go lots of PlanetDesigner-specific methods
end
class SaviourOfTheUniverse < ActiveRecord::Base
# same scenario here
end
# and a few similar classes more
But how?
When I Company.find(42).managers.create(name: 'Slartibartfast', email: 'vip_planets#magrathea.com', position: 'Designer'), I expectedly get ActiveRecord::UnknownAttributeError: unknown attribute: name.
I sure can do it with callbacks and stuff, but tell me, can't Rails do it itself?
To me this is not the case of a polymorphic association. It's having several different models with different behaviour and their own attributes built on top of Person.
Thank you in advance for your time.
class Manager < ActiveRecord::Base
belongs_to :company
belongs_to :person
delegate :name, :email, to: :person
validates_presence_of :position
accepts_nested_attribute_for :person
end
Company.find(42).managers.create(position: 'Designer', person_attributes: {
name: 'Slartibartfast', email: 'vip_planets#magrathea.com',
})
After some additional research I figured that the answer to my question is obvious and it's called Multiple Table Inheritance (MTI) or Class Table Inheritance (CTI).
One particular solution is ActiveRecord::ActsAs gem written by Hassan Zamani, which is probably the most elegant and up-to-date. I've checked out every other alternative from Ruby Toolbox and they all seem either abandoned or outdated.
It bugs me though why there's no native support for CTI in Rails yet, as the problem is so common. A few searches in Google and ruby-forum.com showed no active discussions about implementing the feature.
I thank everyone for their answers and the time taken.
If you'd like to extract naming logic from Manager, you can do smth like that:
class Manager < ActiveRecord::Base
belongs_to :company
def person
#person ||= Person.new(name, email)
end
end
# plain Ruby class
class Person
def initialize(name, email)
#name, #email = name, email
end
# person methods
end
# and then use it:
Manager.find(42).person.name_with_mail
This is my scenario:
class User < ActiveRecord::Base
has_many :things
# attr_accessible :average_rating
end
class Thing < ActiveRecord::Base
belongs_to :user
has_one :thing_rating
end
class ThingRating < ActiveRecord::Base
belongs_to :thing
attr_accessible :rating
end
I want to have an attribute in my User model which has the average calculation of his related ThingsRating.
What would be the best practice to manage this?
Thanks
May be you can use relation not sure but you can try this
class User < ActiveRecord::Base
has_many :things
has_many :thing_ratings, through: :things
# attr_accessible :average_rating
def avg_rating
#avg_rating ||= thing_ratings.average("thing_ratings.rating")
end
end
The easy way :
class User < ActiveRecord::Base
has_many :things
def avg_rating
#avg_rating ||= average(things.map(&:thing_rating))
end
private
def average(values)
values.inject(0.0) { |sum, el| sum + el } / arr.size
end
end
This is fine as a starter. But if you have a bit of trafic, you might find yourself with scaling problems.
You'll then have to refactor this to avoid making an SQL query to the things every time you call the method for a different user.
You could then have several possibilites :
Add a field in your User database, avg_rating, which would be updated by the ThingRating when it's created or updated.
Use a memcached or redis database to cache the value and invalidate the cache every time a ThingRating is updated or created.
These solutions aren't exhaustive of course. And you could find other ones which would better fit your needs.
I've got the following two classes
class Car < Vehicle
has_one :steering_wheel, as: :attached
end
class SteeringWheel < ActiveRecord::Base
belongs_to :attached
has_many :components
has_one :rim, class_name: 'Components', order: 'id DESC'
attr_accessible :components
end
class Component < ActiveRecord::Base
include SpecificationFileService::Client
attr_accessible :created_by
belongs_to :steering_wheel
end
Then in my specs:
context "given an attachment", :js do
before do
#car = create(:car, make: "honda")
#steering_wheel = SteeringWheel.create(attached: #car)
#steering_wheel.save
#car.save
#car.reload
end
specify "test the setup", :js do
puts #car.steering_wheel
end
end
Which prints: nil
A way that I have found fixes this is explicitly setting steering_wheel on car like so:
#car.steering_wheel = #steering_wheel
just before the save.
EDIT:
As suggested in the comments below, I have tried adding polymorphic: true, which did not resolve the issue. Also, I've fleshed out more of the SteeringWheel model above
My question is why, and how can I add this to the callback chain implicitly
Like #abraham-p mentioned in a comment, you need to declare the belongs_to relation as:
belongs_to :attached, polymorphic: true
Otherwise it will attempt to look for an Attached model, and be sure to include these fields in your SteeringWheel model:
attached_type
attached_id
The rest is worked out by Rails :)