Proxy Overview

mongo_objects provides proxy classes to manage MongoDB subdocuments (sub-dictionaries) as their own custom classes. Proxying offers several advantages:

  • Once the parent document is loaded, no additional database access is required to access the subdocuments

  • No data is copied from parent to proxy, so the parent document remains the single source of truth

  • All proxies built from the same parent document object refer to the same in-memory data reducing the risk of duplicate, inconsistent objects.

  • Proxy subdocument classes organize the methods required for the subdocument separate from methods for the parent document itself

Note that parent here refers to the top-level MongoDB document that may contain various levels of subdocuments. No object oriented inheritance between the top-level document and subdocument classes is implied.

Proxy Data Structures

mongo_objects supports three different types of proxies. See the specific class documentation for usage details.

  • MongoDictProxy uses a dictionary to contain a group of related subdocuments:

    {                     # parent document
        {                 # container is a dictionary
            { ... },      # proxies point to individual subdocuments
            { ... },
        }
    }
    
  • MongoListProxy uses a list to contain a group of related subdocuments:

    {                     # parent document
        [                 # container is a list
            { ... },      # proxies point to individual subdocuments
            { ... },
        ]
    }
    
  • MongoSingleProxy references a single subdocument as its own class:

    {                     # parent document
                          # no container; the subdocument is a direct
                          # member of the parent document
        { ... },          # proxy points to a single subdocument
    }
    

You can create as many proxies as needed to describe the data structure of your document. Proxies can also be nested to access subdocuments within subdocuments.

The same proxy may be used as a subdocument in multiple document collections, for example, a MongoSingleProxy Address subdocument might used in the Customer, Vendor and Employee collections.

CRUD Operations

All mongo_objects proxy classes support the full set of CRUD operations.

We’ll use the following classes from the sample code to demonstrate CRUD operations on all proxy types.

# Subdocument classes are typically defined first
# so the parent document class can reference them
class TicketType( mongo_objects.MongoDictProxy ):
    container_name = 'ticket_types'

class Ticket( mongo_objects.MongoListProxy ):
    container_name = 'tickets'

class Venue( mongo_objects.MongoSingleProxy ):
    container_name = 'venue'

class Seat( mongo_objects.MongoListProxy ):
    """a list of seats within the venue subdocument"""
    container_name = 'seats'


# The parent document class
class Events( mongo_objects.MongoUserDict ):
    collection_name = 'events'
    database = ... pymongo database connection object ...

Since the subdocument data is proxied from the parent document, the parent document object must exist first before any proxies can be used.

# Create a new event
event = Event()

Create

Now we can create new subdocuments within the parent document. Unique keys will be auto-assigned to distinguish each MongoDictProxy and MongoListProxy subdocument. MongoSingleProxy doesn’t need unique keys.

tt = TicketTypes.create( event, { 'name' : 'VIP Ticket', ... } )
ticket = Ticket.create( event, { 'name' : 'Fred', ... } )
venue = Venue.create( event, { 'name' : 'Grand Auditorium', ... } )

# as a second-level proxy, seats are created
# using the venue object as the parent
seat = Seat.create( venue, { 'position' : 'A1', ... } )

Since the proxied subdocument data only exists within the parent, saving a subdocument actually saves the entire parent document. These four method calls are identical and save the event object created above.

tt.save()
ticket.save()
venue.save()
seat.save()

Read

If we know the proxy key, we can initialize an instance directly from the parent.

freds_ticket = Ticket( event, '1' )

get_proxy() accomplishes the same thing. For polymorphic subdocument classes you must use get_proxy() to create the correct subclass type.

sallys_ticket = Ticket.get_proxy( event, '2' )

get_proxies() allows us to loop through all the proxies in a container:

for tickets in Ticket.get_proxies( event ):
    ...

Update

Use any standard method of modifying a dictionary to update the data in a proxy object. Call the save() method to save the subdocument. This in turn calls MongoUserDict.save() to save the parent document to the database.

# updating the VIP Ticket subdocument created above
tt['desc'] = "Includes wider seats and a free plushie"
tt.update( { 'cost' : 200 } )
tt.setdefault( 'currency', 'eur' )

tt.save()

Delete

Use delete() to delete a subdocument. By default the parent document is saved so the database is updated immediately.

freds_ticket.delete()

Best Practices

Passing Objects

Each proxy object contains a reference to its immediate parent in the parent attribute. The top-level MongoDB document is referenced in the ultimate_parent attribute.

Instead of passing multiple objects between functions, pass the lowest level proxy object and determine the others based on that. In the example classes above, iterate the ticket types for the event from a seat object with the following code.

# seat.ultimate_parent is an Event object
TicketTypes.get_proxies( seat.ultimate_parent )

Subdocument IDs

Each proxy object has a unique identifier generated by id(). This ID is simply the parent document ObjectId and the proxy key joined with g. For multi-level proxies, the ID for each proxy is appended in order from top-level proxy on down.

This ID can be passed to the class method load_proxy_by_id() to load the parent document and return the proxy object.

ticket_type_id = tt.id()

vip_tickets = Event.load_proxy_by_id( ticket_type_id, TicketType )

It is common to add convenience classmethods to the parent document MongoUserDict class that wrap load_proxy_by_id() for specific subdocuments. For example:

class Event( mongo_objects.MongoUserDict ):

    ... other configuration and code ...

    @classmethod
    def load_ticket_type_by_id( cls, ticket_type_id ):
        """Conveniently load ticket types from an ID"""
        return cls.load_proxy_by_id( ticket_type_id, TicketType )

It is safe to nest multiple levels of proxies. Provide the full set of subdocument classes to load_proxy_by_id() starting with the topmost proxy. We can load a seat proxy with:

# this will return an instance of "Seat"
seat = Event.load_proxy_by_id(
    seatId,
    Venue,   # start with the top-level proxy class
    Seat     # end with the lowest-level proxy class
    )

There is also a parallel set of methods proxy_id() and load_proxy_by_local_id() that omit the parent document ObjectId. These “local IDs” server as convenient subdocument foreign keys within the same document.

Note that for a top-level proxy, the result of proxy_id() is the proxy key. get_proxy() and load_proxy_by_local_id() then perform the same function. Assuming ticketId is the ticket proxy key, the following two lines of code are identical:

ticket = Ticket.get_proxy( event, ticketId )
ticket = Event.load_proxy_by_local_id( ticketId, Ticket )

Polymorphism

Each proxy class has a polymorphic variant that supports returning separate subdocument classes from the same container.

Polymorphism is entirely mix-and-match. A polymorphic parent document may have non-polymorphic proxies and a non-polymorphic parent document may include polymorphic proxies.

Subclass Keys

Each polymorphic subdocument subclass must define a unique proxy subclass key which create() adds to the subdocument. get_proxy() inspects the subclass key and instantiates the correct subclass type.

As a special case, one subclass may define a None key. Usually this is the base class for the rest of the subclass tree. Any subdocuments with a missing or invalid proxy subclass key will be instantiated as this default class.

Note the strong recommendation to define an empty proxy_subclass_map so each set of polymorphic classes use their own namespace for proxy subclass keys.

# create a base proxy class for the container
class Ticket( mongo_objects.PolymorphicMongoListProxy ):
    container_name = 'tickets'

    # Recommended: define an empty proxy_subclass_map in the base class
    # This creates a separate namespace for the polymorphic
    # proxy subclass keys.
    # Otherwise, subclasses will share the base proxy subclass namespace
    # from PolymorphicMongoBaseProxy and risk name collisions with other proxies.
    proxy_subclass_map = {}

    .. your generally useful ticket methods ...

# now create subclasses for each object variation
# each subclass requires a unique key
class OneWayTicket( Ticket ):
    proxy_subclass_key = 'single'

    .. your one-way specific ticket methods ...

class RoundTripTicket( Ticket ):
    proxy_subclass_key = 'return'

    .. your round-trip specific ticket methods ...

class MultiCityTicket( Ticket ):
    proxy_class_key = 'multi'

    .. your multi-city specific ticket methods ...

Creating Subdocuments

Create and save the objects using a subclass.

multi = MultiCityTicket( event )
multi.save()

# save the subdocument ID for later
ticketId = multi.id()

Loading Subdocuments

If you load subdocuments using the base class, all available documents will be returned. The resulting objects will each be an instance of the correct subclass based on the proxy subclass key.

# multi_again is an instance of MultiCityTicket
multi_again = Event.load_proxy_by_id( ticketId, Ticket )

# retrieve all proxies as the correct type
Ticket.get_proxies( event )

If you load subdocuments using a subclass, only those documents of that subclass type will be returned.

# Only retrieve multi-city ticket subdocuments
MultiCityTicket.get_proxies( event )

If a subdocument has a missing or invalid proxy subclass key, an instance of the subclass with a None proxy subclass key is returned. If no such subclass is defined, MongoObjectsPolymorphicMismatch is raised.