DEV Community

Eloy Pérez
Eloy Pérez

Posted on • Edited on

Part 2 - Saving data. Creating a Ruby App to get notified about Epic Free Games.

Part 1 | Part 2

Now that we know how to fetch the information we want, we can insert it into a database for later use. In this series we are going to use PostgreSQL so first of all we need to install the gem pg in our project:

bundle add pg 
Enter fullscreen mode Exit fullscreen mode

And in order to have a database to play with we can use docker-compose to set up our development (and test) postgres instance. So in a new docker-compose.yml file:

services: db: image: postgres volumes: - ./.data/db:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: password ports: - "5432:5432" 
Enter fullscreen mode Exit fullscreen mode

With that we can create our postgres container with docker compose up -d.

For connecting to the database in our Ruby applications we are going to use sequel so let's add it with bundle add sequel

Once we have our postgres instance up and running and sequel installed we need to create the database itself. Sequel gives us all the tools to manage our database but doesn't provide rake tasks, so we are going to create them for easier database management.

Add the gem rake with bundle add rake and, in a new Rakefile file, add the db:create task:

namespace :db do task :create do |t, args| require "sequel" development_database = "free_game_development" db = Sequel.connect(ENV["POSTGRES_URL"].to_s) db.run("DROP DATABASE IF EXISTS #{development_database}") db.run("CREATE DATABASE #{development_database}") end end 
Enter fullscreen mode Exit fullscreen mode

This task expects an environment variable called POSTGRES_URL that points to the default postgres database. For managing environment variables in your local environment you can use many different tools, I use direnv

# In a .envrc file export POSTGRES_URL="postgres://postgres:password@127.0.0.1:5432/postgres" 
Enter fullscreen mode Exit fullscreen mode

Then we can run the task with bundle exec rake "db:create"

For running Sequel migrations:

 namespace :db do task :create do |t, args| require "sequel" development_database = "free_game_development" db = Sequel.connect(ENV["POSTGRES_URL"].to_s) db.run("DROP DATABASE IF EXISTS #{development_database}") db.run("CREATE DATABASE #{development_database}") end + + task :migrate do + require "sequel" + require "sequel/extensions/migration" + + Sequel::Migrator.run(Sequel.connect(ENV["DATABASE_URL"].to_s), "db/migrations") + end end 
Enter fullscreen mode Exit fullscreen mode

This task will connect to a database using the DATABASE_URL environment variable, for example:

 export POSTGRES_URL="postgres://postgres:password@127.0.0.1:5432/postgres" + export DATABASE_URL="postgres://postgres:password@127.0.0.1:5432/free_game_development" 
Enter fullscreen mode Exit fullscreen mode

The rake task expects migrations to be placed in a folder db/migrations, create it and lets move to creating models and their migrations.

We want to store games and their promotions, either active or expired. The migrations for games need to include data like the game's title and the url slug for crafting the Store URL we will send in the notification.

# In a new file 'db/migrations/001_create_games.rb' Sequel.migration do up do create_table(:games) do primary_key(:id) String(:title, null: false) String(:url_slug, null: true) end end down do drop_table(:games) end end 
Enter fullscreen mode Exit fullscreen mode

Each game may have many promotions so we are going to store promotion data in a different table. For each promotion we are going to store the reference to the game itself, start/end dates and the discount itself.

# In a new file 'db/migrations/002_create_promotions.rb' Sequel.migration do up do create_table(:promotions) do primary_key(:id) foreign_key(:game_id, :games, null: false) DateTime(:start_date, null: false) DateTime(:end_date, null: false) String(:discount_type, null: false) Integer(:discount_percentage, null: false) end end down do drop_table(:promotions) end end 
Enter fullscreen mode Exit fullscreen mode

Before running migrations lets modify the rake task to create a schema.rb file with our database structure after running migrations. Having this file is useful when we want to quickly check the structure of the database without reading all migrations.

 require "sequel" require "sequel/extensions/migration" + ENV["RACK_ENV"] ||= "development" Sequel::Migrator.run(Sequel.connect(ENV["DATABASE_URL"].to_s), "db/migrations") + if ENV["RACK_ENV"] == "development" + system("sequel -d #{ENV["DATABASE_URL"]} > db/schema.rb") + end 
Enter fullscreen mode Exit fullscreen mode

And finally run bundle exec rake db:migrate, this should have created a schema.rb with:

Sequel.migration do change do create_table(:games) do primary_key :id String :title, :text=>true, :null=>false String :url_slug, :text=>true end create_table(:schema_info) do Integer :version, :default=>0, :null=>false end create_table(:promotions) do primary_key :id foreign_key :game_id, :games, :null=>false, :key=>[:id] DateTime :start_date, :null=>false DateTime :end_date, :null=>false String :discount_type, :text=>true, :null=>false Integer :discount_percentage, :null=>false end end end 
Enter fullscreen mode Exit fullscreen mode

To make use of these tables we need two Sequel models, Game and Promotion. In our app directory create a new models directory to store them.

# app/models/game.rb class Game < Sequel::Model one_to_many :promotions end # app/models/promotion.rb class Promotion < Sequel::Model many_to_one :game end 
Enter fullscreen mode Exit fullscreen mode

Saving data into the database

In the previous post we created a class to fetch all games and return them as Data structures. We need to create a new class that make use of that and insert the data into the database.

Create a new file app/update_games.rb with our new class:

class UpdateGames def initialize(adapter) @adapter = adapter end def call end end 
Enter fullscreen mode Exit fullscreen mode

As you can see this class receives the adapter to use and contains a method to do all the logic. The call method will fetch all free games and insert them (and their promotions) into the database:

 class UpdateGames def initialize(adapter) @adapter = adapter end def call + @adapter.get_free_games.each do |game_data| + game = Game.update_or_create(title: game_data.title) do |new_game| + new_game.url_slug = game_data.url_slug + end + + game_data.promotions.each do |promotion_data| + Promotion.update_or_create( + game_id: game.id, + start_date: promotion_data.start_date, + end_date: promotion_data.end_date, + discount_type: promotion_data.discount_type, + discount_percentage: promotion_data.discount_percentage + ) + end + end + end  end 
Enter fullscreen mode Exit fullscreen mode

We use update_or_create because we are going to run this everyday and we will receive multiple times the same games.

Now lets create a simple test that checks that we have insert the correct number of games and promotions. In a new file spec/app/epic_store_adapter_spec.rb

require_relative "../../app/update_games" RSpec.describe UpdateGames do subject { described_class.new(adapter) } let(:adapter) { EpicStoreAdapter.new("https://store-site-backend-static.ak.epicgames.com") } it "inserts game data into the database" do VCR.use_cassette("free_games") do expect { subject.call }.to change { Game.count }.from(0).to(6) end end it "inserts promotion data into the database" do VCR.use_cassette("free_games") do expect { subject.call }.to change { Promotion.count }.from(0).to(5) end end end 
Enter fullscreen mode Exit fullscreen mode

Now before we run the test we have to update our testing configuration to connect to the database. First of all we need to create a different database for testing.

Update the .envrc file to add a new environment variable TEST_DATABASE_URL:

export POSTGRES_URL="postgres://postgres:password@127.0.0.1:5432/postgres" export DATABASE_URL="postgres://postgres:password@127.0.0.1:5432/free_game_development" export TEST_DATABASE_URL="postgres://postgres:password@127.0.0.1:5432/free_game_test" 
Enter fullscreen mode Exit fullscreen mode

And create a new rake tast to prepare the test environment:

 namespace :db do ... + namespace :test do + task :prepare do + require "sequel" + require "sequel/extensions/migration" + + test_database = "free_game_test" + + db = Sequel.connect(ENV["POSTGRES_URL"].to_s) + db.run("DROP DATABASE IF EXISTS #{test_database}") + db.run("CREATE DATABASE #{test_database}") + + Sequel::Migrator.run(Sequel.connect(ENV["TEST_DATABASE_URL"].to_s), "db/migrations") + end + end  end 
Enter fullscreen mode Exit fullscreen mode

Now in our spec_helper we have to connect to the new database using Sequel.

 - require "vcr" + Bundler.require(:default, :test)  + Sequel.connect(ENV["DATABASE_URL"].to_s) + Sequel::Model.plugin(:update_or_create) 
Enter fullscreen mode Exit fullscreen mode

We have removed the vcr require and updated it to use the bundler require method that will require every gem we have defined in our Gemfile, either outside any groups or in the testing one.

Is mandatory to enable the plugin update_or_create because we use in our UpdateGames class.

To allow our test to access our models we need to require them too in spec_helper:

 Bundler.require(:default, :test) Sequel.connect(ENV["DATABASE_URL"].to_s) Sequel::Model.plugin(:update_or_create) + require_relative "../app/models/game" + require_relative "../app/models/promotion"  VCR.configure do |config| config.cassette_library_dir = "spec/fixtures/vcr_cassettes" config.hook_into(:faraday) end 
Enter fullscreen mode Exit fullscreen mode

And that's it, let's run the test

 bundle exec rspec ..F Failures: 1) UpdateGames inserts promotion data into the database Failure/Error: expect { subject.call }.to change { Promotion.count }.from(0).to(5) expected `Promotion.count` to have initially been 0, but was 5 # ./spec/app/update_games_spec.rb:17:in `block (3 levels) in <top (required)>' # ./spec/app/update_games_spec.rb:16:in `block (2 levels) in <top (required)>' Finished in 0.07583 seconds (files took 0.30525 seconds to load) 3 examples, 1 failure Failed examples: rspec ./spec/app/update_games_spec.rb:15 # UpdateGames inserts promotion data into the database 
Enter fullscreen mode Exit fullscreen mode

Woops. The first one succeeded but the second one failed. It says that initially it should have had 0 promotions in the database which is correct but it found 5 already. The issue is that we already inserted promotions in the previous spec so we have to clean up the database between test cases.

For that we can add the gem database_cleaner-sequel which will truncate all data created in each test. Add it as usual with bundle add database_cleaner-sequel and configure it in our spec_helper.rb file:

 RSpec.configure do |config| + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.around(:each) do |example| + DatabaseCleaner.cleaning do + example.run + end + end  ... end 
Enter fullscreen mode Exit fullscreen mode

And if we run our tests again:

bundle exec rspec ... Finished in 0.06822 seconds (files took 0.30429 seconds to load) 3 examples, 0 failures 
Enter fullscreen mode Exit fullscreen mode

Nice!

We've reached the end of the second part. We have created the logic needed to save games and promotions into our database. The next step is to notify about new promotions to a telegram channel that users can join.

Top comments (0)