Cannot add new value to MongoDB's BSON field using Ruby - ruby

I have a document with the field admins and am looking to add new users into this field. The value for these new users is a simple number string.
def modify_admin(identity, doc)
ip_addr = "127.0.0.1:27017"
client = Mongo::Client.new([ip_addr], :database => "camp")
if doc[0] == 'r'
doc = doc[2..-1]
client[:inventory].update_one({"name": doc}, {$push => {"admins" => identity}})
client.close
end
The collection I'm trying to add is in this line: client[:inventory].update_one({"name": doc}, {$push => {"admins" => identity}}),
However I am running into the error NilClass instances are not allowed as keys in a BSON document. (BSON::InvalidKey).
I have tried different syntax for the $push method but nothing seems to work.
My document structure is as follows, I'm using symbols as the field value.
document = {:name => build_array[1], :owner => identity, :admins => identity}
How can I add new values to the :owner field using Ruby?

$push in ruby usually means global variable. So, all you need is to wrap $push operation into parentheses:
- client[:inventory].update_one({"name": doc}, {$push => {"admins" => identity}})
+ client[:inventory].update_one({"name": doc}, {"$push" => {"admins" => identity}})
And you should be fine

Related

How to use .tap method to build associated record?

I have an omniauth authentication model I'm building that's associated to a user.
aka user has many authentications.
I wish to build up key-value pairs of this authentication models using tap because twitter provides a secret key while facebook does not.
So if I have this, I want to accomplish the following conditional statement using the .tap method instead.
class User < ActiveRecord::Base
def apply_omniauth(omni)
if omni['credentials']['secret']
self.authentications.build(:provider => omni['provider'],
:uid => omni['uid'],
:token => omni['credentials']['token'],
:token_secret => omni['credentials']['secret']
else
self.authentications.build(:provider => omni['provider'],
:uid => omni['uid'],
:token => omni['credentials']['token']
end
end
end
UPDATE:
I'm trying it this way. Does this accomplish the same as the above?
self.authentications.build.tap do |auth|
auth[:provider] = omni['provider'] if omni['provider']
auth[:uid] = omni['uid'] if omni['uid']
auth[:token] = omni['credentials']['token'] if omni['credentials']['token']
auth[:token_secret] = omni['credentials']['secret'] if omni['credentials']['secret']
end
I think you could simply do (self is obsolete here):
authentications.build(:provider => omni['provider'],
:uid => omni['uid'],
:token => omni['credentials']['token'],
:token_secret => omni['credentials']['secret'])
If any key is missing, this will simply assign nil value. Unless you have some custom logic for setting those attributes, there is no difference between assigning nil or not assigning anything for a new records.

Ruby find key by name inside converted JSON array of hashes

I have a Ruby hash converted from JSON data, it looks like this:
{ :query => {
:pages => {
:"743958" => {
:pageid => 743958,
:ns => 0,
:title => "Asterix the Gaul",
:revisions => [ {
:contentformat => "text/x-wiki",
:contentmodel => "wikitext",
:* => "{{Cleanup|date=April 2010}}\n{{Infobox graphic novel\n<!--Wikipedia:WikiProject Comics-->...
All the good stuff is inside the revisions array and then the Infobox hash.
The problem I have is getting to the Infobox hash. I can't seem to get to it. The pages and pageid hashes might not exist for other entries and of course the ID would be different.
I've tried all sorts of methods I could think of like .map, .select, .find, .include?, etc to no avail because they are not recursive and will not go into each key and array.
And all the answers I've seen in StackOverflow are to get the value by name inside a one-dimensional array which doesn't help.
How can I get the Infobox data from this?
Is this what you're looking for?
pp data
=> {:query=> {:pages=>
{:"743958"=>
{:pageid=>743958,
:ns=>0,
:title=>"Asterix the Gaul",
:revisions=>
[{:contentformat=>"text/x-wiki",
:contentmodel=>"wikitext",
:*=>"{{Cleanup..."}]}}}}
# just return data from the first revisionb
data[:query][:pages].map{|page_id,page_hash| page_hash[:revisions].first[:"*"]}
=> ["{{Cleanup..."]
# get data from all revisions
data[:query][:pages].map{|page_id,page_hash| page_hash[:revisions].map{|revision| revision[:"*"] }}.flatten
=> ["{{Cleanup..."]

How to get raw Mongo data results from a MongoMapper Plucky::Query object?

Let's say we have a MongoDB collection called "images", and a MongoMapper-powered application with a corresponding "Image" model. If we set up a MongoMapper query using this model, we see that it is of type Plucky::Query and returns results of type Image:
>> Image.where(:file_type => 'image/jpeg').class
=> Plucky::Query
>> Image.where(:file_type => 'image/jpeg').first.class
=> Image
We can run the corresponding query directly on the Mongo adapter, mostly bypassing MongoMapper, by accessing the MongoMapper.connection. If we do it this way, the query is of type Mongo::Cursor and returns raw data results of type BSON::OrderedHash:
>> MongoMapper.connection.db(dbname).collection('images').find({ :file_type => 'image/jpeg' }).class
=> Mongo::Cursor
>> MongoMapper.connection.db(dbname).collection('images').find({ :file_type => 'image/jpeg' }).first.class
=> BSON::OrderedHash
The question is, is there a way to take a Plucky::Query like above and convert it to (or retrieve from it) a basic, non-extended Mongo::Cursor object?
At first I thought I found a solution with find_each, which does actually take a Plucky::Query and return a Mongo::Cursor:
>> Image.where(:file_type => 'image/jpeg').find_each.class
=> Mongo::Cursor
But it turns out this Mongo::Cursor is somehow extended or otherwise different from the above one because it still returns Image objects instead of BSON::OrderHash objects:
>> Image.where(:file_type => 'image/jpeg').find_each.first.class
=> Image
Update: I can't simply bypass MongoMapper query magic altogether like I did in the second case because I need to access features of MongoMapper (specifically named scopes) to build up the query, so what I end up with is a Plucky::Query. But then I want the results to be plain data objects, not models, because all I need is data and I don't want the overhead of model instantiation.
If you drop to the driver, the transformer is nil by default:
1.9.3p194 :003 > Image.collection.find({ :file_type => 'image/jpeg' }, { :limit => 1 }).first.class
=> BSON::OrderedHash
MongoMapper achieves the conversion by setting a "transformer" lambda on the plucky query. You can see this in the MongoMapper source code:
def query(options={})
query = Plucky::Query.new(collection, :transformer => transformer)
...
end
...
def transformer
#transformer ||= lambda { |doc| load(doc) }
end
So after each mongo document retrieval, this Plucky::Query runs the transformation that loads the model. Looking at the Plucky source code we see that there is a simple setter method [] we can use to disable this. So this is the solution:
plucky_query = Image.where(:file_type => 'image/jpeg')
plucky_query.first.class
# => Image
plucky_query[:transformer] = nil
plucky_query.first.class
# => BSON::OrderedHash
If you don't mind monkey-patching you can encapsulate like so:
module Plucky
class Query
def raw_data
self[:transformer] = nil
self
end
end
end
Then you could simply write:
Image.where(:file_type => 'image/jpeg').raw_data.first.class
# => BSON::OrderedHash

With Mongoid, can I "update_all" to push a value onto an array field for multiple entries at once?

Using Mongoid, is it possible to use "update_all" to push a value onto an array field for all entries matching a certain criteria?
Example:
class Foo
field :username
field :bar, :type => Array
def update_all_bars
array_of_names = ['foo','bar','baz']
Foo.any_in(username: foo).each do |f|
f.push(:bar,'my_new_val')
end
end
end
I'm wondering if there's a way to update all the users at once (to push the value 'my_new_val' onto the "foo" field for each matching entry) using "update_all" (or something similar) instead of looping through them to update them one at a time. I've tried everything I can think of and so far no luck.
Thanks
You need call that from the Mongo DB Driver. You can do :
Foo.collection.update(
Foo.any_in(username:foo).selector,
{'$push' => {bar: 'my_new_val'}},
{:multi => true}
)
Or
Foo.collection.update(
{'$in' => {username: foo}},
{'$push' => {bar: 'my_new_val'}},
{:multi => true}
)
You can do a pull_request or a feature request if you want that in Mongoid builtin.

Is it possible to specify what index a query should use in Mongoid?

MongoDB seems like it is using an inefficient query pattern when one index is a subset of another index.
class Model
field :status, :type => Integer
field :title, :type => String
field :subtitle, :type => String
field :rating, :type => Float
index([
[:status, Mongo::ASCENDING],
[:title, Mongo::ASCENDING],
[:subtitle, Mongo::ASCENDING],
[:rating, Mongo::DESCENDING]
])
index([
[:status, Mongo::ASCENDING],
[:title, Mongo::ASCENDING],
[:rating, Mongo::DESCENDING]
])
end
The first index is being used both when querying on status, title and subtitle and sorting on rating and when querying on just status and title and sorting on rating even though using explain() along with hint() in the javascript console states that using the second index is 4 times faster.
How can I tell Mongoid to tell MongoDB to use the second index?
You can pass options such as hint to Mongo::Collection using Mongoid::Criterion::Optional.extras
An example:
criteria = Model.where(:status => true, :title => 'hello world').desc(:rating)
criteria.extras(:hint => {:status => 1, :title => 1, :rating => -1})
extras accepts anything that Mongo::Collection can handle
http://www.mongodb.org/display/DOCS/Optimization#Optimization-Hint
While the mongo query optimizer often
performs very well, explicit "hints"
can be used to force mongo to use a
specified index, potentially improving
performance in some situations.
db.collection.find({user:u, foo:d}).hint({user:1});
You need to work from http://www.rdoc.info/github/mongoid/mongoid/master/Mongoid/Cursor here as I do not know Ruby enough. It mentions hint.

Resources