はじめに
日次バッチをつくるときに「ActiveRecordを使ってデータベースの処理スクリプト書きたいんだけど、別にRails入れるほどじゃないなー」ってのがあったので、そのときにやり方を調べたのをメモっておきます。
途中、Ruby単体でActiveRecordを使うベストプラクティス
https://qiita.com/tsugitta/items/a778f26e5d88e3cd5ab1
をかなり参考にさせてもらいました。 @tsugitta さんどうもありがとうございます!
環境つくる
壊れてもすぐにクリーンな環境に戻せるようにDockerで。
docker-composeで
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するようになると面倒になってくるので、作っておく。
# !/usr/bin/env ruby
require "bundler/setup"
require "irb"
IRB.start(__FILE__)
bundle gem したときにできるやつから使わない部分を削ぎ落としただけ。
root@5f20d34ccd2c:/usr/src/app# bin/console
irb(main):001:0>
db:create, db:migrateできるようにするぞ
Gemfileに
gem 'activerecord'
gem 'mysql2'
gem 'rake'
を追記して、 bundle install
したあと
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を使うベストプラクティスをかなり参考にさせてもらってます。ありがとうございます。
rake db:create
してみる
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
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 にしておく。
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 とかで書いて、
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 --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">
いけたいけた。
スクリプトを実行してみる
適当な集計処理を書いたスクリプトを実行してみる。
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接続とかできるようになるので、そこも時間あったら調べてみようかな。