

すいません、タイトルをできるだけわかりやすく書こうとしたのですが意味不明になってしまいこれ以上うまく書けませんでした。

`Phrase`という自分自身を`Translation`をいう名前のクラスがあり、そのクラスが`has_many`で`Voice`というクラスを持つのですが、特定の`Phrase`の`Translation`が存在し、その`Voice`が一つもない要素を取得したいのですがクエリがかけずに躓いています。

### app/models/phrase.rb

[code]

    class Phrase < ActiveRecord::Base
      has_many :translatabilities
      has_many :translations, through: :translatabilities
      has_many :voices
    end
    
[/code]

### app/models/translatability.rb

[code]

    class Translatability < ActiveRecord::Base
      belongs_to :phrase
      belongs_to :translation, :class_name => 'Phrase'
    end
    
[/code]

以下の様なクエリを試してみたのですが、これでは`Translation`として取得したモデルの`Voice`が一つもなくても`Phrase`側の`Voice`が存在すると取得対象外になってしまいます。

[code]

    Phrase.includes(:translations).
        where.not(translatabilities: {phrase_id: nil}).
        includes(:voices).where(voices: {phrase_id: nil})
    
[/code]

どのようにすれば「`Translations`が存在し、そのどの`Translation`も`Voice`持たない」要素をとってこれるでしょうか？

Rubyの式で書くと

[code]

    phrase.translations.present? && phrase.translations.all?{|t| t.voices.empty? }
    
[/code]

が真になる要素を抜き出したいです。


CC BY-SA 3.0
2016-01-03T07:32:56.730
20558
2016-01-04T02:51:16.297
2016-01-04T01:46:45.247
3271
3271
question
ruby-on-rails
自身のクラスをhas_manyとして持つクラスの子供側の特定のhas_manyの数がゼロの要素を取得する方法


こういうケースはSQLで書いてしまうのが手っ取り早いと思います。

[code]

    class Phrase < ActiveRecord::Base
      has_many :translatabilities
      has_many :translations, through: :translatabilities, class_name: 'Phrase'
      has_many :voices
    
      # 特定のPhraseのTranslationが存在し、そのVoiceが一つもない要素を取得したい
      scope :with_translation_without_voice, -> {
        query = <<-SQL
    EXISTS (
      SELECT *
      FROM translatabilities ta
      INNER JOIN phrases t
      ON t.id = ta.translation_id
      WHERE
      ta.phrase_id = phrases.id
      AND NOT EXISTS (
        SELECT *
        FROM voices v
        WHERE
        v.phrase_id = t.id
      )
    )
        SQL
        where(query)
      }
    end
    
[/code]

以下はテストコードです。

[code]

    require 'test_helper'
    
    class PhraseTest < ActiveSupport::TestCase
      test '特定のPhraseのTranslationが存在し、そのVoiceが一つもない要素を取得したい' do
        translation_with_voice = Phrase.create! name: '翻訳、Voiceあり'
        translation_with_voice.voices.create! name: '音声１'
        translation_without_voice = Phrase.create! name: '翻訳、Voiceなし'
    
        phrase_with_translation_and_voice = Phrase.create! name: 'フレーズ、音声付きの翻訳あり'
        Translatability.create! phrase: phrase_with_translation_and_voice, translation: translation_with_voice
    
        phrase_with_translation_without_voice = Phrase.create! name: 'フレーズ、音声なしの翻訳あり'
        Translatability.create! phrase: phrase_with_translation_without_voice, translation: translation_without_voice
    
        phrase_without_translation = Phrase.create! name: 'フレーズ、翻訳なし'
    
        phrases_by_ruby = Phrase.all.select {|phrase| phrase.translations.present? && phrase.translations.all?{|t| t.voices.empty? } }
        assert_equal [phrase_with_translation_without_voice].map(&:name), phrases_by_ruby.map(&:name)
    
        phrases_by_scope = Phrase.with_translation_without_voice
        assert_equal [phrase_with_translation_without_voice].map(&:name), phrases_by_scope.map(&:name)
      end
    end
    
[/code]

schema.rbは以下のようになっています。

[code]

    ActiveRecord::Schema.define(version: 20160104001330) do
    
      create_table "phrases", force: :cascade do |t|
        t.string   "name"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false
      end
    
      create_table "translatabilities", force: :cascade do |t|
        t.integer  "phrase_id"
        t.integer  "translation_id"
        t.datetime "created_at",     null: false
        t.datetime "updated_at",     null: false
      end
    
      add_index "translatabilities", ["phrase_id"], name: "index_translatabilities_on_phrase_id"
      add_index "translatabilities", ["translation_id"], name: "index_translatabilities_on_translation_id"
    
      create_table "voices", force: :cascade do |t|
        t.string   "name"
        t.integer  "phrase_id"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false
      end
    
      add_index "voices", ["phrase_id"], name: "index_voices_on_phrase_id"
    
    end
    
[/code]

コードはGitHubにもアップしています。

<https://github.com/JunichiIto/not-exists-sandbox>

ちなみに「生SQLは絶対に書きたくない、Arelを駆使してなんとしてでも生SQLを避ける！」と考える人もときどき見かけますが、僕は「そんなに無理せずSQL書けばいいやん」と考える人です。  
Arelを駆使したコードは非常に読みづらいですし、Railsのバージョンアップで動かなくなるケース（[参考](http://qiita.com/joker1007/items/5c851526e73b3bc0273a)）も何度か見てきたので。


CC BY-SA 3.0
2016-01-03T22:33:10.390
20573
2016-01-04T02:51:16.297
2016-01-04T02:51:16.297
85
85
20558
answer