petitviolet blog

    Ruby on Rails on YugabyteDB

    2020-07-13

    RubyDB

    YugabyteDB is "The Leading High-Performance Distributed SQL Database".

    https://www.yugabyte.com/

    It has been developed as an open source and called Distributed SQL.
    YugabyteDB is a Distributed SQL product, like Google Cloud Spanner which is a well-known global distributed databased developed by Google, but it has PostgreSQL compatibile interface, not like Spanner, we can connect to YugabyteDB via PostgreSQL driver. In addition to PostgreSQL interface, YugabyteDB also provides Redis interface called Yedis. Ruby on Rails applications usually depend on Redis as its cache layer and backgound workers storage, such as Sidekiq.

    I assumed using YugabyteDB as a backend storage layer of Ruby on Rails application perhaps is a great way to manage storages into one place.
    This post describes how to get started and how to use YugabyteDB as backend of Ruby on Rails applications.
    Example source codes are available at petitviolet/yuga-rails in GitHub.

    Getting Started with YugabyteDB

    Official document describes how to launch YugabyteDB instances using docker-compose.

    https://docs.yugabyte.com/latest/deploy/docker/docker-compose/

    Copy and paste it to local docker-compose.yaml, then launch docker containers by docker-compose up -d as written in the document.

    $ docker-compose up -d
    $ docker-compose ps
        Name                   Command               State                                                                Ports
    -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    yb-master-n1    /home/yugabyte/bin/yb-mast ...   Up      10100/tcp, 11000/tcp, 12000/tcp, 5433/tcp, 6379/tcp, 0.0.0.0:7000->7000/tcp, 7100/tcp, 7200/tcp, 9000/tcp, 9042/tcp, 9100/tcp
    yb-tserver-n1   /home/yugabyte/bin/yb-tser ...   Up      10100/tcp, 11000/tcp, 12000/tcp, 0.0.0.0:5433->5433/tcp, 0.0.0.0:6380->6379/tcp, 7000/tcp, 7100/tcp, 7200/tcp,
    

    Initial Configurations

    As a preparation for the following sections, update credentials using ysqlsh in yb-tserver-n1 container.

    $ docker exec -it yb-tserver-n1 /home/yugabyte/bin/ysqlsh -h yb-tserver-n1
    ysqlsh (11.2-YB-2.0.5.7-b0)
    Type "help" for help.
    yugabyte=# alter role yugabyte with password 'password';
    ALTER ROLE
    

    Then, we can connect to the YugabyteDB instance just as connecting to PostgreSQL.

    $ psql -Uyugabyte -h127.0.0.1 -p5433 -W
    Password:
    psql (12.3, server 11.2-YB-2.0.5.7-b0)
    Type "help" for help.
    

    In order to use YugabyteDB as backend database of a Rails application, create a database for the app in advance.

    yugabyte=# create database yuga_rails_development;
    CREATE DATABASE
    
    yugabyte=# \l
                                          List of databases
              Name          |  Owner   | Encoding | Collate |    Ctype    |   Access privileges
    ------------------------+----------+----------+---------+-------------+-----------------------
     postgres               | postgres | UTF8     | C       | en_US.UTF-8 |
     system_platform        | postgres | UTF8     | C       | en_US.UTF-8 |
     template0              | postgres | UTF8     | C       | en_US.UTF-8 | =c/postgres          +
                            |          |          |         |             | postgres=CTc/postgres
     template1              | postgres | UTF8     | C       | en_US.UTF-8 | =c/postgres          +
                            |          |          |         |             | postgres=CTc/postgres
     yuga_rails_development | yugabyte | UTF8     | C       | en_US.UTF-8 |
     yugabyte               | postgres | UTF8     | C       | en_US.UTF-8 |
    (6 rows)
    
    yugabyte=# \c yuga_rails_development
    Password for user yugabyte:
    psql (12.3, server 11.2-YB-2.0.5.7-b0)
    You are now connected to database "yuga_rails_development" as user "yugabyte".
    yuga_rails_development=# \d
    Did not find any relations.
    

    Now, it's ready to develop an application depending on the YugabyteDB schema. Setting user and password to environmental value will reduce our future typing efforts :)

    $ export PGUSER=yugabyte
    $ export PGPASSWORD=password
    

    Initialize Ruby on Rails application

    Create a new Rails app by using rails new command.

    $ ruby -v
    ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]
    
    $ rails -v
    Rails 6.0.3.2
    
    $ rails new yuga-rails --database=postgresql --api
    ...
    

    After creating a new app, edit config/database.yml to configure connection settings to YugabyteDB.

    ./config/database.yml
    default: &default
        adapter: postgresql
    
    development:
        <<: *default
        database: yuga_rails_development
        username: <%= ENV.fetch("PGUSER") %>
        password: <%= ENV.fetch("PGPASSWORD") %>
        host: 127.0.0.1
        port: 5433
    

    No need to add additional database drivers, configurations, etc. to use YugabyteDB. It's ready to use YugabyteDB as a database of the Rails app!

    Manipulate YugabyteDB data through Rails app

    Defining Model using rails g command.

    $ bundle exec rails g model user email:string name:string gender:integer
    ...
    $ vim ./db/migrate/20200621050634_create_users.rb # edit
    
    ./db/migrate/20200621050634_create_users.rb
    class CreateUsers < ActiveRecord::Migration[6.0]
    def change
        create_table :users do |t|
        t.string :email, null: false, index: { unique: true }
        t.string :name, null: false
        t.integer :gender, null: false
    
        t.timestamps
        end
    end
    end
    

    Then, apply database migration.

    $ bundle exec rails db:migrate
    == 20200621050634 CreateUsers: migrating ======================================
    -- create_table(:users)
    -> 3.6016s
    == 20200621050634 CreateUsers: migrated (3.6016s) =============================
    

    Launch Rails console to play with the app.

    $ bundle exec rails c
    Running via Spring preloader in process 20732
    Loading development environment (Rails 6.0.3.2)
    irb(main):001:0> User.all
      User Load (11.4ms)  SELECT "users".* FROM "users" LIMIT $1  [["LIMIT", 11]]
    => #<ActiveRecord::Relation []>
    
    irb(main):002:0> 30.times.each { |i| User.create!(email: "user-#{i}@example.com", name: "user-#{i}", gender: i % 3) }
    ...
    => 30
    
    irb(main):003:0> User.count
       (23.2ms)  SELECT COUNT(*) FROM "users"
    => 30
    
    irb(main):004:0> User.first
      User Load (13.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
    => #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
    

    It's working expectedly, though it seems slow than expected.

    Using Yedis as a Cache of Rails

    To enable Redis API of YugabyteDB which called Yedis API, run this command that described in the document.

    $ docker exec -it yb-master-n1 /home/yugabyte/bin/yb-admin --master_addresses yb-master-n1:7100 setup_redis_table
    
    I0623 13:42:41.866884    45 mem_tracker.cc:250] MemTracker: hard memory limit is 1.651742 GB
    I0623 13:42:41.867004    45 mem_tracker.cc:252] MemTracker: soft memory limit is 1.403981 GB
    I0623 13:42:41.945003    45 table_creator.cc:261] Created table system_redis.redis of type REDIS_TABLE_TYPE
    I0623 13:42:41.945096    45 yb-admin_client.cc:465] Table 'system_redis.redis' created.
    
    $ redis-cli -p 6379 # can connect to Yedis from redis-cli
    127.0.0.1:6379> set key value
    OK
    127.0.0.1:6379> get key
    "value"
    127.0.0.1:6379> del key
    (integer) 1
    127.0.0.1:6379> get key
    (nil)
    

    Now we can use YugabyteDB as Redis. Update Rails configuration to enable to use Redis as its cache store.

    • Install necessary gems to use Redis.
      gemfile
      +gem 'redis', '~> 4.0'
      +gem 'redis-rails'
      
    • Update config file to specify :redis_store as cache_store in Rails
      ./config/environments/development.rb
      +  config.cache_store = :redis_store, 'redis://localhost:6379/0/yuga_rails_cache', { expires_in: 30.seconds }
      +  config.active_record.cache_versioning = false
      

    After these procedures, it's able to use Rails.cache like:

    $ bundle exec rails c
    Running via Spring preloader in process 68202
    Loading development environment (Rails 6.0.3.2)
    irb(main):001:0> Rails.cache.class
    => ActiveSupport::Cache::RedisStore
    irb(main):002:0> Rails.cache.fetch("User.first") { User.first }
      User Load (32.9ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
    => #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
    irb(main):003:0> Rails.cache.fetch("User.first") { User.first }
    => #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
    irb(main):004:0> Rails.cache.read("User.first")
    => #<User id: 1, email: "user-0@example.com", name: "user-0", gender: 0, created_at: "2020-06-23 13:34:12", updated_at: "2020-06-23 13:34:12">
    irb(main):005:0> sleep 30
    => 30
    irb(main):006:0> Rails.cache.read("User.first")
    => nil
    

    Yay!

    Summary

    YugabyteDB offers both PostgreSQL and Redis interfaces which can be used as backend database layers of Ruby on Rails applications. Thus, it frees us from the hell of "polyglot" databases. In addition to these, it also offers Cassandra API called Ycql so that it's easy to use key-value store if needed. However, the performance of YugabyteDB looks slower than using PostgreSQL, so it seems mandatory to evaluate its performance before using it in production environments.