last update: 2024-03-15
Sometimes it is necessary to perform changes on a Django model that cannot, or should not, be reflected in the database.
A few examples of when that needs to happen:
db_index
property.More on the second item now.
When a CharField
in Django is created with db_index=True
Django will create
two indexes: one for exact match, and another for text index
(varchar_pattern_ops
).
# Field in the model "Table"
field = CharField(db_index=True)
// Generated migration sql as seen from `sqlmigrate`
CREATE INDEX "app_table_field_9878d9_idx" ON "app_table" ("field");
CREATE INDEX "app_table_field_9878d9_idx_like" ON "app_table" ("field", varchar_pattern_ops);
The second index is almost always not what you want. Specially if the
CharField is a choice field. But also, this kind of index only works for
prefixed anchored search (LIKE 'pre%'), it does not work for postfixed anchored search (
LIKE '%post') as it will do a table scan.
If your changes are already in production, you now have to create a new
migration that has state changes and operations split up. You can do so by
using SeparateDatabaseAndState
.
From the docs:
A highly specialized operation that lets you mix and match the database (schema-changing) and state (autodetector-powering) aspects of operations.
It accepts two lists of operations. When asked to apply state, it will use the
state_operations
list (this is a generalized version of RunSQL’sstate_operations
argument). When asked to apply changes to the database, it will use thedatabase_operations
list.
To create a migration that removes that index, for example, start with an empty canvas:
./manage.py makemigrations --empty myapp --name remove_my_sweet_index
You will get this:
# Generated by Django 4.1.12 on 2024-03-12 21:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0005_myapp_previous_migration'),
]
operations = [
]
Make sure the migration is not atomic, and that the index is removed concurrently:
class Migration(migrations.Migration):
+ atomic = False
dependencies = [
("events", "0005_externalevent_account_user_id"),
]
operations = [
+ migrations.SeparateDatabaseAndState(
+ database_operations=[
+ # This index is being removed because it is not being used by the
+ # application.
+ migrations.operations.RunSQL(
+ sql="DROP INDEX CONCURRENTLY IF EXISTS app_table_field_9878d9_idx_like;",
+ reverse_sql=migrations.operations.RunSQL.noop,
+ ),
+ ],
+ state_operations=[
+ # This is only here so that we can remove the `db_index=True` from the
+ # Django model without resorting to extra migrations.
+ migrations.AlterField(
+ model_name="table",
+ name="field",
+ # note that db_index=True was removed.
+ field=models.CharField(max_length=256),
+ ),
+ # This was added from adding the index to the Meta class.
+ migrations.AddIndex(
+ model_name="table",
+ index=models.Index(
+ fields=["field"], name="app_table_field_9878d9_idx"
+ ),
+ ),
+ ],
+ )
]
And also make sure to update the models:
field = CharField()
class Meta:
# Now that the db_index=False, we just put the exact-match index
# in here.
models.Index(fields=["field"], name="app_table_field_9878d9_idx"),