I'm using Ruby on Rails 3.1 with mongoid and trying to set up som rather simple relations between posts, comments, users and tags.
I'm very new to mongodb, and no-sql in general so I'm a bit confused.
What I am trying to accomplish is this:
Users, posts and comments should be able to have multiple tags.
Tags should have name, type and a count of how many times it has been used.
I need to be able to get all available tags so that users kan choose from them.
And the other way around, be able to retrieve tags from users, posts and comments.
I've read a lot about it, and still can't seem to figure out which approach I should take. Should I use referencial or embedded relationships?
I've looked at a couple of gems but no-one seems to work as i described above.
Sidenote: I am going to use Tire for my search-function later on.
Cool, welcome to MongoDB! This is hard to get right and depends on your application but I'll try and give you some pointers based on what you've written and what I think will work best.
This isn't always the case but the general theory is that if an object is always manipulated and viewed in context of another you should embed it inside that object. This is probably the case with comments and posts in your application. Therefore you may want to embed comments inside posts.
However, because you use the tag object in multiple contexts I would make it its own collection like this:
class Tag
include Mongoid::Document
field :tag, type: String
field :type, type: String
field :count, type: Integer
end
Let's run down your requirements and build the models.
Tags should have name, type and a count of how many times it has been used.
Done via above code for Tag class.
Users, posts and comments should be able to have multiple tags.
Ok so let's give each of these classes a "tags" field that has an array of tag IDs, this would be a referential relationship.
class User
include Mongoid::Document
field :first_name, type: String
field :last_name, type: String
field :email, type: String
field :tags, type: Array
end
Also here we will embed comments inside of the posts along with having the array of tag IDs like we do for Users.
class Post
include Mongoid::Document
field :subject, type: String
field :body, type: String
field :tags, type: Array
embeds_many :comments
end
class Comment
include Mongoid::Document
field :name, type: String
field :type, type: String
field :count, type: Integer
embedded_in :post
end
Make sense? There is some more info here on modeling these sorts of relationships in Rails but using mongomapper instead of mongoid (so don't pay attention to the syntax but pay attention to the ideas presented)
Related
I'm trying to retrieve only books where only authors in favorite_authors are among the authors for the book. (note: not an exact match, but an exclusion of all books with any authors other than those in favorite_authors).
favorite_authors = User.favorite_authors
=> [BSON::ObjectId('5363c73c4d61635257516000'),
BSON::ObjectId('5363c73c4d61635257516001'),
BSON::ObjectId('5363c73c4d61635257516002'),
BSON::ObjectId('5363c73c4d61635257516003'),
BSON::ObjectId('5363c73c4d61635257516004'),
BSON::ObjectId('5363c73c4d61635257516005'),
BSON::ObjectId('5363c73c4d61635257516006')]
class Author
include Mongoid::Document
field :name, type: String
class Book
include Mongoid::Document
field :name, type: String
field :authors, type: Array
Some naive pseudo code to give you an idea of what I'd like to do:
Book.where(authors: relevant_authors).not.where(authors: !relevant_authors)
Ideally, I'd like to accomplish this using a Mongoid Origin query.
I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.
Basically, I'd like to do something like:
# override authors_attributes
def authors_attributes(attrs)
attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
super(attrs)
end
But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).
I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).
Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...
The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.
With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.
This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.
If I have a model Paper:
class Paper
include Mongoid::Document
embeds_many :authors
accepts_nested_attributes_for :authors
field :title, type: String
end
And a model Author embedded in Paper:
class Author
include Mongoid::Document
embedded_in :paper, inverse_of: :authors
field :name, type: String
end
I can do this in the console:
> paper = Paper.create(title: "My Paper")
> paper.authors_attributes = [ {name: "Raviolicode"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]
> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]
As you can see, I can update and add authors in the same authors_attributes hash.
For more information see Mongoid nested_attributes article
I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:
accepts_nested_attributes_for :authors, reject_if: :check_author
def check_author(attrs)
if existing = Author.where(label: attrs['label']).first
self.authors << existing
true
else
false
end
end
This still seems like a hack, but it works in Mongoid as well...
I'm new to MongoDB and Mongoid, using Mongoid 3.1.4 for my Rails (3.2.13) app. I saw in the legacy code people using both type: Moped::BSON::ObjectId and type: String somewhere for the referenced model.
Example
class Team
include Mongoid::Document
field :room_id, type: String
field :leader_id, type: Moped::BSON::ObjectId
...
end
(Using git blame I know these 2 lines written by 2 different people)
I did a search but haven't found the answer for the question when/why to use what type? String or Moped::BSON::ObjectId
Thanks,
Both are a bit odd, you'd usually make relations instead of storing the id directly either as a string or an ObjectId:
class Team
include Mongoid::Document
belongs_to :room
belongs_to :leader
...
end
This will also allow you to read the id directly by calling room_id or leader_id.
For more info check out http://mongoid.org/en/mongoid/docs/relations.html or http://two.mongoid.org/docs/relations.html
If I'd have to choose between the two I'd store the ObjectId instead of the string though, it takes less storage space.
I know this is a (somewhat) common question but I'm a newbie and I always appreciate a second (or 100th) pair of eyes on a problem I'm been noodling.
I'm wondering if I'm taking the right approach here -- I'm trying to model a product with several variations and the associated gift certificates sold against that product.
A product, for example, for a restaurant might be a "tasting menu for 2" whereas variations might be "food only", "with partial wine pairing", and "with full wine pairing", etc. (just an illustrative example).
Here's a distilled version of what I've come up with:
class Product
include Mongoid::Document
field :name, type: String
has_many :gift_certificates
embeds_many :variations
end
class Variation
include Mongoid::Document
field :name, type: String
embedded_in :product
has_many :gift_certificates
end
class GiftCertificate
include Mongoid::Document
field :recipient, type: String
field :amount, type: Float
belongs_to :product
belongs_to :variation
end
So when we create a GiftCertificate, it's defined as belonging to both a product and a variation (is that correct?). An example might be a "$200 gift certificate for tasting menu for 2 [product] with partial wine pairing [variation]", etc.
I feel like the embeds_many / embedded_in relation here for Products/Variations is ideal as it fits the profile for when it should be used: I'll never be accessing a product without its variation, and I never need to access a variation independant of a product -- they go hand-in-hand.
So the question is whether I'm doing this the right way.
I've managed to massage the basic CRUD forms into working using ryanb's excellent nested_form gem, with a bit of tweaking. Editing the Products & Variations are easy (the basic use case), but the trouble I'm having comes when working with the Gift Certificates.
Mongoid is giving me a bit of trouble ... the GiftCertificate belonging to both a product and its variation seems kludgy, and I have to manually set the GiftCertificate.variation each time I do a find, i.e.
#gc = GiftCertificate.find(params[:id]) // as normal
#gc.variation // ==> nil, doesn't work by itself
#gc.variation = #gc.product.variations.find(#gc.variation_id) // Have to manually set this
#gc.variation // Now this works as it's supposed to
Anyway, before I go on -- would love to get some thoughts on the above.
Thanks in advance!
I'm trying my first application with mongodb on Rails using mongo_mapper and I'm weighing my options on an STI model like below.
It works fine, and I will of course add to this in more ways than I can currently count, I just curious if I wouldn't be better off with Embedded Documents or some such.
I'd like my models to share as much as possible, IE since they all inherit certain attributes, a shared form partial on property/_form.html.erb... in addition to their own unique form elements etc. I know the views will differ but I'm not sure on the controllers yet, as I could use property controller I assume for most things? And I'm sure it will get more complex as I go along.
Any pointers resources and/or wisdom (pain saving tips) would be greatly appreciated
property.rb
class Property
include MongoMapper::Document
key :name, String, :required => true
key :_type, String, :required => true
key :location_id, Integer, :required => true
key :description, String
key :phone, String
key :address, String
key :url, String
key :lat, Numeric
key :lng, Numeric
key :user_id, Integer, :required => true
timestamps!
end
restaurant
class Restaurant < Property
key :cuisine_types, Array, :required => true
end
bar
class Bar < Property
key :beers_on_tap, Array
end
Don't be afraid of more models, the idea of OO is to be able to cut up your concerns into tiny pieces and then treat each of them in the way they need to be treated.
For example, your Property model seems to be doing a whole lot. Why not split out the geo stuff you've got going on into an EmbeddedDocument (lat, lng, address, etc)? That way your code will remain simpler and more readable.
I use this sort of STI myself and I find it makes my code much simpler and more useable. One of the beauties of using a DB like Mongo is that you can do very complex STI like this and still have a manageable collection of data.
Regarding your cuisine_types and beers_on_tap etc, I think those are fine concepts. It might be useful to have Cuisine and Beer models too, so your database remains more normalized (a concept that is easy to lose in Mongo). e.g.:
class Bar < Property
key :beer_ids, Array
many :beers, :in => :beer_ids
end
class Beer
include MongoMapper:Document
key :name, String
end
Do you expect to return both Restaurants and Bars in the same query?
If not, you might want to reconsider having them derive from a base type.
By default, Mongo_Mapper is going to put both Restaurants and Bars in a single collection. This could hamper performance and make things harder to scale in the future.
Looking through some of the Mongo_Mapper code, it looks like you might be able to set this on the fly with set_collection_name.