"Not equal" named scope in rails with Mongoid

Annie picture Annie · May 19, 2013 · Viewed 9.3k times · Source

I have two models Content and ContentType. In Content model I can do:

def get_all_content_except_poking_message
  Content.all.where(:name.ne => "no forking, just poking")
end

Now, I am trying to apply scope on ContentType. In Content model again:

# Associations
belongs_to :content_type

def get_all_content_except_certain_content_type(content_type)
  Content.all.where(:content_type.name.ne => content_type)
end

but the error suggests that its the wrong syntax to apply scope on association's field.

What is the right way to apply scope on association's fields in model?

Also I am using has_scope gem. Can I apply the same filter in controller too? Something like:

@contents = apply_scopes(
  if params[:type]
    @content_type = ContentType.find_by_slug(params[:type])
    @content_type.contents.all
  else
    Content.all.where (:content_type.name.ne => "blogs")
  end
)

Update

For clarification, here is the irb output:

irb(main):020:0> ContentType.all(:name=>"blogs").count 
=> 1

irb(main):023:0> Content.last.content_type.name 
=> "blogs" 

irb(main):024:0> Content.all.where(:content_type => {:name => {'$ne' => "blogs"}}).count
=> 0 

irb(main):026:0> Content.all.count
=> 4

Answer

Gary Murakami picture Gary Murakami · Jul 10, 2013

The quick answer is that a MongoDB server query operates only on a single collection. There is no join that crosses collections. You are querying on the contents collection, but specifying a field that is in the content_types collection.

You can use embedding to put the two models into one collection, and then your query can work against an embedded document (sub)field.

I can supply more detail if you like, but hopefully this will get you past the current astonishment.

Addendum as requested without embedding:

Important note: accessing data from multiple collections will require multiple queries, at least one query per collection.

The following example is based on what I could extract from your post, with modifications to work as you want for your methods. The "not equals" query makes use of knowledge of the association implementation which is just a quick answer for now, but the link structure is pretty obvious from inspection. Also note that the actual Moped queries to MongoDB show up in the appropriate Rails log.

I'm not familiar with the particulars of plataformatec/has_scope. Mongoid has its own scopes which you should investigate, I'm willing to help when you get there.

app/models/content_type.rb

class ContentType
  include Mongoid::Document
  field :name, type: String
  has_many :contents
end

app/models/content.rb

class Content
  include Mongoid::Document
  field :name, type: String
  belongs_to :content_type

  def self.get_all_content_except_poking_message
    Content.where(:name.ne => "no forking, just poking")
  end

  def self.get_all_content_except_certain_content_type(content_type_name) # 2 queries - one each for ContentType and Content
    content_type = ContentType.where(:name => content_type_name).first
    Content.where(:content_type_id.ne => content_type.id)
  end
end

test/unit/content_test.rb

require 'test_helper'

class ContentTest < ActiveSupport::TestCase
  def setup
    Content.delete_all
    ContentType.delete_all
  end

  test "not equal blogs" do
    blogs = ContentType.create(:name => "blogs")
    tweets = ContentType.create(:name => "tweets")
    blogs.contents << Content.create(:name => "no forking, just poking")
    tweets.contents << Content.create(:name => "Kilroy was here")
    assert_equal 2, ContentType.count
    assert_equal 2, Content.count
    puts "all content_types: #{ContentType.all.to_a.inspect}"
    puts "all contents: #{Content.all.to_a.inspect}"
    puts "get_all_content_except_poking_message: #{Content.get_all_content_except_poking_message.to_a.inspect}"
    puts "get_all_content_except_certain_content_type(\"blogs\"): #{Content.get_all_content_except_certain_content_type("blogs").to_a.inspect}"
  end
end

rake test

Run options: 

# Running tests:

[1/1] ContentTest#test_not_equal_blogsall content_types: [#<ContentType _id: 51ded9d47f11ba4ec1000001, name: "blogs">, #<ContentType _id: 51ded9d47f11ba4ec1000002, name: "tweets">]
all contents: [#<Content _id: 51ded9d47f11ba4ec1000003, name: "no forking, just poking", content_type_id: "51ded9d47f11ba4ec1000001">, #<Content _id: 51ded9d47f11ba4ec1000004, name: "Kilroy was here", content_type_id: "51ded9d47f11ba4ec1000002">]
get_all_content_except_poking_message: [#<Content _id: 51ded9d47f11ba4ec1000004, name: "Kilroy was here", content_type_id: "51ded9d47f11ba4ec1000002">]
get_all_content_except_certain_content_type("blogs"): [#<Content _id: 51ded9d47f11ba4ec1000004, name: "Kilroy was here", content_type_id: "51ded9d47f11ba4ec1000002">]
Finished tests in 0.046370s, 21.5657 tests/s, 43.1313 assertions/s.
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

For this simple case, you can "simplify" by departing from strict relational normalization, e.g., just add a "content_type_name" field to Content with string values like "blogs".

But to really take advantage of MongoDB, you shouldn't hesitate to embed.

Hope that this helps.