Differences and best practices between integer, indexed column, foreign key, references - ruby-on-rails

There are different possibilities in Rails to link models and tables together.
add_column :books, :user_id, :integer, index: true
add_reference :books, :user, index: true, foreign_key: true
As far as I have read, I see that some Databases have different behaviors concerning this options.
I'm interested in finding what is the best practice to adopt for performances and for code readability.
I was using integer indexed column for a long time and I want to know if I should use reference with foreign_keys instead.
UPDATE: Exemple
In a brand new test app I've run:
rails g model book author_id:integer:index user:references
The migration file looks like:
class CreateBooks < ActiveRecord::Migration
def change
create_table :books do |t|
t.integer :author_id
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
add_index :books, :author_id
end
end
The ddl for the table
-- Table: public.books
-- DROP TABLE public.books;
CREATE TABLE public.books
(
id integer NOT NULL DEFAULT nextval('books_id_seq'::regclass),
author_id integer,
user_id integer,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
CONSTRAINT books_pkey PRIMARY KEY (id),
CONSTRAINT fk_rails_bc582ddd02 FOREIGN KEY (user_id)
REFERENCES public.users (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
OIDS=FALSE
);
ALTER TABLE public.books
OWNER TO my_user_name;
-- Index: public.index_books_on_author_id
-- DROP INDEX public.index_books_on_author_id;
CREATE INDEX index_books_on_author_id
ON public.books
USING btree
(author_id);
-- Index: public.index_books_on_user_id
-- DROP INDEX public.index_books_on_user_id;
CREATE INDEX index_books_on_user_id
ON public.books
USING btree
(user_id);
EDIT 2: Here is a link that describe the problem and points out the foreign_keys importance
Link

Expanding on my comment a little: yes, the primary function of foreign keys is to maintain referential integrity (https://en.wikipedia.org/wiki/Referential_integrity).
As a side benefit, they provide a little more explicit information about your schema: query and design tools can make use of those relationships, rather than rely on shared column names.
It's also easy to check that all columns references by foreign keys are indexed, whereas otherwise you may only discover a missing index at query time.
http://www.postgresql.org/docs/current/static/ddl-constraints.html#DDL-CONSTRAINTS-FK
The performance implications are pretty easy to reason about: you're going to be doing an index lookup against the parent table whenever the data in a child table changes (insert/delete/update). For most small-medium tables you never have to worry about it.

Related

Models need to be changed when migration files are changed?

I'm very new to Rails, and currently for our class, we have to create an application with databases.
I have a bunch of tables that have foreign keys to other tables, and I've defined these associations in the migration files (using execute("ALTER TABLE...")), but not in my models (I only have self.primary_key defined in my models).
My question is, since I've set these associations in my migrations, do I also have to set the associations in my models (i.e. has_may, belongs_to, validates_uniqueness_of, etc)?
I'll be creating an app that would allow user input, and these input would be inserted to the database.
Hoping that my confusion can be cleared up. Thank you!
Example
Migration file:
class CreateSchools < ActiveRecord::Migration[5.0]
def up
create_table :schools, {:id => false} do |t|
t.integer :school_id
t.string :school_name
t.string :contact_number
t.integer :a_id
t.timestamps
end
execute "ALTER TABLE schools ADD PRIMARY KEY(school_id);"
execute "ALTER TABLE schools ADD CONSTRAINT a_id UNIQUE(a_id);"
execute "ALTER TABLE schools ADD CONSTRAINT school_references_location FOREIGN KEY (a_id) REFERENCES locations(a_id) ON DELETE CASCADE ON UPDATE CASCADE;"
change_column_null :schools, :school_name, false
change_column_null :schools, :a_id, false
end
def down
execute "ALTER TABLE schools DROP PRIMARY KEY;"
execute "ALTER TABLE schools DROP FOREIGN KEY school_references_location;"
execute "ALTER TABLE schools DROP INDEX a_id;"
drop_table :schools
end
end
Model:
class School < ApplicationRecord
self.primary_key = :school_id
end
Yes you'll need to define the associations in Model as well.
For all the rails applications,
Migrating and changing db according to associations is the first step.
Then add association statements in the models so that the associations from db can be accessed using simple methods. Like user.posts etc.
Adding association statements in Models provide you methods that generates queries according to the method called to retrieve data from db easily. You can also query db using join and select statements if you don't want to include association statements in Models.
So, in short, designing schema to manage associations is necessary where as mentioning them in Model is not. But we do it to make our life and code simpler.

index: true vs foreign_key: true (Rails)

Following a guide, I ran the following command:
rails g migration CreateSnippetsUsers snippet:belongs_to user:belongs_to
This created the following migration:
class CreateSnippetsUsers < ActiveRecord::Migration[5.0]
def change
create_table :snippets_users do |t|
t.belongs_to :snippet, foreign_key: true
t.belongs_to :user, foreign_key: true
end
end
end
In the past I've seen the same thing, but with index: true instead of foreign_key: true. What's the difference between the two?
Indexes, foreign keys and foreign keys constraints are strictly related concepts in databases that are often confused or misunderstood.
REFERENCES
When you declare a reference, you're simply saying to include a column whose values should match those of another table (and in Rails you also get some useful methods to navigate through the associated models). In the example:
create_table :appointments do |t|
t.references :student
end
the appointments table will have a column named student_id whose values should be in the pool of students' id values.
INDEXES
Since when you add a reference you will probably use that column often, you may (and probably should!) also tell you database to boost the look up speed using the reference column. You can do this with the option index: true (which by the way is a default option in the reference method since Rails 5). Indexes have few drawbacks, the main being a larger memory consumption.
FOREIGN KEY CONSTRAINTS
From what said so far, reference column and foreign column are synonyms. But do you remember when I said that a reference column's values should match those of another table? If you simply declare a reference, it's your responsibility to ensure that a matching row on the referenced table exists, or someone will end up doing nonsense actions like creating appointments for non-existing students. This is an example of database integrity, and fortunately there are some mechanisms that will grant a stronger level of integrity. These mechanisms are called ' database constraints'. What the option foreign_key: true does is exactly to add this kind of constraint on the reference column, to reject any entry whose foreign key values are not in the referenced table.
Database integrity is a complex task, growing in difficulty with the database's complexity. You probably should add also other kind of constraints, like using they keywords dependent: :destroy in your class to ensure that when you delete a student, all of its existing appointments are also destroyed.
As usual, here's a RTFM link: https://guides.rubyonrails.org/association_basics.html
Index improve speed of data retrieval operations on database tables. When we write index: true to any column, it adds a database index to this column. For example I was creating a table:
create_table :appointments do |t|
t.references :student, index: true
end
It will create student_id column in appointments table.
A foreign key have different use case, it is a relationship between tables. It allow us to declare an index in one table that is related to an index in another table and also some constraints are placed.The database enforces the rules of this relationship to maintain referential integrity. For example we have two table profiles and educations, and a profile may have many educations.
create_table :educations do |t|
t.belongs_to :profile, index: true, foreign_key: true
end
Now we have profile_id column in educations table which is foreign key of profiles table. It prevents a record from being entered into the educations table unless it contains a profile_id value that exists in the profiles table. So referential integrity will be maintained.

Setting options for create_join_table migration in Rails

I have some issues. I'm new to RoR
I'm trying to create a join table using Rails migration. Documentation of this is here...
http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_join_table
When I do...
rails g migration CreateJoinTableUserOffer users offers
...it creates the following migration
class CreateJoinTableUserOffer < ActiveRecord::Migration
def change
create_join_table:users, :offers do |t|
t.index [:user_id, :offer_id]
t.index [:offer_id, :user_id]
end
end
end
and when I do...
rake db:migrate
it creates...
-- Table: offers_users
-- DROP TABLE offers_users;
CREATE TABLE offers_users
(
user_id integer NOT NULL,
offer_id integer NOT NULL
)
WITH (
OIDS=FALSE
);
ALTER TABLE offers_users
OWNER TO sudeepkaushik;
-- Index: index_offers_users_on_offer_id_and_user_id
-- DROP INDEX index_offers_users_on_offer_id_and_user_id;
CREATE INDEX index_offers_users_on_offer_id_and_user_id
ON offers_users
USING btree
(offer_id, user_id);
-- Index: index_offers_users_on_user_id_and_offer_id
-- DROP INDEX index_offers_users_on_user_id_and_offer_id;
CREATE INDEX index_offers_users_on_user_id_and_offer_id
ON offers_users
USING btree
(user_id, offer_id);
What I want to do is that I first of all want the table name to be users_offers instead of offers_users, for this you can specify the :table_name in the create_join_table migration. I'm not able to get the syntax of setting this option correctly. Need help here!
2nd, I noticed that this migration doesn't create the foreign keys that I would expect with the Users and Offers tables. Need your comments here also. Do I need to manually create the foreign keys myself?
You can define your join tables name with the table_name option.
create_join_table :users, :offers, table_name: :users_offers
There is also an option for setting column options called column_options, but I only got it to work for indexes and not for foreign keys.
create_join_table :users, :offers, column_options: { index: true }
This will create the desired indexes, but it ignores foreign_key: true. So you need to create them separately.
add_foreign_key :users_offers, :users
add_foreign_key :users_offers, :offers
In your models you need to add the relation like this:
# user.rb
has_and_belongs_to_many :offers, join_table: :users_offers
And:
# offers.rb
has_and_belongs_to_many :users, join_table: :users_offers

ActiveRecord Migrations : Postgres : Strings as primary keys [duplicate]

So I've got two models, State and Acquisition. State has_many Acquisitions. I felt like an autoincrementing integer primary key for 51 records was rather silly. So I altered the model for the State to be the PK (State being the two letter abbreviation; I'm not storing the actual state name anywhere:
class State < ActiveRecord::Base
self.primary_key = "state"
has_many :acquisition_histories
end
The problem is when I created my Acquisition model, it created the foreign key column state_id as an integer. More specifically, the script/generated migration did:
class CreateAcquisitions < ActiveRecord::Migration
def self.up
create_table :acquisitions do |t|
t.date :date
t.string :category
t.text :notes
t.references :state
t.timestamps
end
end
end
I'm assuming that t.references data type sets it to int. The problem is my create method on my Acquisition class is trying to put a state abbreviation into the state_id field on the table acquisitions (and yes, it's called state_id on the database, even though it says :state in the migration script). The method doesn't fail, but it does put a 0 in the state_id field and the records go into the ether.
Though, I agree that this might be more trouble than it's worth considering the extra effort of working against the defaults elsewhere, just in case you actually want to do what you've asked:
Create states migration:
class CreateStatesTable < ActiveRecord::Migration
def change
create_table :states, id: false do |t|
t.string :state, limit: 2
t.string :name
t.index :state, unique: true
end
end
end
states model:
class State < ActiveRecord::Base
self.primary_key = :state
end
Note that before Rails 3.2, this was set_primary_key = :state instead of self.primary_key= see: http://guides.rubyonrails.org/3_2_release_notes.html#active-record-deprecations
if you find yourself here... leave as quickly as you can and go to:
Using Rails, how can I set my primary key to not be an integer-typed column?
In Rails 5.1 you can specify the type of the primary key at creation:
create_table :states, id: :string do |t|
# ...
end
From the documentation:
A Symbol can be used to specify the type of the generated primary key column.
I'm working on a project that uses UUIDs as primary keys, and honestly, I don't recommend it unless you're certain you absolutely need it. There are a ton of Rails plugins out there that will not work unmodified with a database that uses strings as primary keys.
Note that mkirk's answer creates a faux primary key. This explains why ActiveRecord needs to be told what the primary key is. Inspecting the table reveals
Table "public.acquisitions"
Column | Type | Modifiers
--------+----------------------+-----------
state | character varying(2) |
name | character varying |
Indexes:
"index_acquisitions_on_state" UNIQUE, btree (state)
In practice this works as expected so nothing wrong there, but it could be nicer.
We can keep the id column and change its type to string*. The migration looks like
class CreateAcquisitionsTable < ActiveRecord::Migration
def change
create_table :acquisitions do |t|
t.string :name
end
change_column :acquisitions, :id, :string, limit: 2
end
end
Inspecting the table reveals that you have an actual primary key with all the goodies such as the unique key constraint (no unique index needed), not null constraint, and auto-incrementing key.
Table "public.acquisitions"
Column | Type | Modifiers
--------+----------------------+---------------------------------------------------
id | character varying(2) | not null default nextval('acquisitions_id_seq'::regclass)
name | character varying |
Indexes:
"acquisitions_pkey" PRIMARY KEY, btree (id)
And you won't need to explicitly tell ActiveRecord what the primary is.
You'll want to consider setting a default id if none is provided.
class MyModel < ActiveRecord::Base
before_create do
self.id = SecureRandom.uuid unless self.id
end
end
* Disclaimer: you should not change the default primary key unless you have good reason to
You want to follow the Rails conventions. The extra primary key is not an issue in any way. Just use it.
I had a bit of experience with string used as primary keys and it's a pain in the ***. Remember that by default if you want to pass an object with the default :controller/:action/:id pattern, the :id will be a string and this will probably lead to routing problems if some ids get weirdly formatted ;)
class CreateAcquisitions < ActiveRecord::Migration
def self.up
create_table :acquisitions, :id => false do |t|
t.date :date
t.string :category
t.text :notes
t.references :state
t.timestamps
end
end
end
Rails works best when you don't fight against the defaults. What harm does it do to have an integer primary key on your state table?
Unless you're stuck with a legacy schema that you have no control over, I'd advise you to stick to the Rails defaults—convention over configuration, right?—and concentrate on the important parts of your app, such as the UI and the business logic.

Cannot add a NOT NULL column with default value NULL in Sqlite3

I am getting the following error while trying to add a NOT NULL column to an existing table. Why is it happening ?. I tried rake db:reset thinking that the existing records are the problem, but even after resetting the DB, the problem persists. Can you please help me figure this out.
Migration File
class AddDivisionIdToProfile < ActiveRecord::Migration
def self.up
add_column :profiles, :division_id, :integer, :null => false
end
def self.down
remove_column :profiles, :division_id
end
end
Error Message
SQLite3::SQLException: Cannot add a NOT NULL column with default value NULL: ALTER TABLE "profiles" ADD "division_id" integer NOT NULL
This is (what I would consider) a glitch with SQLite. This error occurs whether there are any records in the table or not.
When adding a table from scratch, you can specify NOT NULL, which is what you're doing with the ":null => false" notation. However, you can't do this when adding a column. SQLite's specification says you have to have a default for this, which is a poor choice. Adding a default value is not an option because it defeats the purpose of having a NOT NULL foreign key - namely, data integrity.
Here's a way to get around this glitch, and you can do it all in the same migration. NOTE: this is for the case where you don't already have records in the database.
class AddDivisionIdToProfile < ActiveRecord::Migration
def self.up
add_column :profiles, :division_id, :integer
change_column :profiles, :division_id, :integer, :null => false
end
def self.down
remove_column :profiles, :division_id
end
end
We're adding the column without the NOT NULL constraint, then immediately altering the column to add the constraint. We can do this because while SQLite is apparently very concerned during a column add, it's not so picky with column changes. This is a clear design smell in my book.
It's definitely a hack, but it's shorter than multiple migrations and it will still work with more robust SQL databases in your production environment.
You already have rows in the table, and you're adding a new column division_id. It needs something in that new column in each of the existing rows.
SQLite would typically choose NULL, but you've specified it can't be NULL, so what should it be? It has no way of knowing.
See:
Adding a Non-null Column with no Default Value in a Rails Migration (2009, no longer available, so this is a snapshot at archive.org)
Adding a NOT NULL Column to an Existing Table (2014)
That blog's recommendation is to add the column without the not null constraint, and it'll be added with NULL in every row. Then you can fill in values in the division_id and then use change_column to add the not null constraint.
See the blogs I linked to for an description of a migration script that does this three-step process.
If you have a table with existing rows then you will need to update the existing rows before adding your null constraint. The Guide on migrations recommends using a local model, like so:
Rails 4 and up:
class AddDivisionIdToProfile < ActiveRecord::Migration
class Profile < ActiveRecord::Base
end
def change
add_column :profiles, :division_id, :integer
Profile.reset_column_information
reversible do |dir|
dir.up { Profile.update_all division_id: Division.first.id }
end
change_column :profiles, :division_id, :integer, :null => false
end
end
Rails 3
class AddDivisionIdToProfile < ActiveRecord::Migration
class Profile < ActiveRecord::Base
end
def change
add_column :profiles, :division_id, :integer
Profile.reset_column_information
Profile.all.each do |profile|
profile.update_attributes!(:division_id => Division.first.id)
end
change_column :profiles, :division_id, :integer, :null => false
end
end
You can add a column with a default value:
ALTER TABLE table1 ADD COLUMN userId INTEGER NOT NULL DEFAULT 1
The following migration worked for me in Rails 6:
class AddDivisionToProfile < ActiveRecord::Migration[6.0]
def change
add_reference :profiles, :division, foreign_key: true
change_column_null :profiles, :division_id, false
end
end
Note :division in the first line and :division_id in the second
API Doc for change_column_null
Not to forget that there is also something positive in requiring the default value with ALTER TABLE ADD COLUMN NOT NULL, at least when adding a column into a table with existing data. As documented in https://www.sqlite.org/lang_altertable.html#altertabaddcol:
The ALTER TABLE command works by modifying the SQL text of the schema
stored in the sqlite_schema table. No changes are made to table
content for renames or column addition. Because of this, the execution
time of such ALTER TABLE commands is independent of the amount of data
in the table. They run as quickly on a table with 10 million rows as
on a table with 1 row.
The file format itself has support for this https://www.sqlite.org/fileformat.html
A record might have fewer values than the number of columns in the
corresponding table. This can happen, for example, after an ALTER
TABLE ... ADD COLUMN SQL statement has increased the number of columns
in the table schema without modifying preexisting rows in the table.
Missing values at the end of the record are filled in using the
default value for the corresponding columns defined in the table
schema.
With this trick it is possible to add a new column by updating just the schema, operation that took 387 milliseconds with a test table having 6.7 million rows. The existing records in the data area are not touched at all and the time saving is huge. The missing values for the added column come on-the-fly from the schema and the default value is NULL if not otherwise stated. If the new column is NOT NULL then the default value must be set to something else.
I do not know why there is not a special path for ALTER TABLE ADD COLUMN NOT NULL when the table is empty. A good workaround is perhaps to create the table right from the beginning.

Resources