A recipe to represent a Directed Graph between different Entities (models), such that different connection types are supported. Allows for easy querying on connections. For example a social graph with Users and Images as entities; the connections are Follow and Like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | from google.appengine.ext import db
from google.appengine.ext.db import polymodel
_connection_model_superclass = polymodel.PolyModel
class ConnectionModelMetaclass(type(_connection_model_superclass)):
def __new__(cls, name, bases, dct):
myname = name.replace('ConnectionModel','').lower()
if myname:
#this is not the baseclass
to_collection_name = 'myto_%s_connections' % myname #or any other naming scheme you like
from_collection_name = 'myfrom_%s_connections' % myname #or any other naming scheme you like
myto = 'myto_%s'%myname
myfrom = 'myfrom_%s'%myname
dct[myto] = db.ReferenceProperty(collection_name = to_collection_name)
dct[myfrom] = db.ReferenceProperty(collection_name = from_collection_name)
if 'put' in dct:
myput = dct['put']
else:
myput = None
def put(self):
setattr(self, myto, self.myto)
setattr(self, myfrom, self.myfrom)
self._validate_connected_types()
if myput is not None:
myput(self)
else:
MyClass = eval(name)
super(MyClass, self).put()
dct['put'] = put
return super(ConnectionModelMetaclass, cls).__new__(cls, name, bases, dct)
class ConnectionModel(_connection_model_superclass):
__metaclass__ = ConnectionModelMetaclass
ALLOWED_CONNECTIONS = {}#empty dict means anything goes. dict if of kind tuple->tuple
timestamp = db.DateTimeProperty(auto_now = True)
myto = db.ReferenceProperty(collection_name = 'myto_connections')
myfrom = db.ReferenceProperty(collection_name = 'myfrom_connections')
connection_index = db.StringProperty()#for strict sorting and paging of connections
def _validate_connected_types(self):
if None in (self.myfrom, self.myto):
raise AttributeError
if not self._check_connection():
raise AttributeError(\
'Connection %s --> %s is not allowed for class %s',
self.myfrom.__class__.__name__,
self.myto.__class__.__name__,
self.__class__)
def _check_connection(self):
if len(self.ALLOWED_CONNECTIONS) == 0:
return True
for froms, tos in self.ALLOWED_CONNECTIONS.iteritems():
if isinstance(self.myfrom, froms):
if isinstance(self.myto, tos):
return True
return False
def put(self):
if not self.connection_index:
self.connection_index = '%s|%s|%s' % \
(self.timestamp, self.myfrom.key().name(),\
self.myto.key().name())
super(ConnectionModel, self).put()
class LikeConnectionModel(ConnectionModel):
ALLOWED_CONNECTIONS = {UserModel : ImageModel}
class FollowConnectionModel(ConnectionModel):
ALLOWED_CONNECTIONS = {UserModel : (UserModel, ImageModel) }#users can follow users and (what the heck) follow images
|
This structure is a scalable(i hope) way of representing a graph with many connection types on GAE
For instance: user = UserModel.get(some_key) #get the Follow connections of the user query = user.myfrom_follow_connections #get any connection in which the user is the "from" query = user.myfrom_connections
For me the advantage of this approach lies in the polymorphism and extensibility. This is preferable than having each entity hold lists of keys for other entities. For instance creating a CommentConnectionModel is a 2-line class instead of adding a ListProperty on the UserModel. If we had for instance:
class CategoryConnectionModel(ConnectionModel):
pass
class SubCategory1ConnectionModel(ConnectionModel):
pass
class SubCategory2ConnectionModel(ConnectionModel):
pass
we could query a user for each of the following:
user.myfrom_subcategory1_connections # return subcategory1 connection
user.myfrom_subcategory2_connections # return subcategory2 connections
user.myfrom_category_connections # return any (nonstrict)subclasses of CategoryConnectionModel
the ALLOWED_CONNECTIONS class property allows you to enforce that only connections of logical types can be created, and hopefully reduce/prevent some bugs.
Another note: the reason for this code in the metaclass
dct[myto] = db.ReferenceProperty(collection_name = to_collection_name)
is that i wanted to be able to use the reverse reference set that comes with ReferenceProperty for any node in the connection class inheritance tree. it might be a bit wasteful but imo it is worth it since the cost is not very high. (2 additional properties per inheritance depth)
Hope this is of use to anyone.
Reading:
https://developers.google.com/appengine/docs/python/datastore/polymodelclass https://developers.google.com/appengine/docs/python/datastore/typesandpropertyclasses#ReferenceProperty