blog.kotamiyake.me

為せば成る、為さねば成らぬ何事も

Dockerを使って好きなバージョンのMySQLで開発を進める方法を紹介したいと思います。

Dockerのインストール

DockerとDocker Composeをインストールして下さい。Macを利用している方は以下のリンクからインストールして下さい。

Docker For Mac | Docker

これでDockerとDocker Composeが利用できるようになります。

docker-compose.ymlの準備

次にdocker-compose.ymlを準備します。

version: '3'

services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
      - ./docker/mysql:/etc/mysql/conf.d
    ports:
      - "3307:3306"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: railsdevroot
      MYSQL_USER: railsdev
      MYSQL_PASSWORD: railsdev
volumes:
  db_data:

プロジェクトと紐づくようにRailsのルートフォルダにファイルを作成して下さい。

細かい設定の意味については下記のドキュメントを参考にして下さい。

Compose file version 3 reference | Docker Documentation

volumes:
  - db_data:/var/lib/mysql
  - ./docker/mysql:/etc/mysql/conf.d

MySQLは/etc/mysql/conf.dに*.cnfというファイルが存在するとそちらを設定ファイルとして読み込んでくれるので、個別に設定が必要な場合は独自にファイルを用意してvolumnsセクションでパスを設定します。

今回はDocker用の設定ファイルということで便宜上docker/mysql/というフォルダを作ってそこに設定ファイルを作り読み込むようにしています。

こちらは各プロジェクトに合わせてて適宜読み替えて下さい。

Rails側の設定

あとはRailsのdatabase.ymlで接続先を設定するだけです。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock

development:
  <<: *default
  database: sample_development
  port: 3307
  password: railsdevroot
  host: 127.0.0.1

気をつける箇所はhostを設定していないとホスト側のDBへ繋ごうとするので忘れずに指定する必要があります。

これで複数のプロジェクトで異なるバージョンのMySQLを使用していても、互いに影響することなく開発を進めることができます。

皆さんもぜひ試して下さい。

Rails 5.2.0.rc1でdevise 4.4.1を利用としたら以下のようなエラーが発生した。

$ bin/rails db:migrate
rails aborted!
Devise.secret_key was not set. Please add the following to your Devise initializer:

  config.secret_key = '998631ea6a17156d9b2c957614c720b11e0e65d5217e7126eb39cd08de9cad06c61eb526bd055e6eab6f5d433d8effefea0ec30829f5359cef8cc4a6503e9fe9'

Please ensure you restarted your application after installing Devise or setting the key.
/Users/kotamiyake/code/private/projects/xxx/config/routes.rb:2:in `block in <main>'
/Users/kotamiyake/code/private/projects/xxx/config/routes.rb:1:in `<main>'
/Users/kotamiyake/code/private/projects/xxx/config/environment.rb:5:in `<main>'
/Users/kotamiyake/code/private/projects/xxx/bin/rails:9:in `<top (required)>'
/Users/kotamiyake/code/private/projects/xxx/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate => db:load_config => environment
(See full trace by running task with --trace)

原因はRails5.2から複数の機密情報設定ファイルがconfig/credentials.yml.encにまとめられたので、それに伴ってnamespaceが代わってdeviseのキー設定がうまく行かなくなっていました。

masterでは修正されているようですが、deviseの4.4.1ではまだ反映されていないので、以下のように設定するとエラーを修正することができます。

config.secret_key = Rails.application.credentials.secret_key_base

Railsの進化が早くて新しい機能がモリモリ追加されて変更も多いので、随時キャッチアップが必要ですね。

参考

自社で開発しているRailsアプリでGROUP BYとORDER BYを組み合わせて使いたい場合があったので調べたことを共有したいと思います。

GROUP BYとORDER BYを併用する際の注意点

そもそもGROUP BYとORDER BYを普通に1つのクエリで実行すると思った結果がでません。

例えば以下のようなテーブルからauthor_idごとに一番古い記事のデータを取得するとします。

mysql> select * from entries;
+----+-------+------+-----------+---------------------+
| id | title | body | author_id | created_at          |
+----+-------+------+-----------+---------------------+
|  1 | text  | text |         1 | 2018-01-30 21:05:25 |
|  2 | text  | text |         2 | 2018-01-30 21:05:35 |
|  3 | text  | text |         2 | 2018-01-31 21:05:38 |
|  4 | text  | text |         1 | 2018-01-31 21:05:45 |
+----+-------+------+-----------+---------------------+
4 rows in set (0.00 sec)

GROUP BYとORDER BYを使ってみると思ったような結果が返ってきません。

mysql> select * from entries group by author_id order by created_at desc;
+----+-------+------+-----------+---------------------+
| id | title | body | author_id | created_at          |
+----+-------+------+-----------+---------------------+
|  2 | text  | text |         2 | 2018-01-30 21:05:35 |
|  1 | text  | text |         1 | 2018-01-30 21:05:25 |
+----+-------+------+-----------+---------------------+
2 rows in set (0.00 sec)

Googleで調べたところGROUP BYとORDER BYを併用すると、先にGROUP BYが実行されるため意図した結果とならないようです。

参考:MySQLでGROUP BYとORDER BYを同時に使用する場合に気をつけたいこと | 日記の間 | あかつきのお宿

サブクエリでORDER BYを優先実行

ではどうするかというとサブクエリを使います。

参考:[SQL] 7. サブクエリ 1 | TECHSCORE(テックスコア)

mysql> select * from (select * from entries order by created_at desc) A  group by author_id;
+----+-------+------+-----------+---------------------+
| id | title | body | author_id | created_at          |
+----+-------+------+-----------+---------------------+
|  4 | text  | text |         1 | 2018-01-31 21:05:45 |
|  3 | text  | text |         2 | 2018-01-31 21:05:38 |
+----+-------+------+-----------+---------------------+
2 rows in set (0.01 sec)

期待通りの結果になりました。

Railsでの実現方法

ではここからが本題でRailsでどうやって表現するか。

最初に思いついたのはfind_by_sql で直にSQLを書く方法です。

ただ、これだとActiveRecordの便利な機能が全く使えなくなる(includes など)のでイマイチでした。

そこで色々と調べる内にActiveRecord::QueryMethods にfrom というメソッドがあるということがわかりました。

参考:http://api.rubyonrails.org/v4.2.10/classes/ActiveRecord/QueryMethods.html#method-i-from

今回の例で言うと以下のような形になるかと思います。

require 'active_record'
require 'pp'

config = {
  adapter: 'mysql2',
  host: 'localhost',
  database: 'sample',
  port: 3306,
  username: 'root',
  password: '',
  encoding: 'utf8',
  timeout: 5000,
}

ActiveRecord::Base.establish_connection(config)

class Entry < ActiveRecord::Base
end

entries = Entry.order(created_at: :desc)
pp Entry.group(:author_id).from(entries, :entries)
=>
[#<Entry:0x00007fda0417e3d8
  id: 4,
  title: "text",
  body: "text",
  author_id: 1,
  created_at: 2018-01-31 21:05:45 UTC>,
 #<Entry:0x00007fda04175120
  id: 3,
  title: "text",
  body: "text",
  author_id: 2,
  created_at: 2018-01-31 21:05:38 UTC>]

先に、

entries = Entry.order(created_at: :desc)

の部分でORDER BYによる整列をしてから、

Entry.group(:author_id).from(entries, :entries)

でGROUP BYするという流れになります。

これで思った通りの処理ができるようになりました。

APPENDIX

速度的な所はどうなのだろうと気になって調べてみる以下のような記事を見つけました。

参考:MySQL のサブクエリって、ほんとに遅いの? | Developers.IO

どうやらMySQLのバージョンが5.6以上であれば、それなりの速度が出るようです。

今回必要になった箇所については、そこまでのデータ件数ではなかったので、とりあえずはこの方法で行く予定ですが、パフォーマンスによっては別の方法を考える必要がでてきそうです。

まとめ

Railsって本当に便利ですよね!皆さんもぜひ上手にRailsを乗りこなして下さい。

テーマ開発のためにdockerを使ってWordPressの環境をMacに構築しようと思います。

Dockerのホームページにガイドがあったので、そちらを参考にして進めていきたいと思います。

https://docs.docker.com/compose/wordpress/

下準備

まずはDocker for Macをインストールしましょう。

https://docs.docker.com/docker-for-mac/install/#download-docker-for-mac

作業用のディレクトリを作成します。

mkdir wordpress_dev
cd wordpress_dev

それからdocker-compose.ymlを用意します。

version: '3'

services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    volumes:
      - ./themes/your_theme:/var/www/html/wp-content/themes/your_theme # 環境に合わせて変更
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
volumes:
  db_data:

それからWordPressのテーマを./themes/your_theme に用意します。

ビルド

docker-compose up -d

ローカルにイメージがない場合はダウンロードしてくるので少々待ちます。

doneになったらlocalhost:8000にアクセスします。



これで環境が整いました。

コンテナとネットワークの設定を削除するには以下のコマンドを実行します。

docker-compose down

テーマを増やしたい場合には、

volumes:
  - ./themes/your_theme:/var/www/html/wp-content/themes/your_theme
  - ./themes/your_theme2:/var/www/html/wp-content/themes/your_theme2

と言った感じに増やしていけばOKです。もちろんパス指定を置き換えるだけでも良いです。

まとめ

駆け足で説明しましたが、これでMacにWordPressのテーマを開発するための環境が整いました。このブログのテーマも刷新しようかと思っているので乞うご期待!

中間テーブルに権限属性を持たせて、権限をコントロールするということがよくあります。以下のコードはシンプルな構成例です。

class User < ApplicationRecord
  has_many :memberships, dependent: :destroy
  has_many :accounts, through: :memberships
end

class Account < ApplicationRecord
  has_many :memberships, dependent: :destroy
  has_many :users, through: :memberships
end

# user_id :integer
# account_id :integer
# admin :boolean
class Membership < ApplicationRecord
  belongs_to :user
  belongs_to :account
end

そんな時、今ままでは以下のようにチェックしていました。

class User < ApplicationRecord
  def admin?(account)
    memberships.find_by(account_id: account.id).admin
  end
end

user = User.first
account = user.accounts.first
user.admin?(account)

これだとわざわざ権限をチェックするために、毎回中間テーブルを参照する必要があります。これでは権限チェックのたびにクエリが走って効率が悪いことこの上ありません。

そこで関連テーブルの取得と同時に中間テーブルの権限属性を引っ張ってくることはできないかと考えました。

考えた結果、何ということはない、単純にselect文の中に中間テーブルの属性を設定してあげれば関連先のテーブルで当該属性を取得できました。

accounts = current_user.accounts.select("accounts.*, memberships.admin AS admin")
accounts.first.admin
=> 1

注意する点は中間テーブルから引っ張ってきたbooleanの属性はtrueまたはfalseではなく、1または0となることです。単純にaccount.admin?とするだけであれば、問題なくtrue, falseの判定ができますが中身のデータを使って何かをするようなときには気をつけなくてはいけません。

 

今回はRails 5.2から導入されるActive Storageを試してみたいと思います。

個人で開発しているRails 5.1のアプリに導入してみることにしました。

開発自体もまだ始めたばかりで、5.2がbetaになったこともあり、Rails自体をアップグレードしてActive Storageを利用してみることにします。

Railsのアップグレード

ではさっそくRailsをアップグレードしてみます。

Gemfileに現在の最新バージョンである5.2.0.beta2を指定してbundle update railsします。

gem 'rails', '~> 5.2.0.beta2'

Railsアップグレードと同時にActive Storageがインストールされているのが確認できます。

# snip...

Fetching actionmailer 5.2.0.beta2 (was 5.1.4)
Installing actionmailer 5.2.0.beta2 (was 5.1.4)
Fetching activemodel 5.2.0.beta2 (was 5.1.4)
Installing activemodel 5.2.0.beta2 (was 5.1.4)
Fetching arel 9.0.0 (was 8.0.0)
Installing arel 9.0.0 (was 8.0.0)
Fetching activerecord 5.2.0.beta2 (was 5.1.4)
Installing activerecord 5.2.0.beta2 (was 5.1.4)
Fetching activestorage 5.2.0.beta2
Installing activestorage 5.2.0.beta2

# snip...

READMEに従いコマンドを実行します。

rails active_storage:install

そしてエラーが発生します…

rails aborted!
Don't know how to build task 'active_storage:install' (see --tasks)
/Users/kotamiyake/code/private/projects/xxx/bin/rails:9:in `require'
/Users/kotamiyake/code/private/projects/xxx/bin/rails:9:in `<top (required)>'
/Users/kotamiyake/code/private/projects/xxx/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
(See full trace by running task with --trace)

gemをアップグレードしたのですが、アプリ自体のアップグレードをやっていませんでした。

rails app:update

とりあえずroutes.rbと、deviseの導入で修正していたdevelopment.rb以外はアップグレード後のコードを取り込む形にしました。

しかしpumaを起動したところでさらにエラーが発生。

bin/rails s -p 3005
/Users/kotamiyake/code/private/projects/seamless/config/boot.rb:4:in `require': cannot load such file -- bootsnap/setup (LoadError)
        from /Users/kotamiyake/code/private/projects/seamless/config/boot.rb:4:in `<top (required)>'
        from bin/rails:3:in `require_relative'
        from bin/rails:3:in `<main>'

5.2ではbootsnapというRuby, Railsの高速化のためのgemがデフォルトで導入されるのですが、そちらをインストールしていませんでした。

bootsnapについては、Bootsnapについて という記事で詳しく説明されているので参考にして下さい。

gem 'bootsnap', require: false

無事pumaが起動しました。

では気を取り直して再度Active Storageを導入を開始したいと思います。

rails active_storage:install
rails aborted!
Don't know how to build task 'active_storage:install' (see --tasks)
bin/rails:4:in `<main>'
(See full trace by running task with --trace)

あれ…?どうやらconfig/application.rbでActive Storageを読み込む部分がコメントアウトされていたためRakeタスクが見つからなかったようです。

require "active_storage/engine"

これで無事インストールが完了しました。

下記のようなマイグレーションファイルが生成されました。

# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
  def change
    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true
    end

    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
    end
  end
end
rails db:migrate

Active Storageを使ってファイルをアップロードしてみる

それではさっそくファイルアップロードを試してみます。

今回は開発環境で試すのでクラウド環境へのアップロードではなくローカルへファイルを保存することにします。

Active Storage Overview — Ruby on Rails Guides を参考に進めます。

まずはActive Storageの設定ファイルであるconfig/storage.ymlを作成します。

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

また使用するストレージの種類を設定します。Local以外にもAmazon S3、Google Cloud Storage、 Microsoft Azure Storageなどが利用できるようです。

今回はlocalを設定します。

config.active_storage.service = :local

ファイルを添付するモデルに定義を追加します。

class User < ApplicationRecord
  has_one_attached :avatar
end

StrongParameterに上記の属性を追加します。deviseを利用しているので定義の内容は違いますが、やっていることは基本的に同じです。

devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar, accounts_attributes: [:name]])

最後にフォームにフィールドを追加して準備完了です。

  <div class="field">
    <%= f.label :avatar %><br />
    <%= f.file_field :avatar %>
  </div>

これでcarrierwaveやpaperclipのようにファイルアップロードが可能になりました。

アップロードした画像を表示する

今度はアップロードしたファイルを表示してみます。表示にはurl_forを使うようです。

    <% if f.object.avatar.attached? %>
      <div>
        <%= image_tag url_for(f.object.avatar) %>
      </div>
    <% end %>

サイズが大きかったりした場合はresizeすることができます。

resizeするにはmini_magickが必要なのでインストールします。

gem 'mini_magick'

これでファイルをresizeして表示できるようになりました。

<%= image_tag f.object.avatar.variant(resize: "100x100") %>

まとめ

今回は駆け足でActive Storageの使い方を紹介しました。まだまだ新しいライブラリなので色々と地雷がありそうですが、使い続ける中で発生した問題はTipsなどは随時紹介していきたいと思います。

 

技術ネタもこちらで書くことにしました!


みなさんはテストを書いていますか?今回は先日RSpecでテストを書いている時に起こったafter_commitが動かないという問題についての内容となっています。

実行環境

  • rails 4.2.10
  • rspec-rails 3.5.2

RSpecのafter_commitのテストで問題発生

先日、RSpecでafter_commitのテストしようと以下のようなコードを書いたのですが思った通りの結果になりませんでした。

class Model < ActiveRecord::Base
  after_commit :do_somthing

  private

  def do_something
    update_columns(total: 100)
  end
end
it 'do something after save' do
  model.save
  expect(model.total).to eq(100)
end

どうにも原因がわからずGoogleで調べてみると、use_transactional_fixturesがtrueとなっているとcommitが行われず、一つ一つのexampleがtransaction内で実行され、テスト後にはロールバックされるようになるということです。

ちなみにRSpecのuse_transactional_fixturesはそのままRails側の設定に渡されるようになっています。

          if ::Rails::VERSION::STRING > '5'
            self.use_transactional_tests = RSpec.configuration.use_transactional_fixtures
          else
            self.use_transactional_fixtures = RSpec.configuration.use_transactional_fixtures
          end
          self.use_instantiated_fixtures  = RSpec.configuration.use_instantiated_fixtures

https://github.com/rspec/rspec-rails/blob/v3.5.2/lib/rspec/rails/fixture_support.rb#L25-L30

## 強制的にcommitを実行して問題解決

ではどうやってcommitさせるかというと保存処理をしたあとに強制的にtransaction情報をクリアして、そのあとにrun_callbacksというメソッドを使ってcommitを実行させます。

it 'do something after save' do
  model.save
  model.send(:force_clear_transaction_record_state)
  model.run_callbacks(:commit)
  expect(model.total).to eq(100)
end

これでafter_commitが正常に動作し無事テストが通るようになりました。

https://stackoverflow.com/questions/33940268/after-commit-callback-on-update-doesnt-trigger を参考に最初はmodel.send(:clear_transaction_record_state)を試してみたのですがうまくいきませんでした。

Railsのコードを読んでみるとどうやらclear_transaction_record_stateだとtransaction情報がうまくクリアされておらず、正しくはforce_clear_transaction_record_stateを呼び出す必要がありました。

    # Clear the new record state and id of a record.
    def clear_transaction_record_state #:nodoc:
      @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
      force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
    end


    # Force to clear the transaction record state.
    def force_clear_transaction_record_state #:nodoc:
      @_start_transaction_state.clear
    end

https://github.com/rails/rails/blob/v4.2.10/activerecord/lib/active_record/transactions.rb#L379-L388

まとめ

それぞれの設定の意味を理解することはとても大事です。恥ずかしながら今回はそれが出来ていなかったために遭遇した問題でした。

みなさんもデフォルトのままで特に気にしていなかった設定を見返してみるのもいいかもしれません。

技術系のネタはPROGRAMMER.KOTAMIYAKE.MEで書くことにしました。

Railsで自作中のブログエンジンを使っています。

helperのテストをしていた時にApplicationControllerで設定しているdefault_url_optionsが動いていないという問題が発生。

確かにhelper単体ではcontrollerの動きは関知していないわけで、当然ApplicationControllerの処理は動いていないわけですね。

ということで以下のようなモックを作成して対応しました。

allow(ActionController::Base).to receive(:default_url_options).and_return(locale: I18n.locale)

プロジェクトでredisを使う機会があったのですが、macでredis-serverを起動すると、そのディレクトリにdump.rdbが生成されて困っていました。

とりあえずディレクトリ設定はどうすればいいのか調べてみると、redis.confのdirパラメータに設定するようでした。

http://stackoverflow.com/questions/11737440/location-of-redis-temp-file-for-replication

私はhomebrewでredisをインストールしたので/usr/local/etc/redis.confに設定ファイルが設置されていました。

中身を確認してみると、ちゃんと別の場所に保存するように設定されていました。

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir /usr/local/var/db/redis/

じゃあ実際の設定はどうなっているのか調べてみました。

$ redis-server
$ redis-cli
127.0.0.1:6379> config get dir
1) "dir"
2) "/Users/kotamiyake"

カレントディレクトリになっている。。。

さらに調べてみると、どうやらそもそもredis-serverの起動時に設定ファイルのパスを指定してあげる必要があったようです。

$ redis-server /usr/local/etc/redis.conf
$ ls /usr/local/var/db/redis/
dump.rdb

ちゃんと設定通りの場所にdump.rdbが生成されました。めでたしめでたし。