pockestrap

Programmer's memo

arpry: どこでもpryでActive Record

Rails Console、便利ですよね。これさえあればデータの表示や簡単なデータの修正、作成などが簡単にできてしまいます。

ところがそんな便利なRails Consoleにも1つ大きな欠点があります。Rails ConsoleはRailsでしか使えません。
当然です。ですが、不便です。RailsではないプロジェクトでもRails Consoleの便利さを享受したいですよね。

今回はそのためのツールであるarpryを作ったので紹介します。
arpryを使うと、どのようなプロジェクトでもデータベースの情報だけでRails ConsoleのようにActive Recordを使ってデータベースを扱えるようになります。1

github.com

名前はActive Record pryが由来です。2

Installation

arpry gemをインストールしてください。Ruby 2.4以上が必要です。

$ gem install arpry

また使いたいデータベースのアダプタもインストールする必要があります。

$ gem install sqlite3 # もしくは mysql2, pgなど

Usage

Start arpry command

arpryにデータベースの情報を渡すにはいくつかの方法があります。

1つは、データベースのファイルを直接指定する方法です。この方法はsqlite3の場合のみ使えます。

$ arpry /path/to/databasefile.sqlite3

またdatabase.ymlに書くような情報をオプションで与えることもできます。mysqlpostgresqlを使用する場合はこの形式を主に使うことになるでしょう。

$ arpry --adapter postgresql --host localhost --user YOUR_USER_NAME --password YOUR_PASSWORD --database YOUR_DB_NAME
# オプションの短縮形を使った場合
$ arpry -a postgresql -h localhost -u YOUR_USER_NAME -p YOUR_PASSWORD -d YOUR_DB_NAME

そしてarpryはDATABASE_URL環境変数もサポートしています。

$ DATABASE_URL="postgres://user_name:password@hostname:1234/database_name" arpry

これらの方法でarpryコマンドを実行すると、与えられたデータベースの情報を元にActive Recordのクラスを作成し、pryを立ち上げます。

Explore database with pry

pryでデータベースを扱う方法は、Rails Consoleの場合とほとんど差がありません。
たとえば次のようなテーブル定義とデータを考えてみましょう。シンプルなブログをイメージしたテーブルです。

-- test.sqlite3

-- Schema
CREATE TABLE articles ( id int primary key not null, title text not null, content text not null);
CREATE TABLE comments ( id int primary key not null, article_id int not null, content text not null);
CREATE TABLE tags ( id int primary key not null, name text not null);
CREATE TABLE article_tags ( id int primary key not null, article_id int not null, tag_id int not null);

-- Data
INSERT INTO articles(id, title, content) VALUES (1, 'Awesome Article', 'Hello, world!');
INSERT INTO comments(id, article_id, content) VALUES (1, 1, 'It is fantastic!');
INSERT INTO tags(id, name) VALUES (1, 'tech'), (2, 'hobby'), (3, 'game');
INSERT INTO article_tags(id, article_id, tag_id) VALUES (1, 1, 1), (2, 1, 2);
$ sqlite3 db < test.sqlite3

このデータベースに対してarpryコマンドを使用すると、次のようにデータベースを扱うことができます。
articlesテーブルに対してArticleクラスが定義されていて、普段Active Recordを使うのと同様にデータベースを扱えるのがわかると思います。

またarpryはbelongs_to, has_many, has_many throughを自動で定義します。
そのような1対多や多対多の関係にあるテーブルも簡単に扱うことができます。

$ arpry db
[1] pry(Arpry::Namespace)> a = Article.first
D, [2018-12-31T22:27:02.801294 #10176] DEBUG -- :   Arpry::Namespace::Article Load (0.2ms)  SELECT  "articles".* FROM "articles" ORDER BY "articles"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Arpry::Namespace::Article:0x000055d1066d6fa0 id: 1, title: "Awesome Article", content: "Hello, world!">

[2] pry(Arpry::Namespace)> a.comments
D, [2018-12-31T22:27:08.291179 #10176] DEBUG -- :   Arpry::Namespace::Comment Load (0.3ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 1]]
=> [#<Arpry::Namespace::Comment:0x000055d10684cb78 id: 1, article_id: 1, content: "It is fantastic!">]

[3] pry(Arpry::Namespace)> a.tags
D, [2018-12-31T22:27:11.505179 #10176] DEBUG -- :   Arpry::Namespace::Tag Load (0.2ms)  SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ?  [["article_id", 1]]
=> [#<Arpry::Namespace::Tag:0x000055d105f6b2c0 id: 1, name: "tech">, #<Arpry::Namespace::Tag:0x000055d105f6b018 id: 2, name: "hobby">]

How arpry generate classes

最後にarpryがどのようにActive Recordのクラスを生成しているかを簡単に解説します。

まずarpryはActiveRecord::Base.establish_connectionを呼び出して、データベースとのコネクションを確立します。
https://github.com/pocke/arpry/blob/23b532aa146a3be4e24d2c84e966d56a02517f62/lib/arpry/class_factory.rb#L30-L38

そのコネクションからテーブル名の一覧を取得し、テーブル名に対してString#classifyを呼び出してクラス名を取得し、ActiveRecord::Baseを継承したクラスを動的に生成します。

base_class.connection.tables.map do |table|
  namespace.const_set(table.classify, Class.new(base_class) do
    self.table_name = table
  end)
end

https://github.com/pocke/arpry/blob/23b532aa146a3be4e24d2c84e966d56a02517f62/lib/arpry/class_factory.rb#L41-L45

ここまででテーブルに対する基本的な操作は行えるようになりました。
あとはhas_manyなどのアソシエーションを定義するだけです。

arpryではアソシエーションを定義するために2つの方法を用いています。

1つは外部キーを推測する方法です。
arpryはuser_idもしくはuserIDといった形式のカラムがあった場合、それを外部キーとみなしてアソシエーションを定義します。
この例ではuserもしくはusersテーブルがある場合、そのテーブルを持つクラスへのbelongs_toおよびそのクラスからのhas_manyが定義されます。

classes.each.with_index do |klass, idx|
  klass.columns.each do |col|
    ref_name = col.name[/\A(.+)(_id|ID)\z/, 1]
    next unless ref_name
    ref_klass_idx = classes.find_index {|c| c.table_name == ref_name.singularize || c.table_name == ref_name.pluralize}
    next unless ref_klass_idx
                                                                                                                        
    relations[idx][ref_klass_idx] = col.name
  end
end

https://github.com/pocke/arpry/blob/23b532aa146a3be4e24d2c84e966d56a02517f62/lib/arpry/class_factory.rb#L53-L61

もう1つはデータベースから外部キー情報を取得する方法です。
前述の外部キーを推測する方法は、データベース上には外部キーが定義されていないとしても、Rails wayに近いテーブル定義をしているデータベースならばうまく動きます。
ですが、Rails wayとは遠いテーブル定義の場合にはうまく動きません。

arpryではそのようなケースを拾うため、データベースの外部キー定義を使用してアソシエーションを定義します。
これにより、Rails wayとは遠いテーブル定義でもアソシエーションを定義することができます。
具体的には、ActiveRecord::ConnectionAdapter#foreign_keysメソッドから外部キーの一覧を取得します。

classes.each.with_index do |klass, idx|
  klass.connection.foreign_keys(klass.table_name).each do |fk|
    ref_klass_idx = classes.find_index {|c| c.table_name == fk.to_table }
    next unless ref_klass_idx
    relations[idx][ref_klass_idx] = fk.options[:column]
  end
end

https://github.com/pocke/arpry/blob/23b532aa146a3be4e24d2c84e966d56a02517f62/lib/arpry/class_factory.rb#L63-L67

このあたりの実装はclass_factory.rbにあるので、興味があったら読んでみてください。
https://github.com/pocke/arpry/blob/23b532aa146a3be4e24d2c84e966d56a02517f62/lib/arpry/class_factory.rb

Conclusion

arpryという、どこでも使えるActive Recordのインターフェイスについて解説をしました。
Railsではないプロジェクトで思うようにデータベースを探索できていない方は、ぜひ使っていただけると嬉しいです。


  1. Active Record以外の機能は使えません。

  2. こういうの書いておかないと忘れちゃうからね