Assume, that a user wants to create a set consisting of items created by other users. Mongoid Document for item has versioning, and the user who creates the set might not enjoy changes the item authors do with the items of the set. Therefore I would like the set document to refer to specific versions of items, allowing the set author to update item references if wanted. I am planning adding an array of item version numbers to the set document, and some methods for getting set items of certain version and for updating the item versions. Do you find this approach reasonable? How would you solve this problem?
class Item
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Versioning
field :title, type: String
has_and_belongs_to_many :item_sets
end
class ItemSet
include Mongoid::Document
field :name, type: String
field :item_versions, type: Array
has_and_belongs_to_many :items
end
I solved problems like this by creating a model in the "middle" like "ItemReference"
MongoDB is a document store and not a relational database, so it is legitimate to store duplicate information when necessary. MongoDB has the ability to store embedded documents, so we're gonna use this great feature.
The ItemReference holds all crucial information about the Item which is needed for creating a view. This reduces the queries on the view side but in increases the queries on the insert/update side.
The thing is that you need a "composite primary key" that consists of the item_id and the version number.
Let's talk code:
The Item Model:
class Item
include Mongoid::Document
include Mongoid::Paranoia
include Mongoid::Versioning
field :title, :type => String
# create a reference
def to_reference
# create new reference, containing all crucial attributes for displaying
ItemReference.new(
:item_id => self._parent.nil? ? self.id : self._parent.id,
:version => self.version,
:title => self.title
)
end
# get a certain version of this item
def get_version(version_number)
return self if version_number == self.version
self.versions.where(:version => version_number).first
end
end
The ItemSet Model
class ItemSet
include Mongoid::Document
field :name, :type => String
embeds_many :item_references
end
The ItemReference Model
class ItemReference
include Mongoid::Document
embedded_in :item_sets
field :title, :type => String
# this points to the newest version
belongs_to :item
# get the original version
def original
self.item.get_version(self.version)
end
# update this reference to a certain version
def update_to!(new_version)
new_version = self.item.get_version(new_version)
if new_version.present?
# copy attribute, except id
self.attributes = new_version.to_reference.attributes.reject{|(k,v)| k == "_id"}
self.save
else
# version not found
false
end
end
# update to the newest version
def update_to_head!
self.update_to!(self.item.version)
end
end
This combination allows you to create sets holding Items with different versions, and you can update certain ItemReferences in the set to a specific version.
Here's an example:
first = Item.create(:title => 'Item 1')
first.title = 'Item 1.1'
first.save
myset = ItemSet.create(:title => 'My Set')
myset.item_references << first.to_reference
myset.save
first.title = 'Item 1.2'
first.save
p myset.item_references.first.title # prints Item 1.1
p myset.item_references.first.update_to_head!
p myset.item_references.first.title # prints Item 1.2
p myset.item_references.first.update_to!(1)
p myset.item_references.first.title # prints Item 1
Related
I am building a simple script to populate a MongoDB database. This is my first time using a NoSQL DB and I feel I may be thinking about this problem from a SQL DB standpoint.
The basis of this script is to populate a database that holds a few collections that relate to one another. But when I run my script, I see an invalid errors when building/saving the documents.
I have three collections; Book, Author, and Style, with the following relationships.
A Book has many Authors
An Author has many Books
An Author has many Styles
A Style has many Authors
The models are defined as followed:
# Book Model
class Book
include Mongoid::Document
include Mongoid::Timestamps
field :title, type: String
validates :title, presence: true
has_and_belongs_to_many :authors
index({ title: 'text' })
end
# Author Model
class Author
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
validates :name, presence: true
has_and_belongs_to_many :books
has_and_belongs_to_many :styles
index({ name: 1 }, { unique: true })
end
# Style Model
class Style
include Mongoid::Document
include Mongoid::Timestamps
field :type, type: String
validates :type, presence: true
has_and_belongs_to_many :authors
index({ type: 1 }, { unique: true, name: "type_index" })
end
And then this is my script:
# script.rb
book = Book.new
book.title = "Good Omens"
['Neil Gaiman', 'Terry Pratchett'].each do |author_name|
author = Author.find_by(name: author_name)
if author.nil?
author = Author.new(name: author_name)
end
# a list of writing styles this author can have
# pretend that there's a list of styles per author
literary_styles.each do |style_name|
style = Style.find_by(type: style_name)
if style.nil?
author.styles.build(Style.new(type: style_name))
else
unless author.styles.include? style.id
author.styles << style
end
end
end
author.valid? #=> false
author.errors #=> #messages={:styles=>["is invalid"]}
book.author.build(book.attributes)
book.save
end
The Book document is created, but the Author and Style do not persist due to the invalid Style validation error. I wish I could see exactly what is causing the validations to fail, but the messaging is very vague. I suspect it is coming from some built in validation from the has_and_belongs_to_many relation between Author and Style but I can't put my finger on it.
What I find interesting is that the Book document has an author_ids property which is populated with id's but when I jump into the console, there are no authors that can be pulled up or tied to the Book.
Happy to give more info if needed.
I think I'd need to know a bit more to definitively tell you what the issue is -- What version of Mongoid are you using? Are you running this script on an empty database, or do the Author and Style documents already exist? What do those look like?
That being said, I do see a couple of bugs in your script, though I'm not sure if they're causing your problem:
if style.nil?
author.styles.build(Style.new(type: style_name))
else
...
I believe the line inside the if statement will throw an error. Instead, it should say author.styles.build(type: style_name).
unless author.styles.include? style.id
author.styles << style
end
The expression after unless is always going to evaluate to false because author.styles is an array of Style objects, not style ids. Instead, it should probably say author.style_ids.include? style.id or author.styles.include? style
Let me know if that helps! I'm happy to debug some more if you provide me with the extra info I requested.
Simply trying to work out how to copy attributes from one Active Model to another without having to do it one by one.
I have two models one is RFM (ruby to filemaker) one is mongoid both mixin active model.
gem "ginjo-rfm"
the model
require 'rfm'
class StudentAdmin < Rfm::Base
config :layout => 'STUDENT_ADMIN_LAYOUT'
attr_accessor :name_first,:name_last
end
Mongoid model
class Student
include Mongoid::Document
field :first_name, type: String
field :last_name, type: String
end
Is there a quicky copy I can do? I found a sample between active record objects e.g.
student_admin = ... #load StudentAdmin
Student.new(student_admin.attributes.slice(Student.attribute_names))
but RFM doesn't provide a attributes method.
EDIT
Sorry what I am trying to achive is a better way than this
student_admins = #get student admins from external service
students = []
student_admins.each() do |sa|
students.push(Student.create!(first_name: sa.name_first, last_name: sa.name_last))
end
This example only shows 2 attributes, but in practice there is over 50 and was wondering if there is a way to do it without having to specify every attribute e.g. if the attribute names are the same on two objects copy them automatically.
Try this:
students = student_admins.map do |sa|
attrs = sa.methods.inject({}) do |hash, m|
next unless Student.column_names.include? m.to_s
hash[m] = sa.send m
end
Student.create(attrs)
end
Student would have to be a class that inherits from ActiveRecord::Base:
class Student < ActiveRecord::Base
...
end
Let's say I have a Model Item which uses Mongoid
class Item
include Mongoid::Document
field :title, type: String
...
...
end
I want to add some dynamic fields to Item right in Model before passing data to a Controller - because Item is being used by several Controllers.
For example I want to add thumb field which I will generate by adding /path/to + filename.
I tried some solutions with attr_accessor:
class Item
include Mongoid::Document
field :title, type: String
...
...
attr_accessor :thumb
def prepare_data
#thumb = "/path/to/thumb"
end
end
...And later in some Controller:
#items_all = Item.all
#thumbs = []
#items_all.each do |i]
i.prepare_data
#thumbs.push(i[:thumb])
end
# #thumbs >>> (empty)
So it seems that I'm missing some point here because it doesn't work.
Also can I avoid calling prepare_data each time manually? May be with help of after_initialize? (which didn't work for me also).
I found my mistake. First I forgot to add after_initialize :do_something and then I found that I can user #attributes.merge!
after_initialize :do_after_initialize
def do_after_initialize
#attributes.merge!({
:title => self.get_title,
:type => self.get_type,
:thumb => ImageUploader::thumb(self[:pictures][0]["filename"])
:price_f => ActionController::Base.helpers.number_to_currency(self[:price], {:precision=>0})
})
end
My application model allows Patients to have CustomFields. All patients have the same customs fields. Customs fields are embedded in the Patient document. I should be able to add, update and remove custom fields and such actions are extended to all patients.
class Patient
include Mongoid::Document
embeds_many :custom_fields, as: :customizable_field
def self.add_custom_field_to_all_patients(custom_field)
Patient.all.add_to_set(:custom_fields, custom_field.as_document)
end
def self.update_custom_field_on_all_patients(custom_field)
Patient.all.each { |patient| patient.update_custom_field(custom_field) }
end
def update_custom_field(custom_field)
self.custom_fields.find(custom_field).update_attributes({ name: custom_field.name, show_on_table: custom_field.show_on_table } )
end
def self.destroy_custom_field_on_all_patients(custom_field)
Patient.all.each { |patient| patient.remove_custom_field(custom_field) }
end
def remove_custom_field(custom_field)
self.custom_fields.find(custom_field).destroy
end
end
class CustomField
include Mongoid::Document
field :name, type: String
field :model, type: Symbol
field :value, type: String
field :show_on_table, type: Boolean, default: false
embedded_in :customizable_field, polymorphic: true
end
All pacients have the same customs fields embedded in. Adding a custom field works very well. My doubt is about updating and destroying.
This works, but it is slow. It makes a query for each pacient. Ideally I would just be able to say to MongoDB 'update the document with id: that is embedded in the array *custom_fields* for all documents in the Patient collection'. Idem for destroy.
How can I do this in Mongoid?
I am using Mongoid 3.1.0 & Rails 3.2.12
I don't think there is a way you can do that with a good efficiency with embedded documents.
Maybe you should consider having a referenced relationship between your models, so that you can use the delete_all and update_all methods on the collection.
I have problem with mongomapper associations. I have one class names User and other named Model. User has many models but...
user = User.first
=> <User ...
user.models
=> []
Model.find_by_user_id(user.id.to_s)
=> <Model ...
Model.find_by_user_id(user.id.to_s).user == user
=> true
Class code (simplified):
class User
include MongoMapper::Document
# some keys definition
many :models
end
class Model
include MongoMapper::Document
# some keys definitions
belongs_to :user
end
What I am doing wrong?
It appears that MM no longer uses String format for the FK column, so
Model.find_by_user_id(user.id.to_s)
should be
Model.find_by_user_id(user.id)
Furthermore, the datatype of the Model.user_id column should be set to
key :user_id, Mongo::ObjectID
When I ran into this problem, I had to delete and recreate my collection to get it to work- in other words I used to have user_id as a String, but it would only "take" when I switched it when I rebuilt my database. Luckily I am working with test data so that was easy enough.
What kind of errors or exceptions are you getting? The code you posted looks fine.
ah, this is poorly documented in the mm docs. You need to do this here:
class User
include MongoMapper::Document
# some keys definition
many :models, :in => :model_ids
end
class Model
include MongoMapper::Document
# some keys definitions
# no belongs_to necessary here
end
You can then add models to your user via:
# use an existing object
u = User.create ...
m = Model.create ...
# and add the model to the user
u.models << m
# don't forget to save
u.save
# you can then check if it worked like so:
# u.model_ids => [ BSON::ID 'your user id']
Hope that helped.