Hello again, World!
It has been a while since I wrote my last blog on API > CLI > Space Exploration. I was sooo exhilarated in passing my first capstone project, and now slowly diving into the world of SQL (Structured Query Language), ORM (Object Relational Mapping), Dynamic ORMs, Rake and Active Record.
Understanding SQL is imperative to managing databases. Now I know how to Create, Read, Update and Delete (CRUD) database inputs. Special thanks to DB Browser for SQLite. After one week of exposure, I feel comfortable with basic SQL queries, aggregate functions, complex joins, grouping and sorting data. ORM is where SQL (the database systems) and Ruby Object-Oriented Programming (OOP) communicate with one another. Ruby does not store or manage this data directly. However, with the amazing Ruby gem sqlite3
and a database connection being fully established, it allows us to CRUD our database inputs with Ruby OOP methods. Persisting more data will result in a more complex application, and ORM design pattern is essential to the implementation effort. I find meta-programming for abstract ORM is highly challenging (and fun!). As a novice programmer, this meta-programming exercise is my first exposure, and a great foundation to fully grasp how Active Record works in the background.
Active Record
Active Record creates a mapping between our database and Ruby models. In the MVC (Model-View-Controller), Active Record is the model responsible for providing the ORM framework. I will get to the Active Record mechanics by building a case study from scratch.
We all know the Lakers recently won the 2020 NBA champion (YAYYY!), making its historic return to the top of the league since 2009 and 2010 when Kobe led the Lakers back to back titles. Today's Active Record is derived from the Lakers stats. We are going to focus on AR Associations mainly on belongs_to
, has_many
, and has_many :through
. I provide basic schema and structure of the intended object relationships and database.
Lakers has_many
Players. Lakers has_many
Fans :through
Player.
Player belongs_to
Lakers team. Player has_many
Fans.
Fan belongs_to
Player.
Gemfile, Environment and Rakefile
I set up our required gems on Gemfile
, then bundle install
.
source 'https://rubygems.org'
gem 'sqlite3'
gem 'pry'
gem 'rake'
gem 'activerecord', '<6.0.0'
gem 'sinatra-activerecord', :require => 'active_record'
Moving on to our config/environment.rb
, we need to establish connection in between our sqlite database and Ruby OOP models.
require 'rake'
require 'active_record'
require 'date'
require 'sinatra/activerecord'
require 'bundler/setup'
Bundler.require
Dir[File.join(File.dirname(__FILE__), "../app/models", "*.rb")].each {|f| require f}
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'db/lakers.sqlite')
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
ActiveRecord::Base.logger
, or SQL logger, provides information at each execution, which becomes useful when users perform database migration often.
Rake is one of the Ruby task management tools. rake db:migrate
and rake console
are our most common Rake tasks.
require_relative './config/environment.rb'
require 'sinatra/activerecord/rake'
desc 'drop into the Pry console'
task :console do
Pry.start
end
The sinatra/activerecord/rake provides a set of common administration tasks. When you type rake -T into the Terminal, you will see the following:
rake console # drop into the Pry console
rake db:create # Creates the database from DATABASE_URL or config/database.yml ...
rake db:create_migration # Create a migration (parameters: NAME, VERSION)
rake db:drop # Drops the database from DATABASE_URL or config/database.yml fo...
rake db:environment:set # Set the environment value for the database
rake db:fixtures:load # Loads fixtures into the current environment's database
rake db:migrate # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE...
rake db:migrate:status # Display status of migrations
rake db:rollback # Rolls the schema back to the previous version (specify steps w...
rake db:schema:cache:clear # Clears a db/schema_cache.yml file
rake db:schema:cache:dump # Creates a db/schema_cache.yml file
rake db:schema:dump # Creates a db/schema.rb file that is portable against any DB su...
rake db:schema:load # Loads a schema.rb file into the database
rake db:seed # Loads the seed data from db/seeds.rb
rake db:setup # Creates the database, loads the schema, and initializes with t...
rake db:structure:dump # Dumps the database structure to db/structure.sql
rake db:structure:load # Recreates the databases from the structure.sql file
rake db:version # Retrieves the current schema version number
When I type rake db:create_migration create_lakers
, a new file will be created under db/migrate
folder with filename 20201013230646_create_lakers.rb
. Timestamp hierarchy is critical to the naming convention. It is displayed as year-month-date-hour-minute-second. I can now populate the columns and datatypes of our lakers
table.
I will also create our second and third tables for players
and fans
with rake db:create_migration create_players
and rake db:create_migration create_fans
.
Building lakers
table prior to players
and fans
is a better approach. The lakers
table does not have any foreign keys or dependencies. Both players
and fans
have foreign keys and dependencies.
class CreateLakers < ActiveRecord::Migration[5.2]
def change
create_table :lakers do |t|
t.string :season
t.integer :wins
t.integer :losses
t.string :coach
end
end
end
class CreatePlayers < ActiveRecord::Migration[5.2]
def change
create_table :players do |t|
t.string :name
t.integer :yrs_exp
t.integer :jersey_number
t.integer :laker_id
end
end
end
class CreateFans < ActiveRecord::Migration[5.2]
def change
create_table :fans do |t|
t.string :name
t.integer :age
t.integer :player_id
end
end
end
Once files have been saved, we can execute the rake db:migrate
command in the terminal. Let's double check by using rake command rake db:migrate:status
.
database: db/lakers.sqlite
Status Migration ID Migration Name
--------------------------------------------------
up 20201013230646 Create lakers
up 20201013230742 Create players
up 20201014025021 Create fans
We have now successfully created our three tables (lakers, players, fans). Another new file schema.rb
is also created under the db
folder. The file is auto-generated as we update our database, and it should not be altered or modified.
ActiveRecord::Schema.define(version: 2020_10_14_025021) do
create_table "fans", force: :cascade do |t|
t.string "name"
t.integer "age"
t.integer "player_id"
end
create_table "lakers", force: :cascade do |t|
t.string "season"
t.integer "wins"
t.integer "losses"
t.string "coach"
end
create_table "players", force: :cascade do |t|
t.string "name"
t.integer "yrs_exp"
t.integer "jersey_number"
t.integer "laker_id"
end
end
ActiveRecord Associations
I created a new folder directory app/models/
for our Ruby classes laker.rb
, player.rb
, and fan.rb
.
class Laker < ActiveRecord::Base
has_many :players
has_many :fans, through: :player
end
class Player < ActiveRecord::Base
belongs_to :laker
has_many :fans
end
class Fan < ActiveRecord::Base
belongs_to :player
end
The class Laker
, Player
and Fan
along with its associations are inheriting AR macros via ActiveRecord::Base
. Laker has_many
players (not player). The pluralization naming system is prudent in establishing object relationships. The same goes to the player has_many
fans, and laker has_many
fans through:
player. Note the singular use of player. The use of singularization and pluralization has to semantically align with the intended object relationships.
This allows the use of Ruby meta-programmed methods as the classes inheriting from ActiveRecord::Base
.
ActiveRecord Macros (or methods)
We can now execute rake console
, and practice some of these AR macros.
[1] pry(main)> Laker.methods.size
=> 637
[2] pry(main)> Player.methods.size
=> 625
[3] pry(main)> Fan.methods.size
=> 613
[4] pry(main)> Laker.methods - Player.methods
=> [:before_add_for_players,
:before_add_for_players?,
:before_add_for_players=,
:after_add_for_players,
:after_add_for_players?,
:after_add_for_players=,
:before_remove_for_players,
:before_remove_for_players?,
:before_remove_for_players=,
:after_remove_for_players,
:after_remove_for_players?,
:after_remove_for_players=]
Insane how Ruby gem Active Record
meta-programmed, and made these methods readily available for us, programmers, to use. Active Record allows us to eliminate the low level programming, and dive right into the implementation methods.
Laker
class has built-in 637 methods, Player
class has 625 and Fan
class has 613! If you are curious with the additional 12 methods Laker
class has, refer to pry console [4].
[5] pry(main)> latest_champ = Laker.new(season: "2019-20", wins: 52, losses: 19, coach: "Frank Vogel")
=> #<Laker:0x00007fc297985a88 id: nil, season: "2019-20", wins: 52, losses: 19, coach: "Frank Vogel">
[6] pry(main)> latest_champ.save
=> true
[7] pry(main)> latest_champ
=> #<Laker:0x00007fc297985a88 id: 1, season: "2019-20", wins: 52, losses: 19, coach: "Frank Vogel">
[8] pry(main)> previous_champ = Laker.create(season: "2009-10", wins: 57, losses: 25, coach: "Phil Jackson")
=> #<Laker:0x00007fc298378d60 id: 2, season: "2009-10", wins: 57, losses: 25, coach: "Phil Jackson">
[9] pry(main)> Laker.all
=> [#<Laker:0x00007fc2983a3cb8 id: 1, season: "2019-20", wins: 52, losses: 19, coach: "Frank Vogel">, #<Laker:0x00007fc2983a1e90 id: 2, season: "2009-10", wins: 57, losses: 25, coach: "Phil Jackson">]
On pry console [5] and [7], Active Record
gives us the pre-programmed reader and writer methods capabilities (getter and setter, respectively). If you recall, we used to build associations manually with Ruby methods attr_reader
, attr_writer
and attr_accessor
.
The .new
method instantiates an object instance, and will only be stored in the database with .save
method. Once saved, an id: 1
value will be provided. Alternatively, by using .create
method, the id value will be created upon instantiation. Refer to pry console [8].
As programmers, we do not directly manipulate the id
value to our object instances, and let the Active Record meta-programming methods execute the id values. Similar concept to our previous SQL exercise by having id as a default integer primary key. Our database will be less error prone by self-organizing the data inputs upon execution.
[10] pry(main)> lebron = Player.new(name: "LeBron James", yrs_exp: 17, jersey_number: 23)
=> #<Player:0x00007f89c3b99200 id: nil, name: "LeBron James", yrs_exp: 17, jersey_number: 23, laker_id: nil>
[11] pry(main)> latest_champ.players << lebron
=> [#<Player:0x00007f89c3b99200 id: 1, name: "LeBron James", yrs_exp: 17, jersey_number: 23, laker_id: 1>]
Shovel <<
(or push) method is similar to our .save
method. The one downside of <<
method, it will return all player instances every single time we execute. Imagine how many rosters currently we have in our team, and our terminal possibly clogging up. With Active Record association methods, Lebron's foreign key laker_id: 1
is automatically assigned.
[12] pry(main)> ad = Player.all.build(name: "Anthony Davis", yrs_exp: 8, jersey_number: 3)
=> #<Player:0x00007f89c0e539d0 id: nil, name: "Anthony Davis", yrs_exp: 8, jersey_number: 3, laker_id: nil>
[13] pry(main)> ad.save
=> true
[14] pry(main)> ad
=> #<Player:0x00007f89c0e539d0 id: 2, name: "Anthony Davis", yrs_exp: 8, jersey_number: 3, laker_id: nil>
In the Player
class, we can utilize Player.all
method along with .build
to instantiate a new player, Anthony Davis. .build
only instantiates new objects. We need to explicitly .save
the object in order to store in our database. Once saved, the id primary key will be assigned to id: 2
.
[15] pry(main)> Player.find_or_create_by(name: "Kyle Kuzma", yrs_exp: 3, jersey_number: 0)
=> #<Player:0x00007f89c3d6b538 id: 3, name: "Kyle Kuzma", yrs_exp: 3, jersey_number: 0, laker_id: nil>
[16] pry(main)> Player.find_by(name: "Alex Caruso")
=> nil
[17] pry(main)> alex = latest_champ.players.build(name: "Alex Caruso", yrs_exp: 3, jersey_number: 4)
=> #<Player:0x00007f89c098e288 id: nil, name: "Alex Caruso", yrs_exp: 3, jersey_number: 4, laker_id: 1>
[18] pry(main)> alex.save
=> true
[19] pry(main)> Player.find_by(name: "Alex Caruso")
=> #<Player:0x00007f89c0c555e8 id: 4, name: "Alex Caruso", yrs_exp: 3, jersey_number: 4, laker_id: 1>
The find_or_create_by
method helps you to find an object instance. If not found, the instance will be created and stored in our database. Refer to pry console [15]. Another similar method is find_by
, we do not yet have Alex Caruso, and hence, the return is nil.
We can now get more creative by chaining the Laker
class object instance latest_champ
in order to build its players
' instance. Active Record assigns Alex Caruso's laker_id: 1
from latest_champ
's id. Quick reminder that the .build
method does not save the object instance, and thus, the player id: nil
on pry console [17]. Once saved, and we can double check its existence with find_by
method.
[20] pry(main)> david = alex.fans.create(name: "David", age: 37)
=> #<Fan:0x00007f89bf8c3c28 id: 1, name: "David", age: 37, player_id: 4>
[21] pry(main)> Laker.first.players.find(4).fans
=> [#<Fan:0x00007f89c3b590d8 id: 1, name: "David", age: 37, player_id: 4>]
As we went through our Laker
class and Player
class, you are most likely getting the overall schema implementation and how to apply CRUD functionality with Ruby OOP logic via Active Record
. Moving forward to our third Fan
class, we can create fan's object instance via Player
class, as well as finding out a specific fan of a player from Laker
's recent 2020 championship.
Let's review our overall folder structure hierarchy. When it comes to consolidating Ruby OOP and database, model structure and organization are critical at its inception.
> app/models
laker.rb
player.rb
> config
environment.rb
> db
> migrate
20201013230646_create_lakers.rb
20201013230742_create_players.rb
20201014025021_create_fans.rb
lakers.sqlite
schema.rb
> Gemfile
> Gemfile.lock
> Rakefile
As you saw earlier, we have hundreds of meta-programmed methods and I have only applied a few. Good enough for me to get intimate with our rudimentary objects. I shall end my Active Record case study, and jump right into Sinatra. Woot!
I have this feeling of finally setting myself one level up from plain old Ruby objects (PORO) onto these highly matured objects. I am able to articulate relationships amongst classes without having to worry about building low level programming methods. With Active Record
, we are operating on a higher level of abstraction.
External Sources:
Active Record Basics
Los Angeles Lakers
Top comments (9)
That was interesting and even easier to do when you practise with things you like, difficult year but amazing win for the lakers :)
hahah yes, you got it right.
I know, it has been a while since the Kobe era. :)
Indeed, I've never tried the Ruby language but I've done some school assignements about object oriented programming with java and javascript, maybe I'll try Ruby in the nearly future and we can discuss about code!
Kobe was my favourite player, so this is like a special win :)
Have a nice day!
That'd be great Jordi.
Stay touch, and you too - have a nice day!
My last adventure with ActiveRecord was 4 years ago, did they optimize anything meanwhile?
I was only recently exposed to ActiveRecord (about 2 weeks ago), and not sure if they've optimized anything new.
Two of the things I like, NBA and Active record!
Great job on explaining the topics using a timely and easy to understand examples!
Hahaha I need to find something that I love and/or like for case studies, it motivates me. Thanks Lenmor!
In the last 4 days I have been studying sqlite3, ORM, and Active Record. This article just reminds me of whatever I have been reading. Nice one