Rails Console、便利ですよね。これさえあればデータの表示や簡単なデータの修正、作成などが簡単にできてしまいます。
ところがそんな便利なRails Consoleにも1つ大きな欠点があります。Rails ConsoleはRailsでしか使えません。
当然です。ですが、不便です。RailsではないプロジェクトでもRails Consoleの便利さを享受したいですよね。
今回はそのためのツールであるarpryを作ったので紹介します。
arpryを使うと、どのようなプロジェクトでもデータベースの情報だけでRails ConsoleのようにActive Recordを使ってデータベースを扱えるようになります。1
名前は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に書くような情報をオプションで与えることもできます。mysqlやpostgresqlを使用する場合はこの形式を主に使うことになるでしょう。
$ 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
ここまででテーブルに対する基本的な操作は行えるようになりました。
あとは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
もう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
このあたりの実装はclass_factory.rb
にあるので、興味があったら読んでみてください。
https://github.com/pocke/arpry/blob/23b532aa146a3be4e24d2c84e966d56a02517f62/lib/arpry/class_factory.rb
Conclusion
arpryという、どこでも使えるActive Recordのインターフェイスについて解説をしました。
Railsではないプロジェクトで思うようにデータベースを探索できていない方は、ぜひ使っていただけると嬉しいです。