Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Copyright (c) 2012 Kenneth Falck <[email protected]> http://kfalck.net
Copyright (c) 2013 Merchant Atlas Inc. http://www.merchantatlas.com

[Original Work] Copyright (c) 2012 Kenneth Falck <[email protected]> http://kfalck.net
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Expand Down
126 changes: 109 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,176 @@
Django MongoDB ManyToManyField Implementation
=============================================

Created in 2012 by Kenneth Falck <[email protected]> http://kfalck.net.
Created in 2012 by Kenneth Falck, Modified/Extended by:
* Merchant Atlas Inc. 2013

Released under the standard BSD License (see below).

Overview
--------

This is a simple implementation of ManyToManyFields for django-mongodb-engine. The MongoDBManyToManyField
This is a simple implementation of ManyToManyFields for django-mongodb-engine. The _MongoDBManyToManyField_
stores references to other Django model instances as ObjectIds in a MongoDB list field.

Optionally, MongoDBManyToManyField will also embed a "cached" copy of the instances inside the list. This
Optionally, _MongoDBManyToManyField_ will also embed a "cached" copy of the instances inside the list. This
allows fast access to the data without having to query each related object from the database separately.

MongoDBManyToManyField attempts to work mostly in the same way as Django's built-in ManyToManyField.
_MongoDBManyToManyField_ attempts to work mostly in the same way as Django's built-in ManyToManyField.
Related objects can be added and removed with the add(), remove(), clear() and create() methods.

To enumerate the objects, the all() method returns a simulated QuerySet object which loads non-embedded
objects automatically from the database when needed.

On the reverse side of the relation, an accessor property is added (usually called OtherModel.modelname\_set,
On the reverse side of the relation, an accessor property is added (usually called OtherModel.modelname\_set,
can be overridden with the related\_name attribute) to return the related objects in the reverse direction.
It uses MongoDB's raw\_query() to find all related model objects. Because of this, any data model that
uses MongoDBManyToManyField() must have a default MongoDBManager() instead of Django's normal Manager().


Django compability
------------------
Django compatibility
--------------------

This implementation has been tweaked to be mostly compatible with Django admin, which means you can use
TabularInlines or filter\_horizontal and filter\_vertical to administer the many-to-many fields.

Don't be surprised, however, if some things don't work, because it's all emulated. There is no real
"through" table in the database to provide the many-to-many association.

Supported version: [django-nonrel-1.6](https://github.com/django-nonrel/django/tree/nonrel-1.6)

Usage
-----

### Define a field
Example model using a many-to-many field:

from django.db import models
from mongom2m.fields import MongoDBManyToManyField
from django_mongom2m.fields import MongoDBManyToManyField
from django_mongodb_engine.contrib import MongoDBManager

class Category(models.Model):
objects = MongoDBManager()
title = models.CharField(max_length=254)

class Article(models.Model):
objects = MongoDBManager()
categories = MongoDBManyToManyField(Category)
title = models.CharField(max_length=254)
text = models.TextField()

### Add an instance
To store categories in the field, you would first create the category and then add it:

category = Category(title='foo')
category.save()

article = Article(title='bar')
article.categories.add(category)
article.save()


for cat in article.categories.all():
print cat.title

for art in category.article_set.all():
print art.title

### Basic Querying
Querying with _MongoDBManyToManyField_ is similar to normal SQL Django, but with some caveats.
In order to have true Django behavior, we would have had to change some combination of: Django-nonrel,
Djangotoolbox, and mongodb-engine; so instead, we went a different route.

How Django does it:

Article.objects.filter(categories=category)
[<Article: Article object>, <Article: Article object>]

article.categories.all()
[<Category: Category object>, <Category: Category object>]

How _MongoDBManyToManyField_ does it:

Article.categories.filter(pk=category)
[<Article: Article object>, <Article: Article object>]

article.categories.all()
[<Category: Category object>, <Category: Category object>]

### Embed Models for Performance and Querying
To enable embedding, just add the embed=True keyword argument to the field:

class Article(models.Model):
categories = MongoDBManyToManyField(Category, embed=True)
categories = _MongoDBManyToManyField_(Category, embed=True)

**Note about embedding**: If you change a _MongoDBManyToManyField_ from `embed=False` to
`embed=True` (or vice versa), the host model objects will not be automatically updated.
You will need to re-save every object of that model.
Example:

for article in Article.objects.all():
article.save() # Re-saving will now embed the categories automatically

### Query with or without cache
To query with or without cache, just passing `use_cached=<True or False>` argument to supported query.

Examples:

# return all instances in cache
article.categories.all(use_cached=False)
# return a list of ids
# to be compatible with admin site, values_list use `use_cached=False` by default
article.categories.values_list('pk', flat=True)

### Refresh cache
To remove instances already deleted by other other actions:

article.categories.remove_nonexists()

To refresh cache and remove instances already deleted:

article.categories.reload_from_db()

### Advanced Querying (Embedded models)
If you use `embed=True`, _MongoDBManyToManyField_ can do more than just query on 'pk'.
You can do any of: get, filter, and exclude; while using Q objects and A objects
(from mongodb-engine). _MongoDBManyToManyField_ will automatically convert them to
query correctly on the embedded document.
Note: The models have to be embedded because MongoDB doesn't support joins.

Article.categories.filter(title="shirts")
Article.categories.filter(Q(title="shirts") | Q(title="hats"))
Article.categories.filter(Q(title="shirts") & ~Q(title="men's"))

# If categories had an embedded model 'em', you could even query it with A()
Article.categories.filter(em=A("name", "em1"))

### Limitations
There are some things that won't work with _MongoDBManyToManyField_:
#### Chaining multiple filters or excludes together
Under the covers, the initial filter is being called on the Article QuerySet, so what
gets returned is an Article QuerySet. That means calling filter() again will not get
any of the magic provided by _MongoDBManyToManyField_.

# filter(title="hats").exclude(title="") will act on Article, not categories
Article.categories.filter(title="shirts").filter(title="hats").exclude(title="")
# However, the same can be accomplished with Q objects
Article.categories.filter(Q(title="shirts") & Q(title="hats") & ~Q(title=""))

#### Double-underscore commands
This is actually an issue with djangotoolbox/mongobd-engine. Under the covers,
_MongoDBManyToManyField_ uses A() objects to generate the queries. Double-underscore
commands from A() objects don't work.

# Will query the field 'title__contains', which doesn't exist
Article.categories.filter(title__contains="men")

# Unfortunately, the only solution for this would be to do a raw_query
import re
Article.objects.raw_query({"categories.title": re.compile("men")})


Signals
-------

MongoDBManyToManyField supports Django's m2m\_changed signal, where the action can be:
_MongoDBManyToManyField_ supports Django's m2m\_changed signal, where the action can be:

* pre\_add (triggered before adding object(s) to the field)
* post\_add (triggered after adding object(s) to the field)
Expand Down Expand Up @@ -136,8 +227,9 @@ Also make sure that the "id" field is properly indexed (see previous section).

BSD License
-----------
Copyright (c) 2013 Merchant Atlas Inc. http://www.merchantatlas.com

Copyright (c) 2012 Kenneth Falck <[email protected]> http://kfalck.net
[Original Work] Copyright (c) 2012 Kenneth Falck <[email protected]> http://kfalck.net
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Expand Down
16 changes: 16 additions & 0 deletions django_mongom2m/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import django_mongodb_engine.query

from . import fields
from . import manager
from . import query


# How much to show when query set is viewed in the Python shell
REPR_OUTPUT_SIZE = 20

# Sort of hackish, but they left me no choice! Without this, 'A' objects are
# rejected for this field because it's not in "DJANGOTOOLBOX_FIELDS"
django_mongodb_engine.query.DJANGOTOOLBOX_FIELDS += \
(fields.MongoDBManyToManyField,)

163 changes: 163 additions & 0 deletions django_mongom2m/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from django.db import models
from django.db.models.fields.related import add_lazy_relation, RelatedObject
from django.db.models.query_utils import DeferredAttribute
from django_mongodb_engine.query import A
from djangotoolbox.fields import ListField, EmbeddedModelField

from .manager import (MongoDBManyToManyRel, MongoDBM2MRelatedManager,
MongoDBM2MReverseDescriptor,
MongoDBManyToManyRelationDescriptor)
from .utils import create_through


class MongoDBManyToManyField(models.ManyToManyField, ListField):
"""
A generic MongoDB many-to-many field that can store embedded copies of
the referenced objects. Inherits from djangotoolbox.fields.ListField.

The field's value is a MongoDBM2MRelatedManager object that works similarly to Django's
RelatedManager objects, so you can add(), remove(), creaet() and clear() on it.
To access the related object instances, all() is supported. It will return
all the related instances, using the embedded copies if available.

If you want the 'real' related (non-embedded) model instances, call all_objs() instead.
If you want the list of related ObjectIds, call all_refs() instead.

The related model will also gain a new accessor method xxx_set() to make reverse queries.
That accessor is a MongoDBM2MReverseManager that provides an all() method to return
a QuerySet of related objects.

For example, if you have an Article model with a MongoDBManyToManyField 'categories'
that refers to Category objects, you will have these methods:

article.categories.all() - Returns all the categories that belong to the article
category.article_set.all() - Returns all the articles that belong to the category
"""
description = 'ManyToMany field with references and optional embedded objects'
generate_reverse_relation = False
requires_unique_target = False

def __init__(self, to, related_name=None, embed=False, *args, **kwargs):
# Call Field, not super, to skip Django's ManyToManyField extra stuff
# we don't need
self._mm2m_to_or_name = to
self._mm2m_related_name = related_name
self._mm2m_embed = embed
if embed:
item_field = EmbeddedModelField(to)
else:
item_field = None
ListField.__init__(self, item_field, *args, **kwargs)

def contribute_after_resolving(self, field, to, model):
# Setup the main relation helper
self.rel = MongoDBManyToManyRel(self, to, self._mm2m_related_name,
self._mm2m_embed)
# The field's default value will be an empty MongoDBM2MRelatedManager
# that's not connected to a model instance
self.default = MongoDBM2MRelatedManager(self, self.rel,
self._mm2m_embed)
self.rel.model = model
self.rel.through = create_through(self, self.rel.model, self.rel.to)
# Determine related name automatically unless set
if not self.rel.related_name:
self.rel.related_name = model._meta.object_name.lower() + '_set'

# Add the reverse relationship
setattr(self.rel.to, self.rel.related_name,
MongoDBM2MReverseDescriptor(model, self, self.rel,
self.rel.embed))
# Add the relationship descriptor to the model class for Django
# admin/forms to work
setattr(model, self.name,
MongoDBManyToManyRelationDescriptor(self, self.rel.through))
#TODO: deprecated self.related used in django nonrel-1.6, remove later
other = self.rel.to
self.do_related_class(other, model)


def do_related_class(self, other, cls):
self.related = RelatedObject(other, cls, self)

def contribute_to_class(self, model, name):
self.__m2m_name = name
# Call Field, not super, to skip Django's ManyToManyField extra stuff
# we don't need
ListField.contribute_to_class(self, model, name)
# Do the rest after resolving the 'to' relation
add_lazy_relation(model, self, self._mm2m_to_or_name,
self.contribute_after_resolving)

def db_type(self, *args, **kwargs):
return 'list'

def get_internal_type(self):
return 'ListField'

def formfield(self, **kwargs):
from django import forms
db = kwargs.pop('using', None)
defaults = {
'form_class': forms.ModelMultipleChoiceField,
'queryset': self.rel.to._default_manager.using(db).complex_filter(
self.rel.limit_choices_to)
}
defaults.update(kwargs)
# If initial is passed in, it's a list of related objects, but the
# MultipleChoiceField takes a list of IDs.
if defaults.get('initial') is not None:
initial = defaults['initial']
if callable(initial):
initial = initial()
defaults['initial'] = [i._get_pk_val() for i in initial]
return models.Field.formfield(self, **defaults)

def pre_save(self, model_instance, add):
return self.to_python(getattr(model_instance, self.attname))

#get_db_prep_save changed for ListField from djangotoolbox 1.6.2
#using Field.get_db_prep_save which is compitable
def get_db_prep_save(self, value, connection):
"""
Returns field's value prepared for saving into a database.
"""
return self.get_db_prep_value(value, connection=connection,
prepared=False)

def get_db_prep_lookup(self, lookup_type, value, connection,
prepared=False):
# This is necessary because the ManyToManyField.get_db_prep_lookup will
# convert 'A' objects into a unicode string. We don't want that.
if isinstance(value, A):
return value
else:
return models.ManyToManyField.get_db_prep_lookup(
self, lookup_type, value, connection, prepared)

def get_db_prep_value(self, value, connection, prepared=False):
# The Python value is a MongoDBM2MRelatedManager, and we'll store the
# models it contains as a special list.
if not isinstance(value, MongoDBM2MRelatedManager):
# Convert other values to manager objects first
value = MongoDBM2MRelatedManager(self, self.rel,
self.rel.embed, value)
# Let the manager to the conversion
return value.get_db_prep_value(connection, prepared)

def to_python(self, value):
# The database value is a custom MongoDB list of ObjectIds and embedded
# models (if embed is enabled). We convert it into a
# MongoDBM2MRelatedManager object to hold the Django models.

if not isinstance(value, MongoDBM2MRelatedManager) and \
not isinstance(value, DeferredAttribute):
manager = MongoDBM2MRelatedManager(self, self.rel, self.rel.embed)
manager.to_python(value)
value = manager
return value






Loading