I'm trying to find a way to update a single array item using mongoid. I'm using Rails 4 and Mongodb.
My model looks like this
class User
include Mongoid::Document
include Mongoid::Timestamps
field :my_book_list, type: Array, default: []
field :book_name, type: String
I'm able to add entry to the array field using the following code:
User.where(id: self.user_id).add_to_set("my_book_list" => self.book_name)
After I have added data to the array, in the database it looks like this
db.collection.users
{
"_id" : ObjectId("56e09d54a0d00b0528000001"),
"status" : true,
"sign_in_count" : 3,
"my_book_list" :
["Learning Ruby", "MongoDB for Dummies"]
}
What I'm struggling with is to find a Rails / Mongoid way of updating the value of an item in the array by looking for the name.
Simply put, how do I change the value of my_book_list[1] by searching for it through name and not knowing its index. In this case index 1 is "MongoDB for Dummies" and needs to be updated to "MongoDB". So that the "my_book_list" array field looks like this after its updated:
db.collection.users
{
"_id" : ObjectId("56e09d54a0d00b0528000001"),
"status" : true,
"sign_in_count" : 3,
"my_book_list" :
["Learning Ruby", "MongoDB"]
}
How do I achieve this ?
Instead of updating, think of it as adding & removing. You can use pull (https://docs.mongodb.org/ecosystem/tutorial/mongoid-persistence/#atomic)
Where your add to set uniquely adds it to an array, pull removes it based on the name. So assuming this:
user = User.find_by(id: self.user_id)
user.add_to_set(my_book_list: 'First Story')
p user.my_book_list
=> ['First Story']
user.add_to_set(my_book_list: 'Second Story')
p user.my_book_list
=> ['First Story', 'Second Story']
user.add_to_set(my_book_list: 'Third Story')
p user.my_book_list
=> ['First Story', 'Second Story', 'Third Story']
user.pull(my_book_list: 'Second Story')
p user.my_book_list
=> ['First Story', 'Third Story']
If you had duplicates in the set you can use pull_all, but you are using add to set so you won't need to.
This was more of a conceptual problem being a ruby beginner. The answer lies that mongo arrays are simple ruby array and we can use standard ruby methods to update the array.
In this case in my rails console I did this and it worked. The second line finds the array item and replaces with the new wird,
u = User.first
u.my_book_list.map! { |x| x == "MongoDB for Dummies" ? "MongoDB": x}
u.save
Related
Each user has one address.
class User
include Mongoid::Document
has_one :address
end
class Address
include Mongoid::Document
belongs_to :user
field :street_name, type:String
end
u = User.find(...)
u.address.update(street_name: 'Main St')
If we have a User without an Address, this will fail.
So, is there a good (built-in) way to do u.address.update_or_initialize_with?
Mongoid 5
I am not familiar with ruby. But I think I understand the problem. Your schema might looks like this.
user = {
_id : user1234,
address: address789
}
address = {
_id: address789,
street_name: ""
user: user1234
}
//in mongodb(javascript), you can get/update address of user this way
u = User.find({_id: user1234})
u.address //address789
db.address.update({user: u.address}, {street_name: "new_street name"})
//but since the address has not been created, the variable u does not even have property address.
u.address = undefined
Perhaps you can try to just create and attached it manually like this:
#create an address document, to get _id of this address
address = address.insert({street_name: "something"});
#link or attached it to u.address
u.update({address: address._id})
I had this problem recently. There is a built in way but it differs from active records' #find_or_initialize_by or #find_or_create_by method.
In my case, I needed to bulk insert records and update or create if not found, but I believe the same technique can be used even if you are not bulk inserting.
# returns an array of query hashes:
def update_command(users)
updates = []
users.each do |user|
updates << { 'q' => {'user_id' => user._id},
'u' => {'address' => 'address'},
'multi' => false,
'upsert' => true }
end
{ update: Address.collection_name.to_s, updates: updates, ordered: false }
end
def bulk_update(users)
client = Mongoid.default_client
command = bulk_command(users)
client.command command
client.close
end
since your not bulk updating, assuming you have a foreign key field called user_id in your Address collection. You might be able to:
Address.collection.update({ 'q' => {'user_id' => user._id},
'u' => {'address' => 'address'},
'multi' => false,
'upsert' => true }
which will match against the user_id, update the given fields when found (address in this case) or create a new one when not found.
For this to work, there is 1 last crucial step though.
You must add an index to your Address collection with a special flag.
The field you are querying on (user_id in this case)
must be indexed with a flag of either { unique: true }
or { sparse: true }. the unique flag will raise an error
if you have 2 or more nil user_id fields. The sparse option wont.
Use that if you think you may have nil values.
access your mongo db through the terminal
show dbs
use your_db_name
check if the addresses collection already has the index you are looking for
db.addresses.getIndexes()
if it already has an index on user_id, you may want to remove it
db.addresses.dropIndex( { user_id: 1} )
and create it again with the following flag:
db.addresses.createIndex( { user_id: 1}, { sparse: true } )
https://docs.mongodb.com/manual/reference/method/db.collection.update/
EDIT #1
There seems to have changes in Mongoid 5.. instead of User.collection.update you can use User.collection.update_one
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/
The docs show you need a filter rather than a query as first argument but they seem to be the same..
Address.collection.update_one( { user_id: user_id },
'$set' => { "address": 'the_address', upsert: true} )
PS:
If you only write { "address": 'the_address' } as your update clause without including an update operator such as $set, the whole document will get overwritten rather than updating just the address field.
EDIT#2
About why you may want to index with unique or sparse
If you look at the upsert section in the link bellow, you will see:
To avoid multiple upserts, ensure that the filter fields are uniquely
indexed.
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/
I have a model Event that is connected to MongoDB using Mongoid:
class Event
include Mongoid::Document
include Mongoid::Timestamps
field :user_name, type: String
field :action, type: String
field :ip_address, type: String
scope :recent, -> { where(:created_at.gte => 1.month.ago) }
end
Usually when I use ActiveRecord, I can do something like this to group results:
#action_counts = Event.group('action').where(:user_name =>"my_name").recent.count
And I get results with the following format:
{"action_1"=>46, "action_2"=>36, "action_3"=>41, "action_4"=>40, "action_5"=>37}
What is the best way to do the same thing with Mongoid?
Thanks in advance
I think you'll have to use map/reduce to do that. Look at this SO question for more details:
Mongoid Group By or MongoDb group by in rails
Otherwise, you can simply use the group_by method from Enumerable. Less efficient, but it should do the trick unless you have hundreds of thousands documents.
EDIT: Example of using map/reduce in this case
I'm not really familiar with it but by reading the docs and playing around I couldn't reproduce the exact same hash you want but try this:
def self.count_and_group_by_action
map = %Q{
function() {
key = this.action;
value = {count: 1};
emit(key, value);
# emit a new document {"_id" => "action", "value" => {count: 1}}
# for each input document our scope is applied to
}
}
# the idea now is to "flatten" the emitted documents that
# have the same key. Good, but we need to do something with the values
reduce = %Q{
function(key, values) {
var reducedValue = {count: 0};
# we prepare a reducedValue
# we then loop through the values associated to the same key,
# in this case, the 'action' name
values.forEach(function(value) {
reducedValue.count += value.count; # we increment the reducedValue - thx captain obvious
});
# and return the 'reduced' value for that key,
# an 'aggregate' of all the values associated to the same key
return reducedValue;
}
}
self.map_reduce(map, reduce).out(inline: true)
# we apply the map_reduce functions
# inline: true is because we don't need to store the results in a collection
# we just need a hash
end
So when you call:
Event.where(:user_name =>"my_name").recent.count_and_group_by_action
It should return something like:
[{ "_id" => "action1", "value" => { "count" => 20 }}, { "_id" => "action2" , "value" => { "count" => 10 }}]
Disclaimer: I'm no mongodb nor mongoid specialist, I've based my example on what I could find in the referenced SO question and Mongodb/Mongoid documentation online, any suggestion to make this better would be appreciated.
Resources:
http://docs.mongodb.org/manual/core/map-reduce/
http://mongoid.org/en/mongoid/docs/querying.html#map_reduce
Mongoid Group By or MongoDb group by in rails
I create a Model that has a Boolean field, but when catch the value it gave me 1 or 0. I discover that it's because BSON type for Boolean is "\x00" and "\x01".
So my question is, how can I get the "boolean" value of the field? Do I need to do a method on a model or a controller that returns me true if value is 1 or false if 0? Or will Mongoid do this for me?
Mongoid Version: 4.0.0 38de2e9
EDIT
Mongo Shell
db.feedbacks.find().limit(1).pretty()
{
"_id" : ObjectId("52290a2f56de969f8d000001"),
"like" : "1",
...
}
Explain:
I create a app with scaffold:
rails g scaffold Feedback like:Boolean
When I insert a new record, in Mongo the Document stay as I sad.
When I do Feedback.first, the field like in Model has the "0" or "1" value.
class Feedback
include Mongoid::Document
include Mongoid::Timestamps
field :comment, type: String
field :like, type: Boolean
def isLike?
like=="1"
end
end
This is the repo:
https://github.com/afucher/4kFeedback/blob/master/app/models/feedback.rb
Mongoid handles that in a transparent manner if you use the Boolean type. Checkout the documentation.
EDIT :
From the rails console (in an app with an Indicator model defining a field global of type Boolean) :
Indicator.first.global?
# => true
Indicator.first.global?.class
# => TrueClass
The equivalent from the mongo shell :
> db.indicators.find().limit(1).pretty()
{
"_id" : ObjectId("52319eeb56c02cc74200009c"),
...
"global" : true,
...
}
EDIT
The spec for the Boolean extension clearly shows that for any of true, "true", "t", "yes", "y", 1, 1.0 on the MongoDB side you'll get a TrueClass instance. Same for false.
I can resolve my problem, reading the check_box documentation:
http://apidock.com/rails/ActionView/Helpers/FormHelper/check_box
The default value of Check Box is "0" or "1". To change this value, is just pass the values that you want to the tag:
check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
So, I change to this:
<%= f.check_box :like,{}, "true", "false" %>
Thanks Jef for help me!!
I have the following array:
#unregistered_users = ['my#email.com', 'your#email.com', ...]
Now, I want to create a document for each array element:
#unregistered_users.each do |email_address|
Model.create(email: email_address, user: self.user, detail: self)
end
But it only creates a single document (the first element of the array). The other array elements are simply not created. Why?
We're using Ruby 1.9.3-p385, Rails 3.2.12, MongoID 3.0.0 and MongoDB 2.2.3
Update #1
So, we had a custom _id field with a custom random token using SecureRandom.hex(64).to_i(16).to_s(36)[0..127].
After I removed it worked normally, but with regular mongo ID's (which is not what we want).
Update #2
This is how the token are being generated:
class Model
include Mongoid::Document
include Mongoid::Timestamps
...
field :_id, default: SecureRandom.hex(64).to_i(16).to_s(36)[0..127]
...
index( { _id: 1 }, { unique: true } )
end
Try something like this to check what are the errors on the mongoid model:
#unregistered_users.each do |email_address|
model = Model.create(email: email_address, user: self.user, detail: self)
puts model.errors.inspect unless model.persisted?
end
or use create! to raise an exception and see what's happening
Is there a shorter way to do the following (
#user.employees.map { |e| { id: e.id, name: e.name } }
# => [{ id: 1, name: 'Pete' }, { id: 2, name: 'Fred' }]
User has_many employees. Both classes inherit from ActiveRecord::Base.
Two things I don't like about the above
It loads employees into memory before mapping,
It's verbose (subjective I guess).
Is there a better way?
UPDATE:
see #jamesharker's solution: from ActiveRecord >= 4, pluck accepts multiple arguments:
#user.employees.pluck(:id, :name)
PREVIOUS ANSWER:
for a single column in rails >= 3.2, you can do :
#user.employees.pluck(:name)
... but as you have to pluck two attributes, you can do :
#user.employees.select([:id, :name]).map {|e| {id: e.id, name: e.name} }
# or map &:attributes, maybe
if you really need lower-level operation, just look at the source of #pluck, that uses select_all
In ActiveRecord >= 4 pluck accepts multiple arguments so this example would become:
#user.employees.pluck(:id, :name)
If you are stuck with Rails 3 you can add this .pluck_all extension : http://meltingice.net/2013/06/11/pluck-multiple-columns-rails/
Another option is to:
#user.employees.select(:id, :name).as_json
#=> [{"id" => 1, "name" => "Pete"}, {"id" => 2, "name" => "Fred"}]
I can imagine that you'd rather have symbolized keys.
If that's the case use the #symbolize_keys method.
#user.employees.select(:id, :name).as_json.map(&:symbolize_keys)
#=> [{id: 1, name: "Pete"}, {id: 2, name: "Fred"}]
See: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json
Add this monkey patch which provides the multi columns pluck functionality in Rails 3.
# config/initializers/pluck_all.rb
if Rails.version[0] == '3'
ActiveRecord::Relation.class_eval do
def pluck(*args)
args.map! do |column_name|
if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
else
column_name.to_s
end
end
relation = clone
relation.select_values = args
klass.connection.select_all(relation.arel).map! do |attributes|
initialized_attributes = klass.initialize_attributes(attributes)
attributes.map do |key, attr|
klass.type_cast_attribute(key, initialized_attributes)
end
end
end
end
end
Rename the method from pluck to pluck_all if you dont want to override the original pluck functionality
In terms of making a rails 3 method that behaves the same as the Rails 4 pluck with multiple columns. This outputs a similar array (rather than a hashed key value collection). This should save a bit of pain if you ever come to upgrade and want to clean up the code.
module ActiveRecord
class Relation
def pluck_all(*args)
args.map! do |column_name|
if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
else
column_name.to_s
end
end
relation = clone
relation.select_values = args
klass.connection.select_all(relation.arel).map! do |attributes|
initialized_attributes = klass.initialize_attributes(attributes)
attributes.map do |key, attribute|
klass.type_cast_attribute(key, initialized_attributes)
end
end
end
end
end
Standing on the shoulders of giants and all
The pluck_all method worked well until I'm going to upgrade from Rails 3.2 to Rails 4.
Here is a gem pluck_all to solve this, making pluck_all method support not only in Rails 3 but in Rails 4 and Rails 5. Hope this will help those who are going to upgrade rails version.