1. SAME KIND.
Base models
# /product/models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=255)
class Variant(models.Model):
name = models.CharField(max_length=255)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
price = models.PositiveIntegerField()
I have an app named product registered in INSTALLED_APPS settings.
price = models.PositiveIntegerField() needs to be decimal.
There are database schemas having primitive types: char, int, varchar, etc.
But in the end, it's just numbers and text.
Simply change the type to DecimalField
class Variant(models.Model):
name = models.CharField(max_length=255)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
price = models.DecimalField(decimal_places=3, max_digits=999)
and then run makemigrations
python manage.py makemigrations product
# /product/migrations/0002_alter_variant_price.py
# Generated by Django 4.1.7 on 2023-02-23 22:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("product", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="variant",
name="price",
field=models.DecimalField(decimal_places=3, max_digits=999),
),
]
Django will automatically generate a new migration file based on our changes as you can see above.
And then migrate it
python manage.py migrate product
Now just find where we implemented this field and make additional logic changes, syntax updates, etc.
Everything will work.
Well, it's just a simple case because it is the same type in general: NUMBER
2. NEW FIELD BASED ON EXISTING FIELD.
Now let's do something harder: migrating from int to datetime.
I have a subscribes app similar to the product example above.
# subscribe/models.py
class Subscriber(models.Model):
email = models.EmailField()
age = models.SmallIntegerField()
This model has been running for a long time and has thousands of records in it.
For better logic and to reduce code complexity, it should be date of birth (DOB).
I have to migrate existing data to the new type, right?
class Subscriber(models.Model):
email = models.EmailField()
age = models.SmallIntegerField()
dob = models.DateField()
Well, just add a dob field and run makemigrations & migrate.
python manage.py makemigrations
It is impossible to add a non-nullable field 'dob' to subscriber without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
Select an option:
Ahhhh, that's it.
If I set null=True as an attribute in the model's field, it will work perfectly with null values. But what's the point?
So now we have to do more calculations for it.
Create an empty migration from the Django template.
python manage.py makemigrations subscribe --name convert_age_dob --empty
and the output
Migrations for 'subscribe':
subscribe/migrations/0002_convert_age_dob.py
# subscribe/migrations/0002_convert_age_dob.py
# Generated by Django 4.1.7 on 2023-02-23 23:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("subscribe", "0001_initial"),
]
operations = []
Here's the battlefield.
We need a function to convert from age to DOB, right?
from django.utils import timezone
def age_to_dob(apps, schema_editor):
Subscriber = apps.get_model('subscribe', 'Subscriber')
today = timezone.now()
for sub in Subscriber.objects.all():
sub.dob = today.replace(year=today.year-sub.age) # that's too dirty but I'll fix it later
sub.save()
and let operation run it
operations = [
migrations.RunPython(age_to_dob, migrations.RunPython.noop),
]
migrations.RunPython.noop is the backward step if we revert the migration. So to do nothing, I added noop.
Now my completed migration file is:
# Generated by Django 4.1.7 on 2023-02-23 23:09
from django.db import migrations, models
from django.utils import timezone
def age_to_dob(apps, schema_editor):
Subscriber = apps.get_model('subscribe', 'Subscriber')
today = timezone.now()
for sub in Subscriber.objects.all():
sub.dob = today.replace(year=today.year-sub.age) # that's too dirty but I'll fix it later
sub.save()
class Migration(migrations.Migration):
dependencies = [
("subscribe", "0001_initial"),
]
operations = [
migrations.AddField(
model_name='Subscriber',
name='dob',
field=models.DateField(null=True, default=None),
preserve_default=False,
),
migrations.RunPython(age_to_dob, migrations.RunPython.noop),
migrations.AlterField(
model_name='Subscriber',
name='dob',
field=models.DateField(),
preserve_default=True,
),
]
Well, a little big, right?
We have migrations.AddField and migrations.AlterField—it's just a trick to bypass the not-null constraint from Django's migration schema, because we need this field to always have data.
Otherwise, you will get the error django.db.utils.IntegrityError: NOT NULL constraint failed:...
Then run migrate on this file.
3. SAME FIELD AND DIFFERENT TYPE.
This is the hard part of refactoring a project.
Adding a new field and then copying data into it will cause many critical errors on release, and if we run incontainerswe have to create lots of releases, which increases time and effort!
As we all know, Django models have a pk by default for your records—it is a biginteger with auto increment on the backend.
Now if we expose this to the public like a post ID or user ID, this is a door that invites crawlers to get our data because it's predictable.
So it must be unpredictable by using UUID or another algorithm to create identifiers instead.
I have my user model here
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
alias = models.CharField(max_length=255)
...
This has been running for quite a long time and has a lot of users.
At this point, if we call .id or .pk, it will return an int for each record.
And there are tons of code implemented previously that would take time to refactor. Would you add another field like uuid and then go fix, add, and delete old logic???
NOOOOOOOOOOOOOOO! I'm a lazy guy. So I implemented it like this.
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
alias = models.CharField(max_length=255)
...
Then make a function generate new id inside of migration stage. make it.
user/migrations/0002_alter_user_id.py
# Generated by Django 4.1.7 on 2023-02-24 01:14
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
("user", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="user",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
]
Django generated it for me, but it only applies to new records that will be inserted in the future. What should I do for existing IDs?
So I applied my workflow here and ran it without any errors.
from django.db import migrations, models
import uuid
def create_uuid(apps, schema_editor):
User = apps.get_model('user', 'User')
for user in list(User.objects.all()):
user.id = uuid.UUID(int=user.id)
user.save()
class Migration(migrations.Migration):
dependencies = [
("user", "0001_initial"),
]
operations = [
# add new one
migrations.AddField(
model_name='user',
name='uuid',
field=models.UUIDField(null=True),
),
# seed new one
migrations.RunPython(create_uuid, migrations.RunPython.noop),
# change attributes
migrations.AlterField(
model_name='user',
name='uuid',
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
# remove old one
migrations.RemoveField('User', 'id'),
# rename to new
migrations.RenameField(
model_name='User',
old_name='uuid',
new_name='id'
),
# Update attributes
migrations.AlterField(
model_name='user',
name='id',
field=models.UUIDField(
primary_key=True, default=uuid.uuid4, serialize=False, editable=False
),
),
]