Many to many relationship in Rails - ruby-on-rails

I'm trying to create a many to many relationship between two models in Rails 3.2.11.
A User can be associated with many Incidents and vice versa.
class User < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
has_many :incident_participants, foreign_key: "participant_id"
has_many :participated_incidents, through: :incident_participants
end
class Incident < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
has_many :incident_participants, foreign_key: "participated_incident_id"
has_many :participants, through: :incident_participants
end
The join table:
class IncidentParticipant < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
t.belongs_to :participant, class_name: "User"
t.belongs_to :participated_incident, class_name: "Incident"
end
Table for IncidentParticipants
create_table "incident_participants", :force => true do |t|
t.integer "participant_id"
t.integer "participated_incident_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
So, why doesn't rails get this relationship? When I try to do #incident.participants in my view I get this error:
"Could not find the source association(s) :participant or
:participants in model IncidentParticipant. Try 'has_many
:participants, :through => :incident_participants, :source => '.
Is it one of ?"
Any ideas?

Try taking out the t.belongs_to and replace with belongs_to.

To create a many to many association you should consider creating an association table. That is to say you will have two 1-M relationships that point to a sort interim table. For instance:
In your first model:
class Example < ActiveRecord::Base
has_and_belongs_to_many :example2
end
In your second model:
class Example2 < ActiveRecord::Base
has_and_belongs_to_many :example
end
Then you need to write a migration to link the two tables together:
class CreateTableExamplesExamples2 < ActiveRecord::Migration
create_table :examples_examples2 do |t|
t.integer :example_id
t.integer :example2_id
end
end
Then just let rails magic work. Check out the guides for more information.

Related

`#post.userview.users << current_user`

Every post has a userview, and each userview has many users. I want one single many to many to have a simple .add() and .remove() function like django. How do I place the current_user into the many-to-many relationship of the views?
I found this:
#post.userview.users << current_user
But it brings up some SQL error. It's suggesting I add a post_id:
ERROR: column userviews.post_id does not exist
LINE 1: SELECT "userviews".* FROM "userviews" WHERE "userviews"."po...
^
HINT: Perhaps you meant to reference the column "userviews.posts_id".
After the answer, now the error is:
can't write unknown attribute `userview_id`
Because I changed the migrations around a little, userview has references to posts and users. Post has_one userview, has_many users through userview, userview has_many posts and has_many users.
create_table "posts", force: :cascade do |t|
t.integer "userview_id"
t.bigint "userviews_id"
t.index ["userviews_id"], name: "index_posts_on_userviews_id"
end
create_table "userviews", force: :cascade do |t|
t.bigint "users_id"
t.bigint "posts_id"
t.integer "post_id"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["posts_id"], name: "index_userviews_on_posts_id"
t.index ["users_id"], name: "index_userviews_on_users_id"
end
class User < ActiveRecord::Base
has_many :viewed_posts, through: :userview, class_name: 'Post'
...
class Userview < ApplicationRecord
belongs_to :post
belongs_to :user
end
class Post < ApplicationRecord
belongs_to :user
has_many :userviews
has_many :viewers, through: :userview, class_name: 'User'
...
Could not find the source association(s) "viewer" or :viewers in model Userview. Try 'has_many :viewers, :through => :userviews, :source => <name>'. Is it one of post or user?
Although, that's for unless #post.viewers.include?(current_user)
Not sure if I understood correctly what are you trying to achieve, but I'll try to fix your associations.
To associate 2 models you need to store foreign key in one of them. Model which stores the key belongs_to other model. Eg in posts table you have user_id column, it means Post belongs_to: user and User has_many :posts (or has_one :post if you need one-to-one association). For such association you can write:
user = User.first
post = Post.last
user.posts << post
If you want all this stuff to work automatically you should follow the convention about naming. Foreign keys should be in singular form, like user_id, not users_id
If you want many-to-many association you need to create an intermediate table, which stores both foreign keys. It can be done using simple direct has_and_belongs_to_many association or with more complex has_many through:
I suppose that in your case it should be:
class User < ActiveRecord::Base
has_many :userviews
# it is posts viewed by the user
# you need to specify class name because it differs from association name
has_many :viewed_posts, through: :userviews, class_name: 'Post'
# posts written by the user
has_many :posts
end
class Userview < ApplicationRecord
belongs_to :post
belongs_to :user
end
class Post < ApplicationRecord
belongs_to :user
has_many :userviews
has_many :viewers, through: :userviews, source: :user
end
It means that you need post_id and user_id columns in userviews table and user_id in posts table. Please, remove useless columns and add needed in migration. When you set it all correctly you'll be able to do
#post.viewers << current_user
to add current_user to viewers list. Corresponding userview instance is created automagically
Changed has_many :viewers, through: :userviews, class_name: 'User'
to has_many :viewers, through: :userviews, source: :user

Rails 5 error when creating model instance with foreign keys

I have two models in use at this point: Airports and Flights.
Flights belong to both an origin airport and destination airport. When I try to create a Flight model instance, I get the following error:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such table: main.destination_airports
I have set up my code to match the example here: https://www.theodinproject.com/courses/ruby-on-rails/lessons/active-record-associations
I've researched the problem (here and elsewhere), but can't turn up anything that seems to match my situation. I can't figure out what's going wrong. Has to be something simple. Something with the migrations, maybe? Thanks in advance for the help.
Relevant code:
Airport model:
class Airport < ApplicationRecord
has_many :departing_flights, foreign_key: "origin_airport_id",
class_name: "Flight"
has_many :arriving_flights, foreign_key: "destination_airport_id",
class_name: "Flight"
validates :abbreviation, presence: true, length: { is: 3 }
end
Flight model:
class Flight < ApplicationRecord
belongs_to :origin_airport, class_name: "Airport"
belongs_to :destination_airport, class_name: "Airport"
has_many :bookings
has_many :passengers, through: :bookings
end
Migrations:
class CreateAirports < ActiveRecord::Migration[5.1]
def change
create_table :airports do |t|
t.string :abbreviation
t.string :full_name
t.string :city
t.string :state
t.string :zip
t.timestamps
end
end
end
class CreateFlights < ActiveRecord::Migration[5.1]
def change
create_table :flights do |t|
t.references :origin_airport, foreign_key: true
t.references :destination_airport, foreign_key: true
t.datetime :depart_time
t.datetime :arrive_time
t.integer :capacity
t.string :airline
t.string :flight_number
t.timestamps
end
end
end
Check the filenames of the migrations, perhaps they are not being run in the correct order?
Also, you may just need to run the migration in the right environment (RAILS_ENV=development)
Update: I found the (an?) answer - changing the migration for the flights table to replace t.references with t.integer and the column name with "x_id" corrects the problem. So, does references only work if using the model name for the column id?

Rails Joining multiple models on a single table

New to rails (using 4.1), making a small project management tool as a learning project. I have run into a slightly more complex model association, and I want to make sure I am on the right track here, and ask how to extend it a bit.
Consider this situation:
Models:
class Website < ActiveRecord::Base
has_many :website_user
has_many :users, through: :website_user
has_many :tasks, through: :website_user
end
class User < ActiveRecord::Base
has_many :websites, through: :website_user
has_many :website_user
end
class WebsiteUser < ActiveRecord::Base
belongs_to :website
belongs_to :user
belongs_to :role
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :website_user
has_one :website, through: :website_user
end
class Role < ActiveRecord::Base
has_many :website_user
end
DB:
create_table "roles", force: true do |t|
t.string "name"
end
create_table "tasks", force: true do |t|
t.text "description"
t.string "title"
t.integer "website_user_id"
end
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.string "password"
t.string "password_hash"
t.string "password_salt"
end
create_table "website_users", force: true do |t|
t.integer "website_id"
t.integer "user_id"
t.integer "role_id"
end
create_table "websites", force: true do |t|
t.string "name"
t.string "url"
end
What I have going on here is basically Websites get users (team members working on sites) associated though the website_user table. That table belongs to roles, so that a team member would have a specific job on this website, and finally, tasks belong to the website_user association, so that you could swap out a user, but the task would stay associated with the role and website.
I am looking into extending it more, so that the task would be associated on website_user twice, once for the assigner, once for the assigned user of the task. However, at this point, it feels like I will have an awful lot of things attached to a big join table in the middle, and without a ton of experience under my belt, it is starting to smell like there might be a better way.
If this all looks good, how would you join the tasks to the website_user twice, once for assigner, once for assigned? Or alternatively, how would rearrange the model association?
A simple solution that first comes to head is to keep assigner and assignee ids in Task model.
Migration AddAssigneeAssignerToTask
class AddAssigneeAssignerToTask < ActiveRecord::Migration
change do
add_reference :task, :assignee, index: true
add_reference :task, :assigner, index: true
end
end
Adding belongs_to into Task model
class Task < ActiveRecord::Base
belongs_to :assignee, class: 'WebsiteUser'
belongs_to :assigner, class: 'WebsiteUser'
has_one :website, through: :assignee
end
Modifying WebsiteUser
class WebsiteUser < ActiveRecord::Base
belongs_to :website
belongs_to :user
belongs_to :role
has_many :assigned_tasks, class_name: 'Task', foreign_key: 'assigner_id'
has_many :received_tasks, class_name: 'Task', foreign_key: 'assignee_id'
end
So afterwards you can use it like this
#website_user.assigned_tasks # => []
#website_user.received_tasks # => [Task1, Task2]
BUT
If you think to add some different functionality to either assigner or assignee, you should consider to use STI or MTI
class Task < ActiveRecord::Base
belongs_to :assignee, class_name: WebsiteUser, foreign_key:website_user_id
belongs_to :assigner, class_name: WebsiteUser, foreign_key:assigner_user_id
end
class WebsiteUser < ActiveRecord::Base
has_many :assigned_tasks, class_name: Task, inverse_of: :assignee, dependent: :destroy, foreign_key: :website_user_id
has_many :tasks_assigned, class_name: Task, inverse_of: assigner, dependent: :destroy, foreign_key: :assigned_user_id
end
You will have to add another foreign key in your tasks table..
just a starting point but this should get you going..
I can not advice you in the database design, but you can assign users twice using an option called class_name. You can read more here: http://guides.rubyonrails.org/association_basics.html#belongs-to-association-reference
But you will have to add additional foreign_key to your Tasks model as well.
And I also advice you to read following chapter of M. Hartle book, as it have really good explanation between relationships of models: https://www.railstutorial.org/book/following_users#cha-following_users

Migration for Users Following Posts

In my application, I have models for Users and Projects.
I want users to have the ability to follow many projects. So users has_many projects, and projects belongs_to users that not only created them but users that follow them too.
So I generated a migration called ProjectRelationship and tried to make it flow below, but it doesn't seem to work. Can somebody help me fix my associations?
Thanks for the help!
project_relationship.rb
class ProjectRelationship < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
project.rb
belongs_to :user
has_many :project_relationships
has_many :followers, through: :project_relationships, source: :user
user.rb
has_many :projects
has_many :project_relationships
has_many :projects_followed, through: :project_relationships, source: :project
schema.rb
create_table "project_relationships", :force => true do |t|
t.integer "follower_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "projectuser_id"
end
add_index "project_relationships", ["follower_id"], :name => "index_project_relationships_on_follower_id", :unique => true
add_index "project_relationships", ["projectuser_id"], :name => "index_project_relationships_on_projectuser_id"
projects/show.html.erb
<%= #project.followers.count %>
You need to specify the foreign keys. The ProjectRelationship model will be expecting the corresponding table to have a "user_id" and "project_id" columns. However, you used different names. So either specify the foreign keys:
class ProjectRelationship < ActiveRecord::Base
belongs_to :user, foreign_key: "follower_id"
belongs_to :project, foreign_key: "projectuser_id"
end
or change the column names in your migration:
create_table :project_relationships do |t|
t.integer :user_id
t.integer :project_id
...
end
You will also need to specify the foreign key in your other models:
class Project < ActiveRecord::Base
belongs_to :user
has_many :project_relationships, foreign_key: "projectuser_id"
has_many :followers, through: :project_relationships, source: :user
end
class User < ActiveRecord::Base
has_many :projects
has_many :project_relationships, foreign_key: "follower_id"
has_many :projects_followed, through: :project_relationships, source: :project
end
#roma149 - Thanks for your response. I updated the controllers, routes, and what you said. No errors generate, but when I click the button follow in _follow.html.erb, it does not seem to follow the project or update the count "#project.followers.count"
Moved details to here: Why doesn't my user follow/unfollow button work?

has_many :through a has_and_belongs_to_many association

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.

Resources