I'm relatively new to Mongoid/MongoDB and I have a question about how to model a specific many-to-many relationship.
I have a User model and a Project model. Users can belong to many projects, and each project membership includes one role (eg. "administrator", "editor", or "viewer"). If I were using ActiveRecord then I'd set up a many-to-many association between User and Project using has_many :through and then I'd put a field for role in the join table.
What is a good way to model this scenario in MongoDB and how would I declare that model with Mongoid? The example below seems like a good way to model this, but I don't know how to elegantly declare the relational association between User and the embedded ProjectMembership with Mongoid.
Thanks in advance!
db.projects.insert(
{
"name" : "Test project",
"memberships" : [
{
"user_id" : ObjectId("4d730fcfcedc351d67000002"),
"role" : "administrator"
},
{
"role" : "editor",
"user_id" : ObjectId("4d731fe3cedc351fa7000002")
}
]
}
)
db.projects.ensureIndex({"memberships.user_id": 1})
Modeling a good Mongodb schema really depends on how you access your data. In your described case, you will index your memberships.user_id key which seems ok. But your document size will grow as you will add viewers, editors and administrators. Also, your schema will make it difficult to make querys like:
Query projects, where user_id xxx is editor:
Again, you maybe do not need to query projects like this, so your schema looks fine. But if you need to query your projects by user_id AND role, i would recommend you creating a 'project_membership' collection :
db.project_memberships.insert(
{
"project_id" : ObjectId("4d730fcfcedc351d67000032"),
"editors" : [
ObjectId("4d730fcfcedc351d67000002"),
ObjectId("4d730fcfcedc351d67000004")
],
"viewers" : [
ObjectId("4d730fcfcedc351d67000002"),
ObjectId("4d730fcfcedc351d67000004"),
ObjectId("4d730fcfcedc351d67000001"),
ObjectId("4d730fcfcedc351d67000005")
],
"administrator" : [
ObjectId("4d730fcfcedc351d67000011"),
ObjectId("4d730fcfcedc351d67000012")
]
}
)
db.project_memberships.ensureIndex({"editors": 1})
db.project_memberships.ensureIndex({"viewers": 1})
db.project_memberships.ensureIndex({"administrator": 1})
Or even easier... add an index on your project schema:
db.projects.ensureIndex({"memberships.role": 1})
Related
I have an application with "Categories" "Players" & "Matches".
I want to create a new Table: "Suggestions", with the params:
name: string,
description: text,
type: integer,
element_id: integer
Users will be able to create "Suggestions" for a Player, Category or a Match. type:integer will indicates what type of suggestion is this
"Category Suggestion" -> 1
"Player Suggestion" -> 2
"Match Suggestion" -> 3
My question is, how do I reference multiple tables with only one foreign key(element_id)? This foreign key can be from "Category", "Player" or "Match", depending the "type" of the Suggestion.
I thought about a solution in the models where I just place the foreign key there, but I'm not sure if this can cause problems in my application.
Also I thought about creating 3 tables CategorySuggestions, PlayersSuggestions and MatchesSuggestions but that would just be overkill and I wouldn't like that so much.
What you are looking for is Polymorphic Associations. You can add something like,
class Suggestion
...
belongs_to :suggestable, polymorphic: true
...
end
In the database, you'll add two columns to the suggestions table.
t.bigint :suggestable_id
t.string :suggestable_type
Then you can simply use suggestion.suggestable which will give you the corresponding object of the correct type, without having to manage any of the types or integers yourself.
So I have product with multiple features, and features belong_to a feature_key and a feature_value.
Here is the query I'm running:
Product.family_features.joins(:feature_key, :feature_value)
.where(feature_keys: { name: ["Color", "Size"] }, feature_values: { name: ["Red", "Large"] })
.group_by(&:product_id)
Note: .family_features is a scope on Product that gets all the features related to the product's "family" -- which is a group of other products.
Now I want only one product that has a combination of features that have "color" => "red" and "size" => "large"
But I get features that have "color" => "red" and other sizes, as well as features with "size" => "large" and other colors.
Is there a way to constrain this query to the exact combination of values I need?
The relations look like this:
Product has_many :features
Feature belongs_to :product, :feature_key, :feature_value
FeatureKey has_many :features, :products (through features)
FeatureValue has_many :features, :products (through features)
You should be able to do this using arel.
fkt, fvt = FeatureKey.arel_table, FeatureValue.arel_table
color_red = fkt[:name].eq('Color').and(fvt[:name].eq('Red'))
size_large = fkt[:name].eq('Size').and(fvt[:name].eq('Large'))
Feature.joins(:feature_key, :feature_value)
.where(color_red.or(size_large))
.group(:product_id)
I have not tested the code, but I think it should work.
Check the arel documentation for more details.
Note that I also changed the group_by to group. group_by is the Enumerable module Ruby method, while group is the ActiveRecord relation method performing an SQL GROUP BY, which should be much faster than grouping in Ruby.
Edit:
Though your comment is saying that it worked, if you need all products that have size => large and color => red combination, I do not think this was what you were looking for.
You could try something like this:
red_products = Product.joins(features: [:feature_key, :feature_value]).where({feature_keys: { name: 'color' }, feature_values: { name: 'red' }})
large_products = Product.joins(features: [:feature_key, :feature_value]).where({feature_keys: { name: 'size' }, feature_values: { name: 'large' }})
products = red_products & large_products
The issue with the above solution is that & is the array intersection in Ruby, therefore it will make two DB queries, and intersect the result in Ruby, which could become a problem for large datasets.
Fast Example,
class Band
include Mongoid::Document
embeds_many :albums
end
class Album
include Mongoid::Document
field :name, type: String
embedded_in :band
end
and the document will look like this,
{
"_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
"albums" : [
{
"_id" : ObjectId("4d3ed089fb60ab534684b7e0"),
"name" : "Violator",
}
]
}
lets say, i want to make a method to find the Band with albums name
if this was ActiveRecord, it is simple
Album.find_by(name: "Violator").band
but what about like this situation?
Do i have to iterate the whole collection and find it like this?
Band.select {|band| band.albums.select{|album| album.name == "Violator"}}
Sounds crazy...
Or do i have to do the data modeling with Referenced relations not Embedded relations?
Embedded documents are best for items which don't need to query independently. If you need something to query independently, then consider using references. In your case, you can better find bands first by using specific album name and then process these bands
#bands = Band.where("albums.name" => "Violator")
#albums = #bands.collect{|band| band.albums.where(name: 'Violator') }.flatten
Here are more details on mongoid relations http://mongoid.org/en/mongoid/docs/relations.html
I've a rails app with a full-text search based on Elasticsearch and Tire, it is already working on a MongoDB model called Category, but now I want to add a more complex search based on MongoID Embedded 1-n model User which embeds_many :watchlists
Now I have to bulk import and indexing all the field in Watchlist, and I'd like to know :
how can I do that ?
can index just the watchlists children fields, without the user parents fields ?
The Embedded 1-N MongoDB/MongoID model looks like the following :
app/models/user.rb ( the parent ) :
class User
include Mongoid::Document
include Tire::Model::Search
include Tire::Model::Callbacks
index_name 'users'
field :nickname
field ... many others
embeds_many :watchlists
end
app/models/watchlist.rb ( the embedded "many" childrens ) :
class Watchlist
include Mongoid::Document
include Tire::Model::Search
include Tire::Model::Callbacks
index_name 'watchlists'
field :html_url
embedded_in :user
end
Any suggestion on how to accomplish the task ?
UPDATE:
here it is a chunk of the model seen with mongo shell
> user = db.users.findOne({'nickname': 'lgs'})
{
"_id" : ObjectId("4f76a16cf2a6a12f88cbca43"),
"encrypted_password" : "",
"sign_in_count" : 0,
"provider" : "github",
"uid" : "1573",
"name" : "Luca G. Soave",
"email" : "luca.soave#gmail.com",
"nickname" : "lgs",
"watchlists" : [
{
"_id" : ObjectId("4f76997f1d41c81173000002"),
"tags_array" : [ git, peristence ],
"html_url" : "https://github.com/mojombo/grit",
"description" : "Grit gives you object oriented read/write access to Git repositories via Ruby.",
"fork_" : false,
"forks" : 207,
"watchers" : 1258,
"created_at" : ISODate("2007-10-29T14:37:16Z"),
"pushed_at" : ISODate("2012-01-27T01:05:45Z"),
"avatar_url" : "https://secure.gravatar.com/avatar/25c7c18223fb42a4c6ae1c8db6f50f9b?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png"
},
...
...
}
I'd like to index & query any fields owned by the embedded child watchlists doc :
... "tags_array", "html_url", "description", "forks"
but I don't want elasticsearch to include the parent user fields :
... "uid", "name", "email", "nickname"
so that when I query for "git persistence", it will look into each 'watchlists' indexed fields of each 'user' of the original MongoDB.
(sorry for mismatching singular and plurals here, I was just indicating the doc object names)
It really depends on how you want to serialize your data for the search engine, based on how you want to query them. Please update the question and I'll update the answer. (Also, it's better to just remove the ES logs, they are not relevant here.)
I'm not sure how the Rake task works with embedded documents in Mongo, and also why it seems to "hang" at the end. Is your data in the "users" index when you run the task?
Notice that it's quite easy to provide your own indexing code, when the Rake task is not flexible enough. See the Tire::Index#import integration tests.
My question is this, I have my postgres database called gas_stations and only has a field called name, and I need that the form shows all the data in combobox from the table. also one of the options should appear "new station" and the truth I don't know how to do this
I think you tale about table and not database. To do this, you must to create a model called GasStation like this :
class GasStation < ActiveRecord::Base
end
After that, you could create a list in your view like this :
select("gas_station", "name", GasStation.all.collect {|gs| [ gs.name, gs.id ] }, { :include_blank => true })
See the guide here : http://guides.rubyonrails.org/getting_started.html.
select("form_name", "gas_station_id", GasStation.all.collect {|g| [ g.name, g.id ] }, { :include_blank => true })
Something like this should get you started. Of course you need to create the Model etc ...
If you don't know how, use a quick tutorial to learn rails basics!