I created a FriendRequest.rb model in my Rails app with the following table columns.
create_table "friend_requests", force: true do |t|
t.integer "requesting_user_id"
t.integer "requested_friend_id"
t.datetime "created_at"
t.datetime "updated_at"
end
With relationships defined as you see below, I added this code to a /views/users/show.html.erb page show the friend requests that have been made for each user. However, I'm getting this error
PG::UndefinedColumn: ERROR: column friend_requests.user_id does not exist
because, I obviously didn't create a user_id column. Is there a way that I can make this code work by adding more information to the relationships? or should I scrap my work and do it differently?
<% for user in #user.friend_requests %>
<li><%= h user.name %></li>
<%= link_to "Add Friend", friend_requests_path(:friend_id => user), :method => :post %>
<% end %>
User.rb
has_many :friend_requests
FriendRequest.rb
belongs_to :user
Just change your has_many association for:
has_many :friend_requests, foreign_key: 'requesting_user_id'
By default, Rails will look for [model_name]_id in the other table, this is why it is looking for user_id, but by adding the foerign_key: option, you can override this default behaviour and tell Rails what is the name of the foreign_key you want to use.
You can use this configuration:
class User < ActiveRecord::Base
has_many :friend_requests
has_many :requesters, through: friend_requests
end
class FriendRequest < ActiveRecord::Base
belongs_to :requester, foreign_key: 'requesting_user_id'
belongs_to :requested, foreign_key: 'requested_friend_id'
validates :requester_id, presence: true
validates :requested_id, presence: true
end
Take a look at:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Look especially for the options :primary_key and :foreign_key
Related
I would like to add a review section to my app. To be more specific, a user can leave a review for a shop and the shop can then reply to that review. But I'm not sure if the model associations and review table migrations I have are correct.
class User < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :user
end
class ReviewReply < ApplicationRecord
belongs_to :user, optional: true
belongs_to :review, optional: true
end
class Shop < ActiveRecord::Base
has_many :reviews
end
class CreateReviews < ActiveRecord::Migration[6.0]
def change
create_table :reviews do |t|
t.text :body
t.integer :rating
t.references :shop, null: false, foreign_key: true
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
Architecture:
1. CALCULATING RATING
console:
rails g migration add_rating
migration:
def change
add_column :shops, :average_rating, :integer, default: 0, null: false
add_column :reviews, :rating, :integer, default: 0, null: false
end
user.rb
has_many :reviews
shop.rb
has_many :reviews
def update_rating
if reviews.any? && reviews.where.not(rating: nil).any?
update_column :average_rating, reviews.average(:rating).round(2).to_f
else
update_column :average_rating, 0
end
end
review.rb
belongs_to :user
belongs_to :shop
has_one :review_reply
after_save do
unless rating.nil? || rating.zero?
shop.update_rating
end
end
after_destroy do
shop.update_rating
end
review_reply.rb
belongs_to :review
2. REPLY TO A REVIEW
views/reviews/show.html.erb:
<% unless #review.review_reply.present? %>
<%= link_to "Write a Reply", new_review_reply_path(review_id: #review.id) %>
<% end %>
views/review_replies/form.html.erb
= f.input :review_id, input_html: {value: params[:review_id]}, as: :hidden
There are a couple of things things that I would change if it's a real application (= not just something you're toying around with):
I'd remove the optional: true from the two associations in ReviewReply since replies don't make sense (data-wise) if they don't have an author or aren't connected to a review.
I'd also set these two columns in review_replies to null: false.
You should think about adding some deletion cascades (either adding dependent: :some_action in models or using on_delete: :some_action on the database columns – I'd recommend the latter):
delete review replies when a review is deleted?
delete review replies when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
delete reviews when a shop is deleted?
delete reviews when a user is deleted? (or just set it to NULL and then show "Deleted User" in the UI?)
In your migration polymorphic relation for reviewable is set up incorrectly - unique index on reviewable_type will prevent adding multiple records, better use
t.references :reviewable, polymorphic: true
in modern rails it adds non-unique index on both columns by default (you can be explicit with index: true, but in any way this is different from two separate indexes on each column alone)
Also most probably you want your unique index to include user id so that each user can review each reviewable once:
t.index [:reviewable_type, :reviewable_id, :user_id], unique: true, name: 'idx_unique_user_review'
Reason for including both shop and reviewable is not clear, but it depends on your application and goals. If the shop itself is the reviewable (as suggested by has_many :reviews, as: :reviewable) - then shop reference is useless. But in fact if you do not plan on extending reviews on something other than shops - it's easier to go with non-polymorphic reference for now.
In large app it's better to have common name prefixes for related things, so ReviewReply model name is better. belongs_to :review is most probably not optional, it's very strange to reply to nothing. Also it will most likely have belongs_to :shop (also not optional) not user, since the shop is the one replying.
recently I have a migration that adds a user_id column to the watch_events tables. and thus I want to change the watch_event models to handle belongs_to but with multiple approach
create_table 'users', force: :cascade do |t|
t.integer 'id'
t.integer 'customer_id'
end
create_table 'watch_events', force: :cascade do |t|
t.integer 'customer_id'
t.integer 'user_id'
end
previously
class WatchEvent < ApplicationRecord
belongs_to :user, foreign_key: :customer_id, primary_key: :customer_id
end
what I want:
if watch_event.customer_id is present, i want to use belongs_to :user, foreign_key: :customer_id, primary_key: :customer_id
if watch_event.customer_id is not present, i want to use normal belongs_to :user
how can I achieve this on the watch_event model?
I do not think that Rails supports 'fallback foreign keys' on associations. However, you can write a simple wrapper for your problem. First, relate your WatchEvent class twice to the user model, using your two keys and two 'internal' association names (:user_1 and :user_2). Then, add a 'virtual association reader' (user) and a 'virtual association setter' (user=(new_user)):
class WatchEvent < ApplicationRecord
belongs_to :user_1,
class_name: 'User',
foreign_key: :customer_id
belongs_to :user_2,
class_name: 'User',
foreign_key: :user_id
def user
user_1 || user_2
end
def user=(new_user)
self.user_1 = new_user
end
end
With this solution, the requirements "use customer_id to find user" and "use user_id as fallback foreign key if customer_id is nil or doesn't yield a result" is satisfied. It happens in the association reader method user. When there is a reader method, you'll need a setter method, which is user=(). Feel free to design the setter's internals as required, mine is just a suggestion.
BTW: You may need to re-add the declaration of the foreign primary_key. I omitted that for clarity.
If I understand your question correctly, then what you are looking for is a Polymorphic association.
If you see the code below, what it basically does is create two columns in the watch_events table, watcher_type and watcher_id. And the belongs_to :watcher then uses the watcher_type column to identify which model it should associate to,
create_table 'watch_events', force: :cascade do |t|
t.references 'watcher', polymorphic: true, null: false
end
class WatchEvent < ApplicationRecord
belongs_to :watcher, polymorphic: true
end
Versions: CENTOS7, mysql2('>= 0.3.13', '< 0.5'), rails('4.2.6')
index.html.erb
<% #sections.each do |section| %>
<tr>
<td><%= section.course_id %></td>
<td><%= section.term_id %></td>
<td><%= section.user_id %></td>
</tr>
<% end %>
sections controller
class SectionsController < ApplicationController
before_action :authenticate_account!, except: [:show]
def index
#sections = User.find_by_account_id(current_user).courses
end
def show
end
end
createSections migration
class CreateSections < ActiveRecord::Migration
def change
create_table :courses do |t|
t.integer :course_id
t.timestamps null: false
end
create_table :terms do |t|
t.integer :term_id
t.timestamps null: false
end
create_table :users do |t|
t.integer :user_id
t.timestamps null: false
end
create_table :sections do |t|
t.belongs_to :course, index: true
t.belongs_to :term, index: true
t.belongs_to :user, index: true
t.timestamps null: false
end
end
end
course.rb model
belongs_to :user
has_many :sections
has_many :terms, :through => :sections
term.rb model
belongs_to :user
has_many :sections
has_many :courses, :through => :sections
section.rb model
belongs_to :course
belongs_to :term
belongs_to :user
user.rb
has_many :sections
has_many :courses, :through => :sections
has_many :terms, :through => :sections
Expected result: List the current(logged in) user's courses/terms/ID
Current result: blank
This is my first time working with rails and SO, I tried changing the relationships a few times to see if anything would change but not sure how to approach this. I have tried using ActiveRecord:Associations as a reference. What do I need to do to make this work?
If you already have current_user then you can just call the related models directly.
#sections = User.find_by_account_id(current_user).courses
to
#sections = current_user.courses
The first thing to address is that you typically want separate migrations for each distinct thing that you're migrating. It's a convention that helps you keep fine-grained control over your changes, and it helps keep your migrations clean.
Sometimes, though, you actually want to have a "mass" migration. You have a mass-migration here. In the mass-migration circumstance, the migration name should reflect the combined purpose, so you'd want to name it something like CreateCoreTables; CreateSections is too narrow a name for creating multiple tables. You will also need to change the name of the migration file to be 2016XXXXXXXX_create_core_tables.rb, where XXXXXXXXX is left as the previous value.
Next, you'll want to correct your use of keys (the _id columns), as these are improperly declaring the association fields, which will cause the associations to not work (or work incorrectly).
Instead, you want something like this:
class CreateCoreTables < ActiveRecord::Migration
def change
create_table :courses do |t|
t.timestamps
end
create_table :terms do |t|
t.timestamps
end
create_table :users do |t|
t.timestamps
end
create_table :sections do |t|
t.integer :user_id, null: false
t.integer :term_id, null: false
t.integer :course_id, null: false
t.timestamps
end
end
end
It's difficult to tell what the actual intended relationships are from the code provided. It would be worth reading on Active Record Migrations to make sure that you understand what relationships you intend, and how to describe them. While you're working out the migrations, keep revisiting the model relationships, as well. These are the bedrock of the application, so you want to spend time getting them right.
Remember, for ActiveRecord relationships, these rules will guide you:
If a model owns another model, use has_many or has_one
In the owned model, use belongs_to
If a model needs access to another model (but doesn't own it), use a has_many, :through relationship
Models cannot have has_many or has_one relationships directly to each other; you need an intermediate table in that case with the requisite belongs_to for each of the other tables
Once you have the migrations and relationships worked out, you can move on to the query. You can use Rails Eager Loading to optimize the query to retrieve the associations at the same time. This will address your current functional needs, and prevent an N+1 query issue at the same time.
#sections = Section.joins(courses: :terms).where(user: current_user)
When you have retrieved #sections, you can do these types of actions to get the data that you want. The courses and terms members are collections, and you can interact with them as though they were arrays:
#sections.each do |section|
puts "Section: #{section.id}"
puts "Number of user sections: #{#sections.courses.length}"
section.courses.each do |course|
puts "Course: #{course.id}"
end
puts "Number of user terms: #{#sections.terms.length}"
#sections.terms do |term|
puts "Term: #{term.id}"
end
puts "User's email: #{#sections.user.email}"
end
Once you've mastered these, you've got the basics of Rails. Work on one model/controller at a time to keep from overcomplicating the work; you can always add on more once once component is working like you expect. Always make sure that you have your foundation working before you move onto new aspects of the app that will depend on it.
Also, remember to use the Rails guides. They're very helpful, so keep them on hand at all times while you're learning. SO is also a great resource, and make sure that you ask pointed questions, so that you can get direct answers.
I've looked for many solutions on the web and I can't seem to find my answer.
I have a polymorphic association for a table links that it linked to many other tables.
Here is my models a bit simplified:
links.rb
class Links < ActiveRecord::Base
belongs_to :linkable, polymorphic: true
end
events.rb
class Event < ActiveRecord::Base
has_many :links, as: :linkable
accepts_nested_attributes_for :links
end
here is the admin form
events.rb
ActiveAdmin.register Event do
form do |f|
f.has_many :links do |link_f|
link_f.inputs "links" do
link_f.input :url
end
end
f.actions
end
end
Here's what in my schema.rb
create_table "links", force: true do |t|
t.string "url"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "linkable_id"
t.string "linkable_type"
end
It throws me that error: uninitialized constant Event::Link
I can't seem to find the problem and it is driving me nuts...
It seems like a relation is missing or something but I can't find it.
Thanks a lot for every one that can help!
I think the problem is in the way you named your models. Models are always declared as singular entities, not plural.
You should:
Rename links.rb to link.rb
Rename events.rb to event.rb
Rename class Links < ActiveRecord::Base to class Link < ActiveRecord::Base
and see if that helps.
My app uses a :has_many :through association, and I'm having trouble figuring out how to most efficiently load and display data from both ends of the association and the association itself.
Here are my classes:
class Person < ActiveRecord::Base
has_many :people_ranks
has_many :ranks, :through => :people_ranks
has_many :institutions_people
has_many :institutions, :through => :institutions_people
belongs_to :school
belongs_to :department
end
class Institution < ActiveRecord::Base
has_many :institutions_people
has_many :people, :through => :institutions_people
end
class InstitutionsPerson < ActiveRecord::Base
belongs_to :institution
belongs_to :person
end
and their corresponding models:
create_table :people, :force => true do |t|
t.string :name
t.string :degree
t.integer :year_grad
t.integer :year_hired
end
create_table :institutions, :force => true do |t|
t.string :name
t.integer :ischool
end
create_table :institutions_people, :id => false do |t|
t.integer :institution_id
t.integer :person_id
t.string :rel_type
end
I want to show a person's institution info with something like #person.year_hired, #person.institution.name, and #person.institution.institutions_people.rel_type (where rel_type is either "graduated" or "hired:), but I know that third part won't work. Using the following in the show bit in the person_controller:
#person = Person.find(params[:id], :include => [:school, :department, :institutions_people, :people_ranks, {:institutions_people => :institution}, {:people_ranks => :rank}])
gives me access to #person.institutions and #person.institutions_people, but how do I connect the rel_type attribute from the join to the person-institution relationship? (I'm coming from PHP and now how to build the SQL and loop through it there, but RoR has me stumped.)
I've looked for help under "eager loading" and "associations with :has_many :through", but I get answers about building the associations. My question is really about accessing the association's data after it exists. My app uses static data, and I'm not worried about the update, destroy, or create methods. Thank you for your help!
The way to access the data is through the institutions_people association. So, you would do something like:
me = Person.first
rel = me.institutions_people.first
And then in the view
<%= rel.rel_type %> from <%= rel.institution.name %>
Alternatively, you can give yourself a full list of institutions along with their info:
me = Person.first
And then in the view:
<% for ip in me.institutions_people %>
<%= ip.rel_type %> from <%= ip.institution.name %>
<% end %>