The following instructions describe a set of processes allowing you to run Django database migrations against a production database without having to bring the web service down.
Note that in the below instructions, migrations are all run manually at explicit points, and are not an automatic part of the deployment process.
- Make the model or column addition in your code.
- Generate the model-change migrations locally.
- Make two separate pull requests, one containing the model change, one containing the newly-generated migrations. Note that the model-change migration should only contain the model change code, not any supplemental code using the newly-structured model.
- Merge and deploy the migration pull request.
- Run the new migration on the production database. Additional fields in the production database will not cause any problems if the production model code is not expecting them to be there.
- Merge the model-change pull request.
- Deploy the code with the model change. It will now seamlessly start reading from the previously created column/table.
In order to add a NOT NULL field, you should first add a nullable field with the steps above, then follow the following additional steps:
- Make the model change to null=False. Also ensure your code never inserts or updates None values into the field you are converting to NOT NULL.
- Generate the migration for this change.
- Make two separate pull requests for these changes: one for the models, one for the migration.
- Merge and deploy the code change pull request.
- Merge the migration pull request.
- Run the migration on the production database.
Django migrations will insist on a default value when making a NOT NULL field. This value is used to populate empty columns during the migration. This update process will lock the postgres table for writes until it has completed. For relatively small tables (<100,000 rows), this is probably fine. For exceptionally large tables though, this could be a problem, because the locked tables could prevent requests from executing while the migration completes. For this reason, consider making new fields nullable in especially large tables (and to a lesser extent in all tables). This may have a cost to your application code, but is probably optimal in comparison to failing to respond to requests.
Essentially, this is the same as adding, but with the steps in a slightly different order.
- Remove all usages of the model or column to be deleted from your application code.
- Deploy a version of the code which still has the model/column to be deleted present, but not used by any of the code.
- Make the model or column removal in your application code.
- Generate the removal migration.
- Merge the model change pull request.
- Deploy the code with the model changes. Ensure there are no errors resulting from lingering references to what is being deleted.
- Merge the migration pull request.
- Deploy the code with the migration.
- Explicitly run the migration on the production database.
In order to remove a NOT NULL field, you first need to migrate it to a nullable field, then follow the steps above to remove a nullable field. The steps to convert a NOT NULL field to a nullable field are:
- Set your field's model to null=True
- Generate your migration for this change.
- Make two separate pull requests: one with the model-change, one with the migration change.
- Merge and deploy the migration pull request.
- Run the migration on the production database.
- Merge and deploy your model change code.
After following these steps, follow the steps above to remove the nullable field.
Good post! I'd just add a note that if the table is too large (my case almost 1B rows) the migration with adding a null=True field can still cause downtime. In this case it's nice to add this to the migration:
https://docs.djangoproject.com/en/3.1/howto/writing-migrations/#non-atomic-migrations