How can I name the relationship between two records? - ruby-on-rails

Let's say I have a model Movie. Movies can have_many of each other through an intermediary model AssociatedMovie.
How can I specify the nature of the relationship between two Movies? For any given pair of Movies, the relationship may be prequel/sequel, or remake/original, or inspired/inspired by, or related/related, etc. Right now, I can't give the relationships names.
Here's my schema and associations:
create_table "movies", force: true do |t|
t.string "title"
end
create_table "associated_movies", force: true do |t|
t.integer "movie_a_id"
t.integer "movie_b_id"
end
class Movie < ActiveRecord::Base
has_many :movies, :through => :associated_movies
end
class AssociatedMovie < ActiveRecord::Base
has_many :movies
end
And here's the query for setting each Movie's associated Movies:
def movie_associated_movies
associated_movie_ids = AssociatedMovie.
where("movie_a_id = ? OR movie_b_id = ?", self.id, self.id).
map { |r| [r.movie_a_id, r.movie_b_id] }.
flatten - [self.id]
Movie.where(id: associated_movie_ids)
end
I think I'd probably have to add movie_a_type and movie_b_type attributes to AssociatedMovie. But I'm not sure how I could specify which Movie is attached to which type.
Anyone have any ideas?

You're already half-way there with has_many :through (using an intermediary model) - this allows you to add as many extra attributes as you like.
I think your problem is down to your relationships, which I'll explain below:
#app/models/movie.rb
class Movie < ActiveRecord::Base
has_many :associated_movies, foreign_key: :movie_a_id
has_many :movies, through: :associated_movies, foreign_key: :movie_b_id
end
#app/models/associated_movie.rb
class AssociatedMovie < ActiveRecord::Base
belongs_to :movie_a, class_name: "Movie"
belongs_to :movie_b, class_name: "Movie"
end
The above will give you access to:
#movie = Movie.find params[:id]
#movie.associated_movies #-> collection of records with movie_a and movie_b
#movie.movies #-> all the movie_b objects
--
Because you're using has_many :through, rather than has_and_belongs_to_many, you'll be at liberty to add as many attributes to your join model as you need:
To do this, you just have to add a migration:
$ rails g migration AddNewAttributes
#db/migrate/add_new_attributes_________.rb
class AddNewAttributes < ActiveRecord::Migration
def change
add_column :associated_movies, :relationship_id, :id
end
end
$ rake db:migrate
-
... I apologize if this is a little off-course; however I would actually add a separate model for your relationships (considering you have them predefined):
#app/models/relationship.rb
class Relationship < ActiveRecord::Base
#columns id | movie_a_type | movie_b_type | created_at | updated_at
has_many :associated_movies
end
#app/models/associated_movie.rb
class AssociatedMovie < ActiveRecord::Base
belongs_to :movie_a, class_name: "Movie"
belongs_to :movie_b, class_name: "Movie"
belongs_to :relationship
delegate :movie_a_type, :movie_b_type, to: :relationship
end
This may seem a little bloated (it is), but it will provide extensibility.
You'll have to add another table, but it will ultimately provide you with the ability to call the following:
#movie.associated_movies.each do |associated|
associated.movie_a #-> current movie
associated.movie_b #-> related movie
associated.movie_a_type #-> "Original"
associated.movie_b_type #-> "Sequel"
end
You'd then be able to pre-populate the Relationship model with the various relationships you'll have.
I can add to the answer as required.

Related

Creating an ignorable polymorphic relationship

I am looking to create a table called ignorables with ignorable_type and ignorable_id where I can store IDs of different types of objects I'd like to ignore in the UI.
How can I leverage ActiveRecord's polymorphic associations to achieve this?
I have to say that I don't 100% get your question but this would be my take
1) you would need to create the table. I am sure you know how to do this, but this is how
rails g migration CreateIgnores
#db/migrate/20180106072916_create_ignorables.rb
class CreateIgnors < ActiveRecord::Migration[5.0]
def change
create_table :ignors do |t|
t.integer :ignorable_id
t.string :ignorable_type
...
end
add_index :ignors, [:ignorable_type, :ignorable_id]
end
end
2) now you can create your models
class Ignore < ApplicationRecord
belongs_to :ignorable, polymorphic: true
end
3) what belongs to the ignorable
class Other < ApplicationRecord
has_many :ignored, as: :ignorable
end
4) now last but not list you want to do do something
class Other < ApplicationRecord
has_many :ignored, as: :ignorable
validate :belongs_to_ignored
...
private
def belongs_to_ignored
if ignored
false
end
end
end
I hope that this can lead you in the right direction

Rails 5 form field to custom params

I have a class Reservation in which I want to put a collection (HABTM) of Resources, through an intermidiate class of ReservedResources. This is not a classic HABTM as in ReservedResources I want to store the quantity of the resource reserved.
On the form I want to display the available resources dynamically, something like this:
<% #resources.each do |r| %>
<%= f.label "#{r.name}" %>
<%= f.number_field "reserved_resources#{r.id}".intern %><br>
<% end %>
The problem is that there is no method named "reserved_resourcesXX", where XX is the resource ID. I want to store the quantity of the resource per its ID on the params hash, but I can't manage to do that...
Is it possible? If so, how? I want to avoid JS for this...
Actually what you should have is something like:
class Reservation < ApplicationRecord
has_many :reserved_resources
has_many :resources, through: :reserved_resources
end
class Resource < ApplicationRecord
has_many :reserved_resources
has_many :reservations, through: :reserved_resources
end
class ReservedResource < ApplicationRecord
belongs_to :reservation
belongs_to :resource
end
Now assuming you have reserved_quantity attribute in reserved_resources table.
Example of use from rails console:
resource = Resource.create(name: 'resource 1')
reservation = Reservation.create(name: 'reservation 1')
resource.reserved_resources.create(reservation: reservation, resource_quantity: 20)
reservation = Reservation.create(name: 'reservation 2')
resource.reserved_resources.create(reservation: reservation, resource_quantity: 10)
Sanity check:
resource.reservations.count
=> 2
you can then get total quantity of reserved resources for a particular resource by doing:
resource.reserved_resources.sum(:resource_quantity)
=> 30
I'll just start off with an example which I guess will get you going.
Scenario
Imagine a store selling products. Users can reserve products to later buy them. They can also define how many of each product they want to reserve.
class User
has_many :reservations
has_many :products, through: :reservations
end
class Product
has_many :reservations
has_many :users, through: :reservations
end
class Reservation
belongs_to :user
belongs_to :product
end
Now when creating your reservations model you can simply add an extra field quantity, resulting in this reservations schema:
create_table "reservations" do |t|
t.integer "user_id",
t.integer "product_id",
t.integer "quantity",
t.datetime "created_at"
t.datetime "updated_at"
end
So you take a basic HABTM relationship and add the extra field. Thats exactly what HABTM is for, to add extra info when it is required.
Your usecase
Adapt this to your models and I assume the html forms will be kinda straightforward from there on.

Deleting multiple objects in HABTM reference table

Im trying to destroy multiple records in my database table where :list column has the same name, however I get an error when I click on Destroy link: Could not find table 'bookmarks_posts', it says the error is in my controller line:
if #bookmarks.destroy_all
Why is it expecting a join table? How can I change that? Also I don't want to destory anything outside the given Bookmarks table. (I am using sqlite3, if that changes anything)
My table migration:
class CreateBookmarks < ActiveRecord::Migration
def change
create_table :bookmarks do |t|
t.string :list
t.references :user, index: true, foreign_key: true
t.references :post, index: true, foreign_key: true
t.timestamps null: false
end
end
end
My controller - destroy and show:
def destroy
#list = Bookmark.find(params[:id])
#bookmarks = Bookmark.where(:list => #list.list)
if #bookmarks.destroy_all
redirect_to bookmarks_url
end
end
def show
#lists = Bookmark.where(user_id: current_user.id).where(post_id: nil)
#list = Bookmark.find(params[:id])
#bookmarks = Bookmark.where.not(post_id: nil).where(list: #list.list)
#posts = Post.where(:id => #bookmarks.map(&:post_id))
end
in my show view I use this:
<%= link_to 'Destroy', #list, method: :delete %>
My models:
class Bookmark < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :posts
end
"Why is it expecting a join table?"
Because you have specified a HABTM association between Bookmark and Post models. So when you delete a Bookmark or a Post, it wants to remove any rows in the join table that are referencing the deleted item's ID.
The problem seems to be that you're either specifying the wrong association type in your models, or you've created the wrong migration to support a HABTM association.
For discussion, let's assume your database migration above is correct, eg: you want to store a user_id and post_id in the Bookmarks table. This means that you would have the following associations:
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class User < ActiveRecord::Base
has_many :bookmarks
end
class Post < ActiveRecord::Base
has_many :bookmarks
end
If you actually need a HABTM relationship, then you need to do a migration that creates a join table.
One way to figure out what the type of association you need is to remember that if a table has the ID of another model (eg: Bookmarks table has a user_id column), then that is a belongs_to association.

Ruby on Rails relationship model

In Ruby on Rails 4, how do you create a many-to-many relationship inside a relationship model for a friends list such as Facebook using the has_many :through ... syntax ?? I'm a newbie and currently learning Ruby on Rails 4. I have looked at this link.
But still have a hard time grasping it.
you will need a join table that references both sides of the relations
let us say you have an relation Post and another relation Category with a many to many relationship between them you need a join table to be able to represent the relationship.
migration for a join table would be
class CreateCategoriesPosts < ActiveRecord::Migration
def change
create_table :categories_posts do |t|
t.integer :category_id
t.integer :post_id
t.timestamps
end
add_index :categories_posts, [:category_id, :post_id]
end
end
and in the models/post.rb
Class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
end
and in the models/category.rb
Class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
end
more here:
http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
I think #RAF pretty much nailed it. But to use the OP's example:
class User < ActiveRecord::Base
has_and_belongs_to_many :users_list
end
class UsersList < ActiveRecord::Base
has_and_belongs_to_many :users
end
Although at first it might seem like a User should have only one list of friends (UsersList), that might not always be the case. Think of types within the UserList model, such as: 'close friends', 'work friends', 'all friends' for example.
My advice: dig into the Rails guides. This is a concept worth learning and truly understanding (which I'm still doing :).
many-to_many relationships are a simple concept, but complex when using the database because of the way databases work. A person could have 1 to N different friends, which means that a single entry for a database would need a dynamic amount of memory for each entry, which in the db world is a no-no. So instead of creating a list of friends you would have to make a table that represents the links between friends, for example:
friendship.rb
class Friendship < ActiveRecord::Base
belongs_to :friend, foreign_key: 'friend_A' # this entry has a field called 'friend_A'
belongs_to :friend, foreign_key: 'friend_B' # this entry has a field called 'friend_B'
end
These links will represent your network of friends. However, as the two previous answers have mentioned, Rails has some nifty magic, "has_and_belongs_to_many", which will do this for you.
NOTICE: The problem here is that in my StatusesController, in the index action, the #relationship object only gets the statuses of all your friends, but does not get your own statuses. Is there a better way of approaching this? I am trying to create a view to view all statuses of users that are your friends, and your own statuses too, and so far, I can't seem to figure out how to order it chronologically, even if in my status model, i included "default_scope -> { order(created_at: :desc) } ". Any advice would be deeply appreciated
class User < ActiveRecord::Base
has_many :relationships
has_many :friends, :through => :relationships
has_many :inverse_relationships, class_name: 'Relationship', foreign_key: 'friend_id'
has_many :inverse_friends, through: 'inverse_relationships', :source => :user end
#
class Relationship < ActiveRecord::Base
# before_save...
belongs_to :user
belongs_to :friend, class_name: 'User'
end
#
class RelationshipsController < ApplicationController
def friend_request
user_id = current_user.id
friend_id = params[:id]
if Relationship.where( user_id: user_id, friend_id: friend_id, accepted: false).blank?
Relationship.create(user_id: user_id, friend_id: friend_id, accepted: false)
redirect_to user_path(params[:id])
else
redirect_to user_path(params[:id])
end
end
def friend_request_accept
# accepting a friend request is done by the recipient of the friend request.
# thus the current user is identified by to_id.
relationship = Relationship.where(user_id: params[:id], friend_id: current_user.id).first
if Relationship.exists?(relationship) and relationship.accepted == false
relationship.update_attributes(accepted: true)
end
redirect_to relationships_path
end
def friend_request_reject
relationship = Relationship.where(user_id: params[:id], friend_id: current_user.id).first
relationship.destroy
redirect_to relationships_path
end
################################
def index
#relationships_pending = Relationship.where(friend_id: current_user.id, accepted: false)
end
end
#
class StatusesController < ApplicationController
def index
#status = Status.new
#relationship = Relationship.where('friend_id = ? OR user_id = ?', current_user.id, current_user.id).
where( accepted: true)
end
def new
#status = Status.new
end
end
#

Rails association with multiple foreign keys

I want to be able to use two columns on one table to define a relationship. So using a task app as an example.
Attempt 1:
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
So then Task.create(owner_id:1, assignee_id: 2)
This allows me to perform Task.first.owner which returns user one and Task.first.assignee which returns user two but User.first.task returns nothing. Which is because task doesn't belong to a user, they belong to owner and assignee. So,
Attempt 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
That just fails altogether as two foreign keys don't seem to be supported.
So what I want is to be able to say User.tasks and get both the users owned and assigned tasks.
Basically somehow build a relationship that would equal a query of Task.where(owner_id || assignee_id == 1)
Is that possible?
Update
I'm not looking to use finder_sql, but this issue's unaccepted answer looks to be close to what I want: Rails - Multiple Index Key Association
So this method would look like this,
Attempt 3:
class Task < ActiveRecord::Base
def self.by_person(person)
where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
def tasks
Task.by_person(self)
end
end
Though I can get it to work in Rails 4, I keep getting the following error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
TL;DR
class User < ActiveRecord::Base
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
end
Remove has_many :tasks in User class.
Using has_many :tasks doesn't make sense at all as we do not have any column named user_id in table tasks.
What I did to solve the issue in my case is:
class User < ActiveRecord::Base
has_many :owned_tasks, class_name: "Task", foreign_key: "owner_id"
has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User"
belongs_to :assignee, class_name: "User"
# Mentioning `foreign_keys` is not necessary in this class, since
# we've already mentioned `belongs_to :owner`, and Rails will anticipate
# foreign_keys automatically. Thanks to #jeffdill2 for mentioning this thing
# in the comment.
end
This way, you can call User.first.assigned_tasks as well as User.first.owned_tasks.
Now, you can define a method called tasks that returns the combination of assigned_tasks and owned_tasks.
That could be a good solution as far the readability goes, but from performance point of view, it wouldn't be that much good as now, in order to get the tasks, two queries will be issued instead of once, and then, the result of those two queries need to be joined as well.
So in order to get the tasks that belong to a user, we would define a custom tasks method in User class in the following way:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.
Extending upon #dre-hh's answer above, which I found no longer works as expected in Rails 5. It appears Rails 5 now includes a default where clause to the effect of WHERE tasks.user_id = ?, which fails as there is no user_id column in this scenario.
I've found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) {
unscope(:where).where(owner: user).or(where(assignee: user)
}
end
Rails 5:
you need to unscope the default where clause
see #Dwight answer if you still want a has_many associaiton.
Though User.joins(:tasks) gives me
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
As it is no longer possible you can use #Arslan Ali solution as well.
Rails 4:
class User < ActiveRecord::Base
has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end
Update1:
Regarding #JonathanSimmons comment
Having to pass the user object into the scope on the User model seems like a backwards approach
You don't have to pass the user model to this scope.
The current user instance is passed automatically to this lambda.
Call it like this:
user = User.find(9001)
user.tasks
Update2:
if possible could you expand this answer to explain what's happening? I'd like to understand it better so I can implement something similar. thanks
Calling has_many :tasks on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate a tasks method on its object, which will call this lambda. The generated method would look similar to following pseudocode:
class User
def tasks
#define join query
query = self.class.joins('tasks ON ...')
#execute tasks_lambda on the query instance and pass self to the lambda
query.instance_exec(self, self.class.tasks_lambda)
end
end
I worked out a solution for this. I'm open to any pointers on how I can make this better.
class User < ActiveRecord::Base
def tasks
Task.by_person(self.id)
end
end
class Task < ActiveRecord::Base
scope :completed, -> { where(completed: true) }
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
def self.by_person(user_id)
where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
end
end
This basically overrides the has_many association but still returns the ActiveRecord::Relation object I was looking for.
So now I can do something like this:
User.first.tasks.completed and the result is all completed task owned or assigned to the first user.
Since Rails 5 you can also do that which is the ActiveRecord safer way:
def tasks
Task.where(owner: self).or(Task.where(assignee: self))
end
My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
class User < ActiveRecord::Base
has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
Warning:
If you are using RailsAdmin and need to create new record or edit existing record,please don't do what I've suggested.Because this hack will cause problem when you do something like this:
current_user.tasks.build(params)
The reason is that rails will try to use current_user.id to fill task.user_id,only to find that there is nothing like user_id.
So,consider my hack method as an way outside the box,but don't do that.
Better way is using polymorphic association:
task.rb
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
assigned_task.rb
class AssignedTask < Task
end
owned_task.rb
class OwnedTask < Task
end
user.rb
class User < ActiveRecord::Base
has_many :assigned_tasks, as: :taskable, dependent: :destroy
has_many :owned_tasks, as: :taskable, dependent: :destroy
end
In result, we can use it so:
new_user = User.create(...)
AssignedTask.create(taskable: new_user, ...)
OwnedTask.create(taskable: new_user, ...)
pp user.assigned_tasks
pp user.owned_tasks

Resources