7927


Ruby on Rails 4.2.0で実装を行なっています。  
以下のようなデータ構造をもつMessageモデルがあるとします。

[code]

    # == Schema Information
    #
    # Table name: messages
    #
    #  id           :integer          not null, primary key
    #  from_user_id :integer          not null
    #  to_user_id   :integer          not null
    #  content      :text             not null
    #  created_at   :datetime         not null
    #  updated_at   :datetime         not null
    
[/code]

ここで、ユーザごとにやりとりしたメッセージの新着1件ずつを取得したいと考えています。

例えば、指定したユーザがやりとりしたメッセージの一覧は以下のような`scope`で定義できると思います。

[code]

    scope :related_to, ->(user) { where('from_user_id = :user_id OR to_user_id = :user_id', user_id: user.id) }
    
[/code]

これだとユーザごとにすべてのメッセージが取得されてしまいます。

実現したいことは、イメージとしては「`from_user_id`か`to_user_id`のどちらかについて、1ユーザあたり1件しか取得できない」という条件になると思っています。

汚い書き方をするなら、こうなると思います：

  1. `related_to(user)`の`from_user_id`と`to_user_id`をマージする（配列化）
  2. 配列から自分のIDを取り除く
  3. `uniq`で重複を取り除く（ここで最新メッセージをやりとりしたユーザIDが取得できる）
  4. 3のそれぞれ最新のメッセージを取得する

よい書き方があれば教えていただければと思います。  
よろしくお願いします。

* * *

追記です。  
ざっくりと、以下のようなコードになるかと思います。  
よい書き方があれば、ぜひ回答ください。

[code]

    module Messages
      class HeadlinesFinder
        def initialize(user)
          @user = user
          @messages = []
          @other_user_ids = []
        end
    
        def call
          messages = Message.order(created_at: :desc).related_to(@user)
    
          messages.each do |message|
            other_user_id = (message.from_user_id == @user.id) ? message.to_user_id : message.from_user_id
    
            unless other_user_id.in?(@other_user_ids)
              @messages << message
              @other_user_ids << other_user_id
            end
          end
    
          @messages
        end
      end
    end
    
[/code]

使い方：

[code]

    @headlines = Messages::HeadlinesFinder.new(current_user).call
    
[/code]


CC BY-SA 3.0
2015-03-11T08:09:13.900
7774
2015-04-14T06:54:14.220
2015-03-15T00:23:29.250
85
8786
question
ruby-on-rails rails-activerecord
指定したユーザがやりとりしたメッセージの中から、相手ユーザごとに最新の1件を取得したい


なんとなくINとOUTをこちらで予想してみました。  
たぶんこういうことですよね？

![画像の説明をここに入力](https://i.stack.imgur.com/LkIvL.png)

SQLを活用してこんな実装を考えてみました。

[code]

    class Message < ActiveRecord::Base
      scope :headlines_for, ->(user) do
        where("messages.id IN (#{HEADLINES_SQL})", user_id: user.id).order(id: :desc)
      end
    
      HEADLINES_SQL = <<-SQL
    SELECT MAX(m2.id) AS id
    FROM   (SELECT m.from_user_id AS other_user_id,
                   m.id
            FROM   messages m
            WHERE  m.to_user_id = :user_id
            UNION ALL
            SELECT m.to_user_id AS other_user_id,
                   m.id
            FROM   messages m
            WHERE  m.from_user_id = :user_id) m2
    GROUP  BY m2.other_user_id
      SQL
    end
    
[/code]

実装を簡略化するため、created_at順ではなく、id順で最新かどうかを判断するようにしています。  
SQLiteで動作確認していますが、他のRDBMSでもいけるんじゃないかなーと思います。（たぶん）

ちなみにテストはこんな感じです。

[code]

    # seeds.rb
    User.delete_all
    hiroshi = User.create!(name: 'ひろし')
    hanako = User.create!(name: 'はなこ')
    takashi = User.create!(name: 'たかし')
    
    Message.delete_all
    Message.create!(from_user: takashi, to_user: hanako, content: 'こんにちは、はなこ')
    Message.create!(from_user: hanako, to_user: takashi, content: '何のようですか？たかし')
    Message.create!(from_user: hanako, to_user: hiroshi, content: 'こんばんは、ひろし')
    Message.create!(from_user: hanako, to_user: hiroshi, content: '今ちょっといいですか？')
    Message.create!(from_user: hiroshi, to_user: takashi, content: 'たかし、来週ひま？')
    Message.create!(from_user: hiroshi, to_user: hanako, content: '何だい、はなこ？')
    
    # message_spec.rb
    RSpec.describe Message, type: :model do
      before do
        Rails.application.load_seed
      end
    
      describe '::headlines_for' do
        let(:contents) do
          user = User.find_by_name user_name
          Message.headlines_for(user).map(&:content)
        end
        context 'はなこ' do
          let(:user_name) { 'はなこ' }
          it 'returns headlines' do
            expect(contents).to eq %w(何だい、はなこ？ 何のようですか？たかし)
          end
        end
        context 'たかし' do
          let(:user_name) { 'たかし' }
          it 'returns headlines' do
            expect(contents).to eq %w(たかし、来週ひま？ 何のようですか？たかし)
          end
        end
        context 'ひろし' do
          let(:user_name) { 'ひろし' }
          it 'returns headlines' do
            expect(contents).to eq %w(何だい、はなこ？ たかし、来週ひま？)
          end
        end
      end
    end
    
[/code]

実際にサンプルアプリケーション（画面無し、ModelとSpecのみ）も作ってみたのでよかったら自分で動かしてみてください。

<https://github.com/JunichiIto/message-query-sandbox>


CC BY-SA 3.0
2015-03-14T23:34:40.467
7927
2015-03-15T05:52:11.193
2015-03-15T05:52:11.193
85
85
7774
answer