DEV Community

loading...
Cover image for Lakers < ActiveRecord::Base

Lakers < ActiveRecord::Base

fentybit
full stack SWE at RE/MAX | former architect | flatiron school alumn
Updated on ・9 min read

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.

winners

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.

Alt Text

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'
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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=]
Enter fullscreen mode Exit fullscreen mode

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">]
Enter fullscreen mode Exit fullscreen mode

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>]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>]
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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



fentybit | GitHub | Twitter | LinkedIn

Discussion (8)

Collapse
j_aidle profile image
Jordi

That was interesting and even easier to do when you practise with things you like, difficult year but amazing win for the lakers :)

Collapse
fentybit profile image
fentybit Author

hahah yes, you got it right.
I know, it has been a while since the Kobe era. :)

Collapse
j_aidle profile image
Jordi

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!

Thread Thread
fentybit profile image
fentybit Author

That'd be great Jordi.
Stay touch, and you too - have a nice day!

Collapse
lennythedev profile image
Lenmor Ld

Two of the things I like, NBA and Active record!
Great job on explaining the topics using a timely and easy to understand examples!

Collapse
fentybit profile image
fentybit Author

Hahaha I need to find something that I love and/or like for case studies, it motivates me. Thanks Lenmor!

Collapse
alexandrusimandi profile image
Alexandru Simandi

My last adventure with ActiveRecord was 4 years ago, did they optimize anything meanwhile?

Collapse
fentybit profile image
fentybit Author

I was only recently exposed to ActiveRecord (about 2 weeks ago), and not sure if they've optimized anything new.