suin.io

Rails: sequencedでネストしたリソースのIDを連番にする

suin2016年9月3日

Yahoo知恵袋やStackoverFlowのようなQ&Aサイトを考えてみましょう。こうしたサイトでは、質問1件につき回答が複数つきます。質問に回答がぶら下がった構造とも言えます。

このような場合、URLはRailsのデフォルトでありがちなフラットなものよりも、階層化されたURLのほうがぴったりな場合があります。

フラットなURL
/questions/3 ... 質問のURL
/answers/100 ... 質問3への回答の詳細URL
/answers/123 ... 同上
/answers/145 ... 同上
階層化なURL
/questions/3 ... 質問のURL
/questions/3/answers/1 ... 質問3への回答の詳細URL
/questions/3/answers/2 ... 同上
/questions/3/answers/3 ... 同上

しかしながら、Railsで「質問ごとに回答のIDを1からふりなおす」ということは、宣言的に行うことができません。1そこで、sequencedを使い、親リソースごとに小リソースのIDを新たに1から連番で発番する方法を紹介したいと思います。なお、本稿で作成したサンプルコードはGitHubのsuin/rails-playground at suin.io/548からダウンロードできます。

sequencedとは?

sequencedはActiveRecordを拡張して、親リソースごとに子リソースの連番を、子リソースのプライマリキーとは別に発番してくれるgemです。詳細は後述しますが、使い方も簡単で、基本的にModelに1行宣言するだけです。

「プライマリキーとは別」というのが重要で、ActiveRecordがデフォルトで作るidカラムを壊さないため、既存のgemやコードと衝突しにくいメリットがあります。

sequencedのインストール

Gemfileにsequencedを追加してbundle installします。

Gemfile
gem 'sequenced'
$ bundle install

その後、Railsサーバを再起動します。

モデルを作る

今回はquestionsanswersからなるシンプルなモデルを例に、sequencedを使ってみます。まずモデルを生成します:

$ rails generate model Question
$ rails generate model Answer question:references sequential_id:integer

ここで、連番カラムはsequential_idにします。こうすることで、sequencedが認識してくれます。2

次に、生成されたanswersテーブルのマイグレーションファイルを開き、連番カラムにNOT NULLを、question_idカラムとsequential_idカラムの複合ユニーク制約を追加します。

db/migrate/20160903082623_create_answers.rb
class CreateAnswers < ActiveRecord::Migration
  def change
    create_table :answers do |t|
      # null: falseを追加
      t.references :question, index: true, foreign_key: true, null: false

      # null: falseを追加
      t.integer :sequential_id, null: false

      t.timestamps null: false
    end
    # 複合ユニーク制約
    add_index :answers, [:question_id, :sequential_id], unique: true
  end
end

DBのマイグレーションを行ないます:

$ rake db:migrate

Answerモデルにacts_as_sequencedを追加します:

app/models/answer.rb
class Answer < ActiveRecord::Base
  belongs_to :question

  # この行を追加
  acts_as_sequenced scope: :question_id
end

これで、sequencedを使った連番発行の仕組みが完成です。

レコードを操作してみる

では、ちゃんと連番が発番されるか実際にレコードを作って見てみましょう。

$ rails console
> question1 = Question.create()
> question2 = Question.create()
> Answer.create(question: question1)
> Answer.create(question: question1)
> Answer.create(question: question1)
> Answer.create(question: question2)
> Answer.create(question: question2)
> Answer.create(question: question2)

これで2つの質問に、それぞれ3つずつの回答がついた状態になっていると思います。

では、質問の回答を取り出してみます:

> Answer.where(question_id: 1)
  Answer Load (0.9ms)  SELECT `answers`.* FROM `answers` WHERE `answers`.`question_id` = 1
=> #<ActiveRecord::Relation [
    #<Answer id: 1, question_id: 1, sequential_id: 1, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">, 
    #<Answer id: 2, question_id: 1, sequential_id: 2, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">, 
    #<Answer id: 3, question_id: 1, sequential_id: 3, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">
]>

> Answer.where(question_id: 2)
  Answer Load (0.6ms)  SELECT `answers`.* FROM `answers` WHERE `answers`.`question_id` = 2
=> #<ActiveRecord::Relation [
    #<Answer id: 4, question_id: 2, sequential_id: 1, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">, 
    #<Answer id: 5, question_id: 2, sequential_id: 2, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">, 
    #<Answer id: 6, question_id: 2, sequential_id: 3, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">
]>

それぞれ、sequential_idが1から振られているのが分かります。次に、質問2の回答1を取り出してみます:

> Answer.find_by(question_id: 2, sequential_id: 1)
  Answer Load (0.5ms)  SELECT  `answers`.* FROM `answers` WHERE `answers`.`question_id` = 2 AND `answers`.`sequential_id` = 1 LIMIT 1
=> #<Answer id: 4, question_id: 2, sequential_id: 1, created_at: "2016-09-03 08:38:15", updated_at: "2016-09-03 08:38:15">

ちゃんと取り出せていますね。

まとめ

ネストしたリソースの連番発行はsequencedが宣言的に実装できて便利。


  1. before_createを実装することで対応することもできます。Ruby on RailsのネストしたリソースのIDを個別に1からauto incrementさせたい - スタック・オーバーフロー 

  2. sequential_idカラムは名前をカスタムすることができます。詳しくはsequencedのREADMEを御覧ください。 

RELATED POSTS