ActiveRecordをRailsじゃない環境で使う（2019年版）

## はじめに

日次バッチをつくるときに「ActiveRecordを使ってデータベースの処理スクリプト書きたいんだけど、別にRails入れるほどじゃないなー」ってのがあったので、そのときにやり方を調べたのをメモっておきます。

途中、Ruby単体でActiveRecordを使うベストプラクティス
https://qiita.com/tsugitta/items/a778f26e5d88e3cd5ab1
をかなり参考にさせてもらいました。 @tsugitta さんどうもありがとうございます！

## 環境つくる

壊れてもすぐにクリーンな環境に戻せるようにDockerで。

docker-composeで

```docker-compose.yml
version: '3'
services:
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: hogehoge
    volumes:
      - mysql-data:/var/lib/mysql

  workspace:
    image: ruby:2.6
    stdin_open: true
    tty: true
    environment:
      MYSQL_HOST: mysql
      MYSQL_PASSWORD: hogehoge
    depends_on:
      - mysql
    links:
      - mysql
    volumes:
      - .:/usr/src/app
    working_dir: /usr/src/app

volumes:
  mysql-data:
    driver: local
```

```
$ docker-compose pull
$ docker-compose run --rm workspace bash
Starting ar_without_rails_mysql_1 ... done
root@30179237c853:/usr/src/app# irb
irb(main):001:0> 
```

`—rm` オプションを付けているので、bashを抜けるとコンテナは破棄される。常にクリーンな環境でRubyを実行できるぞ。

## `bin/console` を用意しておく

最初は `irb` コマンドでいいんだけど、後々いろいろrequireするようになると面倒になってくるので、作っておく。

```ruby:bin/console
#!/usr/bin/env ruby

require "bundler/setup"

require "irb"
IRB.start(__FILE__)
```

[bundle gem したときにできるやつ](https://github.com/YusukeIwaki/kt_data_class/blob/master/bin/console)から使わない部分を削ぎ落としただけ。

```
root@5f20d34ccd2c:/usr/src/app# bin/console
irb(main):001:0> 
```

## db:create, db:migrateできるようにするぞ

Gemfileに

```
gem 'activerecord'
gem 'mysql2'
gem 'rake'
```

を追記して、 `bundle install` したあと

```ruby:Rakefile
require 'bundler/setup'
require 'active_record'
require 'erb'
 
include ActiveRecord::Tasks
 
root_dir = File.dirname(__FILE__)

config_database_yml_file = File.join(root_dir, 'config', 'database.yml')
config_database_yml = YAML::load(ERB.new(File.read(config_database_yml_file)).result)

DatabaseTasks.env = 'development'
DatabaseTasks.db_dir = File.join(root_dir, 'db')
DatabaseTasks.database_configuration = config_database_yml
DatabaseTasks.migrations_paths = File.join(root_dir, 'db', 'migrate')
 
task :environment do
  ActiveRecord::Base.configurations = config_database_yml
  ActiveRecord::Base.establish_connection :development
end
 
load 'active_record/railties/databases.rake'
```

こんなかんじのRakefileを用意すればよい。

降って湧いてきたようなこのRakefileは[Ruby単体でActiveRecordを使うベストプラクティス](https://qiita.com/tsugitta/items/a778f26e5d88e3cd5ab1)をかなり参考にさせてもらってます。ありがとうございます。

### `rake db:create` してみる

```config/database.yml
development:
  adapter: mysql2
  encoding: utf8mb4
  database: ar_without_rails_playground
  pool: 5
  username: root
  password: <%= ENV['MYSQL_PASSWORD'] %>
  host: <%= ENV['MYSQL_HOST'] %>
```

を用意して、

```
root@5f20d34ccd2c:/usr/src/app# bundle exec rake db:create
Created database 'ar_without_rails_playground'
```

いけたかも。

### `db:migrate` してみる

`rails g migration` はできないので、マイグレーションファイルは気合で作る。

```
root@5f20d34ccd2c:/usr/src/app# mkdir -p db/migrate
root@5f20d34ccd2c:/usr/src/app# touch db/migrate/`date '+%Y%m%d%H%M%S'`_create_todo_item.rb  
root@5f20d34ccd2c:/usr/src/app# ls db/migrate/
20190712123300_create_todo_item.rb
```

```db/migrate/20190712123300_create_todo_item.rb
class CreateTodoItem < ActiveRecord::Migration[5.2]
  def up
    create_table :todo_items do |t|
      t.string :title, null: false
      t.text :description, null: true
      t.datetime :created_at, null: false, index: true
    end
  end

  def down
    drop_table :todo_items
  end
end
```

```
root@5f20d34ccd2c:/usr/src/app# bundle exec rake db:migrate
== 20190712123300 CreateTodoItem: migrating ===================================
-- create_table(:todo_items)
   -> 0.0064s
== 20190712123300 CreateTodoItem: migrated (0.0065s) ==========================

```

よしできた。

ちなみに完全に蛇足だが、 `db:rollback` も無事にできるようになっている。

```
root@5f20d34ccd2c:/usr/src/app# bundle exec rake db:rollback
== 20190712123300 CreateTodoItem: reverting ===================================
-- drop_table(:todo_items)
   -> 0.0042s
== 20190712123300 CreateTodoItem: reverted (0.0044s) ==========================
```

## モデルを作る

フォルダ構成はRailsおなじみの app/models にしておく。

```app/models/todo_item.rb
class TodoItem < ActiveRecord::Base
end
```

ただ、Railsじゃないので、モデルのオートロードがされない。

```
root@5f20d34ccd2c:/usr/src/app# bin/console
irb(main):001:0> TodoItem.count
Traceback (most recent call last):
        2: from bin/console:6:in `<main>'
        1: from (irb):1
NameError (uninitialized constant TodoItem)
```

なので、適当にモデルクラスの初期化＆ロード処理を config/init.rb とかで書いて、

```config/init.rb
require 'active_record'
require 'active_support/time'
require 'erb'

config_dir = File.dirname(__FILE__)
config_database_yml_file = File.join(config_dir, 'database.yml')
config_database_yml = YAML::load(ERB.new(File.read(config_database_yml_file)).result)


# データベース接続の定義. created_at などで ActiveSupport::TimeWithZone を返してもらうための設定。
Time.zone = 'Tokyo'
ActiveRecord::Base.establish_connection(config_database_yml['development'])
ActiveRecord::Base.time_zone_aware_attributes = true

# モデルのクラスをrequireしておく
project_root_dir = File.expand_path("..", config_dir)
model_files = File.join(project_root_dir, 'app', 'models', '**', '*.rb')
Dir.glob(model_files).map do |model_file|
  require model_file
end
```

bin/consoleでrequireする。

```diff:bin/consoleに追記
diff --git a/bin/console b/bin/console
index 320047a..7ca01de 100755
--- a/bin/console
+++ b/bin/console
@@ -1,6 +1,7 @@
 #!/usr/bin/env ruby
 
 require "bundler/setup"
+require "./config/init.rb"
 
 require "irb"
 IRB.start(__FILE__)
```

そうすれば

```
root@5f20d34ccd2c:/usr/src/app# bin/console
irb(main):001:0> TodoItem
=> TodoItem (call 'TodoItem.connection' to establish a connection)
irb(main):002:0> TodoItem.count
=> 0
irb(main):003:0> TodoItem.create!(title: 'ActiveRecord6')
=> #<TodoItem id: 1, title: "ActiveRecord6", description: "", created_at: "2019-07-12 12:55:16">
irb(main):004:0> TodoItem.count
=> 1
irb(main):005:0> TodoItem.last.update!(description: 'bump activerecord from 5 to 6')
=> true
irb(main):006:0> TodoItem.last
=> #<TodoItem id: 1, title: "ActiveRecord6", description: "bump activerecord from 5 to 6", created_at: "2019-07-12 12:55:16">
```

いけたいけた。

## スクリプトを実行してみる

適当な集計処理を書いたスクリプトを実行してみる。

```scripts/aggregate_todo_count_by_date.rb
require './config/init.rb'

TodoItem.group("DATE_FORMAT(created_at, '%Y-%m-%d')").count.each do |created_at, count|
  puts " #{created_at}: #{count}"
end

puts '-' * 30

puts "      Total: #{TodoItem.count}"
```

```
root@5f20d34ccd2c:/usr/src/app# ruby scripts/aggregate_todo_count_by_date.rb 
 2019-07-09: 12
 2019-07-10: 11
 2019-07-11: 5
 2019-07-12: 79
------------------------------
      Total: 107
```

いけたいけた。

## まとめ

作るべきファイルはたくさんあったけど、だいたい固定の内容なので、実は簡単かも？

ActiveRecord 6で複数DB接続とかできるようになるので、そこも時間あったら調べてみようかな。