Finding all documents in a collection with Mongoid - ruby

I have been fiddling with Mongo, but can't get this simple example to work. I'm simply trying to retrieve all documents in a collection:
require 'mongoid'
# configuration ...
class Category
include Mongoid::Document
field :name, type: String
end
Category.each do |test|
puts test.inspect
end
I get the error: undefined method 'each' for Category:Class (NoMethodError).
Connection to the database is well established, and a collection named categories contains a few documents.

Category indeed doesn't have a method each because it's a model class, not a collection. It has, however, several methods that do return collection-like objects. One of them is all. So the code should look like this:
Category.all.each do |test|
puts test.inspect
end

Related

Getting Sequel associations through Sinatra

I'm trying to return json-formatted data from my Sinatra REST API. I currently have a bunch of associations set up, but I'm having trouble getting the views I want from my API despite getting them easily in Ruby.
For example, from my tables:
DB.create_table?(:calendars) do
primary_key :id
end
DB.create_table?(:schedules) do
primary_key :id
foreign_key :resource_id, :resources
foreign_key :task_id, :tasks
foreign_key :calendar_id, :calendars
end
In Ruby, I'm able to run a block like this and display all the info I need through my associations:
Calendar.each do |c|
c.schedules.each do |s|
puts "RESOURCE ##{s.resource_id}"
s.tasks.each do |t|
p t
end
puts
end
end
the c.schedules call works because my calendar model contains a one_to_many :schedules association.
Now, I'm wondering how this translates to my Sinatra API. In my simple GET route, I've tried many variations trying to get the schedules associated with a calendar, and convert it to JSON:
get '/calendars' do
c = DB[:calendar].first
c.schedules.to_json
content_type :json
end
... but I'll end up with an error like undefined method 'schedules' for {:id=>1}:Hash
So it looks like it's returning a hash here, but I've tried a bunch of stuff and haven't figured out how I'm supposed to work with my associations in Sinatra. How can I do this?
Thanks!
The reason your first block works but the second doesn't is because in the first case, you're using a Sequel model instance of class Calendar, whereas in the second case you're using a Sequel dataset.
When you iterate over Calendar.each do |c|, the c variable gets populated with an instance of a Calendar class Sequel model object. This object has relationship methods defined (one_to_many) and you're able to query schedules and run other model methods on it.
However, c = DB[:calendar].first gets you a Sequel dataset. This object is different than a model instance, it returns a standard Ruby hash (or an array of hashes).
You can change your 2nd block to use a model instead and it will get the result you want:
get '/calendars' do
c = Calendar.first # <=== CHANGE FROM DATASET TO MODEL
c.schedules.to_json
content_type :json
end

Missing ActiveRecord methods (find_by) for object

I am trying to do an assignment which requires me to create and save an ActiveRecord within my Model class, and then return it. The rspec is expecting to use the find_by method to verify this. Here's my Model:
-----------------------
class User < ActiveRecord::Base
attr_accessor :id, :username, :password_digest, :created_at, :updated_at
after_initialize :add_user
def initialize(attributes={})
#username = attributes[:username]
#password_digest = attributes[:password_digest]
super
end
def add_user
self[:username] = #username
self[:password_digest] = #password_digest
self.save
self[:id] = self.id
end
end
----------------
If I do User.new(params), the record is in fact stored properly to the DB. But, the find_by method is missing for the returned object. So, rspec fails. I have looked everywhere but can't seem to find the solution. I am a noob, so sorry if the answer is obvious and I can't see it.
You say
If I do User.new(params), the record is in fact stored properly to the DB. But, the find_by method is missing for the returned object
This is expected behavior. Hopefully you understand by now the difference between class and instance methods. The main important point is that query methods such as find_by are not made available to model instances. If you do something like user = User.find_by(id: params[:id]), you're calling the find_by class method on the User model.
There are a number of methods like where, order, limit, etc. that are defined in ActiveRecord::QueryMethods - these are made available to ActiveRecord::Relation object and your model class. Most of these methods will return ActiveRecord::Relation objects, which is why they're chainable, e.g.
User.where(params).order(created_at: :desc).limit(5)
However find_by is an exception - it returns a model instance so you can't continue to query on the results. In summary User.new(params) returns an instance of the model which doesn't have find_by available

Querying mongoid for value in attribute array

I need to search within Mongoid objects that have array attributes. Here are the relevant objects:
class Author
include Mongoid::Document
field :name, type: String
class Book
include Mongoid::Document
field :name, type: String
field :authors, type: Array
I can see that at least one book has a given author:
Book.all.sample.authors
=> [BSON::ObjectId('5363c73a4d61635257805e00'),
BSON::ObjectId('5363c73a4d61635257835e00'),
BSON::ObjectId('5363c73a4d61635257c75e00'),
BSON::ObjectId('5363c73b4d616352574a5f00')]
But I'm unable to find books that have that author.
Book.where(authors: '5363c73a4d61635257805e00').first
=> nil
I've tried the solution listed here: https://groups.google.com/forum/#!topic/mongoid/csNOcugYH0U but it didn't work for me:
Book.any_in(:author => ["5363c73b4d616352574a5f00"]).first
=> nil
I'm not sure what I'm doing wrong. Any ideas? I'd prefer to use Mongoid Origin commands.
This output:
Book.all.sample.authors
=> [BSON::ObjectId('5363c73a4d61635257805e00'),
BSON::ObjectId('5363c73a4d61635257835e00'),
BSON::ObjectId('5363c73a4d61635257c75e00'),
BSON::ObjectId('5363c73b4d616352574a5f00')]
tells us that authors contains BSON::ObjectIds. ObjectIds are often presented as Strings and sometimes you can use a String instead of a full blown ObjectId (such as with Model.find) but they're still not Strings. You are searching the array for a String:
Book.where(authors: '5363c73a4d61635257805e00')
but '5363c73a4d61635257805e00' and ObjectId('5363c73a4d61635257805e00') are not the same thing inside MongoDB. You need to search for the right thing:
Book.where(authors: BSON::ObjectId('5363c73a4d61635257805e00'))
You might want to monkey patch a to_bson_id method into various places. Something like this:
class String
def to_bson_id
BSON::ObjectId.from_string(self)
end
end
module Mongoid
module Document
def to_bson_id
id
end
end
end
module BSON
class ObjectId
def to_bson_id
self
end
end
end
class NilClass
def to_bson_id
self
end
end
Should do the trick. Then you can say things like:
Book.where(authors: '5363c73a4d61635257805e00'.to_bson_id)
Book.where(authors: some_string_or_object_id.to_bson_id)
and The Right Thing happens.
You might want to rename authors to author_ids to make its nature a little clearer.

Confusion about ways to use JSON in ruby sinatra application

I'm making a Ruby Sinatra application that uses mongomapper and most of my responses will be in the JSON form.
Confusion
Now I've come across a number of different things that have to do with JSON.
The Std-lib 1.9.3 JSON class: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/json/rdoc/JSON.html
The JSON Gem: http://flori.github.io/json/
ActiveSupport JSON http://api.rubyonrails.org/classes/ActiveSupport/JSON.html because I'm using MongoMapper which uses ActiveSupport.
What works
I'm using a single method to handle responses:
def handleResponse(data, haml_path, haml_locals)
case true
when request.accept.include?("application/json") #JSON requested
return data.to_json
when request.accept.include?("text/html") #HTML requested
return haml(haml_path.to_sym, :locals => haml_locals, :layout => !request.xhr?)
else # Unknown/unsupported type requested
return 406 # Not acceptable
end
end
the line:
return data.to_json
works when data is an instance of one of my MongoMapper model classes:
class DeviceType
include MongoMapper::Document
plugin MongoMapper::Plugins::IdentityMap
connection Mongo::Connection.new($_DB_SERVER_CNC)
set_database_name $_DB_NAME
key :name, String, :required => true, :unique => true
timestamps!
end
I suspect in this case the to_json method comes somewhere from ActiveSupport and is further implemented in the mongomapper framework.
What doesn't work
I'm using the same method to handle errors too. The error class I'm using is one of my own:
# Superclass for all CRUD errors on a specific entity.
class EntityCrudError < StandardError
attr_reader :action # :create, :update, :read or :delete
attr_reader :model # Model class
attr_reader :entity # Entity on which the error occured, or an ID for which no entity was found.
def initialize(action, model, entity = nil)
#action = action
#model = model
#entity = entity
end
end
Of course, when calling to_json on an instance of this class, it doesn't work. Not in a way that makes perfect sense: apparantly this method is actually defined. I've no clue where it would come from. From the stack trace, apparently it is activesupport:
Unexpected error while processing request: object references itself
object references itself
/home/id833541/.rvm/gems/ruby-1.9.3-p392/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:75:in `check_for_circular_references'
/home/id833541/.rvm/gems/ruby-1.9.3-p392/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:46:in `encode'
/home/id833541/.rvm/gems/ruby-1.9.3-p392/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `block in encode_json'
/home/id833541/.rvm/gems/ruby-1.9.3-p392/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `each'
/home/id833541/.rvm/gems/ruby-1.9.3-p392/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `map'
/home/id833541/.rvm/gems/ruby-1.9.3-p392/gems/activesupport-3.2.13/lib/active_support/json/encoding.rb:246:in `encode_json'
But where is this method actually defined in my class?
The question
I will need to override the method in my class like this:
# Superclass for all CRUD errors on a specific entity.
class EntityCrudError < StandardError
def to_json
#fields to json
end
end
But I don't know how to proceed. Given the 3 ways mentioned at the top, what's the best option for me?
As it turned out, I didn't need to do anything special.
I had not suspected this soon enough, but the problem is this:
class EntityCrudError < StandardError
..
attr_reader :model # Model class
..
end
This field contains the effective model class:
class DeviceType
..
..
end
And this let to circular references. I now replaced this with just the class name, which will do for my purposes. Now to_json doesn't complain anymore and I'm happy too :)
I'm still wondering what's the difference between all these JSON implementations though.

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

Resources