Foreign Key Issues in Rails - ruby-on-rails

Took me a while to track down this error but I finally found out why. I am modeling a card game using the Rails framework. Currently my database looks (mostly) like this:
cards cards_games games
----- ----------- -----
id id id
c_type card_id ...
value game_id other_stuff
And the Rails ActiveRecord card.rb and game.rb currently look like this
#card.rb
class Card < ActiveRecord::Base
has_and_belongs_to_many :player
has_and_belongs_to_many :game
has_and_belongs_to_many :cardsInPlay, :class_name => "Rule"
end
#game.rb
class Game < ActiveRecord::Base
has_and_belongs_to_many :cards
has_many :players
has_one :rules, :class_name => Rule
end
When I attempt to run a game and there are multiple games (more than 1), I get the error
ActiveRecord::StatementInvalid in GameController#start_game
# example
Mysql::Error: Duplicate entry '31' for key 1: INSERT INTO `cards_games` (`card_id`, `id`, `game_id`) VALUES (31, 31, 7)
Every time the action fails, cardid == id. This, I assume, has something with how Rails inserts the data into the database. Since there is no cardsgames object, I think it is just pulling card_id into id and inserting it into the database. This works fine until you have two games with the same card, which violates the primary key constraint on cardsgames. Being affluent with databases, my first solution to this problem was to try to force rails to follow a "real" definition of this relationship by dropping id and making cardid and gameid a primary key. It didn't work because the migration couldn't seem to handle having two primary keys (despite the Rails API saying that its okay to do it.. weird). Another solution for this is to omit the 'id' column in the INSERT INTO statement and let the database handle the auto increment. Unfortunately, I don't know how to do this either.
So, is there another work-around for this? Is there some nifty Rails trick that I just don't know? Or is this sort of structure not possible in Rails? This is really frustrating because I know what is wrong and I know several ways to fix it but due to the constraints of the Rail framework, I just cannot do it.

has_and_belongs_to_many implies a join table, which must not have an id primary key column. Change your migration to
create_table :cards_games, :id => false do ...
as pointed out by Matt. If you will only sleep better if you make a key from the two columns, create a unique index on them:
add_index :cards_games, [ :card_id, :game_id ], :unique => true
Additionally, your naming deviates from Rails convention and will make your code a little harder to read.
has_and_belongs_to_many defines a 1:M relationship when looking at an instance of a class. So in Card, you should be using:
has_and_belongs_to_many :players
has_and_belongs_to_many :games
Note plural "players" and "games". Similarly in Game:
has_one :rule
This will let you drop the unnecessary :class_name => Rule, too.

To drop the ID column, simply don't create it to begin with.
create_table :cards_rules, :id => false do ...

See Dr. Nics composite primary keys
http://compositekeys.rubyforge.org/

I found the solution after hacking my way through. I found out that you can use the "execute" function inside of a migration. This is infinitely useful and allowed me to put together an non-elegant solution to this problem. If anyone has a more elegant, more Rails-like solution, please let me know. Here's the solution in the form of a migration:
class Make < ActiveRecord::Migration
def self.up
drop_table :cards_games
create_table :cards_games do |t|
t.column :card_id, :integer, :null => false
t.column :game_id, :integer, :null => false
end
execute "ALTER TABLE cards_games DROP COLUMN id"
execute "ALTER TABLE cards_games ADD PRIMARY KEY (card_id, game_id)"
drop_table :cards_players
create_table :cards_players do |t|
t.column :card_id, :integer, :null => false
t.column :player_id, :integer, :null => false
end
execute "ALTER TABLE cards_players DROP COLUMN id"
execute "ALTER TABLE cards_players ADD PRIMARY KEY (card_id, player_id)"
drop_table :cards_rules
create_table :cards_rules do |t|
t.column :card_id, :integer, :null => false
t.column :rule_id, :integer, :null => false
end
execute "ALTER TABLE cards_rules DROP COLUMN id"
execute "ALTER TABLE cards_rules ADD PRIMARY KEY (card_id, rule_id)"
end
def self.down
drop_table :cards_games
create_table :cards_games do |t|
t.column :card_id, :integer
t.column :game_id, :integer
end
drop_table :cards_players
create_table :cards_players do |t|
t.column :card_id, :integer
t.column :player_id, :integer
end
drop_table :cards_rules
create_table :cards_rules do |t|
t.column :card_id, :integer
t.column :rule_id, :integer
end
end
end

You might want to check out this foreign_key_migrations plugin

Related

Rails migration create table w/ string based primary key

What is the proper way to create a table in rails via a migration in which the primary key is a string instead of an int?
I've tried setting primary_key as #oldergod suggested in the answer below but baz seems to get set to an int still:
class CreateFoos < ActiveRecord::Migration
def change
create_table :foos, primary_key: 'baz' do |t|
end
end
end
UPDATE
I've since tried
class CreateFoos < ActiveRecord::Migration
def change
create_table :foos, primary_key: false do |t|
t.string :baz
end
end
end
which gets me a little closer but still missing the PRIMARY index on the column. I've tried add_index :foos, :baz, type: :primary but this generates the following error:
SQLite3::SQLException: near "primary": syntax error: CREATE primary INDEX "index_foos_on_baz" ON "foos" ("baz")/Users/kyledecot/.rvm/gems/ruby-1.9.3-p392/gems/sqlite3-1.3.8/lib/sqlite3/database.rb:91:in `initialize'
It seems like this should work after looking at http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index_options
What's different if it's a string? See the create table doc.
create_table :foos, primary_key: 'baz' do |t|
t.column :baz, :string
end
Also note that this just sets the primary key in the table. You
additionally need to configure the primary key in the model via
self.primary_key=. Models do NOT auto-detect the primary key from
their table definition.

Ruby on Rails: Is it possible to use Camel-cased database field and table names?

The Ruby on Rails convention for database table and field names is snake_case and not CamelCase. However, I have an existing database being used by a PHP application. I would like to write a Rails application that interacts with this database. Is there an easy, "Rails way" to interact with a database using CamelCase database table and field names?
The short answer is yes but it's not always easier than migrating the old database to a new database. If you want both applications to be able to use the same database though then it is probably the quickest approach up front.
You can override the table and foreign key fields by doing the following:
set_table_name "camelCaseName"
set_primary_key "cameCaseIdName"
You can alias all the field names if necessary as well:
alias "camelCaseFieldName", "field_name"
All of the AR relationships can set the primary key field as well.
has_many :comments, :foreign_key_id => "commentCamelCaseID"
It's more work than normal but it is possible.
Sure. In your model, just define your table like so:
class FooBar < ActiveRecord::Base
self.table_name = "FooBar"
end
the same holds true for field names, which you can define in your migration or schema. You can assign any name you like. It takes a bit more work unless you want to override the default mechanic, but it's still possible:
create_table "products", :force => true do |t|
t.column "shop_id", :integer
t.column "creator_id", :integer
t.column "name", :string, :default => "Untitled"
t.column "value", :string, :default => "Untitled"
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
For more info, see Table Definitions
for the datbases tables name you could use set_table_name
class Dog < ActiveRecord::Base
set_table_name 'dog'
end
for the field you could override your accessor .
hope be usefull . bye

A migration to add unique constraint to a combination of columns

What I need is a migration to apply unique constraint to a combination of columns. i.e. for a people table, a combination of first_name, last_Name and Dob should be unique.
add_index :people, [:firstname, :lastname, :dob], unique: true
According to howmanyofme.com, "There are 46,427 people named John Smith" in the United States alone. That's about 127 years of days. As this is well over the average lifespan of a human being, this means that a DOB clash is mathematically certain.
All I'm saying is that that particular combination of unique fields could lead to extreme user/customer frustration in future.
Consider something that's actually unique, like a national identification number, if appropriate.
(I realise I'm very late to the party with this one, but it could help future readers.)
You may want to add a constraint without an index. This will depend on what database you're using. Below is sample migration code for Postgres. (tracking_number, carrier) is a list of the columns you want to use for the constraint.
class AddUniqeConstraintToShipments < ActiveRecord::Migration
def up
execute <<-SQL
alter table shipments
add constraint shipment_tracking_number unique (tracking_number, carrier);
SQL
end
def down
execute <<-SQL
alter table shipments
drop constraint if exists shipment_tracking_number;
SQL
end
end
There are different constraints you can add. Read the docs
For completeness sake, and to avoid confusion here are 3 ways of doing the same thing:
Adding a named unique constraint to a combination of columns in Rails 5.2+
Let's say we have Locations table that belongs to an advertiser and has column reference_code and you only want 1 reference code per advertiser. so you want to add a unique constraint to a combination of columns and name it.
Do:
rails g migration AddUniquenessConstraintToLocations
And make your migration look either something like this one liner:
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
add_index :locations, [:reference_code, :advertiser_id], unique: true, name: 'uniq_reference_code_per_advertiser'
end
end
OR this block version.
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
change_table :locations do |t|
t.index ['reference_code', 'advertiser_id'], name: 'uniq_reference_code_per_advertiser', unique: true
end
end
end
OR this raw SQL version
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
execute <<-SQL
ALTER TABLE locations
ADD CONSTRAINT uniq_reference_code_per_advertiser UNIQUE (reference_code, advertiser_id);
SQL
end
end
Any of these will have the same result, check your schema.rb
Hi You may add unique index in your migration to the columns for example
add_index(:accounts, [:branch_id, :party_id], :unique => true)
or separate unique indexes for each column
In the typical example of a join table between users and posts:
create_table :users
create_table :posts
create_table :ownerships do |t|
t.belongs_to :user, foreign_key: true, null: false
t.belongs_to :post, foreign_key: true, null: false
end
add_index :ownerships, [:user_id, :post_id], unique: true
Trying to create two similar records will throw a database error (Postgres in my case):
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_ownerships_on_user_id_and_post_id"
DETAIL: Key (user_id, post_id)=(1, 1) already exists.
: INSERT INTO "ownerships" ("user_id", "post_id") VALUES ($1, $2) RETURNING "id"
e.g. doing that:
Ownership.create!(user_id: user_id, post_id: post_id)
Ownership.create!(user_id: user_id, post_id: post_id)
Fully runnable example: https://gist.github.com/Dorian/9d641ca78dad8eb64736173614d97ced
db/schema.rb generated: https://gist.github.com/Dorian/a8449287fa62b88463f48da986c1744a
If you are creating a new table just add unique: true
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title, unique: true
t.text :body
t.references :user, foreign_key: true
t.timestamps
end
add_index :posts, :user_id, unique: true
end
end

How can I add a column to reference to another table on RoR?

Here is the Customer:
class CreateCustomer < ActiveRecord::Migration
def self.up
create_table :customers do |t|
t.column :email, :string, :null => false
end
end
def self.down
drop_table :customers
end
end
And this is the customer Info:
class CustomerInfo < ActiveRecord::Migration
def self.up
create_table :statuses do |t|
t.column :statuses, :string, :null => false
end
end
def self.down
drop_table :status
end
end
What I would like to do is the customer and customer Info have a one to one relationship. How can I do it in a new migration? thank you.
When you want a 1 to 1 in Rails, you have to decide which one of the models will store the foreign key. In your case, you probably want status to store the fk, so add an integer column called customer_id to the status table. Then you can add the has_one/belongs_to on Customer and Status. belongs_to always goes on the model with the foreign key.
Also I'm not sure if Rails will like you calling your table with the singular name, so you will probably have to do some extra work if you really want to call it 'status' instead of 'statuses'
You can try following thing in your next migration
add_column :customer_infos , :customer_id , :integer ,:references=>"customers" , :null=>:true
Then you can add the has_one/belongs_to on Customer and Cusomer_infos .
You can also execute an SQL statement.
statement = "ALTER TABLE users CHANGE id id SMALLINT( 5 ) UNSIGNED NOT NULL AUTO_INCREMENT" ActiveRecord::Base.connection.execute(statement)
you can entry manually in your migration
Note this is just an example. The final SQL statement syntax depends on the database.

In a Rails migration, is it possible to indicate that a newly added column should be before or after an existing column in the table?

Let's say I create a table in a Rails migration, specifying to omit the ID column:
create_table :categories_posts, :id => false do |t|
t.column :category_id, :integer, :null => false
t.column :post_id, :integer, :null => false
end
Later I decide I want to add an ID column as a primary key so I create a new migration:
class ChangeCategoriesToRichJoin < ActiveRecord::Migration
def self.up
add_column :categories_posts, :id, :primary_key
end
def self.down
remove_column :categories_posts, :id
end
end
But when I look at the table after I migrate, it looks like this:
category_id
post_id
id
The id column is in the last position in the table, whereas normally an id column would be first.
Is there a way to change the ChangeCategoriesToRichJoin migration to insist on the id column being created BEFORE the category_id column in the table?
Or do I need to drop the table and add the column in the "create table" definition?
Use :after => :another_column_name, e.g.:
change_table :users do |t|
t.integer :like_count, :default => 0, :after => :view_count
end
I haven't been able to put the columns in order, myself, but with Rails you can rollback, alter the old migration file to add the new columns in the order you want, then re-migrate up the old migration including the new field. It's not exactly ideal, but the ability to migrate and rollback easily, it can work if you're OCD enough to require column order. :P
I am not an expert, but I've read a lot of Rails documentation (and very recently) and can't recall finding a solution for this.

Resources