In my Ruby on Rails project I have a User model and a Content model.
A User has_many :contents and a Content belongs_to :user.
Now, I want to create the idea of playlist. There will be more than one playlists, and each one will have some contents in some order. At this moment, it doesn't matter if a user owns a playlist or not, they'll be general.
A playlist doesn't have any kind of association with a User. They will be general, owned by the system.
I think the solution will be something like having a model Playlist and another table with these attributes: playlist_id:integer content_id:integer order:integer. But do I really need to create all the MVC parts to this new relationship?
As I looked into Rails associations, I got confused and I don't know how to do this, if using the through property, using has_and_belongs_to_many in both Content and Playlist or even how to create this new relationship.
I'd be glad if someone could help me, as you can see, I'm a bit confused.
The solution for you is use has_many through
class User < ActiveRecord::Base
... user code in here with no association
end
class Playlist < ActiveRecord::Base
has_many :content_playlists
has_many :contents, through: :content_playlists
end
class Content < ActiveRecord::Base
has_many :content_playlists
has_many :playlists, through: :content_playlists
end
class ContentPlaylist < ActiveRecord::Base
belongs_to :content
belongs_to :playlist
end
The migration:
class CreateAll < ActiveRecord::Migration
def change
create_table :contents do |t|
t.string :name
t.timestamps
end
create_table :playlists do |t|
t.string :name
t.timestamps
end
create_table :content_playlists do |t|
t.belongs_to :content
t.belongs_to :playlist
t.integer :order
t.timestamps
end
add_index(:content_playlists, :content_id)
add_index(:content_playlists, :playlist_id)
end
end
Now, you can assign a order integer on content_playlists, and in the future you can reorder your playlist changing the value on contents_playlists.
To add a new content_playlist:
c = Content.create(name: "Song 2")
p = Playlist.create(name: "My Playlists2)
ContentPlaylist.create(content: c, playlist: p, order: 1)
Reference:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
You can see (fork, clone, do whatever you want) here:
https://github.com/bdfantini/hmt_example
I'm guessing something like this is what you want:
class User < ActiveRecord::Base
...
has_many :contents
has_many :playlists
has_many :playlisted_contents, :through => :playlists
...
end
class Playlist < ActiveRecord::Base
...
has_many :contents
...
end
class Content < ActiveRecord::Base
...
belongs_to :user
belongs_to :playlist
...
end
I'd start there, and write some tests to see if it's behaving as you want. If your design has other constraints, we might need to adjust this some.
Related
I have the following associations set up:
class Book < ActiveRecord::Base
belongs_to :author
belongs_to :category
has_many :users_books
has_many :users, through: :user_books
end
and
class User < ActiveRecord::Base
has_many :users_books
has_many :books, through: :users_books
end
I created a join table migration as I ought to
class CreateUsersBooks < ActiveRecord::Migration[4.2]
def change
create_table :users_books do |t|
t.integer :user_id
t.integer :book_id
end
end
end
Now I need to create a method called check_out_book, that takes in a book and a due_date as arguments. When a user checks out a book, it should create a new UserBook record (or Checkout record or whatever you want to call you join table/model). That new UserBook record should have a attribute (and therefore table column) of returned? which should default to false. How would I go about creating this method and the migrations?
Your tablenames and your associations in Rails should always be singular_plural with the exception of the odd duckling "headless" join tables used by the (pretty useless) has_and_belongs_to_many association.
class CreateUserBooks < ActiveRecord::Migration[4.2]
def change
create_table :user_books do |t|
t.references :user
t.references :book
t.boolean :returned, default: false
end
end
end
class UserBook < ActiveRecord::Base
belongs_to :user
belongs_to :book
end
class Book < ActiveRecord::Base
belongs_to :author
belongs_to :category
has_many :user_books
has_many :users, through: :user_books
end
class User < ActiveRecord::Base
has_many :user_books
has_many :books, through: :user_books
end
But you should really use a better more descriptive name that tells other programmers what this represents in the domain and not just a amalgamation of the two models it joins such as Loan or Checkout.
I would also use t.datetime :returned_at to create a datetime column that can record when the book is actually returned instead of just a boolean.
If you want to create a join record with any additional data except the foreign keys you need to create it explicitly instead of implicitly (such as by user.books.create()).
#book_user = Book.find(params[:id]).book_users.create(user: user, returned: true)
# or
#book_user = current_user.book_users.create(book: user, returned: true)
# or
#book_user = BookUser.new(user: current_user, book: book, returned: true)
I created my models a week ago, but I didn't know many things that I know now so it is time to create it from scratch.
What I want to accomplish is to create:
Lab model that can have many offers
Offer model that can have many labs.
#MIGRATION FILES BELOW:
class CreateLabs < ActiveRecord::Migration[5.0]
def change
create_table :labs do |t|
t.string :name
...
t.timestamps
end
end
end
class CreateOffers < ActiveRecord::Migration[5.0]
def change
create_table :offers do |t|
t.string :name
...
t.timestamps
end
end
end
# Join table:
class CreateLabChain < ActiveRecord::Migration[5.0]
def change
create_table :lab_chain do |t|
t.references :lab, foreign_key: true
t.references :offer, foreign_key: true
t.timestamps
end
end
end
And here is how the model files look like:
class Lab < ApplicationRecord
has_many :offers, through: :lab_chain
has_many :lab_chains
end
class Offer < ApplicationRecord
has_many :labs, through: :lab_chain
has_many :lab_chains
end
class LabChain < ApplicationRecord
belongs_to :lab
belongs_to :offer
end
I just want to know if I wrote it all correctly as I am not sure about all those tutorials I have watched and read.
Bonus question is what if I want my Offer to have many sections, and section to have many offer_items? Should I just add:
to Offer:
has_many :sections
has_many :offer_items, through: :section
and then to Section:
has_many :offer_items
belongs_to :offer
and the to OfferItem:
belongs_to :section
?
As I mentioned before, I volunteered as a guy that would make a website for our school project as I was the only one that had something to do with code (different language). It is harder than I thought.
EDIT
How would I also correctly add an self join in Section, so a section can have a subsection and so on?
Self joins addded to Section model
has_many :child_sections, class_name: "Section", foreign_key: "section_id"
belongs_to :parent_section, class_name: "Section"
Added to migration file
t.references :parent_section, foreign_key: "section_id"
That does look like a correct many to many association. You second also looks correct.
As an aside, it helps to draw a diagram of any database with more than 3 tables and if you plan on doing this as a job, it's well worth it to really grasp the whole table relationships fully as it's core to writing good model code.
I'm trying to decide on the best relationship structure for three models I have in a Rails app.
The models are candidate, an employer, and a video.
Sometimes a video will belong to an employer, other times a candidate. Because of this I am loathe to put foreign keys for both employer and candidate on a video, because it feels wrong that one will always be nil.
The problem with the code below is that the video will not have many candidates, but I can't do a through relationship with belongs_to. What is a better way?
class Video < ActiveRecord::Base
has_many :video_candidates
has_many :candidates, :through => :video_candidates
end
class Candidate < ActiveRecord::Base
has_many :video_candidates
has_many :videos, :through => :video_candidates
end
class VideoCandidate < ActiveRecord::Base
belongs_to :candidate
belongs_to :vieo
end
Edit: Polymorphic association is the way to go. I'm going to put my solution below. Hope this helps someone.
class Video < ActiveRecord
belongs_to :watchable, polymorphic: true
end
class Candidate < ActiveRecord::Base
has_many :videos, as: :watchable
end
class Employer < ActiveRecord::Base
has_many :videos, as: :watchable
end
And the migration
class CreateVideos < ActiveRecord::Migration[5.0]
def change
create_table :videos do |t|
t.string :title
t.belongs_to :watchable, polymorphic: true
t.timestamps
end
add_index :videos, [:watchable_id, :watchable_type]
end
end
This is an ideal case to go for polymorphic association
Also refer Railscast episode
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 have two models that can have tags added to them.
Player
Ticket
and I have a Tag model which belongs to both so I have two join models
tag_ticket
tag_player
I am getting a Could not find the association :tag_tickets in model Ticket error but my association is in there.
class Ticket < ActiveRecord::Base
has_many :tag_tickets
has_many :tags, :through => :tag_tickets
end
I'm just focusing on the Ticket model but the player model should look similar.
this is my migration for TagTicket
class CreateTagTickets < ActiveRecord::Migration
def change
create_table :tag_tickets do |t|
t.integer :ticket_id
t.integer :tag_id
t.timestamps
end
end
end
You need to specify the :tag_tickets join first like this:
class Ticket < ActiveRecord::Base
has_many :tag_tickets
has_many :tags, :through => :tag_tickets
end
You would also need to specify the joins in your TagTicket model:
class TagTicket < ActiveRecored::Base
belongs_to :ticket
belongs_to :tag
end
Alternatively, you can skip all this and use a habtm join (only recommended if the tag_tickets join is truly only used as a join and has no primary key for itself). In this case you would have no TagTicket model (just a tag_tickets table) and the Ticket model would look like this:
class Ticket < ActiveRecord::Base
has_and_belongs_to_many :tags
end