My app that currently depends on Postgres and Django's Postgres-only JSONField. The field works well and I've no interest in another project, but I have prospective-users who want to use my app, but can't while it relies on Postgres.
Django 3.1 has a cross-platform version of this field —which will work for my needs— but I don't want to force everybody up to Django 3.1; I would like to offer people a choice between Postgres or Django 3.1+. On paper, this is simple enough with a conditional import...
try:
from django.db.models import JSONField
except ImportError:
from django.contrib.postgres.fields import JSONField
And if I installed Django 3.1 and generated a migration, it could take me from django.contrib.postgres.fields.JSONField to django.db.models.JSONField. But...
New users will still execute the initial migration. I will still have a dependency on Postgres.
Sub-Django 3.1 users won't be able to execute the new migration. I now have a dependency on Django 3.1.
This is worse than when I started. How do I do this sort of field migration in a way that will work for everybody?
Migrations are just code. Just because they're auto-generated doesn't mean you shouldn't change them. You're encouraged to, at least to check they're generated correctly, but also there's no harm in writing them yourself.
This works for me:
Model:
from django.db import models
try:
from django.db.models import JSONField
except ImportError:
from django.contrib.postgres.fields import JSONField
class MyModel(models.Model):
stuff = JSONField()
Migration:
from django.db import migrations, models
try:
from django.db.models import JSONField
except ImportError:
from django.contrib.postgres.fields import JSONField
class Migration(migrations.Migration):
dependencies = [('testapp', '0001_initial')]
operations = [
migrations.CreateModel(
name='MyModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stuff', JSONField()),
],
),
]
Keep in mind that if you need to change this field in the future, you will need to go through this process again.
I have got this from Django source code
from django.db.models import JSONField as BuiltinJSONField
class JSONField(BuiltinJSONField):
system_check_deprecated_details = {
'msg': (
'django.contrib.postgres.fields.JSONField is deprecated. Support '
'for it (except in historical migrations) will be removed in '
'Django 4.0.'
),
'hint': 'Use django.db.models.JSONField instead.',
'id': 'fields.W904',
}
This indicates that, django.contrib.postgres.fields.JSONField is going to be deprecated. Also, Django uses the django.db.models.JSONField as postgres special JSONField.
Apart from that, I have generated the SQL command using sqlmigrate command and it was like,
BEGIN;
--
-- Create model MyModel
--
CREATE TABLE "myapp_mymodel" ("id" serial NOT NULL PRIMARY KEY, "stuff" jsonb NOT NULL);
COMMIT;
Surprisingly, I have got same SQL command using Django==3.0 and Django==3.1 and in the database, the field is a jsonb type
These pieces of information conclude that you don't have to worry about this new JSONField upgrade.
What changes need to be done?
You don't need to generate a new migration file, but edit existing migration files which have django.contrib.postgres.fields.JSONField reference with the try...except block.
That's it!!!
All new migrations will be correct automatically if this solution with a deconstruct() method will be used.
You can create a compatible custom JSONField that encapsulates both variants.
Create a small file fields.py in your application:
try:
from django.db.models import JSONField as OrigJSONField
except ImportError:
from django.contrib.postgres.fields import JSONField as OrigJSONField
class JSONField(OrigJSONField):
def deconstruct(self):
# the original path was 'django.db.models.JSONField' or 'django.contrib.postgres.fields....'
name, path, args, kwargs = super().deconstruct()
# Substitute 'my_app' by your application name everywhere.
path = 'my_app.fields.JSONField'
return name, path, args, kwargs
Change a line in your models.py:
from my_app.fields import JSONField
Edit all existing migrations that use a JSONField:
import my_app.fields
...
('stuff', my_app.fields.JSONField()),
(or equivalently)
from my_app.fields import JSONField
...
('stuff', JSONField()).
No migration is created after that because no difference is found by makemigrations.
All future makemigrations after a changed model will be created automatically with my_app.fields.JSONField(). Compatibility is a benefit of this solution.
Reflection about Django Release Notes 3.1 and future:
They describe a plain upgrade that requires to create a new migration that only upgrades the import path, but it generates no SQL e.g. by sqlmigrate. It is easier than to edit old migrations.
Maybe you you also upgrade in two years to Django >= 3.1 only and you will have two alternatives:
A) Only the import in models.py will be edited and you create a new nearly empty formal migration similarly to release notes. You will be not able to remove the import path my_app.fields.JSONField because it must be importable from old migrations similarly that Django can never remove the path django.contrib.postgres.fields.JSONField. The file my_app/fields.py could be simplified to one line from django.db.models import JSONField as JSONField after an unconditional upgrade of requirements.
B) Editing old migrations again to only the new JSONField is possible, but unreasonable.
(It is every user's responsibility that an edit in a migrations file doesn't require a changed database state and all migrations remain consistent each other. That is why not much about it can be found, except such clear cases.)
I gave point to Tom Carrick for his answer but I think it should also include the usage of Meta.required_db_vendor = 'postgres' on Django < 3.1
# models.py
from django.db import models
try:
from django.db.models import JSONField
postgres_only = False
except ImportError:
from django.contrib.postgres.fields import JSONField
postgres_only = True
class MyModel(models.Model):
stuff = JSONField()
class Meta:
if postgres_only:
required_db_vendor = 'postgres'
# migrations/0001_initial.py
from django.db import migrations, models
try:
from django.db.models import JSONField
postgres_only = False
except ImportError:
from django.contrib.postgres.fields import JSONField
postgres_only = True
class Migration(migrations.Migration):
dependencies = [('testapp', '0001_initial')]
operations = [
migrations.CreateModel(
name='MyModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stuff', JSONField()),
],
options={'required_db_vendor': 'postgres'} if postgres_only else None,
),
]
This will ensure that users of your reusable app cannot use it on a different database than PostgreSQL on Django < 3.1 and seamlessly enable support on 3.1 without requiring a migration on PostgreSQL users using your library and updating to Django 3.1.
Related
I am currently on Django 1.7 and my database is PostgreSQL. I have the following model.
# myapp/models.py
from django.db import models
from json_field import JSONField # using django-json-field
from json_field.fields import JSONDecoder
class MyModel(models.Model):
the_data = JSONField(
decoder_kwargs={'cls': JSONDecoder, 'parse_float': float},
default='[{}]',
)
...
I am now looking to upgrade to Django 1.10 and take advantage of the new(ish) JSONField in django.contrib.postgres.fields.
I change my model to look like this.
# myapp/models.py
from django.db import models
from django.contrib.postgres.fields import JSONField # Now want to use the new JSONField
class MyModel(models.Model):
the_data = JSONField(
default='{}',
)
...
I then create a migration for the app.
./manage.py makemigrations myapp
When it attempts to create a migration it complains...
from json_field.forms import JSONFormField
File "/Users/travismillward/Projects/amcards_env/lib/python2.7/site-packages/json_field/forms.py", line 5, in <module>
from django.forms import fields, util
ImportError: cannot import name util
I understand why it is complaining. django-json-field is not updated for django 1.10 and it wants to import json_field in one of the original migration files. So I can either go back and modify my original migration file that imports json_field but then it won't actually modify the column data type because it thinks it is already done. Or, I have to fix django-json-fields to work with django 1.10 just so the migration can be created. And I will have to leave that requirement in place even though I don't use it, it's just for the migration!
On my last project I just modified the original migration to make it think that it was using django.contrib.postgres.fields.jsonb.JSONField all along. However, after I ran the migration, it didn't change the column's data type from text to jsonb. So I manually did that since it was a smaller project. For this project, I really don't want to manually alter the database.
Any suggestions on how to migrate away from django-json-field gracefully and with a plan to remove it from my code and requirements?
I am writing a migration that requires me to fill a field with existing data from another field (with same type and constraints). Is it possible in Django to copy the data in a single operation? The destination column already exists when I need to copy the data.
In SQL, I would have written something like that:
UPDATE my_table SET column_b = column_a;
Edit
The current answer proposes to loop over the model instances, but that is what I want to avoid. Can it be done without a loop?
As the comment mentioned, you can simply write a migration for this. I think the below should work though I haven't tested it. It uses the queryset update API and F to avoid looping
from __future__ import unicode_literals
from django.apps import apps
from django.db import migrations, models
from django.db.models import F
def copy_field(apps, schema):
MyModel = apps.get_model('<your app>', 'MyModel')
MyModel.objects.all().update(column_a=F('column_b'))
class Migration(migrations.Migration):
dependencies = [
('<your app>', '<previous migration>'),
]
operations = [
migrations.RunPython(code=copy_field),
]
I want my application to have default data such as user types.
What's the most efficient way to manage default data after migrations?
It needs to handle situations such as, after I add a new table, it adds the default data for it.
You need to create an empty migration file and Do your stuff in operations block, as explained in docs.
Data Migrations
As well as changing the database schema, you can also use migrations to change the data in the database itself, in conjunction with the schema if you want.
Now, all you need to do is create a new function and have RunPython use it
Docs explains this with an example to show ,how to communicate with your models.
From Docs
To create an empty migration file,
python manage.py makemigrations --empty yourappname
And this is the example how to update a newly added field.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def combine_names(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Person = apps.get_model("yourappname", "Person")
for person in Person.objects.all():
person.name = "%s %s" % (person.first_name, person.last_name)
person.save()
class Migration(migrations.Migration):
initial = True
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
migrations.RunPython(combine_names),
]
The accepted answer is fine. But, since OP asked the question in the context of adding new rows and not updating existing entries. Here is the code snippet for adding new entries :
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('loginmodule', '0002_login_avatar'),
]
def insertData(apps, schema_editor):
Login = apps.get_model('loginmodule', 'Login')
user = Login(name = "admin", login_id = "admin", password = "password", email = "admin#pychat.com", type = "Admin", avatar="admin.jpg")
user.save()
operations = [
migrations.RunPython(insertData),
]
Update:
most users are looking for data migration as suggested by #durdenk in https://stackoverflow.com/a/39742847/3627387. But what OP was asking is about a way to add data after migrations, that is why this is accepted answer.
Original answer:
I think what you are looking for is fixtures https://docs.djangoproject.com/en/1.10/howto/initial-data/
From docs
It’s sometimes useful to pre-populate your database with hard-coded data when you’re first setting up an app. You can provide initial data via fixtures.
Also read this https://code.djangoproject.com/wiki/Fixtures
Answer is given above just to show how to insert new rows to the table.
from django.db import migrations, models
from yourapp.models import <yourmodel>
def combine_names(apps, schema_editor):
obj = <yourmodel>(arrib=value)
obj.save()
For example let's say you have model Person
person = Person(first_name='raj', last_name='shah')
person.save()
IMPORTANT: This question is no longer relevant.
In a Django 1.7 migration I try to create Comment entries programatically with the following code:
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
def create_genericcomment_from_bookingcomment(apps, schema_editor):
BookingComment = apps.get_model('booking', 'BookingComment')
Comment = apps.get_model('django_comments', 'Comment')
for comment in BookingComment.objects.all():
new = Comment(content_object=comment.booking)
new.save()
dependencies = [
('comments', '0001_initial'),
('django_comments', '__first__'),
]
operations = [
migrations.RunPython(create_genericcomment_from_bookingcomment),
]
And it produces an error:
TypeError: 'content_object' is an invalid keyword argument for this function
However, the same code (i.e. Comment(content_object=comment.booking)) works when executed in the shell.
I tried to create a blank model with new = Comment() and then set all the necessary fields manually but even though I set content_type and object_pk fields accordingly, they content_type was not actually saved and I received django.db.utils.IntegrityError: null value in column "content_type_id" violates not-null constraint
Any idea how to properly create a model with a generic foreign key in a migration? Or any workaround?
This is an issue of migrations' model loader. You load your models using default
Comment = apps.get_model('django_comments', 'Comment')
It loads the Comment model in some special way, so some features like generic relations don't work.
There is a bit hacky solution: load your models as usual:
from django_comments import Comment
I need to update an existing project to Django 1.5 to take advantage of its newly available custom user model. However, I'm having trouble migrating reusable apps that contain a model with a foreign key to a user. Currently, the foreign key points to auth.User but with a custom user model, it needs to point to myapp.CustomUser. Hence, some kind of migration is needed. I can't simply create a migration file for it because its a reusable app. It wouldn't be future proof because each time the app is updated, I would need to remember to create that migration again (there might even be migration conflicts) so it's not exactly a plausible solution.
Is there a solution to this problem other than to, maybe, fork each project, add a migration file, and then use that instead?
Some code:
models.py in reusable app
from django.conf import settings
from django.db import models
UserModel = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
class ModelA(models.Model):
user = models.ForeignKey(UserModel)
models.py in my project
from django.conf import settings
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
...
settings.py in my project
AUTH_USER_MODEL = 'myapp.CustomUser'
So if the reusable app has a migration that creates a foreign key to a user, the following can be done to support Django 1.5's custom user model.
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
class Migration(SchemaMigration):
def forwards(self, orm):
db.create_table('reusableapp.modela', (
('user', self.gf('django...ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])
models = {
...
# this should replace "auth.user"
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': User.__name__},
}
"reusableapp.modela": {
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)})
}
}
I'm not sure if this is the best solution but it's being used in apps such as django-reversion.
However, this solution still can pose a problem if you originally started with auth.User and then changed to myapp.customuser, simply because south is honors AUTH_USER_MODEL but the migration for the custom user model hasn't been created yet. This can occur during testing. Ticket #1179 of south addresses this issue (http://south.aeracode.org/ticket/1179).