Time for action using the Relation class

Let's have a look at how we would like to use our Relation class:

Chapter5/carexample2.py

from entity import Entity
from relation import Relation
class Car(Entity): pass
class Owner(Entity): pass
Car.threadinit('c:/tmp/cardatabase2.db')
Car.inittable(make="",model="",licenseplate="unique")
Owner.threadinit('c:/tmp/cardatabase2.db')
Owner.inittable(name="")
class CarOwner(Relation): pass
CarOwner.threadinit('c:/tmp/cardatabase2.db')
CarOwner.inittable(Car,Owner)
mycar = Car(make="Volvo",model="C30",licenseplate="12-abc-3")
mycar2 = Car(make="Renault",model="Coupe",licenseplate="45-de-67")
me = Owner(name="Michel")
CarOwner.add(mycar,me)
CarOwner.add(mycar2,me)
owners = CarOwner.list(mycar)
for r in owners:
	print(Car(id=r.a_id).make,'owned by',Owner(id=r.b_id).name)
owners = CarOwner.list(me)
for r in owners:
	print(Owner(id=r.b_id).name,'owns a',Car(id=r.a_id).make)
  • Like before, we first define a Car class and then an Owner class because the CarOwner class we define and initialize in the first highlighted lines are only meaningful if the entities in the relation exist. The highlighted lines show that defining and initializing a relation follows the same general pattern as initializing the entities.
  • We then create two Car entities and an Owner and establish a relation between these (second set of highlighted lines).
  • The final lines show how we can find and print the owners of a car or the cars belonging to an owner.

Many of these requirements for the Relation class are similar to those of the Entity class, so when we take a look at the code, some pieces will look familiar.

What just happened?

The first method we encounter is the threadinit() class method (the full code is available as relation.py). It is identical to the one we encountered in the Entity class and should be called once for every thread.

Chapter5/relation.py

@classmethod
def threadinit(cls,db):
		if not hasattr(cls.threadlocal,'connection') or 
cls.threadlocal.connection is None:
				cls.threadlocal.connection=sqlite.connect(db)
				cls.threadlocal.connection.row_factory = sqlite.Row
				cls.threadlocal.connection.execute(
													"pragma 
foreign_keys=1")

The inittable() class method is the method that should be called once when we start an application:

Chapter5/relation.py

@classmethod
def inittable(cls, entity_a, entity_b,
							reltype="N:N", cascade=None):
	sql='''create table if not exists %(table)s (
			%(a)s_id references %(a)s on delete cascade,
			%(b)s_id references %(b)s on delete cascade,
			unique(%(a)s_id,%(b)s_id)
	);
	'''%{'table':cls.__name__,
				'a':entity_a.__name__,'b':entity_b.__name__}
	with cls.threadlocal.connection as conn:
			cursor=conn.cursor()
			cursor.execute(sql)
	cls.columns=[entity_a.__name__,entity_b.__name__]

It takes the two classes involved in the relations as arguments to construct a proper SQL statement to create a bridging table if it does not exist yet (highlighted).

For example, CarOwner.inittable(Car,Owner) will result in a statement like this:

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

There are a couple of interesting things to note here. There are two columns each referring to a table by way of the references clause. Because we do not explicitly state which column we reference inside the table, the reference is made to the primary key. This is a convenient way to write this down and works because we always define a proper primary key for any table that represents an entity.

Another thing to note is the on delete cascade clause. This helps us to maintain something called referential integrity. It ensures that when the record that is referenced is deleted, the records in the bridging table that refer to it are deleted as well. This way, there will never be entries in a table that represent a relation that points to non-existing entities. To ensure that this referential integrity checking is actually performed, it is necessary to execute a pragma foreign_keys = 1 instruction for each connection to the database. This is taken care of in the threadinit() method.

Finally, there is a unique constraint over both the columns. This effectively ensures that we only maintain, at most, a single entry in this table for each relation between two entities. That is, if I own a car, I can enter this specific relation only once.

If the execution of this statement went well, inittable() finishes with storing the names of the entity classes that this relation refers to in the columns class variable.

Pop quiz how to check a class

How can we make sure that the classes we are passing as arguments to the initdb() method are subclasses of Entity?

Relation instances

The __init__() method constructs an instance of a Relation, that is, we use it to record the relation between two specific entities.

Chapter5/relation.py

def __init__(self,a_id,b_id,stub=False):
		self.a_id=a_id
		self.b_id=b_id
		if stub : return
		cols=self.columns[0]+"_id,"+self.columns[1]+"_id"
		sql='insert or replace into %s (%s) values(?,?)'%(
			self.__class__.__name__,cols)
		with self.threadlocal.connection as conn:
			cursor=conn.cursor()
			cursor.execute(sql,(a_id,b_id))
			if cursor.rowcount!=1:
					raise ValueError()

It takes the IDs of both Entity instances that are involved in this specific relation and a stub parameter.

The __init__() method is not meant to be called directly as it doesn't know nor check whether the IDs passed to it make any sense. It simply stores those IDs if the stub parameter is true or inserts a record in the table if it isn't.

Normally, we would use the add() method to create a new relationship with all necessary type checking. Separating this makes sense as all this checking is expensive and is unnecessary if we know that the IDs we pass are correct. The list() method of the Relation class for example retrieves only pairs of valid IDs so that we can use the __init__() method without the need for costly additional checks.

The SQL statement that is constructed may look like this for a new CarOwner relation:

insert or replace into CarOwner (Car_id,Owner_id) values(?,?)

If we would try to insert a second relation between the same entities, the unique constraint on both columns together would be violated. If so, the insert or replace clause would make sure that the insert statement wouldn't fail, but there still would be just one record with these two IDs.

Note that the insert statement could fail for another reason. If either of the IDs we try to insert does not refer to an existing record in the table it refers to, it would fail with an exception sqlite3.IntegrityError: foreign key constraint failed.

The final sanity check in the last line is to use the rowcount attribute to verify that just one record was inserted.

The add() method does make sure that the instances passed to it are in the correct order by checking the names of the classes against the names of the columns stored by the inittable() method. It raises a ValueError() if this is not correct, otherwise it instantiates a new relation by calling the class constructor with both IDs.

Chapter5/relation.py

@classmethod
def add(cls,instance_a,instance_b):
		if instance_a.__class__.__name__ != cls.columns[0] :
				raise ValueError("instance a, wrong class")
		if instance_b.__class__.__name__ != cls.columns[1] :
				raise ValueError("instance b, wrong class")
		return cls(instance_a.id,instance_b.id)

The list() method is meant to return a list of zero or more Relation objects.

Chapter5/relation.py

@classmethod
def list(cls,instance):
		sql='select %s_id,%s_id from %s where %s_id = ?'%(
				cls.columns[0],cls.columns[1],
				cls.__name__,instance.__class__.__name__)
		with cls.threadlocal.connection as conn:
				cursor=conn.cursor()
				cursor.execute(sql,(instance.id,))
				return [cls(r[0],r[1],stub=True)
								for r in cursor.fetchall()]

It needs to work for both sides of the relation: if we pass a Car instance, for example, to the list() method of the CarOwner class, we should find all records where the Car_id column matches the id attribute of the Car instance.

Likewise, if we pass an Owner instance, we should find all records where the Owner_id column matches the id attribute of the Owner instance. But precisely because we gave the columns in the table that represents the relation meaningful names derived from the names of the classes and hence the tables, this is rather straightforward. For example, the SQL constructed for CarOwner.list(car) might look like the following:

select Car_id,Owner_id from CarOwner where Car_id = ?

Whereas the SQL for CarOwner.list(owner) would look like the following:

select Car_id,Owner_id from CarOwner where Owner_id = ?

This is accomplished by referring to the class name of the instance passed as argument (highlighted).

After executing this SQL statement, the results are fetched with the fetchall() method and returned as a list of relation instances. Note that this list may be of zero length if there weren't any matching relations.

The last method of note defined for the Relation class is the delete() method.

Chapter5/relation.py

def delete(self):
		sql='delete from %s where %s_id = ? and %s_id = ?'%(
			self.__class__.__name__,self.columns[0],self.columns[1])
		with self.threadlocal.connection as conn:
			cursor=conn.cursor()
			cursor.execute(sql,(self.a_id,self.b_id))
			if cursor.rowcount!=1:
					raise ValueError()

It constructs an SQL delete statement which, in our CarOwner example, may look like this:

delete from CarOwner where Car_id = ? and Owner_id = ?

The sanity check we perform in the last line means that an exception is raised if the number of deleted records is not exactly one.

Note

If there was not exactly one record deleted, what would that signify?

If it would be more than one, that would indicate a serious problem because all the constraints are there to prevent that there is never more than one record describing the same relationship, but if it would be zero, this would probably mean we try to delete the same relationship more than once.

You might wonder why there isn't any method to update a Relation object in any way. The reason is that this hardly makes any sense: either there is a relation between two entity instances or there isn't. If we would like to transfer ownership of a car, for example, it is just as simple to delete the relation between the car and the current owner and then add a new relation between the car and the new owner.

Now that we have a simple Entity and Relation framework, let's look at how we can use this to implement the foundation of our books application.

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

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