I am struggling to build a good schema/associations between models in Rails, and hope for some advice. A "player" has multiple "picks", and each "pick" is made up of three "riders", ordered.
Summarizing the schema:
create_table players do |t|
t.string "name"
...
end
create_table picks do |t|
t.integer "player_id"
t.integer "rider1_id"
t.integer "rider2_id"
t.integer "rider3_id"
...
end
create_table riders do |t|
t.string "name"
...
end
I have some of the associations between the models built up:
player.rb:
class Player < ActiveRecord::Base
has_many :picks
end
pick.rb:
class Pick < ActiveRecord::Base
has_many :riders, :foreign_key => "rider1_id"
has_many :riders, :foreign_key => "rider2_id"
has_many :riders, :foreign_key => "rider3_id"
belongs_to :player
end
rider.rb
class Rider < ActiveRecord::Base
belongs_to :pick
end
I feel/know that I am missing some basic understanding of associations in the code above. In particular, I would like to be able to say something like: "myPick.rider1.name". I would also be able to look at which picks include a given rider, like "myRider.picks" (though I am not sure this is even possible). And fundamentally, I am not sure it's the right idea to have three "rider" foreign keys in the pick model.
Thanks in advance for any suggestions.
Cheers,
Jacques
You have your association a bit backwards. The table with the foreign key belongs_to the table without. So you should have 3 belongs_to :rider1, :class => 'Rider' calls in place of those has_many calls.
# player.rb:
class Player < ActiveRecord::Base
has_many :picks
end
# pick.rb:
class Pick < ActiveRecord::Base
belongs_to :rider1, :class => 'Rider'
belongs_to :rider2, :class => 'Rider'
belongs_to :rider3, :class => 'Rider'
belongs_to :player
def riders
# returns an array so you can say pick.riders.each { |rider| ... }
# but it doesn't give you an ActiveRecord::AssociationProxy so you cannot
# do things like pick.riders.where(:condition => true)
[rider1, rider2, rider3]
end
end
# rider.rb
class Rider < ActiveRecord::Base
has_one :pick
end
This still isn't going to be as clean as doing a has_many :riders and enforcing a limit on the number of riders for each pick (using the built-in relationships as intended).
Related
I have a simple relationship
class School < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :schools
end
A user can be part of many schools but at the same time a user might be the admin of a number of schools. I set up a many-to-many relationship to represent this however I'm not sure how I would distinguish between admins and simple users.
I initially thought of setting a table which has a school_id and a user_id and every entry will represent the school id and the user id of any admins that the school has however I'm not sure how I would represent this in rails or if it's the best way to solve this problem? And if it is, how do I access the table without a model associated to it?
What I mean by what I said above:
school_id user_id
1 3
1 4
Which means that the school with id 1 has 2 admins (3 and 4)
What you are looking for is a more complex many_to_many relationship between school and user called has_many :through. This relationship allows you to have many to many relationship with access to the table that represents the relationship. If you use that relationship, your models should look something like this:
class User < ActiveRecord::Base
has_many :school_roles
has_many :schools, through: :school_roles
end
class SchoolRole < ActiveRecord::Base
belongs_to :school
belongs_to :user
end
class School < ActiveRecord::Base
has_many :school_roles
has_many :users, through: :school_roles
end
And the migrations of those tables would look something like this:
class CreateSchoolRoles < ActiveRecord::Migration
def change
create_table :schools do |t|
t.string :name
t.timestamps null: false
end
create_table :users do |t|
t.string :name
t.timestamps null: false
end
create_table :school_roles do |t|
t.belongs_to :school, index: true
t.belongs_to :user, index: true
t.string :role
t.timestamps null: false
end
end
end
I would suggest to make the "role" field in the "school_roles" migration an integer and then use an enum in the model like so:
class SchoolRole < ActiveRecord::Base
belongs_to :school
belongs_to :user
enum role: [ :admin, :user ]
end
which allows you to add more roles in the future, but it's your call
combining polymorphic association with has_many :through in my opinion is best option.
Let's say you create supporting model SchoolRole, which
belongs_to :user
belongs_to :school
belongs_to :rolable, polymorphic:true
This way:
class School ...
has_many :administrators, :as => :schoolroles
has_many :users, :through => :administators
#school.administrators= [..., ...]
It is quite agile.
#user=#school.administrators.build()
class User
has_many :roles, :as => :rolable
def admin?
admin=false
self.roles.each do |r|
if r.role_type == "administator"
admin=true
break
end
end
admin
end
....
I am a newbie to the Ruby on Rails but stuck at this interesting situation :
I have three Models with me :
User
Company
Project
I want to relate them in the following way :
Company has_many Users
Users belong_to Company
Implemented via company_id column in USERS table
User.Company or Company.Users works fine.
Project has_and_belongs_to_many Users
Users has_and_belongs_to_many Project
Implemented via has_and_belongs_to_many relation using in projetcs_users table
Project.Users or User.Projects works fine.
The real problem comes when I want them to join as following as well :
Company belong_to Owners (:through => User)
Owner has_one Company
---A company can have multiple owners but one user can own just one company. He can't be owner of multiple companies.
---A user may or may not be owner of a company but always belong to some company.
---A user cannot be part of two companies.
Project has_one Project_manager (:through => User)
Project_manager belongs_to Projects
---A Project can have just one Project_manager but one can be project manager of multiple projects.
I am not able to figure out how the relationship should be defined in the Models. Also what should be the table structure for all three models.
My objective is to achieve :
Company.Owners
It can be done by :
#owner = User.find(Company.owner_id)
Project.Project_manager
It can be done by :
#Project_manager = User.find(Project.project_manager_id)
But don't want it that way to work !!
Rest is working perfectly fine for me.
This should help you get on the right track:
class User
# the owner relation
#
# user.owned_company
# => company
#
has_one :owned_company, :class_name => "Company", :foreign_key => "owner_id"
# the project manager relation
#
# user.managed_projects
# => [project,...]
#
has_many :managed_projects, :class_name => "Project", :foreign_key => "project_manager_id"
end
class Company
# the owner relation
#
# company.owner
# => user
#
belongs_to :owner, :class_name => "User"
end
class Project
# the project manager relation
#
# company.project_manager
# => user
#
belongs_to :project_manager, :class_name => "User"
end
Rails ActiveRecord relations are very well documented. See the documentation http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Here is the final output I am able to achieve :
-----Models-----
PROJECTS
class Project < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :users
belongs_to :project_manager, :class_name => "User"
end
USERS
class User < ActiveRecord::Base
attr_accessible :name, :project_id
belongs_to :company
belongs_to :owned_company, :class_name => "Company", :foreign_key => "owner_id"
has_many :managed_projects, :class_name => "Project", :foreign_key => "project_manager_id"
has_and_belongs_to_many :projects
end
COMPANIES
class Company < ActiveRecord::Base
attr_accessible :name
has_many :users
has_many :owners, :class_name => "User", :foreign_key => "owner_id"
end
-----Tables & Migrations-----
PROJECTS
class CreateProjects < ActiveRecord::Migration
def change
create_table :projects do |t|
t.string :name
t.integer :project_manager_id
t.timestamps
end
end
end
USERS
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.integer :project_id
t.integer :owner_id
t.timestamps
end
end
end
COMPANIES
class CreateCompanies < ActiveRecord::Migration
def change
create_table :companies do |t|
t.string :name
t.timestamps
end
end
end
Projects - Users - Join Table
class CreateTableProjectsUsers < ActiveRecord::Migration
def self.up
create_table :projects_users,:id => false do |t|
t.integer :project_id
t.integer :user_id
end
end
def self.down
drop_table :projects_users
end
end
I have a model Shop and a model Customer. A shop can have many customers and a Customer
can buy stuff from many shops. for this relationship I've created a join model
ShopCustomers.
create_table :shop_customers do |t|
t.integer :shop_id
t.integer :customer_id
t.timestamps
end
Models
class Shop < ActiveRecord::Base
has_many :shop_customers, :dependent => true
has_many :customers, :through => shop_customers
has_many :customers_groups
end
class Customer < ActiveRecord::Base
has_many :shop_customers, :dependent => true
has_many :shops, :through => shop_customers
belongs_to :customers_group_membership
end
class ShopCustomer < ActiveRecord::Base
belongs_to :shop
belongs_to :customer
end
Shop owners want to be able to group customers and therefore I added another
model CustomersGroups.
class CustomersGroup < ActiveRecord::Base
belongs_to :shop
end
And the Customers are added to a group through another join model.
create_table :customers_group_memberships do |t|
t.integer :customers_group_id
t.integer :customer_id
end
class CustomersGroupMembership < ActiveRecord::Base
has_many :customers
belongs_to :customers_group
end
Is this the correct way of doing such kind of a relationship or this is a recipe
for doom and am I missing something that would make this not to work.
No, this is not the usual way of doing it. Have a look at the "has and belongs to many" association (AKA: HABTM). It will create the shops_customers join table for you and maintain it without you having to do it by hand.
here's a link to the apidock on HABTM: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many
I am using Ruby on Rails 3 and I would like to set a polymorphic association using namespaced classes.
Migrations are:
create_table :users_users do |t|
t.integer :id
t.string :full_name
t.references :userable, :polymorphic => true
end
create_table :users_profiles do |t|
t.integer :id
...
end
create_table :users_accounts do |t|
t.integer :id
...
end
Classes are:
class Users::User < ActiveRecord::Base
# Association ...
end
class Users::Profile < ActiveRecord::Base
# Association ...
end
class Users::Account < ActiveRecord::Base
# Association ...
end
How I must write code associations for above classes (using :class_name => "Users:User", ...?) in order to auto-create and auto-destroy associated model records, "mapping" those in the users_users table and viceversa?
Do you have some advice about that? What string values I will have in userable_type attributes (example: 'Users::Profile', 'Profile', ...)?
To setup the associations, you don't need to use class name...
class Users::User < ActiveRecord::Base
belongs_to :userable, :polymorphic => true
end
class Users::Profile < ActiveRecord::Base
has_one :user, :as => :userable, :dependent => :destroy
end
class Users::Account < ActiveRecord::Base
has_one :user, :as => :userable, :dependent => :destroy
end
:dependent => :destroy will deal with deleting them when the Users::User is destroyed, but in terms of creating, you've got the same options as you have with normal relationships. If you're doing it from a form, it's best to use nested attributes.
In the database, the userable_type column would include the namespace. So it'd be 'Users::Account' or 'Users::Profile'.
I am trying to do the following in a Ruby on Rails project:
class FoodItem < ActiveRecord::Base
has_and_belongs_to_many :food_categories
has_many :places, :through => :food_categories
end
class FoodCategory < ActiveRecord::Base
has_and_belongs_to_many :food_items
belongs_to :place
end
class Place < ActiveRecord::Base
has_many :food_categories
has_many :food_items, :through => :food_category
end
But calling the instance method some_food_item.places gives me the following error:
ActiveRecord::StatementInvalid: PGError: ERROR: column
food_categories.food_item_id does not exist
LINE 1: ...laces".id = "food_categories".place_id WHERE (("food_cate...
: SELECT "places".* FROM "places" INNER JOIN "food_categories" ON "places".id = "food_categories".place_id WHERE (("food_categories".food_item_id = 1))
Which makes perfect sense - because of the HABTMs on FoodItem and FoodCategory I have the mapping table named food_categories_food_items.
What do I have to do to get some_food_item.places to look places up correctly through the mapping table instead of looking for a food_item_id in the food_categories table?
My first version of the answer was incorrect, but this one works perfectly. I made a couple of typos the first time (the hazard of not actually creating an app to test) but this time I verified. And a plugin is needed, but this is easy. first, install the plugin:
script/plugin install git://github.com/ianwhite/nested_has_many_through.git
This installs Ian White's workaround, and it works seamlessly. Now the models, copied directly from the test app I setup to get this working:
class FoodItem < ActiveRecord::Base
has_many :food_category_items
has_many :food_categories, :through => :food_category_items
has_many :places, :through => :food_categories
end
class FoodCategory < ActiveRecord::Base
has_many :food_category_items
has_many :food_items, :through => :food_category_items
belongs_to :place
end
class FoodCategoryItem < ActiveRecord::Base
belongs_to :food_item
belongs_to :food_category
end
class Place < ActiveRecord::Base
has_many :food_categories
has_many :food_category_items, :through => :food_categories
has_many :food_items, :through => :food_category_items
end
Now "far" associations work just as well. place_instance.food_items and food_item.places both work flawlessly, as well as the simpler associations involved. Just for reference, here's my schema to show where all the foreign keys go:
create_table "food_categories", :force => true do |t|
t.string "name"
t.integer "place_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "food_category_items", :force => true do |t|
t.string "name"
t.integer "food_item_id"
t.integer "food_category_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "food_items", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "places", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
Hope this helps!
UPDATE: This question has come up a few times recently. I wrote an article, nesting your has_many :through relationships, to explain in detail. It even has an accompanying example application on GitHub to download and play around with.
A few months ago I wrote an article about this. In short, has_many through a has_and_belongs_to_many association is not allowed by Rails. However, you can partly simulate the relationship by doing something like this:
class FoodItem < ActiveRecord::Base
has_and_belongs_to_many :food_categories
named_scope :in_place, lambda{ |place|
{
:joins => :food_categories,
:conditions => {:food_categories => {:id => place.food_category_ids}},
:select => "DISTINCT `food_items`.*" # kill duplicates
}
}
end
class FoodCategory < ActiveRecord::Base
has_and_belongs_to_many :food_items
belongs_to :place
end
class Place
has_many :food_categories
def food_items
FoodItem.in_place(self)
end
end
This will give you the some_food_item.places method you seek.
I'm using Rails 3.2.13 and Rasmus, your original setup now seems to work fine on a HABTM.
I'd suggest users try first before attempting a workaround.
This is correct, because you can't peform "has many through" on a join table. In essence, you're trying to extend the relationship one degree further than you really can. HABTM (has_and_belongs_to_many) is not a very robust solution to most problems.
In your case, I'd recommend adding a model called FoodCategoryItem, and renaming your join table to match. You'll also need to add back the primary key field. Then setup your model associations like this:
class FoodItem < ActiveRecord::Base
has_many :food_categories, :through => :food_category_items
has_many :places, :through => :food_categories
end
class FoodCategory < ActiveRecord::Base
has_many :food_items, :through => :food_category_items
belongs_to :place
end
class FoodCategoryItems < ActiveRecord::Base
belongs_to :food_item
belongs_to :food_category
end
class Place < ActiveRecord::Base
has_many :food_categories
has_many :food_items, :through => :food_categories
end
Note, I also fixed a typo in "Place -> has_many :food_items". This should take care of what you need, and give you the added bonus of being able to add functionality to your FoodCategoryItems "join" model in the future.