RailsでMySQLとOracleの両方のDBを使う環境を構築

訳あって2種類のDBを使う必要が出てきたので、その環境構築のメモ。
※まだ本番稼働まで進んでいないので、加筆修正の可能性ありです。

以下の環境で対応しました。

* 既存のDBはOracle
* 新規に用意するDBはMySQL
* ローカル開発環境はDocker

# 環境用意
`Rails`で組みたかったので`ruby`のDockerイメージを使いました。

`Oracle`は11系なので
https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html
から11.2.0.4の以下の3ファイルをダウンロード
※要Oracleアカウント
oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm

rpmファイルではなくzipファイルにして、Dockerイメージもalpineにしたかったのですが、
alpineでoracle_clientを使う方法が大変だったため、一旦断念。
`CentOS`のDockerイメージを使って`rbenv`で`ruby`を入れるというのも考えましたが、
コンテナイメージの作成しやすさを考慮して以下のような形にしました。

先程ダウンロードしたファイルも含めて、ファイル構成はこのような感じです。

```:ファイル構成
.
├── Dockerfile
├── docker-compose.yml
└── docker/
	 └── oralce/
		├── Dockerfile
		├── init.sql
		├── oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
		├── oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
		└── oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm
```

それぞれのファイルは以下です。

```docker:Dockerfile
FROM ruby:2.6.3-slim-buster

ENV LANG C.UTF-8

RUN mkdir -p /app

ADD . /app
WORKDIR /app

RUN apt-get -y update && apt-get -y upgrade

# MySQL操作する用
RUN apt-get -y install mariadb-server libmariadb-dev

# Debian系でrpmファイルを使えるようにするライブラリ
RUN apt-get -y install alien

# Oracle設定
RUN alien -i docker/oracle/oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm && \
    alien -i docker/oracle/oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm && \
    alien -i docker/oracle/oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm
ENV LD_LIBRARY_PATH /usr/lib/oracle/11.2/client64/lib
ENV NLS_LANG Japanese_Japan.AL32UTF8
ENV PATH $PATH:/usr/lib/oracle/11.2/client64/bin
```

`Oracle`と`MySQL`のコンテナも合わせて作成したかったのでdocker-composeを使用

```yml:docker-compose.yml
version: "2"

services:
  mysql:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "3306:3306"
    environment:
      - MYSQL_DATABASE=dbname_development
      - MYSQL_ROOT_PASSWORD=password

  oracle:
    build: docker/oracle # 作成時にDB作成など実行したかったのでoracle用のDockerfileを用意
    ports:
      - "1521:1521"
    volumes:
      - .:/tmp/data/

  app:
    build: .
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    links:
      - mysql
      - oracle
    environment: # MySQLとOracleの両方の接続先の情報を環境変数で追加
      - DATABASE_USERNAME=root
      - DATABASE_PASSWORD=password
      - DATABASE_NAME=dbname_development
      - DATABASE_HOST=mysql
      - DATABASE_ORACLE_USERNAME=oralceuser
      - DATABASE_ORACLE_PASSWORD=oraclepassword
      - DATABASE_ORACLE_NAME=oracle/xe
    tty: true
```

```docker:docker/oracle/Dockerfile
FROM wnameless/oracle-xe-11g

# docker-entrypoint-initdb.dとしてファイルを作成するとコンテナ作成時にSQLを実行してくれる
ADD init.sql /docker-entrypoint-initdb.d/
```

```sql:docker/oralce/init.sql
-- 表領域作成
CREATE TABLESPACE indx_oracledb DATAFILE '/tmp/indx_oracledb.dbf' SIZE 1G SEGMENT SPACE MANAGEMENT AUTO;

-- 作成した表領域を参照できるユーザー作成
CREATE USER oracleuser IDENTIFIED BY oraclepassword DEFAULT TABLESPACE indx_oracledb;

-- 作成したユーザーに権限を付与
GRANT connect,dba TO oracleuser;
```

# コンテナ起動

```sh
docker-compose up -d
```
でコンテナを起動して

```sh
docker-compose exec app
```
でコンテナに移動します。

とりあえず`Rails`がほしいので

```
gem install rails
bundle exec rails new . -d mysql
```
で`Rails`アプリを作成します。

# 両方のDBに接続するためのgemを追加
`Oracle`に接続する用のgemは
https://github.com/rsim/oracle-enhanced
https://github.com/kubo/ruby-oci8
を使います。

複数DB接続するために
https://github.com/eagletmt/switch_point
を使います。

```ruby:Gemfile
# 下記gemを追加
gem 'activerecord-oracle_enhanced-adapter'
gem 'ruby-oci8', require: 'oci8'
gem 'switch_point'
```
gemを読み込みます。

```sh
bundle install
```

DB情報を記述します。

```yml:config/database.yml
development:
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  database: <%= ENV['DATABASE_NAME'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development_oracle:
  adapter: oracle_enhanced
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DATABASE_ORACLE_USERNAME'] %>
  password: <%= ENV['DATABASE_ORACLE_PASSWORD'] %>
  database: <%= ENV['DATABASE_ORACLE_NAME'] %>
```

switch_pointの設定を追加します。

```ruby:config/initializers/switch_point.rb
SwitchPoint.configure do |config|
  config.define_switch_point :oracle, writable: :"#{Rails.env}_oracle"
end
```

それぞれのmodelを作成します。

```ruby:app/models/mysql_table.rb
class MysqlTable < ApplicationRecord
end
```

```ruby:app/models/oracle_table.rb
class OracleTable < ApplicationRecord
  # Oracleを参照するのでswitch_pointの設定を記述
  use_switch_point :oracle
end
```
# それぞれのテーブル作成
`MySQL`のほうは`Rails`のmigrationで作成。
※作成までのステップは割愛

`Oracle`のほうは今回既存DBなので、sqlファイルにエクスポートしたものを使用。
任意の場所にファイルを置いて、`sqlplus`でエクスポートします。

```sh
# コンソールからOracleに接続
sqlplus sys/oracle@oracle/xe as sysdba

# 接続したらエクスポートしたファイルを読み込み
SQL> @dump.sql
```

# それぞれのデータベースにRailsから接続
rails consoleで接続します。

```irb_output
irb(main):001:0> MysqlTable.first
   (11.7ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  MysqlTable Load (0.4ms)  SELECT  `mysql_tables`.* FROM `mysql_tables` ORDER BY `mysql_tables`.`id` ASC LIMIT 1
=> #<MysqlTable id: 1, name: "mysql table">

irb(main):002:0> OracleTable.first
  tables (130.2ms)  SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name) FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N'
  views (14.6ms)  SELECT LOWER(view_name) FROM all_views WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
  synonyms (1006.2ms)  SELECT synonym_name, table_owner, table_name, db_link FROM all_synonyms where owner = SYS_CONTEXT('userenv', 'session_user')
  Sequence (4.5ms)  select us.sequence_name from all_sequences us where us.sequence_owner = 'ORACLEUSER' and us.sequence_name = upper('ORACLE_TABLES_seq')
  Primary Key (200.8ms)  SELECT cc.column_name FROM all_constraints c, all_cons_columns cc WHERE c.owner = 'ORACLEUSER' AND c.table_name = 'ORACLE_TABLES' AND c.constraint_type = 'P' AND cc.owner = c.owner AND cc.constraint_name = c.constraint_name
  OracleTable Load (12.2ms)  SELECT * FROM (SELECT  "ORACLE_TABLES".* FROM "ORACLE_TABLES" ORDER BY "ORACLE_TABLES"."ID" ASC ) WHERE ROWNUM <= :a1  [["LIMIT", 1]]
  Column definitions (62.7ms)  SELECT cols.column_name AS name, cols.data_type AS sql_type, cols.data_default, cols.nullable, cols.virtual_column, cols.hidden_column, cols.data_type_owner AS sql_type_owner, DECODE(cols.data_type, 'NUMBER', data_precision, 'FLOAT', data_precision, 'VARCHAR2', DECODE(char_used, 'C', char_length, data_length), 'RAW', DECODE(char_used, 'C', char_length, data_length), 'CHAR', DECODE(char_used, 'C', char_length, data_length), NULL) AS limit, DECODE(data_type, 'NUMBER', data_scale, NULL) AS scale, comments.comments as column_comment FROM all_tab_cols cols, all_col_comments comments WHERE cols.owner = 'ORACLEUSER' AND cols.table_name = 'ORACLE_TABLES' AND cols.hidden_column = 'NO' AND cols.owner = comments.owner AND cols.table_name = comments.table_name AND cols.column_name = comments.column_name ORDER BY cols.column_id
=> #<OracleTable id: 1, name: "oracle table">
```

それぞれのデータベースに接続できました。

# 今後の課題
`Oracle`のほうは基本的に定義変更しないデータベースなのでmigrationはほとんどしない予定ですが、
必要になったときにどうするか？
`switch_point`を使っている場合はmigrationするのによく`ridgepole`の話が出てくるので、それを使うべきかなと思っています。