RailsとTiDBの統合について

![shutterstock_1043482054.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1487691/524a55d6-154e-ea40-76de-66584c740def.png)
*この記事は [dev.to](https://dev.to/hooopo/getting-started-with-rails-tidb-4pni)で発表されたものです。*

このチュートリアルは、RailsとTiDBの統合に関する、おそらくWebで初めてのチュートリアルです。TiDBは、水平的なスケーラビリティ、高可用性、MySQLとの互換性を特徴とする、オープンソースの分散SQLデータベースです。

初心者にとって、ActiveRecordのような複雑なORMをTiDBに統合するのは難しい可能性があります。Web上にはこのトピックを扱っている記事がほとんどないことから、私はRailsユーザーによるTiDB導入を支援するために、このチュートリアルを執筆しました。

## TiDB開発環境をローカルで構築する

Railsとの統合に先立って、TiDBクラスタをローカルマシンにデプロイする必要があります。TiDBでは、TiDBエコシステム用のパッケージマネージャーである[TiUP](https://docs.pingcap.com/tidb/dev/tiup-overview?_ga=2.215423189.371835065.1634184425-1275830970.1620281897)によって、デプロイを円滑に進められるようになっています。

#### TiUPのインストール

[TiUPのインストール](https://docs.pingcap.com/tidb/dev/tiup-overview?_ga=2.21811449.371835065.1634184425-1275830970.1620281897#install-tiup)は、Darwin、Linuxのどちらのオペレーティングシステムからも簡単に行うことができます。次のコマンドを実行するだけです。

```
$ curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh
```

これによって`$HOME/.tiup/bin`が`PATH`環境変数に追加され、TiUPを直接使用できるようになります。

#### ローカルクラスタの確立

一般的にTiDBクラスタは複数のノードを必要とするため、デプロイが複雑で時間がかかる場合があります。そのためTiUPは、TiDBのテスト環境をローカルですばやく構築できる、TiUPコンポーネントの`playground`を用意しています。

このコマンドは次のようにシンプルなものです。

```
$ tiup playground
```

次のように出力されます。

```
$ tiup playground
Starting component ``playground``: /Users/hooopo/.tiup/components/playground/v1.4.1/tiup-playground
Use the latest stable version: v5.0.0
 
    Specify version manually: tiup playground <version>
    The stable version: tiup playground v4.0.0
    The nightly version: tiup playground nightly
 
Playground Bootstrapping...
Start pd instance
Start tikv instance
Start tidb instance
Waiting for tidb instances ready
127.0.0.1:4000 ... Done
Start tiflash instance
Waiting for tiflash instances ready
127.0.0.1:3930 ... Done
CLUSTER START SUCCESSFULLY, Enjoy it ^-^
To connect TiDB: mysql --host 127.0.0.1 --port 4000 -u root -p (no password)
To view the dashboard: http://127.0.0.1:2379/dashboard
To view the Prometheus: http://127.0.0.1:9090
To view the Grafana: http://127.0.0.1:3000

```

これでローカルクラスタが確立されました。

#### TiDB Dashboardへのアクセス

TiDBには、クラスタを監視するためのWeb UI、[TiDB Dashboard](https://docs.pingcap.com/tidb/dev/dashboard-intro?_ga=2.218478043.371835065.1634184425-1275830970.1620281897)が用意されています。これはすでにPDコンポーネントに組み込まれており、http://127.0.0.1:2379/dashboard から直接アクセスできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1487691/c6d7fcd4-199d-108d-f75a-0965b6de127d.png)

TiUPの詳細については、[公式ドキュメント](https://docs.pingcap.com/tidb/stable/tiup-overview?_ga=2.252058955.371835065.1634184425-1275830970.1620281897)をご覧ください。

## RailsをTiDB用に設定する

TiDBクラスタを確立したところで、次にこれをRailsと統合します。

#### Railsプロジェクトの作成

TiDBはMySQLとの互換性があるため、MySQL用に設定されたRailsアプリを作成できます。

```
$ rails new myapp --database=mysql
```

#### `database.yml`の設定

`database.yml`では次の2つの重要な設定があります。

- portを4000に設定します。ローカルのTiDBクラスタは、4000をデフォルトのポートとして使用します。
- データベース接続変数tidb_enable_noop_functionsをONに設定します。Railsは、TiDBではデフォルトで無効になっているget_lock関数を使用する必要があります。

```
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  port: 4000
  username: root
  password:
  host: 127.0.0.1
  variables:
    tidb_enable_noop_functions: ON
```

URIメソッドを使用してデータベース接続を設定する場合も、設定方法は同様です。

```
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  url: <%= ENV.fetch("DB_URL") || "mysql2://root:pass@localhost:4000/myapp" %>
  variables:
    tidb_enable_noop_functions: ON
```

#### プライマリキー、自動インクリメント、一意インデックスの設定

`users`というテーブルを作成します。

```
class CreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email
      t.string :password
      t.string :username

      t.timestamps
    end
  end
end
```

一意インデックスを追加します。

```
class AddUniqueIndexForEmail < ActiveRecord::Migration[6.1]
  def change
    add_index :users, :email, unique: true
  end
end
```

TiDBはMySQLとの互換性があるため、使用方法はスタンドアロンのMySQLデータベースとほとんど同じです。プライマリキー、自動インクリメント、一意インデックスなどの機能について互換性がない他の分散データベースと比較して、追加の処理が不要なTiDBは導入が非常に容易です。

ここまでで、次のようなデータテーブルが生成されます。

```
mysql> show create table users;
+ -------+------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------+
| users | CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,
  UNIQUE KEY `index_users_on_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=30001 |
+-------+------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------+
1 row in set (0.01 sec)
```

#### セーブポイントへのパッチの追加

TiDBとActiveRecordを結合するうえで1つだけ障害になるのが、現時点では[TiDBではセーブポイントがサポートされていない](https://github.com/pingcap/tidb/issues/6840)ことです。そこで、これを解決する簡単なパッチを作成しました。

```
# https://github.com/rails/rails/blob/6-1-stable/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L313
require 'active_record/connection_adapters/abstract/database_statements.rb'
 
module DisableSavepoint
  def transaction(requires_new: nil, isolation: nil, joinable: true)
    if requires_new
      requires_new = nil
      Rails.logger.warn "savepoint statement was used, but your db not support, ignored savepoint."
      Rails.logger.warn caller
      super(requires_new: requires_new, isolation: isolation, joinable: joinable)
    else
      super(requires_new: requires_new, isolation: isolation, joinable: joinable)
    end
  end
end
 
ActiveRecord::ConnectionAdapters::DatabaseStatements.send(:prepend, DisableSavepoint)
```

Railsでセーブポイントを使用するのは、トランザクションが`true`をパラメータ`requires_new`に渡す場合に限られます。その場合は、このパッチによって`requires_new`の値が`nil`に変更され、移行のためのログが出力されます。

私の経験では、Railsプロジェクトでセーブポイントが頻繁に使用されることはほとんどないため、移行が必要になった場合でも大きな問題はありません。マイグレーションを行う場合はセーブポイントを使用しますが、並行して実行する移行がなければ、セーブポイントを削除しても予期しない結果になることはありません。

## 参考資料
- [What's New in TiDB 5.0](https://docs.pingcap.com/tidb/stable/release-5.0.0?_ga=2.253237707.371835065.1634184425-1275830970.1620281897)
- [tidb_enable_noop_functions](https://docs.pingcap.com/tidb/stable/system-variables?_ga=2.251983051.371835065.1634184425-1275830970.1620281897#tidb_enable_noop_functions-new-in-v40)
- [TiDB's Compatibility with MySQL](https://docs.pingcap.com/tidb/stable/mysql-compatibility?_ga=2.18716667.371835065.1634184425-1275830970.1620281897)
- [Source code for rails-tidb Demo](https://github.com/hooopo/rails-tidb)