Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with Many-to-Many relations in MongoDB when Embedding is not the answer?

Tags:

mongodb

Here's the deal. Let's suppose we have the following data schema in MongoDB:

  • items: a collection with large documents that hold some data (it's absolutely irrelevant what it actually is).
  • item_groups: a collection with documents that contain a list of items._id called item_groups.items plus some extra data.

So, these two are tied together with a Many-to-Many relationship. But there's one tricky thing: for a certain reason I cannot store items within item groups, so -- just as the title says -- embedding is not the answer.

The query I'm really worried about is intended to find some particular groups that contain some particular items (i.e. I've got a set of criteria for each collection). In fact it also has to say how much items within each found group fitted the criteria (no items means group is not found).

The only viable solution I came up with this far is to use a Map/Reduce approach with a dummy reduce function:

function map () {
    // imagine that item_criteria came from the scope.
    // it's a mongodb query object.
    item_criteria._id = {$in: this.items};
    var group_size = db.items.count(item_criteria);
    // this group holds no relevant items, skip it
    if (group_size == 0) return;

    var key = this._id.str;
    var value = {size: group_size, ...};

    emit(key, value);
}

function reduce (key, values) {
    // since the map function emits each group just once,
    // values will always be a list with length=1
    return values[0];
}

db.runCommand({
    mapreduce: item_groups,
    map: map,
    reduce: reduce,
    query: item_groups_criteria,
    scope: {item_criteria: item_criteria},
});

The problem line is:

item_criteria._id = {$in: this.items};

What if this.items.length == 5000 or even more? My RDBMS background cries out loud:

SELECT ... FROM ... WHERE whatever_id IN (over 9000 comma-separated IDs)

is definitely not a good way to go.

Thank you sooo much for your time, guys!

I hope the best answer will be something like "you're stupid, stop thinking in RDBMS style, use $its_a_kind_of_magicSphere from the latest release of MongoDB" :)

like image 663
Ewan Doe Avatar asked Aug 09 '11 00:08

Ewan Doe


2 Answers

I think you are struggling with the separation of domain/object modeling from database schema modeling. I too struggled with this when trying out MongoDb.

For the sake of semantics and clarity, I'm going to substitute Groups with the word Categories

Essentially your theoretical model is a "many to many" relationship in that each Item can belong Categories, and each Category can then possess many Items.

This is best handled in your domain object modeling, not in DB schema, especially when implementing a document database (NoSQL). In your MongoDb schema you "fake" a "many to many" relationship, by using a combination of top-level document models, and embedding.

Embedding is hard to swallow for folks coming from SQL persistence back-ends, but it is an essential part of the answer. The trick is deciding whether or not it is shallow or deep, one-way or two-way, etc.


Top Level Document Models

Because your Category documents contain some data of their own and are heavily referenced by a vast number of Items, I agree with you that fully embedding them inside each Item is unwise.

Instead, treat both Item and Category objects as top-level documents. Ensure that your MongoDb schema allots a table for each one so that each document has its own ObjectId.

The next step is to decide where and how much to embed... there is no right answer as it all depends on how you use it and what your scaling ambitions are...

Embedding Decisions

1. Items

At minimum, your Item objects should have a collection property for its categories. At the very least this collection should contain the ObjectId for each Category.

My suggestion would be to add to this collection, the data you use when interacting with the Item most often...

For example, if I want to list a bunch of items on my web page in a grid, and show the names of the categories they are part of. It is obvious that I don't need to know everything about the Category, but if I only have the ObjectId embedded, a second query would be necessary to get any detail about it at all.

Instead what would make most sense is to embed the Category's Name property in the collection along with the ObjectId, so that pulling back an Item can now display its category names without another query.

The biggest thing to remember is that the key/value objects embedded in your Item that "represent" a Category do not have to match the real Category document model... It is not OOP or relational database modeling.

2. Categories

In reverse you might choose to leave embedding one-way, and not have any Item info in your Category documents... or you might choose to add a collection for Item data much like above (ObjectId, or ObjectId + Name)...

In this direction, I would personally lean toward having nothing embedded... more than likely if I want Item information for my category, i want lots of it, more than just a name... and deep-embedding a top-level document (Item) makes no sense. I would simply resign myself to querying the database for an Items collection where each one possesed the ObjectId of my Category in its collection of Categories.

Phew... confusing for sure. The point is, you will have some data duplication and you will have to tweak your models to your usage for best performance. The good news is that that is what MongoDb and other document databases are good at...

like image 108
one.beat.consumer Avatar answered Oct 30 '22 19:10

one.beat.consumer


Why don't use the opposite design ?

You are storing items and item_groups. If your first idea to store items in item_group entries then maybe the opposite is not a bad idea :-)

Let me explain:

in each item you store the groups it belongs to. (You are in NOSql, data duplication is ok!) for example, let's say you store in item entries a list called groups and your items look like : { _id : .... , name : .... , groups : [ ObjectId(...), ObjectId(...),ObjectId(...)] }

Then the idea of map reduce takes a lot of power :

map = function()  {
    this.groups.forEach( function(groupKey) {
        emit(groupKey, new Array(this))
    }
}


reduce = function(key,values) {
   return Array.concat(values);
}


db.runCommand({
   mapreduce : items,
   map : map,
   reduce : reduce,
   query : {_id :  {$in : [...,....,.....] }}//put here you item ids
})

You can add some parameters (finalize for instance to modify the output of the map reduce) but this might help you.

Of course you need to have another collection where you store the details of item_groups if you need to have it but in some case (if this informations about item_groups doe not exist, or don't change, or you don't care that you don't have the most updated version of it) you don't need them at all !

Does that give you a hint about a solution to your problem ?

like image 33
kamaradclimber Avatar answered Oct 30 '22 19:10

kamaradclimber