Django: making a custom PK auto-increment? - django

I've been using custom primary keys for a model in Django. (This was because I was importing values into the database and they already had ID's attached, and it made sense to preserve the existing values.)
class Transaction(models.Model):
id = models.IntegerField(primary_key=True)
transaction_type = models.IntegerField(choices=TRANSACTION_TYPES)
date_added = models.DateTimeField(auto_now_add=True)
However, now I want to add new instances of the model to the database, and I'd like to autogenerate a unique primary key. But if I don't specify the ID at the time of creating the instance, I get an error:
t = Transaction(transaction_type=0)
t.save()
gives:
IntegrityError at /page
(1048, "Column 'id' cannot be null")
How can I autogenerate a unique ID to specify for new values, without having to alter the way I import the existing values?
UPDATE
I've written this custom method, which seems to work...
class Transaction(models.Model):
def save(self, *args, **kwargs):
if not self.id:
i = Transaction.objects.all().order_by('-id')[0]
self.id = i.id+1
super(Transaction, self).save(*args, **kwargs)

You can use AutoField for the column id instead of IntegerField. The following should work for you:
id = models.AutoField(primary_key=True)
id will now increase automatically and won't have concurrency problems as it may encounter in save method.

I've ended up using very similar piece of code, but have made it slightly more generic:
def save(self, *args, **kwargs):
if self.id is None:
self.id = self.__class__.objects.all().order_by("-id")[0].id + 1
super(self.__class__, self).save(*args, **kwargs)
it uses self.__class__ so you can just copy paste this code to any model class without changing anything.

How are you importing the existing values? It would be trivial to write something into your Transactions __init__ to generate a new ID for you, but without knowing how you're importing the other values I can't say for sure whether it will alter the way you work with them.

If you remove your declared id field, django will automatically assume this:
id = models.AutoField(primary_key=True)
In Django 1.8, inspectdb will automatically detect auto_increment and use an AutoField when generating models.

Django migrations will do most of the hard work for you here.
Firstly, stop any access to your app so users can't change the database whilst you are working on it.
It would then be very wise to backup your database, before performing any work, as a precaution.
Remove your manually declared id field from your models.py (i.e. delete it).
Run makemigrations and then migrate. Django will modify the id field to the correct implementation for your database version.
Run this (example) command in psql adapting, if need be, to your table names:
select setval(pg_get_serial_sequence('transactions_transaction', 'id'), max(id)) from transactions_transaction;
This will set your id field to the correct serial sequence value in postgres for your table (i.e. the largest value of the id field of your existing records). This is crucial, as otherwise the value will be 1!
And that's it: from now on everything will be automatic again.

Related

Django slugs not unique with unique=True

I created two articles with the title "test" and this is what the second one generates as an error:
duplicate key value violates unique constraint "xxx_content_slug_xxxx_uniq"
DETAIL: Key (slug)=(test) already exists.
Knowing that this is my model:
class Content()
slug = models.SlugField(unique=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(Content, self).save(*args, **kwargs)
and knowing that I made the migration in the DB.
I don't know how to solve that.
Note: The problem is generated from the class post that inherits the content class and I don't think this information would help in this context.
slugify does not check for uniqueness, so if you create two articles with the same title, slugify(self.title) is going to generate the same slug twice, which will of course cause the database to complain about a violation of the unique constraint.
Setting unique = True on a field, will not automatically create unique values for every new instance, it'll only create a constraint on the database that the same value can not be saved twice.
You can use django-autoslug, which is a library that does exactly what you need.
Simply install the package, and implement your fields like this:
from django.db.models import CharField, Model
from autoslug import AutoSlugField
class Content(models.Model)
title = CharField(max_length=200)
slug = AutoSlugField(populate_from='title')
AutoSlugField does the following:
populate itself from another field,
preserve uniqueness of the value and
use custom slugify() functions for better i18n.
(Also, I assume that Content is a subclass of Model, otherwise your code wouldn't work at all.)

How can I support AutoField(primary_key=False) in django?

I need to add an autoinc field that is not the primary key. I am in the process of migrating a very large production database that uses autoincrementing fields to models.UUIDField. I have been doing a piecewise migration, and all of my relationships are now duplicated with both field types. I'm ready to make the primary key swap, but unfortunately I still need to keep the auto incrementing integer field for old clients as it becomes deprecated.
Since django will not allow me to define an autofield with primary_key=False (even though this is fully supported at the db layer), i'm looking for a simple solution. My initial strategy would be to simply change the field to models.BigIntegerField('GUID', db_index=True, null=True, unique=True) and then manually set the default nextval('my_guid_seq'::regclass) using migrations.RunSQL. So far so good, except not. It turns out, because of my null=True declaration, django at the ORM layer is taking over and inserting null which will not allow defaults at the database layer to do it's job.
The core developers are fast to reject this request because of bad design, which I most definetly agree with, but there are very valid use cases such as this. https://code.djangoproject.com/ticket/8576
I am a very weak django developer so I don't want to get in the weeds metaprogramming at the ORM layer. This is by definition a hack, so i'm looking for the least complex, creative solution that gets me around this limitation
You could subclass AutoField and override the _check_primary_key method.
from django.db.models.fields import AutoField
from django.db.models.fields import checks
class AutoFieldNonPrimary(AutoField):
def _check_primary_key(self):
if self.primary_key:
return [
checks.Error(
"AutoFieldNonPrimary must not set primary_key=True.",
obj=self,
id="fields.E100",
)
]
else:
return []
See AutoField source code here
Edit: Updated Link
I know, changing the primary key to UUID is such a pain.Hence the simple and better solution that I think of is to add another integer field that is auto-incrementing in nature.
Here is my solution:
class ModelName(models.Model):
auto_inc_id = models.IntegerField()
Then override the save model:
def save(self, *args, **kwargs):
self.object_list = ModelName.objects.order_by('auto_inc_id')
if len(self.object_list) == 0: # if there are no objects
self.auto_inc_id = 1
else:
self.auto_inc_id = self.object_list.last().auto_inc_id + 1
super(ModelName, self).save()
Couldn't format this as a comment, but modifying #Abhimanyu's answer to make the save method more concise (and issue only one query). Same model property:
class ModelName(models.Model):
auto_inc_id = models.IntegerField()
And here's the save method on the model:
def save(self, *args, **kwargs):
self.auto_inc_id = ModelName.objects.all().count() + 1
super(ModelName, self).save()

Identifying new Model Instance in Django Save with UUID pk

If I have a model that has a UUID primary key and the the user may set the value on creation, is there any way to tell within the save method that the instance is new?
Previous techniques of checking the auto assigned fields: In a django model custom save() method, how should you identify a new object? do not work.
Use self._state.adding. It defaults to True and gets set to False after saving the model instance or loading it from the DB.
You should also check the force_insert argument of save.
Note that this will not work if you attempt to copy an instance by changing its id and saving (a common shortcut). If you need to detect this, you could override the instance saving and loading to also store the pk on self._state, then compare the current pk with self._state.pk.
In save(), self.pk is None with pk (uuid) dont work because it should has default = uuid.uuid4 and if you set it to default = None primarykey should has default attribute as valid uuid in DB, so let default = uuid.uuid4 in UUID field.
The esay way is to add field created_at:
created_at = models.DateTimeField(auto_now_add=True)
and in save() use :
if self.created_at is None:
your code here
save takes an optional parameter, force_insert. Passing that as True will force Django to do an INSERT. See the documentation.
You can use django-model-utils TimeStampedModel (you can also use django-extensions TimeStampedModel or make your own).
This provides each model a created and modified field. Then, compare the timedelta between the new instance's created and modified fields to an arbitrary time difference (this example uses 5 seconds). This allows you to identify if an instance is new:
def save(self, *args, **kwargs):
super(<ModelName>, self).save(*args, **kwargs)
if (self.modified - self.created).seconds < 5:
<the instance is new>

Override Default Save Method And Create Duplicate

I am looking to create a duplicate instance each time a user tries to update an instance. The existing record is untouched and the full update is saved to the new instance.
Some foreign keys and reverse foreign keys must also be duplicated. The Django documentation
talks about duplicating objects, but does not address reverse foreign keys.
Firstly, is there an accepted way of approaching this problem?
Secondly, I am unsure whether it's best to overwrite the form save method or the model save method? I would want it to apply to everything, regardless of the form, so I assume it should be applied at the model level?
A simplified version of the models are outlined below.
class Invoice(models.Model):
number = models.CharField(max_length=15)
class Line(models.Model):
invoice = models.ForeignKey(Invoice)
price = models.DecimalField(max_digits=15, decimal_places=4)
Here's my shot at it. If you need it to duplicate every time you make any changes, then override the model save method. Note that this will not have any effect when executing .update() on a queryset.
class Invoice(models.Model):
number = models.CharField(max_length=15)
def save(self, *args, **kwargs):
if not self.pk:
# if we dont have a pk set yet, it is the first time we are saving. Nothing to duplicate.
super(Invoice, self).save(*args, **kwargs)
else:
# save the line items before we duplicate
lines = list(self.line_set.all())
self.pk = None
super(Invoice, self).save(*args, **kwargs)
for line in lines:
line.pk = None
line.invoice = self
line.save()
This will create a duplicate Invoice every time you call .save() on an existing record. It will also create duplicates for every Line tied to that Invoice. You may need to do something similar every time you update a Line as well.
This of course is not very generic. This is specific to these 2 models. If you need something more generic, you could loop over every field, determine what kind of field it is, make needed duplicates, etc.

Django: Querying read-only view with no primary key

class dbview(models.Model):
# field definitions omitted for brevity
class Meta:
db_table = 'read_only_view'
def main(request):
result = dbview.objects.all()
Caught an exception while rendering: (1054, "Unknown column 'read_only_view.id' in 'field list'")
There is no primary key I can see in the view. Is there a workaround?
Comment:
I have no control over the view I am accessing with Django. MySQL browser shows columns there but no primary key.
When you say 'I have no control over the view I am accessing with Django. MySQL browser shows columns there but no primary key.'
I assume you mean that this is a legacy table and you are not allowed to add or change columns?
If so and there really isn't a primary key (even a string or non-int column*) then the table hasn't been set up very well and performance might well stink.
It doesn't matter to you though. All you need is a column that is guaranteed to be unique for every row. Set that to be 'primary_key = True in your model and Django will be happy.
There is one other possibility that would be problemmatic. If there is no column that is guaranteed to be unique then the table might be using composite primary keys. That is - it is specifying that two columns taken together will provide a unique primary key. This is perfectly valid relational modelling but unfortunatly unsupported by Django. In that case you can't do much besides raw SQL unless you can get another column added.
I have this issue all the time. I have a view that I can't or don't want to change, but I want to have a page to display composite information (maybe in the admin section). I just override the save and raise a NotImplementedError:
def save(self, **kwargs):
raise NotImplementedError()
(although this is probably not needed in most cases, but it makes me feel a bit better)
I also set managed to False in the Meta class.
class Meta:
managed = False
Then I just pick any field and tag it as the primary key. It doesn't matter if it's really unique with you are just doing filters for displaying information on a page, etc.
Seems to work fine for me. Please commment if there are any problems with this technique that I'm overlooking.
If there really is no primary key in the view, then there is no workaround.
Django requires each model to have exactly one field primary_key=True.
There should have been an auto-generated id field when you ran syncdb (if there is no primary key defined in your model, then Django will insert an AutoField for you).
This error means that Django is asking your database for the id field, but none exists. Can you run django manage.py dbshell and then DESCRIBE read_only_view; and post the result? This will show all of the columns that are in the database.
Alternatively, can you include the model definition you excluded? (and confirm that you haven't altered the model definition since you ran syncdb?)
I know this post is over a decade old, but I ran into this recently and came to SO looking for a good answer. I had to come up with a solution that addresses the OP's original question, and, additionally, allows for us to add new objects to the model for unit testing purposes, which is a problem I still had with all of the provided solutions.
main.py
from django.db import models
def in_unit_test_mode():
"""some code to detect if you're running unit tests with a temp SQLite DB, like..."""
import sys
return "test" in sys.argv
"""You wouldn't want to actually implement it with the import inside here. We have a setting in our django.conf.settings that tests to see if we're running unit tests when the project starts."""
class AbstractReadOnlyModel(models.Model):
class Meta(object):
abstract = True
managed = in_unit_test_mode()
"""This is just to help you fail fast in case a new developer, or future you, doesn't realize this is a database view and not an actual table and tries to update it."""
def save(self, *args, **kwargs):
if not in_unit_test_mode():
raise NotImplementedError(
"This is a read only model. We shouldn't be writing "
"to the {0} table.".format(self.__class__.__name__)
)
else:
super(AbstractReadOnlyModel, self).save(*args, **kwargs)
class DbViewBaseModel(AbstractReadOnlyModel):
not_actually_unique_field = IntegerField(primary_key=True)
# the rest of your field definitions
class Meta:
db_table = 'read_only_view'
if in_unit_test_mode():
class DbView(DbViewBaseModel):
not_actually_unique_field = IntegerField()
"""This line removes the primary key property from the 'not_actually_unique_field' when running unit tests, so Django will create an AutoField named 'id' on the table it creates in the temp DB that it creates for running unit tests."""
else:
class DbView(DbViewBaseModel):
pass
class MainClass(object):
#staticmethod
def main_method(request):
return DbView.objects.all()
test.py
from django.test import TestCase
from main import DbView
from main import MainClass
class TestMain(TestCase):
#classmethod
def setUpTestData(cls):
cls.object_in_view = DbView.objects.create(
"""Enter fields here to create test data you expect to be returned from your method."""
)
def testMain(self):
objects_from_view = MainClass.main_method()
returned_ids = [object.id for object in objects_from_view]
self.assertIn(self.object_in_view.id, returned_ids)