Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Splitting and merging django models with perfect transparency

    This is a howto that might come in handy to some people, but mostly I just want to document how I poked around some very angry django dragons and created something marvelous. There are also people on twitter who were wondering what the fuck I was doing.

    So let's start by describing the problem. We have a base user model named pUser (yes stupid naming convention) that is tied to a cookie, which holds an id. These users are then tied to a number of different API accounts. In my case it is Delicious, Twitter and Facebook. The user_id is also used to tie a bunch of meta data in different other models to them.

    The problem is that we do not want to trouble users with a special login for our service. But they are using different computers and browsers, so the same physical user can have multiple user id's.

    However through their Delicious et al. credentials we can tie them back together into a single entity. But we do not want to trouble the rest of the code with this detail, it should just work seamlessly because otherwise we'd be forced to introduce checking for this stuff at about 50 different places in the project.

    My approach to solving this goes as follows; at the end will be the three tests that indicate that the solution is valid. A hardcore test through the actual UI also confirmed that everything works.

    Funky geek stuff follows, you have been warned

    First we introduce a model that connects different user id's to the main user (i.e. the first id said user was given)

    class UserNormalisation(models.Model):
    	main_id = models.IntegerField()
    	sub_id = models.IntegerField()
    
    	class Meta:
    		unique_together = ("main_id", "sub_id")
    

    Then we give our Delicious model a ModelManager that will perform duplicity checking and tie different users together as needed.

    class DeliciousManager(models.Manager):
    	def create(self, **kwargs):
    		try:
    			old = Delicious.objects.get(username=kwargs['username'])
    			new = super(DeliciousManager, self).create(**kwargs)
    			try:
    				UserNormalisation(main_id = old.user.id,
    						  sub_id = new.user.id).save()
    			except IntegrityError:
    				pass
    			new.delete()
    			return old
    		except Delicious.DoesNotExist:
    			return super(DeliciousManager, self).create(**kwargs)
    
    class Delicious(models.Model):
    	user = models.ForeignKey( pUser )
    	username = models.CharField( max_length=255 )
    	password = models.CharField( max_length=255 )
    	isScrobbled = models.BooleanField( default=False )
    
    	objects = DeliciousManager()
    

    Basically when the _create_function is called it checks whether a Delicious model with the same username already exists and if it does, then a row is added to the UserNormalisation table to tie the two user id's together.

    And here's the real magic, the changes we did to the pUser model.

    class pUserManager(models.Manager):
    	def get(self, **kwargs):
    		user = super(pUserManager, self).get(**kwargs)
    		try:
    			id = UserNormalisation.objects.get(sub_id=user.id).main_id
    			user = super(pUserManager, self).get(id=id)
    		except UserNormalisation.DoesNotExist:
    			pass
    		return user
    
    class pUser(models.Model):
    	username = models.CharField( max_length=50 )
    	password = models.CharField( max_length=255 )
    	creation = models.DateTimeField( auto_now=True )
    
    	objects = pUserManager()
    
    	def __init__(self, *args, **kwargs):
    		super(pUser, self).__init__(*args, **kwargs)
    		try:
    			id = UserNormalisation.objects.get(sub_id=self.id).main_id
    			self.id = id
    
    		except UserNormalisation.DoesNotExist:
    			pass
    

    The pUserManager should have a few more functions that do essentially the same thing for other operations (filter comes to mind). Essentially whenever a pUser is fetched from the db the manager will return the real user as per the UserNormalisation model.

    Another trick that makes this work seamlessly even when used as a connecting model (primary key for instance) in a different table is that __init__ function. What I've discovered is that there it's enough to just change the user's id in place and everything will work.

    Here are the tests that confirm all of this funky stuff actually performs as expected

    	def test_normalisation(self):
    		user = pUser(username="test", password="test")
    		user.save()
    
    		user2 = pUser(username="test2", password="test")
    		user2.save()
    
    		norm = UserNormalisation(main_id=user.id, sub_id=user2.id)
    		norm.save()
    
    		fixture = pUser.objects.get(id=user2.id)
    		self.assertEquals( fixture.id, user.id )
    
    	def test_normalisation2(self):
    		user = pUser()
    		user.save()
    		user2 = pUser()
    		user2.save()
    
    		user.delicious_set.create(username="test", password="test")
    		fixture = user2.delicious_set.create(username="test", password="test")
    
    		self.assertEquals( fixture.user.id, user.id )
    		self.assertEquals( UserNormalisation.objects.get(sub_id=user2.id).main_id, user.id )
    		self.assertEquals( fixture.user, user )
    
    	def test_normalisation3(self):
    		user = pUser()
    		user.save()
    		user2 = pUser()
    		user2.save()
    
    		user.delicious_set.create(username="test", password="test")
    		fixture = user2.delicious_set.create(username="test", password="test")
    
    		norm = UserNormalisation.objects.all()
    
    		Concepts.relate(user=user2, concept1="tag1", concept2="tag2")
    		relation = ConceptRelation.objects.filter(user=user2, concept1="tag1")[0]
    		self.assertEquals( relation.user.id, user.id )
    		self.assertEquals( relation.user, user )
    

    Take special note to the latter two examples. In test_normalisation2 you can see that when a delicious_set is created for user2, the two users become the same thing because both we're using the same delicious username both times. Something similar happens in test_normalisation3, but there we see that creating a ConceptRelation for user2 actually creates it for the first user because they both behave as if they were the original user.

    Published on February 18th, 2010 in django, food for thought, intrigues, Programming, python, Uncategorized

    Did you enjoy this article?

    Continue reading about Splitting and merging django models with perfect transparency

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️