momota.txt

hello, hello, hello, how low?

Rails じゃなくても ActiveRecord を使う

rails には挫折したおれが、rails アプリケーション以外で ActiveRecord を使うようになった件について。

ActiveRecord は O/Rマッパーで RDB のテーブルエントリをオブジェクトとして扱えるようにするやつ。1インスタンスが、テーブルの1レコードに相当する。 Ruby on Rails 標準で、モデル層で使われる。

環境は以下。

  • Mac OSX
  • ruby 2.1.1p76
  • MySQL mysqld5.6.17

最終的なディレクトリ構成は以下のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
│   └── models
│       └── hoges.rb
├── app.rb
├── config
│   └── database.yml
├── db
│   └── migrate
│       └── 20140510_create_hoges.rb
├── log
│   ├── database.log
│   └── trace.log
└── vendor
    └── bundle
            └── ()

前準備

ruby

rbenv で ruby 2.1.1 をインストールする。

1
2
3
4
5
$ rbenv install 2.1.1
$ rbenv local 2.1.1
$ rbenv rehash
$ ruby -v
ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-darwin13.0]

gem

bundler で gem をインストールする。 まず、bundler をインストールする。

1
2
3
$ gem install bundle
$ bundle -v
Bundler version 1.6.2

以下の Gemfile をつくってbundle install --path vendor/bundle して gem をインストールする。

1
2
3
4
5
6
source "https://rubygems.org"

gem "activerecord"
gem "rake"
gem "mysql2"
gem "pry"

mysql2 はDBアダプタ。MySQLと接続するために必要。

pry はなくてもよい。irb の便利バージョン。

mysql

homebrew でインストールした。

my.cnfはよしなに。

create database とか grant でユーザやデータベースなどはあらかじめ作っておく。

create table: rake タスクを使ってテーブルを作成する

rake (makeみたいなもん) でテーブルを作成する。(マイグレーション)

マイグレーションは、SQLを使わずにデータベースのテーブルやカラムなどの構造を変更できる仕組みで、移行と解釈するとややこしい。データベース移行をしやすくする仕組み、くらいに捉えておくとよい。

マイグレーション用ファイルを作成する

db/migrate ディレクトリを作ってマイグレーション用ファイルを作る。

ここでは hoges テーブルを作ることにする。 マイグレーション用ファイルには、テーブル定義を書く。ここでは db/migrate/20140510_create_hoges.rb を作成する。

このファイル名が大事で 20140510 の部分がバージョンとして管理される。schema_migrations テーブルが自動生成され、そこで管理される。また、マイグレーション用ファイル中に class 名をCreateHoges のようにキャメルケースで定義した場合は、ファイル名は VERSION_create_hoge.rb のようにスネークケースとして命名する必要がある。ファイル名がこの命名規約に反するとマイグレーションがこける。

rails文化の「設定より規約」(CoC: Convention over Configuration)ってやつですね。

ActiveRecord::Migration を継承したクラス Createhoges を定義する。 シンボル :hoges がテーブル名。カラム定義は見ての通りだと思う。 主キーは自動的に id というカラム名で生成されるので書かない。(書くとエラーになる)

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateHoges < ActiveRecord::Migration
  def self.up
    create_table :hoges do |t|
      t.string  :name
      t.string  :url
      t.timestamps  # => これでcreated_atとupdated_atカラムが定義される
    end
  end

  def self.down
    drop_table :hoges
  end
end

書き方の詳細は、Active Record Migrations を見れば良いと思う。

データベース接続情報を yaml に書き出す

MySQL への接続情報(DBユーザ名とかパスワードとか使うDB名とか)を yaml ファイルに書き出しておく。めんどくさい、かつ、使い捨てのコードならハードコーディングしといてもOK。

ここでは、以下の内容で config/database.yml を作成した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db:
  production:
    adapter:  mysql2
    host:     localhost
    username: <%= ENV['DATABASE_USERNAME'] %>
    password: <%= ENV['DATABASE_PASSWORD'] %>
    database: <%= ENV['DATABASE_NAME']%>

  development:
    adapter:  mysql2
    host:     localhost
    username: <%= ENV['DEV_DATABASE_USERNAME'] %>
    password: <%= ENV['DEV_DATABASE_PASSWORD'] %>
    database: <%= ENV['DEV_DATABASE_NAME']%>

パスワードなどの秘匿情報は環境変数から読み込むようにする。(ここではERB形式で書いた) パスワードをべた書きしといて、間違えて github とかで公開しちゃうと大変なので。

~/.zshrc とか ~/.bashrc にあらかじめ作っておいたデータベース名とかユーザ名を以下のように足して source ~/.zshrc で読み込めばいいと思う。

1
2
3
export DEV_DATABASE_NAME="hoge_db"
export DEV_DATABASE_USERNAME="hoge_user"
export DEV_DATABASE_PASSWORD="hoge_password"

Rakefile をつくって rakeタスクを実行する

以下の内容で Rakefile を作る。 DB接続用の設定や環境指定(development/production) やバージョン指定の設定を書いてます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require "active_record"
require "yaml"
require "erb"
require "logger"


task :default => :migrate

desc "Migrate database"
task :migrate => :environment do
  ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
end

task :environment do
  db_conf = YAML.load( ERB.new( File.read("./config/database.yml") ).result )

  # `rake ENV=development`/`rake ENV=production`で切り替え可能
  ActiveRecord::Base.establish_connection( db_conf["db"][ENV["ENV"]] )
  ActiveRecord::Base.logger = Logger.new("log/database.log")
end

以下を参考。

rake タスクを実行する

まずは rake タスクの確認。

1
2
$ bundle exec rake -T
rake migrate  # Migrate database

開発環境設定で実行する。(ENV=development)

1
2
3
4
5
6
7
# debug 用に--traceオプションをつけ、標準エラーをlog/trace.txtへリダイレクト。
# bundle exec rake ENV=development でもOK
$ bundle exec rake ENV=development --trace 2> log/trace.txt
== 20140510 CreateHoges: migrating ============================================
-- create_table(:hoges)
   -> 0.1159s
== 20140510 CreateHoges: migrated (0.1160s) ===================================

問題なければテーブルが作成されているはず。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
mysql> show tables;
+--------------------------+
| Tables_in_dev_********** |
+--------------------------+
| hoges                    |
| schema_migrations        |
+--------------------------+
2 rows in set (0.00 sec)

mysql> desc schema_migrations;
+---------+--------------+------+-----+---------+-------+
| Field   | Type         | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| version | varchar(255) | NO   | PRI | NULL    |       |
+---------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

mysql> select * from schema_migrations;
+----------+
| version  |
+----------+
| 20140510 |
+----------+
1 row in set (0.00 sec)

mysql> desc hoges;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| url        | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

mysql> select * from hoges;
Empty set (0.00 sec)

CRUD 操作について

ActiveRecordを使って CRUD 操作する。(insert/select/update/delete)

ActiveRecord では、ActiveRecord::Base を継承したクラスがDBの1テーブルに対応し、そのクラスの属性がテーブルの各カラムに対応する。 このクラスのことを一般的に「モデル」と呼ぶ。 Rails では、Rails アプリを生成した段階で MVC 別にディレクトリが生成されるので、app/models 以下にこの ActiveRecord::Base 継承クラスを作る。 今回は Rails ではないのでそれに従う必要はない。が、モデルが増えた場合を考慮すると app/models 以下に整理できておいたほうがコードの可読性とかメンテナンスはしやすそうなので、app/models/hoges.rb ファイルを作ることにする。 Rails って理にかなっているんだな。

1
2
class Hoges < ActiveRecord::Base
end

この Hoges は対応するテーブル名にあわせる。これもCoC。 レコードを単数形で扱うため、テーブル名を複数形にすることが多いみたい。

Create: テーブルへ insert する

CRUD の C。

app.rb をつくる。

モデルを new して属性値をセットしてあげればOK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require "active_record"
require "yaml"
require "erb"
require "./app/models/hoges"


db_conf = YAML.load( ERB.new( File.read("./config/database.yml") ).result )

# 開発用DB接続パラメータ読み込み, 接続する
ActiveRecord::Base.establish_connection(db_conf["db"]["development"])


test_name = "momota.txt"
test_url  = "http://momota.github.io/"

hoge = Hoges.new { |h|
  h.name = test_name
  h.url  = test_url
}
p hoge
hoge.save
p hoge

なお、save しないと insert されない。

こんな感じで生成時にハッシュを渡してもOK。

1
2
hoge = Hoges.new(:name => test_name, :url => test_url)
hoge.save

それでは実行してみる。

1
2
3
$ bundle exec ruby app.rb
#<Hoges id: nil, name: "momota.txt", url: "http://momota.github.io/", created_at: nil, updated_at: nil>
#<Hoges id: 1, name: "momota.txt", url: "http://momota.github.io/", created_at: "2014-05-10 23:50:46", updated_at: "2014-05-10 23:50:46">

上記から、saveしないと idcreated_at などの値が空なので insert されていないことが分かる。

実際にテーブルの内容を見てみよう。ちゃんと insert されている。

1
2
3
4
5
6
7
mysql> select * from hoges;
+----+------------+--------------------------+---------------------+---------------------+
| id | name       | url                      | created_at          | updated_at          |
+----+------------+--------------------------+---------------------+---------------------+
|  1 | momota.txt | http://momota.github.io/ | 2014-05-10 23:50:46 | 2014-05-10 23:50:46 |
+----+------------+--------------------------+---------------------+---------------------+
1 row in set (0.00 sec)

Read: レコードを select する

CRUD の R。

主キーで select する場合は、find メソッドを使う。

1
hoges = Hoges.find( 1 )

これは以下の SQL と同じ。

1
SELECT * FROM hoges where hoges.id = 1 LIMIT 1;

主キー以外だと、find_by メソッドを使う。

該当するレコードがなければ nil が返ってくる。

1
hoges = Hoges.find_by name: "momota.txt"

これは以下のような書き方もできる。

1
hoges = Hoges.where(name: "momota.txt").take

これらは以下の SQL と同じ。

1
SELECT * FROM hoges where hoges.name = "momota.txt" LIMIT 1;

find_by メソッドを使ってレコードが存在しないときにだけ insert するように app.rb を書き変えてみよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-hoge = Hoges.new { |h|
-  h.name = test_name
-  h.url  = test_url
-}
-p hoge
-hoge.save
-p hoge
+rec = Hoges.find_by url: test_url
+unless rec
+  hoge = Hoges.new { |h|
+    h.name = test_name
+    h.url  = test_url
+  }
+  hoge.save
+  puts "すでにデータは insert 済みなのでここには入らない"
+end

+p rec

実行すると最終行の p rec が呼ばれていることが分かる。

1
2
$ bundle exec ruby app.rb
<Hoges id: 1, name: "momota.txt", url: "http://momota.github.io/", created_at: "2014-05-10 23:50:46", updated_at: "2014-05-10 23:50:46">

find_or_create_by メソッドによりさらにスマートな書き方ができる。

1
2
3
4
5
6
7
8
9
10
11
-rec = Hoges.find_by url: test_url
-unless rec
-  hoge = Hoges.new { |h|
-    h.name = test_name
-    h.url  = test_url
-  }
-  hoge.save
+rec = Hoges.find_or_create_by( url: test_url ) do |h|
+  h.name = test_name
  puts "すでにデータは insert 済みなのでここには入らない"
end

その他いろいろと以下のページが参考になる。

Update: レコードを update する

CRUD の U。

これはオブジェクトの属性を更新して save するだけ。

1
2
3
4
changed_name = "momota.log"
hoges = Hoges.find_by url: test_url
hoges.name = changed_name
hoges.save

Delete: レコードを delete する

CRUD の D。

これも簡単でモデルオブジェクトから destroy or delete メソッドを呼ぶだけ。save は不要。

delete はレコードの削除のみなので高速。destroy はレコードとオブジェクトも削除してくれるが、delete に比べて低速。

1
2
hoges = Hoges.find( 1 )
hoges.destroy

where で複数のレコードをひっかけて全部削除したい場合は、destroy_all or delete_all メソッドを呼ぶ。

1
hoges = Hoges.destroy_all(url: test_url)

find_by してからでもOK。

1
2
hoges = Hoges.find_by url: test_url
hoges.destroy_all

app.rb を書き換えてみる。

1
2
3
4
5
6
7
8
9
-rec = Hoges.find_or_create_by( url: test_url ) do |h|
+hoges = Hoges.find_or_create_by( url: test_url ) do |h|
   h.name = test_name
-  puts "すでにデータは insert 済みなのでここには入らない"
 end

-p rec
+puts "[before delete]record count: #{Hoges.count}"
+Hoges.delete_all(url: test_url)

実行すると、確かにレコードが削除されている。

1
2
3
$ bundle exec ruby app.rb
[before delete]record count: 1
[after delete] record count: 0

sqlでも確認できる。

1
2
mysql> select * from hoges;
Empty set (0.00 sec)

Comments