generating migration for a through association - ruby-on-rails

here are the additions that I have made to my models:
class Event < ApplicationRecord
has_one :lineup
has_many :artists, :through => :lineup
belongs_to :venue
end
class Lineup < ApplicationRecord
belongs_to :artist
belongs_to :event
end
class Artist < ApplicationRecord
has_many :events, :through => :lineups
end
class Venue < ApplicationRecord
has_many :events
end
not asking for help with generating migrations for all of these associations, but could you at least show me how to do it for Event?

Please find below migrations for Event & Lineup (that have the keys to enable your model associations):
class CreateEvents < ActiveRecord::Migration
def change
create_table :events do |t|
t.references :venue, index: true, foreign_key: true
t.timestamps null: false
end
end
end
class CreateLineups < ActiveRecord::Migration
def change
create_table :lineups do |t|
t.references :artist, index: true, foreign_key: true
t.references :event, index: true, foreign_key: true
t.timestamps null: false
end
end
end
To generate them, you can use:
rails g migration create_events venue:references
rails g migration create_lineups artist:references event:references
If Event & Lineup already exist, you can generate the migrations as follows:
rails g migration add_reference_to_events venue:references
rails g migration add_references_to_lineups artist:references event:references
Migrations generated should be as follows:
class AddReferenceToEvents < ActiveRecord::Migration
def change
add_reference :events, :venue, index: true, foreign_key: true
end
end
class AddReferencesToLineups < ActiveRecord::Migration
def change
add_reference :lineups, :artist, index: true, foreign_key: true
add_reference :lineups, :event, index: true, foreign_key: true
end
end

belongs_to will place the foreign key in the declaring model whereas has_one will place it in the other model. There are good resources in this out there that I would recommend taking a look at. Here's one.
So for the event model I would do the following:
$ rails g migration AddVenueToEvents
Then fill it in with:
class AddVenueToEvents < ActiveRecord::Migration
def change
add_reference :events, :venue, index: true, foreign_key: true
end
end
I would strongly recommend making use of the something like the Shoulda gem in combination with RSpec as it provides extremely valuable feedback about what you should be doing. Which would allow you to write some specs:
RSpec.describe Events, type: :model do
#Associations
it { should belong_to(:venue) }
it { should have_one(:lineup) }
it { should have_many(:artists).through(:lineup) }
The awesome thing is that once you run the your specs shoulda/rspec will give you extremely useful feedback in the terminal essentially telling you where a required Foreign Key may be missing. The message might look something like this:
Region should have a city
Failure/Error: should belong_to(:city)
Expected Region to have a belongs_to association called city (Region does not have a city_id foreign key.)
# ./spec/models/region_spec.rb:5:in `block (2 levels) in <top (required)>'
as shown in this other SO post which is somewhat related.

Please check this migration for events.
class CreateEvents < ActiveRecord::Migration
def change
create_table :events do |t|
t.belongs_to :venue, index: true
t.timestamps null: false
end
end
end

Related

rails - can't save data in associated models in RoR

I have 2 Models with association has_many along with cascade property between them.
class ServicesBrandDetail < ApplicationRecord
has_many :services_brands, foreign_key: "brand_id", dependent: :delete_all
end
class ServicesBrand < ApplicationRecord
belongs_to :services_brand_details, foreign_key: "brand_id",
end
Migration for both files
class CreateServicesBrandDetails < ActiveRecord::Migration[6.1]
def change
create_table :services_brand_details do |t|
t.string :brand
t.string :mail_list
t.string :cc_list
t.timestamps
end
end
end
class CreateServicesBrands < ActiveRecord::Migration[6.1]
def change
create_table :services_brands do |t|
t.string :warehouse
t.references :brand, null: false, foreign_key: {to_table: :services_brand_details}
t.timestamps
end
end
end
Now I was able to create and save data in from ServicesBrandDetails model. but the Problem is when i create record from ServiceBrand It created record perfectly but i was not able to store data in DB.
record = ServicesBrandDetail.create(:brand => "a", :mail_list => 'abc#mail.com', :cc_list => 'def#mail.com')
record.save
Record successfully stored in DB.
child = record.services_brands.new(:warehouse => "in") <-- record was created successfully.
child.save
it give me error
C:/Ruby30-x64/lib/ruby/gems/3.0.0/gems/activerecord-6.1.5/lib/active_record/inheritance.rb:237:in `compute_type': uninitialized constant ServicesBrand::ServicesBrandDetails (NameError)
Please follow proper Naming convention
This article might help - https://www.bigbinary.com/learn-rubyonrails-book/summarizing-rails-naming-conventions
In ServiceBrand Model
class ServiceBrand < ApplicationRecord
belongs_to :brand, class_name: 'ServiceBrandDetail'
end
belongs_to should be foreign key name i.e brand in your case
You can delete existing models and tables from your codebase and try below one. (I've tested)
class ServiceBrandDetail < ApplicationRecord
has_many :service_brands, foreign_key: :brand_id, dependent: :delete_all
end
class ServiceBrand < ApplicationRecord
belongs_to :brand, class_name: 'ServiceBrandDetail'
end
Migration for both files
class CreateServiceBrandDetails < ActiveRecord::Migration[6.1]
def change
create_table :service_brand_details do |t|
t.string :brand
t.string :mail_list
t.string :cc_list
t.timestamps
end
end
end
class CreateServiceBrands < ActiveRecord::Migration[6.1]
def change
create_table :service_brands do |t|
t.string :warehouse
t.references :brand, null: false, foreign_key: {to_table: :service_brand_details}
t.timestamps
end
end
end
Then try to create model objects which you tried in your question. It will work 👍🏽
In your model ServicesBrand you have to use singular association name for belongs_to
Change this belongs_to :services_brand_details to this belongs_to :services_brand_detail
class ServicesBrand < ApplicationRecord
belongs_to :services_brand_detail, foreign_key: "brand_id"
end

Two foreign keys in a table referencing one primary key in another table using rails migration

Im trying to create a table with 2 FK Referencing 1 PK in another table.
class CreateJobapps < ActiveRecord::Migration[5.1]
def change
create_table :jobapps do |t|
t.references :job, foreign_key: { job: :id }, index: { unique: true}
t.references :user, foreign_key: { user: :id }, index: { unique: true}
t.timestamps
end
end
end
Is this method correct? if so, how can I get an output if I provide a FK of respected table.
Here is how my Jobapp table looks like
I tried using Jobapp.joins(:user) but to no avail
Am I supposed to write belongs_to or has_many in the model file?
class CreateJobapps < ActiveRecord::Migration[5.1]
def change
create_table :jobapps do |t|
t.references :job, foreign_key: { job: :id }
t.references :user, foreign_key: { user: :id }
t.timestamps
end
# Add a compound index instead - you may need to switch the order to
# tweak the index depending on how it is used.
add_index :jobapps, [:job_id, :user_id], unique: true
end
end
class Jobapp < ApplicationRecord
belongs_to :user
belongs_to :job
end
class User < ApplicationRecord
has_many :jobapps
has_many :jobs, through: :jobapps
end
class Job < ApplicationRecord
has_many :jobapps
has_many :users, through: :jobapps
end

has_one and belong_to associations with the same table

I wonder is it ok to have like 2 associations with the same table. For example:
class CreateTeams < ActiveRecord::Migration[6.0]
def change
create_table :teams do |t|
t.string :name, null: false
t.references :manager, foreign_key: { to_table: 'users' }
end
end
end
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name, null: false
t.references :team
end
end
end
class Team < ApplicationRecord
has_many :users
belongs_to :manager, class_name: 'User', foreign_key: 'manager_id'
end
class User < ApplicationRecord
belongs_to :team
has_one :child_team, class_name: 'Team' # bad name, but just for example
end
Or it would be better to create a join table with team_id, user_id, and member_type?
Ruby/Rails versions do not matter but let's assume Ruby is 2.7.0 and Rails is 6.0.0
From a technical point of view - that's perfectly fine, but be careful with possible foreign key loop in the future.
This is more a question of architecture and your predictions of how system will evolve. A many-to-many relation with a explicit join model is more flexible. For example:
does manager always belong to the team? with a join table it's easier to fetch "all users from the team, no matter the role" or "all the teams a person has relation to, also no matter the role"
if there will be other roles or multiple people at same position - join table will also come handy
What you have seems fine.
Though a join table would be more flexible to provide more roles. This also avoids having a circular dependency in setting up teams and users.
class CreateTeams < ActiveRecord::Migration[6.0]
def change
create_table :teams do |t|
t.string :name, null: false
t.timestamps
end
end
end
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name, null: false
t.timestamps
end
end
end
class CreateTeamMembers < ActiveRecord::Migration[6.0]
def change
create_table :team_members do |t|
t.belongs_to :user, null: false, foreign_key: true
t.belongs_to :team, null: false, foreign_key: true
t.integer :role, null: false
t.timestamps
# Enforce one manager per team
t.index [:team_id],
name: :one_manager,
unique: true,
where: "role = 0"
end
end
end
class TeamMember < ApplicationRecord
enum role: { manager: 0, player: 1, fan: 2 }
belongs_to :user
belongs_to :team
end
class Team < ApplicationRecord
has_many :users, through: :team_members
has_many :team_members, dependent: :destroy
has_one :manager, -> { where(role: :manager) }, class_name: "TeamMember"
has_many :players, -> { where(role: :player) }, class_name: "TeamMember"
has_many :fans, -> { where(role: :fan) }, class_name: "TeamMember"
end
class User < ApplicationRecord
has_many :team_memberships, dependent: :destroy, class_name: "TeamMember"
has_many :teams, through: :team_memberships
end
You could even potentially take advantage of single table inheritance to differentiate your users by their role.
This is something you could migrate to later if necessary.

ActiveModel::MissingAttributeError when using namespaces for models

So, I have a namespace for a part of the project.
class Namespace::Product < ActiveRecord::Base
belongs_to: :namespace_category, class_name: 'Namespace::Category'
...
end
class Namespace::Category < ActiveRecord::Base
has_many :products, :class_name => 'Namespace::Product'
...
end
The migration for Product looks like
create_table :namespace_products do |t|
t.belongs_to :namespace_category, index: true
...
end
So, when I do this
p = Namespace::Product.create(some_params)
c = Namespace::Category.find(id)
c.products << p
it throws me an error, saying ActiveModel::MissingAttributeError: can't write unknown attribute 'category_id', however I have an attribute
t.integer "namespace_category_id"
in my schema.rb, which was created by the migration.
For anyone on Rails 5+ looking back at this, you can also specify the namespace on the migration level if you don't want them on the associations:
def change
create_table :namespace_products do |t|
t.references :category, foreign_key: { to_table: :namespace_categories }
end
end
# or
def change
add_reference :namespace_products, :category, foreign_key: { to_table: :namespace_categories }
end
class Namespace::Product < ActiveRecord::Base
belongs_to :category
end
class Namespace::Category < ActiveRecord::Base
has_many :products
end
https://api.rubyonrails.org/v5.0.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference
I would just omit the namespaces from the associations:
class Namespace::Product < ActiveRecord::Base
belongs_to :category, class_name: 'Namespace::Category'
...
end
class Namespace::Category < ActiveRecord::Base
has_many :products, class_name: 'Namespace::Product'
...
end
class FixColumnName < ActiveRecord::Migration
def change
rename_column :namespace_products, :namespace_category_id, :category_id
end
end
Okay, I just had to specify foreign_key while defining has_many :products association in Namespace::Category, thanks to Max

Callbacks - Set field on model

I am trying the following -
I have Models: Tales, Books, Keywords
class Tale < ActiveRecord::Base
has_many :tale_culture_joins
has_many :cultures, through: :tale_culture_joins
has_many :tale_purpose_joins
has_many :purposes, through: :tale_purpose_joins
has_many :tale_book_joins
has_many :books, through: :tale_book_joins
has_many :tale_keyword_joins
has_many :keywords, through: :tale_keyword_joins
class Book < ActiveRecord::Base
has_many :tale_book_joins
has_many :tales, through: :tale_book_joins
end
class TaleBookJoin < ActiveRecord::Base
belongs_to :tale
belongs_to :book
end
class Keyword < ActiveRecord::Base
has_many :tale_keyword_joins
has_many :tales, through: :tale_keyword_joins
end
class TaleKeywordJoin < ActiveRecord::Base
belongs_to :tale
belongs_to :keyword
end
These are the migrations
class CreateTales < ActiveRecord::Migration
def change
create_table :tales do |t|
t.text :name, null: false, unique: true
t.boolean :exists, default: nil
t.timestamps null: false
end
end
end
class CreateBooks < ActiveRecord::Migration
def change
create_table :books do |t|
t.text :name, null: false, unique: true
t.boolean :exists, default: nil
t.timestamps null: false
end
end
end
class CreateKeywords < ActiveRecord::Migration
def change
create_table :keywords do |t|
t.text :name, null: false, unique: true
t.boolean :exists, default: nil
t.timestamps null: false
end
end
end
What I want to happen is that everytime i delete a join between (Tale, Book) or (Tale,Keyword) through following method
tale_instance_object.book_ids = []
It should go and check if the books for whom the relations have been broken have any other tale relations. If not then set :exists in Book object instance to false.
I am able to do this through controller code.
Wondering how CallBacks or ActiveModel can be used
Join classes should only really be used when the relation is an object on its own.
Consider these cases:
doctors -> appointments <- patients
years -> days <- hours
In these cases the relation object has data of its own (appointments.time, days.weekday) and logic. Otherwise you are just wasting memory since a object has to instantiated for every relation. Use has_and_belongs_to instead.
class Tale < ActiveRecord::Base
# has_and_belongs_to_many :cultures
# has_and_belongs_to_many :purposes
has_and_belongs_to_many :books
has_and_belongs_to_many :keywords
after_destroy :update_books!
end
class Book
has_and_belongs_to_many :tales
def check_status!
self.update_attribute(status: :inactive) unless books.any
end
end
class Keyword
has_and_belongs_to_many :tales
end
Also exists is a really bad naming choice for a model field since it collides with Rails built in exists? methods. This will have unexpected consequences.
A better alternative would be to use an integer combined with an enum.
class Book
# ...
enum :status [:inactive, :active] # defaults to inactive
end
class CreateBooks < ActiveRecord::Migration
def change
create_table :books do |t|
t.text :name, null: false, unique: true
t.integer :status, default: 0
t.timestamps null: false
end
end
This will also add the methods book.inactive? and book.active?.
You could add a callback to Tale which tells Book to update when a tale is updated but this code really stinks since Tale now is responsible for maintaining state of Book.
class Tale < ActiveRecord::Base
# ...
after_destroy :update_books!
def update_books!
self.books.each { |b| b.check_status! }
end
end
class Book
has_and_belongs_to_many :tales
def check_status!
self.update_attribute(status: :inactive) unless tales.any?
end
end
A better alternative to add a callback on Books or to do it in the controller when destroying a tale:
class Book
has_and_belongs_to_many :tales
before_save :check_for_tales!, if: -> { self.tales_changed? }
def check_for_tales!
self.status = :inactive unless self.tales.any?
end
end

Resources