blog.kotamiyake.me

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

自社で開発している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などは随時紹介していきたいと思います。