It seems like Mongoid is now the superior ORM for Mongo based on performance and development activity. Unfortunately, we're on MongoMapper and need to migrate.
Are there any concerns or stumbling blocks we should be aware of? We have found a few outdated articles on Google and tried posting on the Mongoid Google Groups (though we were prohibited), but would love thoughts from SO members who have done this in the past.
We're on Rails 3.2.12.
Thanks!
Both of them are great MongoDB Libraries for Ruby. But if you want to switch, here are some notes:
Migrating MongoMapper ORM to Mongoid ORM - Notes
Configure the database connection.
Replace configuration yaml file(includes replica configuration).
Configure Mongoid specific options. e.g - raise_not_found_error: false. if you don't want an error every time a query returns nothing...
Change all models definations - include MongoMapper::Document to include Mongoid::Document
Change the format for all fields definitions.
In mongoid, you should specipy the timestamp: include Mongoid::Timestamps
Change validation. e.g: :in => ARRAY, will be: validates :name, presence: true, inclusion: { in: ARRAY }
Change indexes.
Change order_by format. e.g: MM: Model.all(:order => 'name'). Mongoid: Model.order_by('name ASC')
Error is a keyword in Mongoid. So if you have a model named Error, you should change it.
Pagination format is different, using another gem.
The primary key index entry in MM is id. In Mongoid it's _id, if you have other code relying on .id in the object JSON, you can override as_json function in your Model to create the JSON structure you want.
In MM, Model.fields(:id, :name) ,limits the fields returned from the database to those supplied to the method. In Mongoid it's Model.only(:name,:id)
Some queries changes:
Selecting objects by array: MM: Model.where(:attr.in => [ ] ) and Model.where(:attr => [ ] ) . Mongoid is only: Model.where(:attr.in => [ ] )
Map option of MM is equivalent to the Mid's pluck. Model.map(&:name) --to-- Model.pluck(:name)
Mongoid doesn't support find query for nil. e.g: value = nil. Model.find(value) will throw an error : "Calling Document .find with nil is invalid". So in mongoid we should do: Model.find(value || "").
In MM: Model.find_or_initialize_by_name("BOB"). In Mongoid Model.find_or_initialize_by(name: "BOB").
MM can be used in those two options: Model.where({:name => 'BOB'}).first, and also Model.first({:name => 'BOB'}). Mongoid has only first option.
In MM, to update multiple objects: Model.set({conditions},attr_to_update). In Mongoid: Model.where(conditions).update_all(attr_to_update).
Related
So........ I have a rails app. The rails app uses Mongoid for mongodb data. When I create mongo records through web forms, they have IDs with type string. When I import records into mongo using mongoimport, they have IDs with type BSON::ObjectId.
The rails app is expecting the mongo record IDs to be strings, and therefore when I import the data, it causes my app to fail because when it looks up the records it complains that it can't convert type BSON::ObjectId to string
I'm confused on a number of levels here. BSON::ObjectId is the default type for IDs in mongo, so I don't understand why the records created through rails and Mongoid have string IDs. I don't see anywhere where Mongoid is specifying that the _id field should be a string. Does anybody have any clues?
What version of Mongoid are you using? From this post, it looks like Mongoid was using strings up until a year ago for _id's but now consistently uses the BSON::ObjectId type.
mongodb: converting object ID's to BSON::ObjectId
It references this gist for converting old documents with String _id's to using BSON::ObjectId type _id's.
When Mongoid inserts a document into a collection, it expects and uses the BSON::ObjectId type. This is an example using the Rails console:
post = Post.new
=> #
post.save
=> true
post._id
=> BSON::ObjectId('4ff5bcb39ef1728393000002')
post._id.class
=> BSON::ObjectId
Mongoid appears to know to look up _id's using the BSON::ObjectId type:
Post.where(:_id => "4ff5bcb39ef1728393000002").count
=> 1
Post.where(:_id => BSON::ObjectId("4ff5bcb39ef1728393000002") ).count
=> 1
Are you, by any chance, manually setting the _id's? If you are, then perhaps you're not setting the _id's as BSON::ObjectId types.
About your last paragraph: MongoDB's specification is expecting for 12 byte string.
So I figured it out. The issue is with the version of Mongoid that is used by my application. Version 1.9.5 uses strings as the default type for the _id field, which is what I'm using.
I thought about updating Mongoid, but I was afraid that the old string IDs would somehow break the application.
The answer was to somehow import the records using the rails app, so that the old version of Mongoid would be in charge of inserting the records. I created a rake task that would parse the CSV file and insert the records from that file within my app environment.
require 'csv'
require 'iconv'
namespace :deadline do
desc "Import data from CSV file"
task :import => :environment do
CSV.parse(File.open("/tmp/deadlines.csv").read).map{ |row|
app_deadline = OrgSpecific::ApplicationDeadline.create!(
:name => row[2] + " " + row[3],
:start_date => Date.strptime('1/1/2012', '%m/%d/%Y'),
:deadline_date => Date.strptime(row[11], '%m/%d/%Y'),
:term => row[2],
:year => row[3],
:comment => Iconv.conv("UTF8", "LATIN1", row[5]) + " : " + Iconv.conv("UTF8", "LATIN1", row[6])
)
}
end
end
And voila! All the data from my CSV file has been imported through my rails environment, meaning that my mongo records have an _id of type string. Thanks for the help guys!
I want to query on a Hash field for a Mongoid Class. I'm not sure how I can do this with conditions?
Here is an example:
class Person
include Mongoid::Document
field :things, :type => Hash
end
So, let's say that I have this:
p = Person.new
p.things = {}
p.things[:tv] = "Samsung"
I want to query for the first person with a tv that is a Samsung...
People.first(:conditions => ?????
Thanks in advance.
Person.where('things.tv' => 'Samsung').first
This is where Mongoid and MongoDB really shine. Mongoid's Criteria methods (Person.where, Person.any_of, Person.excludes, etc.) will give you much more flexibility than the ActiveRecord-style finders (passing a :conditions hash to Person.find, Person.first, etc.)
Mongoid's site has some great documentation on how to use Criteria:
http://mongoid.org/en/mongoid/docs/querying.html
I'm looking for a way to store a serialized value of eg. IDs in a column. In before claims that this is not an optimal design: the column is used for IDs of associated records, but will only be used when displaying the record - so no queries are made with selection on the column and no joins will be made on this column either.
In Rails I can serialize the column by using:
class Activity
serialize :data
end
This encodes the column as YAML. For legacy sake and since I'm only storing one dimensional arrays containing only integers, I find it more suitable to store it as a comma-separated value.
I've successfully implemented basic accessors like this:
def data=(ids)
ids = ids.join(",") if ids.is_a?(Array)
write_attribute(:data, ids)
end
def data
(read_attribute(:data) || "").split(",")
end
This works pretty fine. However I'd like to add array-like methods to this attribute:
activity = Activity.first
activity.data << 42
...
How would I do this?
You can do it with composed_of feature as explained in this post.
It should be something like:
composed_of :data, :class_name => 'Array', :mapping => %w(data to_csv),
:constructor => Proc.new {|column| column.to_csv},
:converter => Proc.new {|column| column.to_csv}
after_validation do |u|
u.data = u.data if u.data.dirty? # Force to serialize
end
Haven't tested it though.
You can use serialize with a custom coder in rails 3.1.
See my answer to this question. :-)
I would like to know if it is possible to get the types (as known by AR - eg in the migration script and database) programmatically (I know the data exists in there somewhere).
For example, I can deal with all the attribute names:
ar.attribute_names.each { |name| puts name }
.attributes just returns a mapping of the names to their current values (eg no type info if the field isn't set).
Some places I have seen it with the type information:
in script/console, type the name of an AR entity:
>> Driver
=> Driver(id: integer, name: string, created_at: datetime, updated_at: datetime)
So clearly it knows the types. Also, there is .column_for_attribute, which takes an attr name and returns a column object - which has the type buried in the underlying database column object, but it doesn't appear to be a clean way to get it.
I would also be interested in if there is a way that is friendly for the new "ActiveModel" that is coming (rails3) and is decoupled from database specifics (but perhaps type info will not be part of it, I can't seem to find out if it is).
Thanks.
In Rails 3, for your model "Driver", you want Driver.columns_hash.
Driver.columns_hash["name"].type #returns :string
If you want to iterate through them, you'd do something like this:
Driver.columns_hash.each {|k,v| puts "#{k} => #{v.type}"}
which will output the following:
id => integer
name => string
created_at => datetime
updated_at => datetime
In Rails 5, you can do this independently of the Database. That's important if you use the new Attributes API to define (additional) attributes.
Getting all attributes from a model class:
pry> User.attribute_names
=> ["id",
"firstname",
"lastname",
"created_at",
"updated_at",
"email",...
Getting the type:
pry> User.type_for_attribute('email')
=> #<ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlString:0x007ffbab107698
#limit=255,
#precision=nil,
#scale=nil>
That's sometimes more information than needed. There's a convenience function that maps all these types down to a core set (:integer, :string etc.)
> User.type_for_attribute('email').type
=> :string
You can also get all that data in one call with attribute_types which returns a 'name': type hash.
You can access the types of the columns by doing this:
#script/console
Driver.columns.each {|c| puts c.type}
If you want to get a list of all column types in a particular Model, you could do:
Driver.columns.map(&:type) #gets them all
Driver.columns.map(&:type).uniq #gets the unique ones
In rails 5 this will give you a list of all field names along with their data type:
Model_Name.attribute_names.each do |k| puts "#{k} = #{Model_Name.type_for_attribute(k).type}" end
Rails 5+ (works with virtual attributes as well):
Model.attribute_types['some_attribute'].type
This snippet will give you all the attributes of a model with the associated database data types in a hash. Just replace Post with your Active Record Model.
Post.attribute_names.map {|n| [n.to_sym,Post.type_for_attribute(n).type]}.to_h
Will return a hash like this.
=> {:id=>:integer, :title=>:string, :body=>:text, :created_at=>:datetime, :updated_at=>:datetime, :topic_id=>:integer, :user_id=>:integer}
Assuming Foobar is your Active Record model. You can also do:
attributes = Foobar.attribute_names.each_with_object({}) do |attribute_name, hash|
hash[attribute_name.to_sym] = Foobar.type_for_attribute(attribute_name).type
end
Works on Rails 4 too
In Rails 4 You would use Model.column_types.
I have an aggregated attribute which I want to be able ask about its _changed? ness, etc.
composed_of :range,
:class_name => 'Range',
:mapping => [ %w(range_begin begin), %w(range_end end)],
:allow_nil => true
If I use the aggregation:
foo.range = 1..10
This is what I get:
foo.range # => 1..10
foo.range_changed? # NoMethodError
foo.range_was # ditto
foo.changed # ['range_begin', 'range_end']
So basically, I'm not getting ActiveRecord::Dirty semanitcs on aggregated attributes. Is there any way to do that? I'm not having a lot of luck with alias_attribute_with_dirty, etc.
You don't get ActiveRecord::Dirty semantics because aggregated attributes are not association proxies, being mere wrappers for your actual attributes - that's why you have no access to ActiveRecords goodness through them.
Note, that foo.range_begin_changed? should work, so you may try to see if you can use foo.class#method_missing (e.g. defined in a module to be included) to automatically map *_was, *_changed? etc methods to those called on actual attributes of foo.class.