Time for action defining new relations: how it should look

The following sample code shows how we would use a Relation class (also available in relation.py):

Chapter7/relation.py

from os import unlink
db="/tmp/abcr.db"
try:
	unlink(db)
except:
	pass
class Entity(AbstractEntity):
	database=db
class Relation(AbstractRelation):
	database=db
class A(Entity): pass
class B(Entity): pass
class AB(Relation):
	a=A
	b=B
a1=A()
a2=A()
b1=B()
b2=B()
a1.add(b1)
a1.add(b2)
print(a1.get(B))
print(b1.get(A))

What just happened?

After defining a few entities, defining the relation between those entities follows the same pattern: we define a Relation class that is a subclass of AbstractRelation to establish a reference to a database that will be used.

Then we define an actual relation between two entities by subclassing Relation and defining two class variables, a and b that refer to the Entity classes that form each half of the relation.

If we instantiate a few entities, we may then define a few relations between these instances by using the add() method and retrieve related entities with the get() method.

Note that those methods are called on the Entity instances, allowing for a much more natural idiom than the use of class methods in the Relation class. These add() and get() methods were added to those entity classes by the MetaRelation metaclass, and in the next section, we will see how this is accomplished.

The class diagram for relation classes looks nearly identical to the one for entities:

What just happened?

Implementing the MetaRelation and AbstractRelation classes

The implementation of the AbstractRelation class is very minimalistic because it is only used to create some thread local storage and establish a relation with the MetaRelation metaclass:

Chapter7/relation.py

class AbstractRelation(metaclass=MetaRelation):
	_local = threading.local()

No methods are specified since the metaclass will take care of adding suitable methods to the entity class that are a part of this relationship.

The MetaRelation class has two goals: creating a database table that will hold records for each individual relation and adding methods to the entity classes involved, so that relations can be created, removed, and queried:

Chapter7/relation.py

class MetaRelation(type):
	@staticmethod
	def findattr(classes,attribute):
		a=None
		for c in classes:
			if hasattr(c,attribute):
				a=getattr(c,attribute)
				break
		if a is None:
			for c in classes:
				a = MetaRelation.findattr(c.__bases__,attribute)
				if not a is None:
					break
		return a
	def __new__(metaclass,classname,baseclasses,classdict):
		def connect(cls):
			if not hasattr(cls._local,'conn'):
				cls._local.conn=sqlite.connect(cls._database)
				cls._local.conn.execute('pragma foreign_keys = 1')
				cls._local.conn.row_factory = sqlite.Row
			return cls._local.conn
		def get(self,cls):
			return getattr(self,'get'+cls.__name__)()
		def getclass(self,cls,relname):
			clsname = cls.__name__
			sql = 'select %s_id from %s where %s_id = ?'%(
				clsname,relname,self.__class__.__name__)
			cursor=self._connect().cursor()
			cursor.execute(sql,(self.id,))
			return [cls(id=r[clsname+'_id']) for r in cursor]
		def add(self,entity):
			return getattr(self,
				'add'+entity.__class__.__name__)(entity)
		def addclass(self,entity,Entity,relname):
			if not entity.__class__ == Entity :
				raise TypeError(
					'entity not of the required class')
			sql = 'insert or replace into %(rel)s '
			sql+= '(%(a)s_id,%(b)s_id) values (?,?)'
			sql%= { 'rel':relname,
				'a':self.__class__.__name__,
				'b':entity.__class__.__name__}
			with self._connect() as conn:
				cursor = conn.cursor()
				cursor.execute(sql,(self.id,entity.id))
		relationdefinition = False
		if len(baseclasses):
			if not 'database' in classdict:
				classdict['_database']=MetaRelation.findattr(
					baseclasses,'database')
				if classdict['_database'] is None:
					raise AttributeError(
						'''subclass of AbstractRelation has no
						database class variable''')
					relationdefinition=True
				if not '_local' in classdict:
					classdict['_local']=MetaRelation.findattr(
						baseclasses,'_local')
				classdict['_connect']=classmethod(connect)
				if relationdefinition:
					a = classdict['a']
					b = classdict['b']
					if not issubclass(a,AbstractEntity) :
						raise TypeError('a not an AbstractEntity')
					if not issubclass(a,AbstractEntity) :
						raise TypeError('b not an AbstractEntity')
						sql = 'create table if not exists %(rel)s '
						sql+= '( %(a)s_id references %(a)s '
						sql+= 'on delete cascade, '
						sql+= '%(b)s_id references %(b)s '
						sql+= 'on delete cascade, '
						sql+= 'unique(%(a)s_id,%(b)s_id))'
						sql%= { 'rel':classname,
							'a':a.__name__,
							'b':b.__name__}
						conn = sqlite.connect(classdict['_database'])
						conn.execute(sql)
						setattr(a,'get'+b.__name__,
							lambda self:getclass(self,b,classname))
						setattr(a,'get',get)
						setattr(b,'get'+a.__name__,
							lambda self:getclass(self,a,classname))
						setattr(b,'get',get)
						setattr(a,'add'+b.__name__,
						lambda self,entity:addclass(self,
									entity,b,classname))
						setattr(a,'add',add)
						setattr(b,'add'+a.__name__,
						lambda self,entity:addclass(self,
									entity,a,classname))
						setattr(b,'add',add)
				return type.__new__(metaclass,
							classname,baseclasses,classdict)

As was the case for the MetaEntity class, MetaRelation performs its magic through its __new__() method.

First, we check if we are creating a subclass of AbstractRelation by checking the length of the baseclasses parameter (remember that MetaRelation is defined as a metaclass for AbstractRelation, meaning that not only its subclasses, but also AbstractRelation itself will be processed by the metaclass machinery, something that is not really needed here).

If it is a subclass, we copy the database and thread local storage references to the class dictionary for quick access.

If there was no database attribute specified, we know the class being defined is a subclass of Relation, that is, a specific relation class and mark this in the relationdefinition variable (highlighted).

If we are dealing with a concrete definition of a relation, we will have to work out which entities are involved. This is done by checking the class dictionary for attributes named a and b, that should be subclasses of AbstractEntity (highlighted). These are both halves of the relation and their names are used to create a bridging table if not already present.

If we were to define a relation like this:

class Owner(Relation):
	a=Car
	b=User

The SQL statement generated would be:

create table if not exists Owner (
	Car_id references Car on delete cascade,
	User_id references User on delete cascade,
	unique(Car_id,User_id)
)

Each column references the primary key in the corresponding table (because we did specify just the table in the references clause) and the on delete cascade constraint will make sure that if an entity is deleted, the relation is deleted as well. The final unique constraint will make sure that if there is a relation between specific instances, there will be only one record reflecting this.

Adding new methods to existing classes

The final part of the __new__() method deals with inserting methods in the entity classes that are involved in this relation. Adding methods to other classes may sound like magic, but in Python, classes themselves are objects too and have class dictionaries that hold the attributes of a class. Methods are just attributes that happen to have a value that is a function definition.

We can, therefore, add a new method at runtime to any class by assigning a reference to a suitable function to a class attribute. The MetaEntity class only altered the class dictionary of an Entity before it was created. The MetaRelation class goes one step further and not only alters the class dictionary of the Relation class, but also those of the Entity classes involved.

Tip

Altering class definitions at runtime is not limited to metaclasses, but should be used sparingly because we expect classes to behave consistently anywhere in the code.

If we have two classes, A and B, we want to make sure each has its own complement of get and add methods. That is, we want to make sure the A class has getB() and addB() methods and the B class has getA() and addA(). We, therefore, define generic getclass() and addclass() functions and assign those with tailored lambda functions to the named attributes in the class concerned (highlighted).

If we assume again that the entity classes are called A and B and our relation is called AB, the assignment:

setattr(a,'get'+b.__name__,lambda self:getclass(self,b,classname))

will mean that the A class will now have a method called getB and if that method is called on an instance of A (like a1.getB()) it will result in a call to getclass like:

getclass(a1,B,'AB')

We also create (or redefine) a get() method that when given a class as an argument will find the corresponding getXXX method.

The getclass() method is defined as follows:

			def getclass(self,cls,relname):
					clsname = cls.__name__
					sql = 'select %s_id from %s where %s_id = 
?'%(clsname,relname,self.__class__.__name__)
					cursor=self._connect().cursor()
					cursor.execute(sql,(self.id,))
					return [cls(id=r[clsname+'_id']) for r in cursor]

First, it constructs an SQL statement. If getclass() was invoked like getclass(a1,B,'AB'), this statement might look like this:

select B_id from AB where A_id = ?

Then it executes this statement with self.id as the argument. The resulting list of IDs is returned as a list of instances.

The add functionality follows the same pattern, so we only take a quick look at the addclass() function. It first checks if the entity we are trying to add is of the required class. Note that if we make a call like a1.addB(b1), it will refer to a function inserted by the MetaRelation class that will then be called like addclass(a1,b1,B,'AB').

The SQL statement that is subsequently constructed may look like this:

insert or replace into AB (A_id,B_id) values (?,?)

Because of the unique constraint we specified earlier, a second insert that specifies the same specific relation may fail in which case we replace the record (that is effectively ignoring the failure). This way, we may call add() twice with the same arguments, yet still end up with just a single record of the relation.

Browsing lists of entities

One of the most important tools for a user to interact with a collection of entities is a table. A table provides a logical interface to page through lists of data and present relevant attributes in columns. Other features often found in such a table interface are the options to sort on one or more attributes and to drill down, that is, to show only those entities that have some specific value for an attribute.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.217.17