Rails application based on a schema and data of the Sakila Sample Database, also used in a great book Learning SQL by Alan Beaulieu
Postgres-based source for inspiration: fspacek/docker-postgres-sakila
The goal was to create a Rails/Postgres application with a schema & data from the Sakila Sample Database, to practice both SQL from the book and Rails with ActiveRecord.
bundle install
rails db:create
rails db:migrate
rails db:seed
ⓘ Important: table names are singualar (actor
, store
) in the original database, but
plural in the project (actors
, stores
)
ⓘ Important: primary keys in the original database
look like actor.actor_id
, and in this project they look like actor.id
.
ⓘ Important: tables only have last_update
field in the original database, but in this project
I use created_at
/updated_at
.
ⓘ Important: geospacial columns (like store.location
in the original database) and binary columns
(like staff.picture
in the original database) are not implemented, at least yet.
ⓘ Views and functions/triggers are dumped to schema.rb
with the help of
scenic and fx
gems. Migrations look nicer, but I don't like external files for each SQL piece 🥲.
Before I found out about those two, I created by own SQL view migration/model generator, it's here: lib/generators/sql_view
- Procedures / functions:
-
inventory_in_stock
-
inventory_held_by_customer
-
- Triggers (not sure about doing it yet)
- Tables & Rails models
- Views
- Rewrite SQL views using scenic-views/scenic
- For functions and procedures use teoljungberg/fx
- Check
ON UPDATE
andON DELETE
for existence
- Create
Actor
- Create
Category
- Create
Language
- Create
Film
- Create
FilmActor
- Create
FilmCategory
- Cross-reference
Films
withActors
,Films
withCategories
- Create
Country
- Create
City
- Cross-reference
City
andCountry
- Create
Address
- Cross-reference
Address
andCity
- Create
Customer
- Cross-reference
Customer
andAddress
- Create
Inventory
- Set
Language
.name
limit to 20 - Create
Rental
- Create
Payment
- Cross-reference
Payment
andCustomer
- Create
Store
- Create
Staff
- Add
manager_staff
toStore
- Add
original_language_id
toFilm
- Rename
original_language_id
andrental_rate
columns - Remove uniqueness index from
city
- Add
store_id
toInventory
- Add
staff_id
toRentals
- Add
staff_id
toPayments
- Fix
Film <-> Actor
associations in models - Fix
Film <-> Category
associations in models - Fix
Film <-> Language
associations in models - Fix
Store <-> Staff
associations in models - Cross-reference
Store
andCustomer
- Cross-reference
Inventory
withStore
andRental
- Cross-reference
Payment
withRental
andStaff
- Cross-reference
Rental
withCustomer
andStaff
- Reference
Store
inAddress
First, generate: rails generate model Actor
Then in migration:
def change
create_table :actors do |t|
t.string :first_name, null: false
t.string :last_name, null: false
t.timestamps
end
add_index :actors, :first_name
add_index :actors, :last_name
end
Then in model:
class Actor < ApplicationRecord
validates :first_name, presence: true
validates :last_name, presence: true
First, generate: rails generate model Category
Then in migration:
def change
create_table :categories do |t|
t.string :name, null: false
t.timestamps
end
add_index :categories, :name, unique: true
end
Then in model:
class Category < ApplicationRecord
validates :name, presence: true, uniqueness: true
First, generate: rails generate model Language
Then in migration:
def change
create_table :languages do |t|
t.string :name, null: false
t.timestamps
end
add_index :categories, :name, unique: true
end
Then in model:
class Language < ApplicationRecord
validates :name, presence: true, uniqueness: true
First, generate: rails generate model Film
Then in migration:
def change
create_enum :rating, %w[G PG PG-13 R NC-17]
create_table :films do |t|
t.string :title, null: false
t.text :description
t.date :release_year
t.integer :rental_duration, null: false
t.decimal :rantal_rate, precision: 4, scale: 2, null: false
t.integer :length, limit: 3
t.decimal :replacement_cost, precision: 5, scale: 2, null: false
t.enum :rating, enum_type: 'rating', default: 'G'
t.string :special_features, array: true
t.timestamps
Another migration to reference language_id
in Film
:
class AddLanguageToFilm < ActiveRecord::Migration[7.0]
def change
add_reference :films, :language, foreign_key: true
Then in Film
model:
class Film < ApplicationRecord
has_one :language
validates :rental_rate, :rental_duration, :replacement_cost,
:title, :language_id, presence: true
First, generate: rails generate model FilmActor
Then in migration:
def change
create_table :film_actors do |t|
t.references :film, null: false, foreign_key: true
t.references :actor, null: false, foreign_key: true
t.timestamps
Then in model:
class FilmActor < ApplicationRecord
belongs_to :film
belongs_to :actor
validates :film, :actor, presence: true
First, generate: rails generate model FilmCategory
Then in migration:
def change
create_table :film_categories do |t|
t.references :film, null: false, foreign_key: true
t.references :category, null: false, foreign_key: true
t.timestamps
Then in model:
class FilmCategory < ApplicationRecord
belongs_to :film
belongs_to :category
validates :film, :category, presence: true
In models/actor.rb
:
class Actor < ApplicationRecord
+ has_many :films, through: :film_actors
validates :first_name, presence: true
validates :last_name, presence: true
end
In models/film.rb
:
class Film < ApplicationRecord
has_one :language
+ has_many :categories, through: :film_categories
+ has_many :actors, through: :film_actors
validates :rental_rate, :rental_duration, :replacement_cost,
:title, :language_id, presence: true
end
First, generate: rails generate model Country
Then in migration:
def change
create_table :countries do |t|
t.string :country, null: false
t.timestamps
end
add_index :countries, :country, unique: true
Then in model:
class Country < ApplicationRecord
validates :country, presence: true, uniqueness: true
First, generate: rails generate model City
Then in migration:
def change
create_table :cities do |t|
t.string :city
t.references :country, null: false, foreign_key: true
t.timestamps
end
add_index :cities, :city, unique: true
Then in model:
class City < ApplicationRecord
belongs_to :country
validates :city, presence: true, uniqueness: true
In models/country.rb
:
class Country < ApplicationRecord
+ has_many :cities
validates :country, presence: true, uniqueness: true
end
First, generate: rails generate model Address
Then in migration:
def change
create_table :addresses do |t|
t.string :address, null: false, limit: 50
t.string :address2, limit: 50
t.string :district, null: false, limit: 20
t.references :city, null: false, foreign_key: true
t.string :postal_code, limit: 10
t.string :phone, null: false, limit: 20
t.timestamps
Then in model:
class Address < ApplicationRecord
belongs_to :city
validates :address, :district, :phone, presence: true
In models/city.rb
:
class City < ApplicationRecord
belongs_to :country
+ has_many :addresses
validates :city, presence: true, uniqueness: true
First, generate: rails generate model Customer
Then in migration:
def change
create_table :customers do |t|
t.string :first_name, null: false, limit: 45
t.string :last_name, null: false, limit: 45
t.string :email, limit: 50
t.references :address, null: false, foreign_key: true
t.integer :active
t.timestamps
Then in model:
class Customer < ApplicationRecord
belongs_to :address
validates :first_name, :last_name, presence: true
validates :email, length: { maximum: 50 }
In models/address.rb
:
class Address < ApplicationRecord
belongs_to :city
+ has_many :customers
First, generate: rails generate model Inventory
Then in migration:
def change
create_table :inventories do |t|
t.references :film, null: false, foreign_key: true
t.timestamps
end
Then in model:
class Inventory < ApplicationRecord
belongs_to :film
First, generate: rails g migration ChangeLanguageNameLengthLimit
Then in migration:
def change
change_column :languages, :name, :string, limit: 20
In models/language.rb
:
class Language < ApplicationRecord
+ validates :name, presence: true, uniqueness: true, length: { maximum: 20 }
First, generate: rails generate model Rental
Then in migration:
def change
create_table :rentals do |t|
t.timestamp :rental_date, null: false
t.references :inventory, null: false, foreign_key: true
t.references :customer, null: false, foreign_key: true
t.timestamp :return_date
t.timestamps
Then in model:
class Inventory < ApplicationRecord
belongs_to :film
First, generate: rails generate model Payment
Then in migration:
def change
create_table :payments do |t|
t.references :customer, null: false, foreign_key: true
t.references :rental, null: false, foreign_key: true
t.decimal :amount, precision: 5, scale: 2, null: false
t.timestamp :payment_date, null: false
t.timestamps
Then in model:
class Payment < ApplicationRecord
belongs_to :customer
belongs_to :rental
validates :amount, :payment_date, presence: true
In models/customer.rb
:
class Customer < ApplicationRecord
belongs_to :address
+ has_many :payments
First, generate: rails generate model Store
Then in migration:
def change
create_table :stores do |t|
t.references :address, null: false, foreign_key: true
t.timestamps
Then in model:
class Store < ApplicationRecord
belongs_to :address
First, generate: rails generate model Staff
Then in migration:
def change
create_table :staff do |t|
t.string :first_name, null: false, limit: 45
t.string :last_name, null: false, limit: 45
t.references :address, null: false, foreign_key: true
t.string :email, limit: 50
t.references :store, null: false, foreign_key: true
t.boolean :active, null: false
t.string :username, null: false, limit: 16
t.string :password, limit: 40
t.timestamps
Then in model:
class Staff < ApplicationRecord
self.table_name = 'staff'
belongs_to :address
belongs_to :store
validates :first_name, :last_name, presence: true, length: { maximum: 45 }
validates :email, length: { maximum: 50 }
validates :username, presence: true, length: { maximum: 16 }
validates :password, length: { maximum: 40 }
First, generate rails g migration AddManagerStaffToStore
Then in migration:
class AddManagerStaffToStore < ActiveRecord::Migration[7.0]
def change
add_reference :stores, :manager_staff, index: true, foreign_key: { to_table: :staff }
In models/store.rb
:
class Store < ApplicationRecord
belongs_to :address
+ has_one :manager_staff, class_name: 'Staff', foreign_key: :manager_staff
First, generate rails g migration AddOriginalLanguageIdToFilm
Then in migration:
class AddOriginalLanguageIdToFilm < ActiveRecord::Migration[7.0]
def change
add_reference :films, :original_language_id, foreign_key: { to_table: :languages }
I made a couple of typos in films
table that needed to be fixed:
rename_column :films, :original_language_id_id, :original_language_id
rename_column :films, :rantal_rate, :rental_rate
Put by mistake, to remove:
remove_index :cities, :city
Missed a reference of store_id
in customers
:
add_reference :customers, :store, foreign_key: true
def change
add_reference :inventories, :store, null: false, foreign_key: true
add_reference :rentals, :staff, null: false, foreign_key: { to_table: :staff }
add_reference :payments, :staff, null: false, foreign_key: { to_table: :staff }
In models/actor.rb
:
class Actor < ApplicationRecord
+ has_many :film_actors
has_many :films, through: :film_actors
In models/film.rb
:
class Film < ApplicationRecord
# ...
+ has_many :film_actors
has_many :actors, through: :film_actors
In models/film.rb
:
+ has_many :film_categories
In models/category.rb
:
+ has_many :film_categories
+ has_many :films, through: :film_categories
In models/film.rb
:
- has_one :language
+ belongs_to :language
In models/store.rb
:
- has_one :manager_staff, class_name: 'Staff', foreign_key: :manager_staff
+ belongs_to :manager_staff, class_name: 'Staff', foreign_key: :manager_staff_id
+ has_many :staff
In models/store.rb
:
+ has_many :customers
In models/customer.rb
:
+ belongs_to :store
In models/inventory.rb
:
+ belongs_to :store
+ has_many :rentals
In models/store.rb
:
+ has_many :inventories
In models/rental.rb
:
+ has_many :payments
In models/staff.rb
:
+ has_many :payments
In models/payments
:
+ belongs_to :staff
In models/rental.rb
:
+ belongs_to :staff
In models/customer.rb
:
+ has_many :rentals
In models/staff.rb
:
+ has_many :rentals
In models/address.rb
:
+ has_one :store