Parts Explosion in Django

(version 1.3.1)

The problem was to retrofit relationships for parts-explosion into an existing parts database. (The database engine was sqlite3.) A few rough tests of the "obvious" ways hadn't worked. The minimal model for Parts Explosion was found in a Test-Driven way by changing model.py, guided by the error messages, until manage.py validate stopped complaining:


from django.db import models

class Part (models.Model):
	part_no = models.CharField (max_length=50, unique=True)
	description = models.TextField (blank=True, default='')
	on_hand = models.IntegerField (default=0)
	partsof = models.ManyToManyField ('self', symmetrical=False, through='PartOf')
	
	def __unicode__ (self):
		return self.part_no

class PartOf (models.Model):
		# this part (the assembly) needs the components in the component_set ..
	assembly = models.ForeignKey (Part, related_name='component_set')
		# this part is a component of assemblies in the assembly_set ..	
	component = models.ForeignKey (Part, related_name='assembly_set')
		# how many of the component are required by the assembly ..
	required = models.IntegerField (default=0)
	
	def __unicode__ (self):
		return '%s/%s' % (self.assembly, self.component)
(OK, not quite minimal. The "description", "on_hand", and "required" fields are the application data that provide the payload for the tables.) The resulting database was exercised in manage.py shell to straighten out the relations between assembly and component_set and between component and assembly_set, noted in the comments.

Retrofitting

With that settled, the next problem was: fit that into the existing Part database without losing all the existing data. It turned out to be surprisingly easy. The schema from manage.py sqlall m2m


BEGIN;
CREATE TABLE "m2m_part" (
    "id" integer NOT NULL PRIMARY KEY,
    "part_no" varchar(50) NOT NULL UNIQUE,
    "description" text NOT NULL,
    "on_hand" integer NOT NULL
)
;
CREATE TABLE "m2m_partof" (
    "id" integer NOT NULL PRIMARY KEY,
    "assembly_id" integer NOT NULL REFERENCES "m2m_part" ("id"),
    "component_id" integer NOT NULL REFERENCES "m2m_part" ("id"),
    "required" integer NOT NULL
)
;
CREATE INDEX "m2m_partof_3d7199d8" ON "m2m_partof" ("assembly_id");
CREATE INDEX "m2m_partof_10fa268c" ON "m2m_partof" ("component_id");
COMMIT;
shows that none of the ManyToMany configuration makes any difference to the schema for m2m_part. As long as m2m_partof has conforming foreign-key column names, all the other machinery is in the Object Reference Model that Django sets on top of the database.

The actual steps in the retrofit were:

  1. Edit the application's models.py by
    1. adding the line
      partsof = models.ManyToManyField ('self', symmetrical=False, through='PartOf')
      to the Part class definition
    2. adding the PartOf class definition.
  2. DROPping a fossil partof table, left over from a failed attempt to implement ManyToMany, from the schema in the database.
  3. Running manage.py syncdb.
and that was it. Adding admin.site.register (PartOf) to the application's admin.py let me start populating PartOf objects and testing.

There are still some wrinkles. Attempting to include the PartOf relationships as detail lines on the admin site page for Part doesn't work. The admin site complains about multiple foreign keys for Part objects in the PartOf objects.

Django's conscientious way of separating the ORM from the schema makes other things easy, too. It's obvious that the model given above lacks a constraint that (assembly, component) pairs be unique in the database. There's no problem retorfitting this; add


    class Meta:
        unique_together = (('assembly', 'component'),)
to the PartOf class definition, then run manage.py syncdb. Django creates the necessary index with no interference to the rest of the schema.


Wed Nov 16, 2011