User belongs_to another User in Rails - ruby-on-rails

I'm trying to determine the appropriate relationship to use in Rails. I'm going to have Users who can belong to one other User (their spouse). They will both be of the same model type (User).
I want to be able to call User.spouse on either user and get the user that is associated with them, and also take advantage of being able to build nested attributes for each other. Can I add a tag in the model specify a call to .spouse should return this user? Or would it just be user.user?

In your user.rb model you can make an association like this:
class AdminUser < ApplicationRecord
has_one :spouse, through: :user, foriegn_key: :spouse_id
This is assuming you have a field in your users table called spouse_id which is a foreign_key to users. (see below)
More information about this can be found here: Rails has_one :through association
When you add your spouse_id (or spouse_user_id might be a better name) via a migration don't forget to add a foreign key to strongly enforce legitimate data at the DB level.
add_column :users, :spouse_user_id, :integer
add_foreign_key :users, :users, column: 'spouse_user_id'
Example usage:
User id: 1, name: 'Fred', spouse_user_id: 2
User id: 2, name: 'Wilma', spouse_user_id: 1
User.find(1).spouse
=> Wilma
User.find(2).spouse
=> Fred

Related

Creating a many-to-many record on Rails

I have a simple task list app that has users and lists on it, the users are managed by Devise, and can create task lists, as well as favorite lists created by other users, or by themself. The relation of ownership between users and lists were easy to establish, but I am having trouble setting up the relation of a user favoriting a list. I envision it being a many-to-many relation after all, a user can favorite many lists and a list can be favorited by many users, this relationship happening on top of another already existing one-to-many relationship of list ownership by a user gave me some pause as to whether this is good practice to do, but I proceeded with my attempt regardless.
Currently I have two models, one for the user, and one for the list, and I tried to create a migration for the favorites by running rails g migration CreateJoinTableFavorites users lists, which resulted in the following migration
class CreateJoinTableFavorites < ActiveRecord::Migration[5.2]
def change
create_join_table :users, :lists do |t|
t.index [:user_id, :list_id] <-- I uncommented this line
# t.index [:list_id, :user_id]
t.timestamps <-- I added this line
end
end
end
I thought this would create a table named "Favorites" that would automatically link users and lists, but instead it created a table called "lists_users". Now I am stuck as to what to do next. I have read that I need to create a model for this join table, but I don't know how to go about doing that. What command do I run? rails g model Favorites? rails g model ListsUsers? do I also inform the fields I want to add such as rails g model Favorites user_id:integer list_id:integer, or is there another better way to do it such as perhaps rails g model Favorites user:references list:references? What's the best practice here
Beyond that, I have added a button inside my list#show view for the user to click to add that list to their favorites, and had some trouble routing it. What I did was create a button like this:
<%= button_to 'Add to favorites', add_favorites_path({list_id: #list.id}), method: :post %>
as well as a new route:
post 'add_favorites', to: 'lists#add_favorites'
Though this I managed to have access to the list id and user id in that action, now I don't know how to proceed to create the "favorite" database entry in my lists_users table. To illustrate, I'll paste here my "add_favorite" action
def add_favorites
user_id = current_user.id
list_id = params[:list_id]
#TODO: create the relation in lists_items table
end
I'm aware that I can't get this to work without the model for the join table, but even if I had that model, I haven't had much luck searching for what to do within the controller to create that relation. Anyway, my models are as follows:
class List < ApplicationRecord
belongs_to :user
has_many :users, through: :lists_users
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :lists
has_many :lists, through: :lists_users
end
So to summarize, I am aware that I am missing a model for the join table, and would like a step-by-step as to how to create it, what name to give it, etc, as well as how to proceed within my action in my controller to create a new favorite entry
There are two ways to create a many-to-many relation in Rails. What you're doing seems to conflate the two, which I suspect is the source of your problem.
Briefly, the two methods are:
1) has_many :other_models, through: :relation or
2) has_and_belongs_to_many :other_models
The main difference being that the "has_many through" method expects the join table to be a separate model which can be handled independently of this relationship if need be, while the "has_and_belongs_to_many" method does not require the join table to have a corresponding model. In the latter case, you will not be able to deal with the join table independently. (This makes timestamps on the join table useless, by the way.)
Which method you should go with depends on your use case. The docs summarize the criteria nicely:
The simplest rule of thumb is that you should set up a has_many :through relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a has_and_belongs_to_many relationship (though you'll need to remember to create the joining table in the database). (emphasis added)
Now for your question: When you use create_join_table, you're treating it as though you're setting things up for a has_and_belongs_to_many relation. create_join_table will create a table named "#{table1}_#{table2}" with ids pointing to those tables. It alphabetizes them too, which is why you got "lists_users" instead of "users_lists". This is in fact the standard naming convention for rails join tables if you are planning on using has_and_belongs_to_many, and generally shouldn't be renamed.
If you really want to use has_and_belongs_to_many, keep the migration with the create_join_table and just do the following in your models:
# user.rb
class User
has_and_belongs_to_many :lists
end
# list.rb
class List
has_and_belongs_to_many :users
end
And voila. No Favorite model is needed, and rails is smart enough to handle the relationships through the table on its own. Although a bit easier, the downside is, as stated above, that you won't be able to deal with the join table as an independent model. (Again, timestamps on the join table are useless in this case, as Rails won't set them.)
Edit: Since you can't directly touch lists_users, you'd create relationships by setting the lists relation on a user, or by setting the users relation on lists, like so:
def add_favorites
list = List.find(params[:list_id])
current_user.lists << list # creates the corresponding entry in lists_users
# Don't forget to test how this works when the current_user has already favorited a list!
# If you want to prevent that from happening, try
# current_user.lists << list unless current_user.lists.include?(list)
# Alternatively you can do the assignment in reverse:
# list.users << current_user
# Again, because the join table is not an independent model, Rails won't be able to do much to other columns on lists_users out of the box.
# This includes timestamps
end
On the other hand, if you want to use "has_many through", don't use create_join_table. If you're using has_many through, the join table should be thought of almost as an entirely separate model, that just happens to have two foreign keys and tie two other models together in a many-to-many relationship. In this case, you'd do something like:
# migration
class CreateFavorites < ActiveRecord::Migration[5.2]
def change
create_table :favorites do |t|
t.references :list
t.references :user
t.timestamps
end
end
end
# user.rb
class User
has_many :favorites
has_many :lists, through: :favorites
end
# list.rb
class List
has_many :favorites
has_many :users, through: :favorites
end
# favorite.rb
class Favorite
belongs_to :list
belongs_to :user
end
# controller
def add_favorites
# You actually have a Favorite model in this case, while you don't in the other. The Favorite model can be more or less independent of the List and User, and can be given other attributes like timestamps.
# It's the rails methods like `save`, `create`, and `update` that set timestamps, so this will track those for you as any other model.
Favorite.create(list_id: params[:list_id], user: current_user)
end
You might want to reflect on which method to use. Again, this really depends on your use case, and on the criteria above. Personally, when I'm not sure, I prefer the "has_many through" method as it gives you more tools to work with and is generally more flexible.
You may try following :
class User
has_and_belongs_to_many :lists
end
class List
has_and_belongs_to_many :users
end
class CreateUsersAndLists
def change
create_table :users do |t|
# Code
end
create_table :lists do |t|
# Code
end
create_table :users_lists id: false do |t|
t.belongs_to :user, index: true
t.belongs_to :list, index: true
t.boolean :is_favourite
end
end
end

rails4 scoped has_many association

In my product_users joint table there is a role column besides the product_id and user_id.
I have this association in my product model.
has_many :owners, -> { where(product_users: { role: "owner" }) },
through: :product_users, source: :user
All of the products will have only one "owner" and the rest will be "member".
What association should I use to to get the owner of the product instead of an owners collection. So in the views I wanna use product.owner. I couldn't figure out how to use either has_one or belongs_to.
I could use this instance method, but I guess it would be better to define a fine association somehow.
def owner
owners.first
end
I guess the easiest way to do that, would be to add a column "owner_id" to the product. Then, on the product:
belongs_to :owner
and on the user something like this
has_many :owned_products, class_name: "Product", foreign_key: "owner_id"
The "class_name" tells the association that you will be looking for a "Product" and the foreign_key, will define what column will be used to compare with users id.
If you don't want to add additional column, then you can name association to "has_many :owner", but that's wrong on so many levels, that you shouldn't do it. So in the case you don't want to add additional column, stick to the method.
def owner
owners.first
end

Custom includes association in rails

I have the following table
Users(user_id, name, last_name)
Relationships(user_id, spouse_id)
I want to run the following query
person = Relationships.includes(:person).where(:name => 'David')
But I don't want the related person to be associated by the user_id. I would rather it be associated by the spouse_id only for this specific query.
Rails is perfectly content to allow multiple associations to the same table, providing they use a different foreign key.
In this case, you'd have something like this:
class User
has_many :relationships
has_many :spouses, class_name: "Relationship", foreign_key: :spouse_id
end
User.first.spouses would then be a collection of all relationships where the user's id was present in the spouse_id field.

Create if record does not exist

I have 3 models in my rails app
class Contact < ActiveRecord::Base
belongs_to :survey, counter_cache: :contact_count
belongs_to :voter
has_many :contact_attempts
end
class Survey < ActiveRecord::Base
has_many :questions
has_many :contacts
end
class Voter < ActiveRecord::Base
has_many :contacts
end
the Contact consists of the voter_id and a survey_id. The Logic of my app is that a there can only be one contact for a voter in any given survey.
right now I am using the following code to enforce this logic. I query the contacts table for records matching the given voter_id and survey_id. if does not exist then it is created. otherwise it does nothing.
if !Contact.exists?(:survey_id => survey, :voter_id => voter)
c = Contact.new
c.survey_id = survey
c.voter_id = voter
c.save
end
Obviously this requires a select and a insert query to create 1 potential contact. When I am adding potentially thousands of contacts at once.
Right now I'm using Resque to allow this run in the background and away from the ui thread. What can I do to speed this up, and make it more efficient?
You can do the following:
Contact.where(survey_id: survey,voter_id: voter).first_or_create
You should add first a database index to force this condition at the lowest level as possible:
add_index :contacts, [:voter_id, :survey_id], unique: true
Then you should add an uniqueness validation at an ActiveRecord level:
validates_uniqueness_of :voter_id, scope: [:survey_id]
Then contact.save will return false if a contact exists for a specified voter and survey.
UPDATE: If you create the index, then the uniqueness validation will run pretty fast.
See if those links can help you.
Those links are for rails 4.0.2, but you can change in the api docks
From the apidock: first_or_create, find_or_create_by
From the Rails Guide: find-or-create-by
It would be better if you let MySQL to handle it.
Create a migration and add a composite unique key to survey_id, voter_id
add_index :contact, [:survey_id, :voter_id], :unique=> true
Now
Contact.create(:survey_id=>survey, :voter_id=>voter_id)
Will create new record only if there is no duplicates.

Rails 3, belongs_to, has one? For 3 models, Users, Instances, Books

I have the following models:
Users (id, name, email, instance_id, etc...)
Instances (id, domain name)
Books (id, name, user_id, instance_id)
In Rails 3, When a new book is created, I need the user_id, and instance_id to be populated based on the current_user.
Currently, user_id is being assigned when I create a new book but not instance_id? What needs to happen in rails to make that field get filled out on book creation?
Also, shouldn't rails be error'ing given that I can create books without that instance_id filled out?
thxs
It looks like you have de-normalized User and Book models by adding reference to Instance model. You can avoid the redundant reference unless you have a specific reason.
I would rewrite your models as follows:
class Instance < ActiveRecord::Base
has_many :users
has_many :books, :through => :users, :order => "created_at DESC"
end
class User < ActiveRecord::Base
belongs_to :instance
has_many :books, :order => "created_at DESC"
end
class Book < ActiveRecord::Base
belongs_to :user
has_one :instance, :through => :user
end
Now to create a new book for a user.
current_user.books.build(...)
To get a list of the books belonging to user's instance:
current_user.instance.books
To get a list of the books created by the user:
current_user.books
Make sure you index the instance_id column in users table and user_id column in books table.
Rails will only produce an error in this case if (a) you have a validation that's failing, or (b) you have database foreign keys that aren't being satisfied.
What's an instance? i.e. if instance_id is to be populated based on the current user, what attribute of the user should supply it? (the instance_id? why?)

Resources