In a (futile) attempt to future-proof the book, I decided to upgrade it to Django 1.7. Here's how that went down.
Overview
Unsurprisingly, the biggest change was to do with migrations. Like any new change, my initial reaction was dislike, and I resented the new things, but I think overall it's a definite improvment.
In brief, here's what happened:
-
The new migrations framework means 'any' change to models needs a migration, or tests won't pass.
-
This meant introducing the concept of migrations much earlier in the book; in fact, at the same time as I introduce the ORM. I resented this because it made the learning curve of the book steeper.
-
On the other hand, because the new version of migrations essentially forces you to have them from the very beginning, I was able to drop an entire chapter that was devoted to retrospectively building migrations after the first deployment, which included all sorts of checking out of old versions, and using
--fake
, and so on. So that's a big win. -
Using step-by-step TDD also forces you to make more migrations than you really want to, if you're adding fields and constraints step-by-step. It means I have to introduce the reader early on to the idea of replacing existing migrations too, another steepening of the learning curve.
Here's some detail on the changes.
Migrations make the introduction of models.py more complex
In chapter 5, where we build the first model, the narrative used to go:
- Write a test
- See it fail
- Add code in models.py, step by step
- Get the tests further, see a different failure
- Add more code in models.py
- Get the tests passing
Now it goes:
- Write a test
- See it fail
- Add code in models.py, step by step
- See a database error
- Create a migration
- See the tests get further, see a different failure
- Add more code in models.py
- See a database error
- Explain the concept of squashing migrations into one
- Delete the existing migration and re-create it.
- See the tests pass
So you can see it's more complicated. On the other hand, understanding how Django gets from models.py to the database is important. I had been just hand-waving and saying "use syncdb, and just delete the database if anything goes wrong", so maybe it's better to address this stuff head-on, rather than wait for a complicated later chapter.
If you're curious, you can view the whole narrative here
(If you're a Django core developer and you're reading this, I'd love to hear your thoughts btw. There's a few weeks before the book goes to print yet, so there's still time to tell me I'm doing it all wrong!)
I still kinda wish I could have kept my nice shallow learning curve - I expended a lot of effort with the book, in trying to make sure concepts are introduced one at a time and gradually, and now I feel I'm slightly forced to lump two concepts onto the reader at the same time. But, there's clearly an upside.
But they save me from a fairly horrible chapter 13...
It was always going to be an unlucky chapter wasn't it. Because I'd glossed over the concept of migrations until then, I would get the readers to deploy their code to a server in chapter 8 or so, and then code some new stuff, including a new database feature.
Then I had a chapter 13 in which we would try and deploy to the staging site, and see the new feature wouldn't work. So then I had to explain migrations, and go through this process:
- Find the old commit that matches the point at which we did the last deployment, and check out the old version of models.py from it.
- Do a
manage.py schemamigration
, and create a migration to match live - Check out the latest version of models.py, and do another
schemamigration
to get the migration we want to apply. - Test it out locally. Check out the old models.py again, delete the databse,
syncdb, then run
migrate 0001 --fake
, then check out the new code, and runmigrate
, check it works - Adjust the deploy script to include
migrate 0001 --fake
followed by amigrate
- Test deploying to staging... OK
- Deploy to live
- And, don't forget to now remove the
migrate 001 --fake
from your deploy script.
Ouch! Quite a lot of pain there! Especially when you consider that the new procedure is:
- Run the deploy script. It just works, because we've had migrations all along.
:-)
Other thoughts.
I found the fact that tests would fail if you didn't have migrations intriguing,
but unfortunately it's not something you can rely on. For example, in chapter
12 I introduce a unique_together
constraint and test it thusly:
def test_duplicate_items_are_invalid(self):
list_ = List.objects.create()
Item.objects.create(list=list_, text='bla')
with self.assertRaises(ValidationError):
item = Item(list=list_, text='bla')
item.full_clean()
To get that passing, I just add my unique_together
constraint:
class Item(models.Model):
text = models.TextField()
list = models.ForeignKey(List)
class Meta:
unique_together = ('list', 'text')
And at this point Django doesn't warn me that I need a migration, because the test is actually happening at the validation layer.
I think that's a bit of a shame, but there's probably nothing to be done about it. It's all because the concepts of data validation and database integrity constraints are separate in Django, even though their implementation in models.py actually often happens in a single place...
One last thing...
I love the pretty colours!