I have kind of a complicated case and am wondering how this would work in rails:
I want to categories the genres of some singers. Singers can belong to more than one genres, and users can assign tags to each genre
For example:
singers <-- singers_genres --> genres <-- genres_tags --> tags
SQL would look something like:
SELECT * FROM singers S
INNER JOIN singers_genres SG ON S.id=SG.singer_id
INNER JOIN genres G ON G.id = SG.genre_id
LEFT OUTER JOIN genre_tags GT ON G.id = GT.genre_id
INNER JOIN tags T ON GT.tag_id = T.id
Here are what my Classes look like:
class Singer
has_and_belongs_to_many :genres, :include => :tag
class Genre
has_and_belongs_to_many :singers
has_and_belongs_to_many :tags
class Tag
has_and_belongs_to_many :genres
Let's create the project ...
rails itunes
cd itunes
create the basic models:
script/generate model Singer name:string
script/generate model Genre name:string
script/generate model Tag name:string
do the migration:
rake db:migrate
update the models:
class Singer < ActiveRecord::Base
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
has_and_belongs_to_many :singers
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :genres
end
create two more migration for joining tables:
script/generate migration CreateGenresSingersJoin
script/generate migration CreateGenresTagsJoin
rake db:migrate
the genres_singers model:
class CreateGenresSingersJoin < ActiveRecord::Migration
create_table 'genres_singers', :id => false do |t|
t.integer 'genre_id'
t.integer 'singer_id'
end
def self.down
drop_table'genres_singers'
end
end
the genres_tags model:
class CreateGenresTagsJoin < ActiveRecord::Migration
create_table 'genres_tags', :id => false do |t|
t.integer 'genre_id'
t.integer 'tag_id'
end
def self.down
drop_table'genres_tags'
end
end
create some seeding data in seeds.db, or whatever means:
Singer.create(:name => 'Lady Ga Ga')
Genre.create(:name => 'Pop')
Genre.create(:name => 'Folk')
Tag.create(:name => 'Top50')
insert some link data:
INSERT INTO genres_singers (genre_id, singer_id) VALUES (1, 1)
INSERT INTO genres_singers (genre_id, singer_id) VALUES (2, 1)
INSERT INTO genres_tags (genre_id, tag_id) VALUES (1, 1)
then we can use the associations, e.g.:
Singer.first.genres.first.tags.first
=> #<Tag id: 1, name: "Top50">
Singer.find_by_name("Lady Ga Ga").genres.first.tags
=> [#<Tag id: 1, name: "Top50">]
Choosing Between has_many :through and has_and_belongs_to_many is a good introduction to associations.
Maybe you can post your models here to see how the associations are designed.
Related
I am trying to write an app like IMDB in rails.
I have created the Movie model. Every movie has many movie recommendations (which are also instances of Movie).
I don't know how to add the "has_many" association, how to write the migration file or how to add recommended movies to each movie.
You have a many-to-many relationship, which means we need a join table Recommendation.
Create model and migration files with a generator:
bin/rails generate model Movie
bin/rails generate model Recommendation
Then update migrations:
# db/migrate/20221023063944_create_movies.rb
class CreateMovies < ActiveRecord::Migration[7.0]
def change
create_table :movies do |t|
# TODO: add fields
end
end
end
# db/migrate/20221023064241_create_recommendations.rb
class CreateRecommendations < ActiveRecord::Migration[7.0]
def change
create_table :recommendations do |t|
t.references :movie, null: false, foreign_key: true
t.references :recommended_movie, null: false, foreign_key: { to_table: :movies }
end
end
end
Run migrations:
bin/rails db:migrate
Setup models:
# app/models/movie.rb
class Movie < ApplicationRecord
# NOTE: this is the relationship for join table
has_many :recommendations, dependent: :destroy
# NOTE: get movies from join table
has_many :recommended_movies, through: :recommendations
# this ^ is the name of the relationship in `Recommendation` we want
end
# app/models/recommendation.rb
class Recommendation < ApplicationRecord
belongs_to :movie
# NOTE: our foreign key is `recommended_movie_id` which rails infers
# from `:recommended_movie`, but we have to specify the class:
belongs_to :recommended_movie, class_name: "Movie"
end
Test it in the console bin/rails console:
>> 3.times { Movie.create }
>> Movie.first.recommended_movies << [Movie.second, Movie.third]
>> Movie.first.recommended_movies
=> [#<Movie:0x00007f15802ec4c0 id: 2>, #<Movie:0x00007f15802ec3d0 id: 3>]
or like this:
>> Movie.second.recommendations << Recommendation.new(recommended_movie: Movie.first)
>> Movie.second.recommended_movies
=> [#<Movie:0x00007f158215ef20 id: 1>]
https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
https://guides.rubyonrails.org/association_basics.html#self-joins
When creating a migration you need to define which model reference you want to assign
create_table :student do |t|
t.references :class, foreign_key: true
end
Here I am telling my class table to store the primary key of student as a foreign key after migration there will be a column in class named student_id which stores pk of the student table.
Then I will define an association in class model file
class student < ApplicationRecord
belongs_to :class
end
This will help me in query so I can write
student= Student.find 'student_id'
class = student.class
This will return the class of that student. For has_many the procedure is same but it will return you the array
We're using ActiveAdmin for our admin view, here's the current index code. The Book model retrieves data from a single "books" postgres table :
class Book < ActiveRecord::Base
has_many :stories, class_name: "BookStory"
...
ActiveAdmin.register Book do
index do
column :id
column :title
column :subtitle
column :isbn_13
default_actions
end
...
I would like to add a "stories" column in our index view. A "story" is an action from an user, associated with a book. The stories are stored in a "book_stories" table.
class BookStory < ActiveRecord::Base
belongs_to :user,
belongs_to :book,
From a SQL point of view, this is how I would like to implement the query into ActiveAdmin. This query gives me the wanted result into pgAdmin3 :
SELECT books.id,books.title,books.subtitle,books.isbn_13,COUNT(book_stories.book_id) AS count
FROM books
INNER JOIN book_stories ON books.id = book_stories.book_id
GROUP BY books.id
ORDER BY count DESC
LIMIT 30;
And I really don't know how to implement a sortable "Stories" column into our admin view. By sortable, I mean, being able to sort the book like by their stories count. I managed to show the stories count per book with this code, but the column isn't sortable :
ActiveAdmin.register Book do
index do
column :id
column :title
column :subtitle
column :isbn_13
column :stories do |book|
BookStory.joins(:book).group.where("books.id = #{book.id}").count
default_actions
end
...
Any ideas ?
Take a look on the belongs_to counter_cache option. http://guides.rubyonrails.org/association_basics.html#counter-cache
class BookStory < ActiveRecord::Base
belongs_to :book, counter_cache: true
end
ActiveAdmin.register Book do
index do
column :id
column :title
column :subtitle
column :isbn_13
column :book_stories_count # will be sortable by default
default_actions
end
end
Okay, it's working. Thank you very much ! Here's my code :
Book model :
class Book < ActiveRecord::Base
has_many :stories, class_name: "BookStory"
Book stories model :
class BookStory < ActiveRecord::Base
belongs_to :user, :counter_cache => true
belongs_to :book, :counter_cache => true
Migration file :
class AddBookStoriesCountColumn < ActiveRecord::Migration
def self.up
add_column :books, :book_stories_count, :integer, :null => false, :default => 0
Book.reset_column_information
Book.find_each do |b|
Book.reset_counters b.id, :stories
end
end
def self.down
remove_column :books, :book_stories_count
end
end
Index view :
ActiveAdmin.register Book do
index do
column :id
column :title
column :subtitle
column :isbn_13
column :book_stories_count
default_actions
end
I also created a rake task to manually update the counter if needed :
task :update_counters => [:environment] do
Book.reset_column_information
Book.find_each do |b|
Book.reset_counters b.id, :stories
end
Now, I would like to implement a stories counter for the users. And I believed I'll just have to use the same code up here, just changing "Book" with "User", but I have another problem.
User model :
class User < ActiveRecord::Base
has_many :book_actions
has_many :book_stories
has_many :books, through: :book_stories
but I encouter an error when I try to update / reset the counter
NoMethodError: undefined method `options' for nil:NilClass
Still investigating...
I've been racking my head around a rails issue for a while and wanted to verify my findings. I was trying to get the Has_and_belongs_to_many relationship working, but couldn't connect my two classes, auctionItem and category. First of all, here was my migration file and the two classes before solving the issue:
Migration file:
class AuctionItemsCategories < ActiveRecord::Migration
def up
create_table 'auction_items_categories', :id=>false do |t|
t.reference :auctionItem_id
t.references :category_id
end
end
def down
drop_table 'auction_items_categories'
end
end
Category.rb
class Category < ActiveRecord::Base
has_and_belongs_to_many :auction_items
end
auction_item.rb
class AuctionItem < ActiveRecord::Base
has_and_belongs_to_many :categories
end
After creating an instance of AuctionItem, I tried
auction_item = AuctionItem.last
auction_item.categories
...and got the following error:
NoMethodError: undefined method `categories' for #<AuctionItem:0x0000010521b870>
After some research, I found adding the specific class to the has_and_belongs_to_many helped:
Category Model
has_and_belongs_to_many :auction_items , :class_name => 'AuctionItem'
auction_item Model
has_and_belongs_to_many :categories , :class_name => 'Category'
This solved that issue and I was able to access the categories table. I went on to try to append a category to the auction item:
auction_item.categories << category
I then got received the following error:
SELECT "auction_items".* FROM "auction_items" INNER JOIN "auction_items_categories" ON "auction_items"."id" = "auction_items_categories"."auction_item_id" WHERE "auction_items_categories"."category_id" = 2
SQLite3::SQLException: no such column: auction_items_categories.auction_item_id: SELECT "auction_items".* FROM "auction_items" INNER JOIN "auction_items_categories" ON "auction_items"."id" = "auction_items_categories"."auction_item_id" WHERE "auction_items_categories"."category_id" = 2
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: auction_items_categories.auction_item_id: SELECT "auction_items".* FROM "auction_items" INNER JOIN "auction_items_categories" ON "auction_items"."id" = "auction_items_categories"."auction_item_id" WHERE "auction_items_categories"."category_id" = 2
If you notice, the query is trying to get auction_item.id rather than AuctionItem.id. To get the connection to work, I had to change my migration file to the following:
class AuctionItemsCategories < ActiveRecord::Migration
def up
create_table 'auction_items_categories', :id=>false do |t|
t.integer :auction_item_id
t.integer :category_id
end
end
def down
drop_table 'auction_items_categories'
end
end
So long story short/TL DR version: For me, it seems that when naming your class with multiple words and using camel case, rails does not singularize your pluralized class name back to it's original state if it has an underscore. So for example, my class name was AuctionItem which became auction_items for the model. Rather than search for auctionitem.id, the sql call that was looked for was auction_item.id, which is the singularize version of auction_items. Why didn't it search for auctionitem.id? In the future, when I am making association tables with multi word classes, do I use the singular underscore id version of the model name?
Your original migration was incorrect. It should have been as follows:
class AuctionItemsCategories < ActiveRecord::Migration
def up
create_table 'auction_items_categories', :id => false do |t|
t.references :auction_item
t.references :category
end
end
def down
drop_table 'auction_items_categories'
end
end
You should specify the symbolized model name when using references.
I have a Genre model, and I want both videos to have many genres and profiles to have many genres. I also want genres to have many videos and genres to have many profiles. I understand the polymorphic and join table stuff, so I'm wondering if my code below will work as I intend it to. Also, I'd appreciate any advice on how to access things in my controller and views.
This is what I envision that the join table should look like (I don't think I need an elaborate :has :through association because all I need in the join table are the associations and nothing else, so the table won't have a model):
genres_videos_profiles:
-----------------------------------------------------
id | genre_id | genre_element_id | genre_element_type
Here's my genre.rb:
has_and_belongs_to_many :genre_element, :polymorphic => true
Here's video.rb:
has_and_belongs_to_many :genres, :as => :genre_element
Here's profile.rb:
has_and_belongs_to_many :genres, :as => :genre_element
Will this work as I intend it to? I'd like some feedback.
As far as I know HABTM associations can´t be polymorphic, I couldn´t find an example like yours in the API documentation. If you want only join tables, your code could look like this:
class Genre
has_and_belongs_to_many :videos
has_and_belongs_to_many :profiles
end
class Video
has_and_belongs_to_many :genres
end
class Profile
has_and_belongs_to_many :genres
end
And access it like Mike already wrote:
#genre.profiles
#profile.genres
#genre.videos
#video.genres
Migrations (for join tables only):
class CreateGenresVideosJoinTable < ActiveRecord::Migration
def self.up
create_table :genres_videos, {:id => false, :force => true} do |t|
t.integer :genre_id
t.integer :video_id
t.timestamps
end
end
def self.down
drop_table :genres_videos
end
end
class CreateGenresProfilesJoinTable < ActiveRecord::Migration
def self.up
create_table :genres_profiles, {:id => false, :force => true} do |t|
t.integer :genre_id
t.integer :profile_id
t.timestamps
end
end
def self.down
drop_table :genres_profiles
end
end
I think that has_and_belongs_to_many can be a bit difficult to follow when it comes to polymorphic (if it even works). So if you want to do the polymorhpic thing, then you can't use any "through" syntax:
class Genre < ActiveRecord::Base
has_many :genres_videos_profiles
end
class GenresVideosProfile
belongs_to :genre
belongs_to :genre_element, :polymorphic => true
scope :videos, where(:genre_element_type => "Video")
scope :profiles, where(:genre_element_type => "Profile")
end
And then you use it like:
# All genre elements
#genre.genres_videos_profiles.each do |gvp|
puts gvp.genre_element.inspect
end
# Only video genre elements
#genre.genres_videos_profiles.videos.each do |gvp|
puts gvp.genre_element.inspect
end
Check out that: http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
For me it was perfect and clean!
I'm new to Rails. I have two models, Person and Day.
class Person < ActiveRecord::Base
has_many :days
end
class Day < ActiveRecord::Base
belongs_to :person
has_many :runs
end
When I try to access #person.days I get an SQL error:
$ script/consoleLoading development environment (Rails 2.3.8)
ree-1.8.7-2010.02 > #person = Person.first
=> #<Person id: 1, first_name: "John", last_name: "Smith", created_at: "2010-08-29 14:05:50", updated_at: "2010-08-29 14:05:50"> ree-1.8.7-2010.02
> #person.days
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: days.person_id: SELECT * FROM "days" WHERE ("days".person_id = 1)
I setup the association between the two before running any migrations, so I don't see why this has not been setup correctly.
Any suggestions?
Telling your model about the association doesn't set up the foreign key in the database - you need to create an explicit migration to add a foreign key to whichever table is appropriate.
For this I'd suggest:
script/generate migration add_person_id_to_days person_id:integer
then take a look at the migration file it creates for you to check it's ok, it should be something like this:
class AddPersonIdToDays < ActiveRecord::Migration
def self.up
add_column :days, :person_id, :integer
end
def self.down
remove_column :days, :person_id
end
end
Run that and try the association again?