Rails model with Mongoid Embedded and Standalone - ruby-on-rails-3

How would I create a Mongoid model that has the ability to be saved in it's own collection, and be embedded in another document?

The short answer: you can't.
When you use an embedded relationship between two Mongoid documents, this is because you don't want the child model in its own collection. An embedded document it literally that: embedded in its parent.
I'm not sure if you're new to Mongoid, so what you may actually be looking for is a referenced relationship, which behaves more like a traditional RDBMS relationship, where the child document stores a reference to the parent document's ID. The Mongoid documentation for this starts here.
It's pretty easy to switch between the two, given these embedded models:
class Person
include Mongoid::Document
field :name
embeds_many :phone_numbers
end
class PhoneNumber
include Mongoid::Document
field :area_code
field :number
embedded_in :person
end
You can just change the embeds_many and embedded_in, so it becomes:
class Person
include Mongoid::Document
field :name
has_many :phone_numbers
end
class PhoneNumber
include Mongoid::Document
field :area_code
field :number
belongs_to :person
end
And it will just work. Now you'll be able to do things like query directly for phone numbers with statements like: PhoneNumber.where(:area_code => "212").

Related

Retrieving sublist 3 level deep in Rails

I have a datamodel that contains a Project, which contains a list of Suggestions, and each Suggestion is created by a User. Is there a way that I can create a list of all distinct Users that made Suggestions within a Project?
I'm using Mongoid 3. I was thinking something like this, but it doesn't work:
#project = Project.find(params[:id])
#users = Array.new
#users.push(#project.suggestions.user) <-- this doesn't work
Any ideas? Here's my model structure:
class Project
include Mongoid::Document
has_many :suggestions, :dependent => :destroy
...
end
class Suggestion
include Mongoid::Document
belongs_to :author, class_name: "User", :inverse_of => :suggestions
belongs_to :project
...
end
class User
include Mongoid::Document
has_many :suggestions, :inverse_of => :author
...
end
While Mongoid can give MongoDB the semblance of relationships, and MongoDB can hold foreign key fields, there's no underlying support for these relationships. Here are a few options that might help you get the solution you were looking for:
Option 1: Denormalize the data relevant to your patterns of access
In other words, duplicate some of the data to help you make your frequent types of queries efficient. You could do this in one of a few ways.
One way would be to add a new array field to User perhaps called suggested_project_ids. You could alternatively add a new array field to Project called suggesting_user_ids. In either case, you would have to make sure you update this array of ObjectIds whenever a Suggestion is made. MongoDB makes this easier with $addToSet. Querying from Mongoid then looks something like this:
User.where(suggested_project_ids: some_project_id)
Option 2: Denormalize the data (similar to Option 1), but let Mongoid manage the relationships
class Project
has_and_belongs_to_many :suggesting_users, class_name: "User", inverse_of: :suggested_projects
end
class User
has_and_belongs_to_many :suggested_projects, class_name: "Project", inverse_of: :suggesting_users
end
From here, you would still need to manage the addition of suggesting users to the projects when new suggestions are made, but you can do so with the objects themselves. Mongoid will handle the set logic under the hood. Afterwards, finding the unique set of users making suggestions on projects looks like this:
some_project.suggesting_users
Option 3: Perform two queries to get your result
Depending on the number of users that make suggestions on each project, you might be able to get away without performing any denormalization, but instead just make two queries.
First, get the list of user ids that made suggestions on a project.
author_ids = some_project.suggestions.map(&:author_id)
users = User.find(author_ids)
In your Project class add this :
has_many :users, :through => :suggestions
You'll then be able to do :
#users.push(#project.users)
More info on :through here :
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
For mongoid, take a look at this answer :
How to implement has_many :through relationships with Mongoid and mongodb?

Issue with pushing additional values in a embeds_many mongoid relation

I have been breaking my head around this for a long time now. Not sure if my approach is correct or if its not possible using mongoid. SO without further adieu, here is the problem:
I have the following 2 models:
def user
embeds_many :needs, :class_name => "Property"
embeds_many :skills, :class_name => "Property"
end
def property
end
Both these models of course have other code but I have skipped that for brevity.
With this structure I am able to access/add "Property" data as embedded "needs" & "skills" on my user model. Something like this works flawlessly
User.first.update_attributes(skills: [Property.first])
The problem is something like this doesn't work.
User.first.skills.push(Property.first)
User.first.skills << Property.first
There is no error. Both the above statements return true on console. But the values don't persist to the DB.
I basically want a Property model which can be maintained/created independent of the User model, thats why the "embedded_in" on Property is missing in my code.
The question is, am I doing it right? Or there is their a different way that I should go about the design of these models?
Cage is right. You will need to put the embedded_in on the Property model if you want the persistence to work properly. If you want to manage the lifecycle of Property outside the User model, you will have to use 'has_many'.
Please add more details as to what exactly is the purpose of doing what you are doing. I am going to make some assumptions-
Needs and skills are a global list of values, that should be maintained separately
User can have a subset of skills and needs
You want to store the needs and skills as 'plain string' and not references so you can query them without referencing the needs and skills collection. This reduces one query
If the above is correct, then you can use custom keys to store the needs and skills-
class Property
include Mongoid::Document
field :text, :type => String
field :_id, type: String, default ->{ text }
end
class User
include Mongoid::Document
has_many :needs, :class_name => "Property"
has_many :skills, :class_name => "Property"
end
Now you can do something like-
User.first.need_ids
This will give the text of the need and you can avoid another query.
Note- that this is potentially very risky if your 'Property' objects are mutable.
For solution try doing this
u = User.first
u.skills.push(Property.first)
u.skills << Property.first
it will work fine.

Rails 3 using MongoDB via mongoid adapter - is there any way to share attribute specifications without using Single-Table Inheritance?

Probably a confusing title, but not sure how else to put it. Example should make it clearer. I have many different models that share many of the same attributes. So in each model I have to specify those same attributes and THEN the attributes that are specific to that particular model.
Is there any way I can create some class that lists these basic attributes and then inherit from that class without using Single-Table Inheritance? Because if I put all the shared attributes and Mongoid includes into a single model and inherit from that base model in the other models, then STI is enforced and all my models are stored in a single mongodb collection, differentiated by a "_type" field.
This is what I have:
class Model_1
include Mongoid::Document
field :uuid, :type => String
field :process_date, :type => String
...
end
class Model_2
include Mongoid::Document
field :uuid, :type => String
field :process_date, :type => String
...
end
But this is the functionality I'm after:
class Base_model
field :uuid, :type => String
field :process_date, :type => String
end
class Model_1 < Base_model
# To ensure STI is not enforced
include Mongoid::Document
# Attribute list inherited from Base_model
end
The issue is that if you don't have the "include Mongoid::Document" in the Base_model, then that base model doesn't know about the "field ..." functionality. But if you do put the mongoid include in the base model and inherit from it, STI is enforced.
I can't do STI for this particular situation but it's a coding nightmare to have multiple models, all with the same attributes list specified over and over (there are a growing number of models and each share about 15-20 attributes, so anytime I have to change a model name it's a lot of effort to change it everywhere...).
You can define the common attributes in a module and include that.
require 'mongoid'
module DefaultAttrs
def self.included(klass)
klass.instance_eval do
field :uuid, :type => String
end
end
end
class Foo
include Mongoid::Document
include DefaultAttrs
field :a, :type => String
end
class Bar
include Mongoid::Document
include DefaultAttrs
field :b, :type => String
end
I had the exact same question and wanted to go for the mixin approach initially. However, after talking to some experts it turns out that using mongoids single table inheritance (one collection for all child elements) may be the way to go, depending on your use case. Please see my post here:
single collection vs. separate collections for inherited objects
You can bundle them into a new module, i.e.
module Mongoid
module Sunshine
extend ActiveSupport::Concern
included do
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::TouchParentsRecursively
include Mongoid::Paranoia
include Mongoid::UUIDGenerator
end
end
end
class School
include Mongoid::Sunshine
end

Modeling inheritance with Ruby/Rails ORMs

I'm trying to model this inheritance for a simple blog system
Blog has many Entries, but they may be different in their nature. I don't want to model the Blog table, my concern is about the entries:
simplest entry is an Article that has title and text
Quote, however, does not have a title and has short text
Media has a url and a comment...
etc...
What is a proper way to model this with Ruby on Rails? That is
Should I use ActiverRecord for this or switch to DataMapper?
I would like to avoid the "one big table" approach with lots of empty cells
When I split the data into Entry + PostData, QuoteData etc can I have belongs_to :entry in these Datas without having has_one ??? in the Entry class? That would be standard way to do it in sql and entry.post_data may be resolved by the entry_id in the postdata table.
EDIT: I don't want to model the Blog table, I can do that, my concern is about the entries and how would the inheritance be mapped to the table(s).
I've come across this data problem several times and have tried a few different strategies. I think the one I'm a biggest fan of, is the STI approach as mentioned by cicloon. Make sure you have a type column on your entry table.
class Blog < ActiveRecord::Base
# this is your generic association that would return all types of entries
has_many :entries
# you can also add other associations specific to each type.
# through STI, rails is aware that a media_entry is in fact an Entry
# and will do most of the work for you. These will automatically do what cicloon.
# did manually via his methods.
has_many :articles
has_many :quotes
has_many :media
end
class Entry < ActiveRecord::Base
end
class Article < Entry
has_one :article_data
end
class Quote < Entry
has_one :quote_data
end
class Media < Entry
has_one :media_data
end
class ArticleData < ActiveRecord::Base
belongs_to :article # smart enough to know this is actually an entry
end
class QuoteData < ActiveRecord::Base
belongs_to :quote
end
class MediaData < ActiveRecord::Base
belongs_to :media
end
The thing I like about this approach, is you can keep the generic Entry data in the entry model. Abstract out any of the sub-entry type data into their own data tables, and have a has_one association to them, resulting in no extra columns on your entries table. It also works very well for when you're doing your views:
app/views/articles/_article.html.erb
app/views/quotes/_quote.html.erb
app/views/media/_media.html.erb # may be medium here....
and from your views you can do either:
<%= render #blog.entries %> <!-- this will automatically render the appropriate view partial -->
or have more control:
<%= render #blog.quotes %>
<%= render #blog.articles %>
You can find a pretty generic way of generating forms as well, I usually render the generic entry fields in an entries/_form.html.erb partial. Inside that partial, I also have a
<%= form_for #entry do |f| %>
<%= render :partial => "#{f.object.class.name.tableize}/#{f.object.class.name.underscore}_form", :object => f %>
<% end %>
type render for the sub form data. The sub forms in turn can use accepts_nested_attributes_for + fields_for to get the data passed through properly.
The only pain I have with this approach, is how to handle the controllers and route helpers. Since each entry is of its own type, you'll either have to create custom controllers / routes for each type (you may want this...) or make a generic one. If you take the generic approach, two things to remember.
1) You can't set a :type field through update attributes, your controller will have to instantiate the appropriate Article.new to save it (you may use a factory here).
2) You'll have to use the becomes() method (#article.becomes(Entry)) to work with the entry as an Entry and not a subclass.
Hope this helps.
Warning, I've actually used Media as a model name in the past. In my case it resulted in a table called medias in rails 2.3.x however in rails 3, it wanted my model to be named Medium and my table media. You may have to add a custom Inflection on this naming, though I'm not sure.
You can handle this easily using ActiveRecord STI. It requires you to have a type field in your Entries table. This way you can define your models like this:
def Blog > ActiveRecord::Base
has_many :entries
def articles
entries.where('Type =', 'Article')
end
def quotes
entries.where('Type =', 'Quote')
end
def medias
entries.where('Type =', 'Media')
end
end
def Entry > ActiveRecord::Base
belongs_to :blog
end
def Article > Entry
end
def Quote > Entry
end
def Media > Entry
end

How to validate presence of associated object in mongoDB?

In my application I have Link model like this:
class Link
include Mongoid::Document
field :url, :type => String
validates_presence_of :url
belongs_to :link_bucket
end
and LinkBucket model, which is inherited from FeedItem model (in my app FeedItem could contain links, message, audio_track and so on, this is why I use inheritance).
class LinkBucket < FeedItem
has_many :links
end
So how can I verify if there is a link before I create LinkBucket object?
You can't make the association unless the objects exist or are being created at the time. But you can create a Link without having an associated LinkBucket, and then later create the LinkBucket and associate them. In other words, only create a LinkBucket when you are sure you have a Link and you need to create one. Does that help?