Suppose we have the following (albeit complicated) setup:
class Company < ActiveRecord::Base
has_many :homes, inverse_of: :company
has_many :item_company_items, class_name: 'CompanyItem', inverse_of: :item_company, foreign_key: :item_company_id
has_many :home_company_items, class_name: 'CompanyItem', inverse_of: :home_company, foreign_key: :home_company_id
has_many :replacement_items, through: :item_company_items, source: :replacement_items, inverse_of: :responsible_company
end
class Home < ActiveRecord::Base
belongs_to :company, inverse_of: :homes
has_many :company_items, through: :company, source: :home_company_items, inverse_of: :homes
has_many :replacement_items, inverse_of: :home
end
class CompanyItem < ActiveRecord::Base
belongs_to :item, inverse_of: :company_items
belongs_to :item_company, class_name: 'Company', inverse_of: :item_company_items, foreign_key: :item_company_id
belongs_to :home_company, class_name: 'Company', inverse_of: :home_company_items, foreign_key: :home_company_id
has_many :homes, through: :home_company, source: :homes, inverse_of: :company_items
has_many :replacement_items, -> company_item {
where item_id: company_item.item_id
}, through: :homes, source: :replacement_items, inverse_of: :company_item
end
class Item < ActiveRecord::Base
has_many :replacement_items, inverse_of: :item
has_many :company_items, inverse_of: :item
end
class ReplacementItem < ActiveRecord::Base
belongs_to :item, inverse_of: :replacement_items
belongs_to :home, inverse_of: :replacement_items
has_one :company_item, -> rep { where item_id: rep.item_id }, through: :home, source: :company_items, inverse_of: :replacement_items
has_one :responsible_company, through: :company_item, source: :item_company, inverse_of: :replacement_items
end
So, in effect, Items have many CompanyItems which represent the Company that is responsible (CompanyItem#item_company) for ReplacementItems belonging to that Item when the ReplacementItem is for a Home belonging to the Company specified by the CompanyItem (CompanyItem#home_company).
Very circuitous, but it's exactly what we need.
Now, as we can see, on the ReplacementItem model, the has_one :company_item, through: :home relationship uses a scope to select the specific CompanyItem belonging to the home that matches the item_id. This makes sense sence just doing replacement_item.home.company_items would give us all of homes company items, when we really just need the one that matches the given ReplacementItem. Calling the ReplacementItem#company_item method works flawlessly and gives us exactly what we want.
Likewise, the CompanyItem model has has_many :replacement_items, through: :homes, using the exact same scope to get just the replacement items that match the given CompanyItem. In this instance, however, we get an error when calling CompanyItem#replacement_items:
NoMethodError: undefined method `item_id' for #<Company id: 2>
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activemodel/lib/active_model/attribute_methods.rb:434:in `method_missing'
bug.rb:59:in `block in <class:CompanyItem>'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:161:in `instance_exec'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:161:in `eval_scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:139:in `block in add_constraints'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:138:in `each'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:138:in `add_constraints'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:28:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association_scope.rb:5:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association.rb:97:in `association_scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/association.rb:86:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/collection_association.rb:327:in `scope'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/collection_proxy.rb:36:in `initialize'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/relation/delegation.rb:101:in `new'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/relation/delegation.rb:101:in `create'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/collection_association.rb:46:in `reader'
/Users/cbankester/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/rails-bf5876af099f/activerecord/lib/active_record/associations/builder/association.rb:111:in `replacement_items'
bug.rb:85:in `test_hmt_with_conditions'
This looks like a Rails bug to me. There's absolutely no way a Company object should be passed in to the has_many :replacement_items scope defined on the CompanyItem model. Before bugging the Rails team, though, I am hoping someone here has seen a similar problem or can point out the fault in my code.
See my gist here for a runnable, failing test: https://gist.github.com/cmbankester/64ef4eb125bf90dbe0274fab8e5427f1
Related
I have two models and controllers, one for Users and one for Tickets.
Each user can have many tickets. Each Admin user (denoted by admin: true) can have many tickets assigned to them.
Here are my associations:
class Ticket < ActiveRecord::Base
belongs_to :sender, class_name: "User", foreign_key: "id", inverse_of: :sent_tickets
belongs_to :admin, class_name: "User", foreign_key: "id", inverse_of: :assigned_tickets
.
.
.
end
class User < ActiveRecord::Base
.
.
.
has_many :sent_tickets, class_name: "Ticket", foreign_key: "sender", inverse_of: :sender, dependent: :nullify
has_many :assigned_tickets, class_name: "Ticket", foreign_key: "assigned", inverse_of: :admin, dependent: :nullify
.
.
.
end
However, anytime I try to run #ticket = user.sent_tickets.build (after running user = User.first in the Rails IRB, I get ActiveRecord::AssociationTypeMismatch in TicketsController#new: User(#number) expected, got Fixnum(#number)
Can anyone help me with this?
Fixed it. I renamed sender and assigned to sender_id and assigned_id. Then I rewrote my associations as such:
class Ticket < ActiveRecord::Base
belongs_to :assigned, class_name: "User"
belongs_to :sender, class_name: "User"
.
.
.
end
class User < ActiveRecord::Base
.
.
.
has_many :help_requests, foreign_key: "sender_id", class_name: "Ticket", dependent: :nullify
has_many :sent_requests, through: :help_requests , source: :sender
has_many :tickets, foreign_key: "assigned_id", dependent: :nullify
has_many :assigned_tickets, through: :tickets, source: :assigned
.
.
.
end
I recently made the following models:
class User < ActiveRecord::Base
has_many :resources
has_many :resource_views, :through => :user_resource_views, :source => 'Resource'
end
class Resource < ActiveRecord::Base
belongs_to :user
has_many :resource_views, :through => :user_resource_views, :source => 'Resource'
end
class UserResourceView < ActiveRecord::Base
attr_accessible :resource_id, :user_id
belongs_to :resource
belongs_to :user_id
end
Now, I want my home#index action to set #resources to the current_user's most recently viewed resources. How would you advise I proceed? Perhaps something similar to current_user.resource_views.find(:all, :order => 'created_at')?
In SQL, I would do something like:
SELECT *
FROM Resource
WHERE id IN (SELECT * FROM UserResourceView WHERE user_id = current_user.id)
... but then the ORDER BY created_at, hmmm
I'll be periodically adding progress updates throughout the day until I figure it out.
Given you're on Rails 3.x, what you're looking for is probably something like this:
class User < ActiveRecord::Base
has_many :resources
has_many :resource_views, :class_name => 'UserResourceView'
has_many :viewed_resources, :through => :resource_views, :source => :resource
def recently_viewed_resources
viewed_resources.order('user_resource_views.created_at DESC')
end
end
class Resource < ActiveRecord::Base
belongs_to :user
has_many :resource_views, :class_name => 'UserResourceView'
end
class UserResourceView < ActiveRecord::Base
attr_accessible :resource_id, :user_id
belongs_to :resource
belongs_to :user_id
end
And to access the collection in your controller:
current_user.recently_viewed_resources
Sinatra, Mongoid 3
There 4 models: User, Book, FavoriteBooks, ReadBooks, NewBooks. Each user has their list of the favourites, read and new books. A book belongs to a list. But it's also possible to request an information about any book which means books should not be embedded into FavoriteBooks, ReadBooks, NewBooks.
The part of the scheme:
class Book
include Mongoid::Document
belongs_to :favourite_books
belongs_to :read_books
belongs_to :new_books
end
class FavoriteBook
include Mongoid::Document
has_many :books
end
#.... the same for ReadBooks and NewBooks
class User
include Mongoid::Document
# what else?
end
It seems like I missed something.
What should I do to make a user "contain" the lists of FavoriteBooks, ReadBooks, NewBooks? Should I use one-to-one relationship?
I think you should rethink your modeling. IMHO it should be book and user as models, and the favorite_books, read_books and new_books should all be relationhips like so:
class User
include Mongoid::Document
has_many :favorite_books
has_many :read_books
has_many :new_books
has_many :books, :through => :favorite_books
has_many :books, :through => :read_books
has_many :books, :through => :new_books
end
class Book
include Mongoid::Document
has_many :favorite_books
has_many :read_books
has_many :new_books
has_many :users, :through => :favorite_books
has_many :users, :through => :read_books
has_many :users, :through => :new_books
end
class FavoriteBook
include Mongoid::Document
belongs_to :books
belongs_to :users
end
#.... the same for ReadBooks and NewBooks
I think this should be a better approach. =)
The setup
I have a data model with 3 major tables (users, links, topics) with 2 join tables (link_saves and link_topics). My models:
User
has_many :link_saves, :class_name => 'LinkSave', :foreign_key => 'user_id'
has_many :links, :through => :link_saves
LinkSave
belongs_to :user
belongs_to :link
Link
has_many :link_saves, :class_name => 'LinkSave', :foreign_key => 'link_id'
has_many :users, :through => :link_saves
has_many :link_topics, :inverse_of => :link
has_many :topics, :through => :link_topics
LinkTopic
belongs_to :link
belongs_to :topic
Topic
has_many :link_topics
has_many :links, :through => :link_topics
The Question
I want to be able to find a list of all the topics a user has saved links for. I would like to be able to do #user.topics and have it hop across all 5 tables from user all the way to topics. More importantly, I want this to return an ActiveRecord relation so that I can scope/sort/page the list of user topics further so this would NOT work:
## app/models/user.rb
def topics
links.collect(&:topics)
end
Am I going down the wrong path? Is there a way to do this through active record without having to write all the custom SQL? Help please!
Possible Answers (Update)
Using multiple has_many :throughs to make all the hops. This works, but can't be best practice, right?
## app/models/user.rb
has_many :link_saves, :class_name => 'LinkSave', :foreign_key => 'user_id'
has_many :links, :through => :link_saves
has_many :link_topics, :through => :links, :uniq => true
has_many :topics, :through => :link_topics, :uniq => true
I think this is called a 'nested' has_many through, basically going from A to B to C.
In Rails 3.1 this functionality is now supported
http://www.warmroom.com/yesterdays/2011/08/30/rails-3-1-nested-associations/
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html (search for 'Nested')
The example they have is a bit simpler than what you have, but I think it should be enough for you to get some ideas.
class Author < ActiveRecord::Base
has_many :posts
has_many :comments, :through => :posts
has_many :commenters, :through => :comments
end
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :commenter
end
#author = Author.first
#author.commenters # => People who commented on posts written by the author
Prior to Rails 3.1 there was a plugin
'https://github.com/releod/nested_has_many_through'
I have a rails 3.0
has_many :X, :through => :something set up and i have a custom validator that does some custom validation on some complicated logic. I want to when you add anything to this many to many relationship both models are valid?
Project Class:
class Project < ActiveRecord::Base
has_many :assignments
has_many :users, :through => :assignments
validates :name, :presence => true
end
Assignment:
class Assignment < ActiveRecord::Base
belongs_to :project
belongs_to :user
end
User class with custom validator:
class MyCustomValidator < ActiveModel::Validator
def validate( record )
if record.projects.length > 3
record.errors[:over_worked] = "you have to many projects!"
end
end
end
class User < ActiveRecord::Base
has_many :assignments
has_many :projects, :through => :assignments
validates :name, :presence => true
validates_with MyCustomValidator
end
What i really want to do is prevent each model from invalidating the other so to say
prevent
my_user.projects << fourth_project
and
my_project.users << user_with_four_projects_already
from happening. Right now it allows the assignment and just the user becomes invalid.
class Project < ActiveRecord::Base
has_many :assignments
has_many :users, :through => :assignments
validates :name, :presence => true
validates_associated :users
end
According to the docs, users must already be assigned to Projects in order to be validated. So:
my_user.projects << fourth_project
would occur, then projects would validate the user and see that it is indeed invalid, making the project invalid as well as in the inverse example.