Referencing a table twice in rails/postgres - ruby-on-rails

I am using PostgreSQL as my db in rails. I have a User model and a Product model. I am trying to make a Transaction model where I want to reference user twice, once as buyer and once as seller. I generated the models as suggested in this post (answer by toasterlovin)
Write a migration with reference to a model twice
However it gives me an error when I use PostgreSQL (with SQLite it worked fine). I get the following error. What can I do to resolve this?
{StandardError: An error has occurred, this and all later migrations canceled:
PG::UndefinedTable: ERROR: relation "buyers" does not exist
: ALTER TABLE "transactions" ADD CONSTRAINT "fk_rails_0b24a7fcc3"
FOREIGN KEY ("buyer_id")
REFERENCES "buyers" ("id")
C:/Users/Powerhouse/Desktop/CodingDojo/ruby/rails/demo/db/migrate/20161024032156_create_transactions.rb:3:in change'
C:inmigrate'
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "buyers" does not exist
: ALTER TABLE "transactions" ADD CONSTRAINT "fk_rails_0b24a7fcc3"
FOREIGN KEY ("buyer_id")
REFERENCES "buyers" ("id")
C:/Users/Powerhouse/Desktop/CodingDojo/ruby/rails/demo/db/migrate/20161024032156_create_transactions.rb:3:in change'
C:inmigrate'
PG::UndefinedTable: ERROR: relation "buyers" does not exist
C:/Users/Powerhouse/Desktop/CodingDojo/ruby/rails/demo/db/migrate/20161024032156_create_transactions.rb:3:in change'
C:inmigrate'
Tasks: TOP => db:migrate
(See full trace by running task with --trace) }
Models
class User < ActiveRecord::Base
has_many :products
has_many :sales, :class_name => 'Transaction', :foreign_key => 'seller_id'
has_many :purchases, :class_name => 'Transaction', :foreign_key => 'buyer_id
end
Product Model
class Product < ActiveRecord::Base
belongs_to :user
end
Transaction model
class Transaction < ActiveRecord::Base
belongs_to :buyer, :class_name => 'User'
belongs_to :seller, :class_name => 'User'
belongs_to :product
end
Migration files
User
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.timestamps null: false
end
end
end
Product
class CreateProducts < ActiveRecord::Migration
def change
create_table :products do |t|
t.references :user, index: true, foreign_key: true
t.string :name
t.timestamps null: false
end
end
end
Transaction
class CreateTransactions < ActiveRecord::Migration
def change
create_table :transactions do |t|
t.references :buyer, index: true, foreign_key: true
t.references :seller, index: true, foreign_key: true
t.references :product, index: true, foreign_key: true
t.timestamps null: false
end
end
end

This is due to PostgreSql not understanding how to make the custom association foreign keys.
Try changing the migration file to,
class CreateTransactions < ActiveRecord::Migration
def change
create_table :transactions do |t|
t.integer :buyer_id
t.integer :seller_id
t.integer :product_id
t.timestamps null: false
end
add_index(:transactions, :buyer_id)
add_index(:transactions, :seller_id)
add_index(:transactions, :product_id)
add_foreign_key :transactions, :users, column: :buyer_id
add_foreign_key :transactions, :users, column: :seller_id
end
end
It automatically links buyer_id to user, taking the power of
belongs_to :buyer, :class_name => 'User'
The above line makes the buyer_id as foreign key.
You can also try,
class CreateTransactions < ActiveRecord::Migration
def change
create_table :transactions do |t|
t.references :buyer, index: true
t.references :seller, index: true
t.references :product, index: true
t.timestamps null: false
end
add_foreign_key :transactions, :users, column: :buyer_id
add_foreign_key :transactions, :users, column: :seller_id
end
end

Your problem is that when ActiveRecord see this:
t.references :buyer, index: true, foreign_key: true
it tries to create a foreign key inside the database (because foreign_key: true) but the naming conventions tell AR that it should be linking to the buyers table (because you're referencing :buyer).
Just like you specify class_name: in your model:
belongs_to :buyer, :class_name => 'User'
to tell AR not use the conventional Buyer name you can override the convention in your migration by modifying your :foreign_key option:
t.references :buyer, index: true, foreign_key: { to_table: :users }
to explicitly name the table. Similarly for your other reference that doesn't use the conventional naming scheme:
t.references :seller, index: true, foreign_key: { to_table: :users }

Related

What kind of relationship should be used for tables Transaction and User - Ruby on Rails?

I am new learner. I just started learning more about Backend with Ruby on Rails.
I have the following tables - User and User_Transaction.
So basically I want to have a transaction which holds information about the sender and the receiver. This personally sounds to me more like a has_and_belongs_to_many relation. However, I am really confused in how to approach this and how should I include the 2 foreign keys.
I am curious to learn more about this and I will be really happy if someone helps me :).
Migrations
User
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.integer :username
t.integer :password
t.timestamps
end
end
end
Transaction
class CreateTransactions < ActiveRecord::Migration[6.0]
def change
create_table :transactions do |t|
t.string :sender
t.string:receiver
t.decimal :amount
t.timestamps
end
end
end
Models
Transaction
class ::Transaction < ApplicationRecord
#We have two users per transaction 'Sender' and 'Receiver'
has_and_belongs_to_many :users
# belongs_to :sender, :class_name => 'User'
# belongs_to :receiver, :class_name => 'User'
end
User
class User < ApplicationRecord
# has_many :accounts
# has_many :transactions
has_and_belongs_to_many :transactions
end
how about this:
migrations
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :username
t.string :password
t.timestamps
end
end
end
class CreateTransactions < ActiveRecord::Migration[6.0]
def change
create_table :transactions do |t|
t.references :sender, index: true, null: false, foreign_key: {to_table: :users}
t.references :receiver, index: true, null: false, foreign_key: {to_table: :users}
t.decimal :amount
t.timestamps
end
end
end
models
class User < ApplicationRecord
has_many :send_transactions, class_name: "Transaction", foreign_key: :sender, inverse_of: :sender
has_many :receive_transactions, class_name: "Transaction", foreign_key: :receiver, inverse_of: :receiver
end
class Transaction < ApplicationRecord
belongs_to :sender, class_name: "User", inverse_of: :send_transactions
belongs_to :receiver, class_name: "User", inverse_of: :receive_transactions
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.

Rails ActiveRecord two fields each referencing the same table

I have a Player class that I want to have high_school_team and club_team properties. So then I figure Player will have high_school_team_id and club_team_id properties that point to the corresponding team. I try to do this in the following migration, but it doesn't work.
class CreatePlayers < ActiveRecord::Migration[6.0]
def change
create_table :players do |t|
t.string :first_name
t.string :middle_name
t.string :last_name
t.decimal :height
t.decimal :weight
t.date :birthday
t.references :team, :high_school_team, foreign_key: true
t.references :team, :club_team, foreign_key: true
t.decimal :gpa
t.string :class_year
t.string :intended_major
t.string :email
t.string :phone_number
t.text :notes
t.timestamps
end
end
end
It gives the following error:
code/scout-db [master●] » rails db:migrate
== 20191218003854 CreatePlayers: migrating ====================================
-- create_table(:players)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
you can't define an already defined column 'team_id'.
/Library/Ruby/Gems/2.6.0/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/abstract/schema_definitions.rb:372:in `column'
...
HighSchoolTeam and ClubTeam are models that do single table inheritance with Team.
I don't see why I'm getting the error. The docs seem to say that the first argument for t.referenes is table_name and the second ref_name. :team is the name of the table and I want the references to be high_school_team_id and club_team_id.
When I switch the order of the arguments to t.references, it still doesn't work. Somehow it gives the same error: you can't define an already defined column 'team_id'..
It looks you you are confusing SchemaStatement#add_reference and TableDefinition#references which have completely different signatures.
If you want to setup a foreign key column where the table can't be derived from the name of the column (the first argument) you just pass foreign_key: { to_table: :teams}.
class CreatePlayers < ActiveRecord::Migration[6.0]
def change
create_table :players do |t|
t.references :high_school_team, foreign_key: { to_table: :teams}
t.references :club_team, foreign_key: { to_table: :teams}
end
end
end
t.references :high_school_team, index: true as recommended by the other answers is NOT equivilent. That just adds a index to the column but no foreign key constraint.
You can then setup the assocations on Player as:
class Player < ApplicationRecord
belongs_to :high_school_team, class_name: 'Team'
belongs_to :club_team, class_name: 'Team'
end
You can't use a single has_many :players assocation on the other end though as the foreign key column can be either players.high_school_team_id or players.club_team_id.
class Team
has_many :high_school_team_players,
foreign_key: :high_school_team_id,
class_name: 'Player'
has_many :club_team_players,
foreign_key: :club_team_id,
class_name: 'Player'
end
But really a better alternative in the first place would have been to setup a join table:
class Player
has_many :placements
has_one :high_school_team,
through: :placements,
source: :team
class_name: 'Team'
has_one :club_team,
through: :placements,
source: :team
class_name: 'Team'
end
class Placement
belongs_to :player
belongs_to :team
end
class Team
has_many :placements
has_many :players, through: :placements
end
The doc you mentioned talks about the case when you need to add reference to an existing table.
For adding a refernence to a new table:
t.references :team, :high_school_team, foreign_key: true
This piece of code is wrong. Instead, it should be
t.references :high_school_team, foreign_key: {to_table: :teams}
to_table is needed to add database referential integrity
So your migration will be like this:
class CreatePlayers < ActiveRecord::Migration[6.0]
def change
create_table :players do |t|
....
t.references :high_school_team, foreign_key: {to_table: :teams}
t.references :club_team, foreign_key: {to_table: :teams}
....
end
end
end

Rails: Relation does not exist for reference with class name in production

I created two references in a migration that are aliases for a reference to my User table:
class CreateInvitations < ActiveRecord::Migration[5.0]
def change
create_table :invitations do |t|
t.references :owner, references: :user, foreign_key: true # the owner
t.references :invitee, references: :user, foreign_key: true # the invitee
t.references :core_bot, foreign_key: true # the associated page (core_bot_active)
t.string :email
t.string :token
t.timestamps
end
end
end
In my User model:
has_many :invitations, foreign_key: :owner_id
has_many :invitations, foreign_key: :invitee_id, dependent: :destroy
In my Invitation model:
belongs_to :owner, class_name: :User
belongs_to :invitee, class_name: :User
Everything works well in development but when I try to migrate in production with Heroku heroku run rake db:migrate, I get the following error:
PG::UndefinedTable: ERROR: relation "owners" does not exist : CREATE
TABLE "invitations" ("id" serial primary key, "owner_id" integer,
"invitee_id" integer, "core_bot_id" integer, "email" character
varying, "token" character varying, "created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL, CONSTRAINT "fk_rails_59e24979a9"
FOREIGN KEY ("owner_id") REFERENCES "owners" ("id") , CONSTRAINT
"fk_rails_00204dc74b" FOREIGN KEY ("invitee_id") REFERENCES
"invitees" ("id") , CONSTRAINT "fk_rails_34505bdb65" FOREIGN KEY
("core_bot_id") REFERENCES "core_bots" ("id") )
I tried without references: :user but I get the same error.
Any idea what's wrong here?
Your development is probably an sqLite database but Heroku uses PostgreSQL and the interpretation of the migration is generating a foregn key to owners
Write the migration like this instead...
class CreateInvitations < ActiveRecord::Migration[5.0]
def change
create_table :invitations do |t|
t.references :owner, index: true # the owner
t.references :invitee, index: true # the invitee
t.references :core_bot, foreign_key: true # the associated page (core_bot_active)
t.string :email
t.string :token
t.timestamps
end
add_foreign_key :invitations, :users, column: :owner_id
add_foreign_key :invitations, :users, column: :invitee_id
end
end
It's one of the risks with developing using a different database product than the production implementation. Migrations may not work exactly the same. If planning to deploy to Heroku you should look at using postgreSQL in development.
I don't how to fix your problem. But I allways creating migrations for Postgres DB with foreign keys like this:
def change
create_table :invitations do |t|
t.integer :owner_id
t.integer :invitee_id
t.references :core_bot, foreign_key: true # the associated page (core_bot_active)
t.string :email
t.string :token
t.timestamps
end
add_index :invitations, :owner_id
add_foreign_key :invitations, :users, column: :owner_id
add_index :invitations, :invitee_id
add_foreign_key :invitations, :users, column: :invitee_id
end
try removing foreign_key: true from the options, since we are giving it a references, I think we don't need a foreign_key: true option
class CreateInvitations < ActiveRecord::Migration[5.0]
def change
create_table :invitations do |t|
t.references :owner, # the owner
t.references :invitee, # the invitee
t.references :core_bot # the associated page (core_bot_active)
t.string :email
t.string :token
t.timestamps
end
end
end

Rails: Multiple references to the same model error

I have a Message model that has a user and a teammember (both are users).
The models are:
# message.rb
class Message < ActiveRecord::Base
belongs_to :user
belongs_to :teammember, :class_name => "User", :foreign_key => 'teammember_id'
end
# user.rb
class User < ActiveRecord::Base
has_many :messages
end
And I have this migration:
class CreateMessages < ActiveRecord::Migration
def change
create_table :messages do |t|
t.references :user, index: true, foreign_key: true
t.references :teammember, index: true, foreign_key: true
t.text :body
t.boolean :read, default: false
t.timestamps null: false
end
end
end
When I run the rake db:migrate locally (with sqlite3) everything works fine.
The problem is that when I deploy to heroku (that uses postgresql) and run
heroku run rake db:migrate
It raises the next error:
PG::UndefinedTable: ERROR: relation "teammembers" does not exist
: ALTER TABLE "messages" ADD CONSTRAINT "fk_rails_7efc67ccc9"
FOREIGN KEY ("teammember_id")
REFERENCES "teammembers" ("id")
Do you know what is the problem and how can I fix it?
Rails guesses based on the association, because you're referencing a table it can't determine from the association you'll have to add it yourself.
class CreateMessages < ActiveRecord::Migration
def change
create_table :messages do |t|
t.references :user, index: true, foreign_key: true
t.references :teammember, index: true
t.text :body
t.boolean :read, default: false
t.timestamps null: false
end
add_foreign_key :messages, :users, column: :teammember_id
end
end

Resources