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 UPDATEandON DELETEfor existence
- Create
Actor - Create
Category - Create
Language - Create
Film - Create
FilmActor - Create
FilmCategory - Cross-reference
FilmswithActors,FilmswithCategories - Create
Country - Create
City - Cross-reference
CityandCountry - Create
Address - Cross-reference
AddressandCity - Create
Customer - Cross-reference
CustomerandAddress - Create
Inventory - Set
Language.namelimit to 20 - Create
Rental - Create
Payment - Cross-reference
PaymentandCustomer - Create
Store - Create
Staff - Add
manager_stafftoStore - Add
original_language_idtoFilm - Rename
original_language_idandrental_ratecolumns - Remove uniqueness index from
city - Add
store_idtoInventory - Add
staff_idtoRentals - Add
staff_idtoPayments - Fix
Film <-> Actorassociations in models - Fix
Film <-> Categoryassociations in models - Fix
Film <-> Languageassociations in models - Fix
Store <-> Staffassociations in models - Cross-reference
StoreandCustomer - Cross-reference
InventorywithStoreandRental - Cross-reference
PaymentwithRentalandStaff - Cross-reference
RentalwithCustomerandStaff - Reference
StoreinAddress
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
endThen in model:
class Actor < ApplicationRecord
validates :first_name, presence: true
validates :last_name, presence: trueFirst, 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
endThen in model:
class Category < ApplicationRecord
validates :name, presence: true, uniqueness: trueFirst, 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
endThen in model:
class Language < ApplicationRecord
validates :name, presence: true, uniqueness: trueFirst, 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.timestampsAnother migration to reference language_id in Film:
class AddLanguageToFilm < ActiveRecord::Migration[7.0]
def change
add_reference :films, :language, foreign_key: trueThen in Film model:
class Film < ApplicationRecord
has_one :language
validates :rental_rate, :rental_duration, :replacement_cost,
:title, :language_id, presence: trueFirst, 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.timestampsThen in model:
class FilmActor < ApplicationRecord
belongs_to :film
belongs_to :actor
validates :film, :actor, presence: trueFirst, 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.timestampsThen in model:
class FilmCategory < ApplicationRecord
belongs_to :film
belongs_to :category
validates :film, :category, presence: trueIn models/actor.rb:
class Actor < ApplicationRecord
+ has_many :films, through: :film_actors
validates :first_name, presence: true
validates :last_name, presence: true
endIn 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
endFirst, 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: trueThen in model:
class Country < ApplicationRecord
validates :country, presence: true, uniqueness: trueFirst, 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: trueThen in model:
class City < ApplicationRecord
belongs_to :country
validates :city, presence: true, uniqueness: trueIn models/country.rb:
class Country < ApplicationRecord
+ has_many :cities
validates :country, presence: true, uniqueness: true
endFirst, 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.timestampsThen in model:
class Address < ApplicationRecord
belongs_to :city
validates :address, :district, :phone, presence: trueIn models/city.rb:
class City < ApplicationRecord
belongs_to :country
+ has_many :addresses
validates :city, presence: true, uniqueness: trueFirst, 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.timestampsThen 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 :customersFirst, 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
endThen in model:
class Inventory < ApplicationRecord
belongs_to :filmFirst, generate: rails g migration ChangeLanguageNameLengthLimit
Then in migration:
def change
change_column :languages, :name, :string, limit: 20In 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.timestampsThen in model:
class Inventory < ApplicationRecord
belongs_to :filmFirst, 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.timestampsThen in model:
class Payment < ApplicationRecord
belongs_to :customer
belongs_to :rental
validates :amount, :payment_date, presence: trueIn models/customer.rb:
class Customer < ApplicationRecord
belongs_to :address
+ has_many :paymentsFirst, generate: rails generate model Store
Then in migration:
def change
create_table :stores do |t|
t.references :address, null: false, foreign_key: true
t.timestampsThen in model:
class Store < ApplicationRecord
belongs_to :addressFirst, 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.timestampsThen 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_staffFirst, 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_ratePut by mistake, to remove:
remove_index :cities, :cityMissed 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_actorsIn models/film.rb:
class Film < ApplicationRecord
# ...
+ has_many :film_actors
has_many :actors, through: :film_actorsIn models/film.rb:
+ has_many :film_categoriesIn models/category.rb:
+ has_many :film_categories
+ has_many :films, through: :film_categoriesIn models/film.rb:
- has_one :language
+ belongs_to :languageIn 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 :staffIn models/store.rb:
+ has_many :customersIn models/customer.rb:
+ belongs_to :storeIn models/inventory.rb:
+ belongs_to :store
+ has_many :rentalsIn models/store.rb:
+ has_many :inventoriesIn models/rental.rb:
+ has_many :paymentsIn models/staff.rb:
+ has_many :paymentsIn models/payments:
+ belongs_to :staffIn models/rental.rb:
+ belongs_to :staffIn models/customer.rb:
+ has_many :rentalsIn models/staff.rb:
+ has_many :rentalsIn models/address.rb:
+ has_one :store