[1.8.x] Fixed #25160 -- Moved unsaved model instance data loss check to Model.save()

This mostly reverts 5643a3b51b and
81e1a35c36.

Thanks Carl Meyer for review.

Backport of 5980b05c1f from master
This commit is contained in:
Tim Graham 2015-07-24 07:51:40 -04:00
parent 90c7078f48
commit e4b813c0e7
15 changed files with 107 additions and 195 deletions

View File

@ -32,6 +32,7 @@ class GenericForeignKey(object):
one_to_one = False
related_model = None
# For backwards compatibility; ignored as of Django 1.8.4.
allow_unsaved_instance_assignment = False
def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True):
@ -243,11 +244,6 @@ class GenericForeignKey(object):
if value is not None:
ct = self.get_content_type(obj=value)
fk = value._get_pk_val()
if not self.allow_unsaved_instance_assignment and fk is None:
raise ValueError(
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
(value, value._meta.object_name)
)
setattr(instance, self.ct_field, ct)
setattr(instance, self.fk_field, fk)

View File

@ -661,6 +661,30 @@ class Model(six.with_metaclass(ModelBase)):
that the "save" must be an SQL insert or update (or equivalent for
non-SQL backends), respectively. Normally, they should not be set.
"""
# Ensure that a model instance without a PK hasn't been assigned to
# a ForeignKey or OneToOneField on this model. If the field is
# nullable, allowing the save() would result in silent data loss.
for field in self._meta.concrete_fields:
if field.is_relation:
# If the related field isn't cached, then an instance hasn't
# been assigned and there's no need to worry about this check.
try:
getattr(self, field.get_cache_name())
except AttributeError:
continue
obj = getattr(self, field.name, None)
# A pk may have been assigned manually to a model instance not
# saved to the database (or auto-generated in a case like
# UUIDField), but we allow the save to proceed and rely on the
# database to raise an IntegrityError if applicable. If
# constraints aren't supported by the database, there's the
# unavoidable risk of data corruption.
if obj and obj.pk is None:
raise ValueError(
"save() prohibited to prevent data loss due to "
"unsaved related object '%s'." % field.name
)
using = using or router.db_for_write(self.__class__, instance=self)
if force_insert and (force_update or update_fields):
raise ValueError("Cannot force both insert and updating in model saving.")

View File

@ -506,7 +506,7 @@ class SingleRelatedObjectDescriptor(object):
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
related_pk = tuple(getattr(instance, field.attname) for field in self.related.field.foreign_related_fields)
if not self.related.field.allow_unsaved_instance_assignment and None in related_pk:
if None in related_pk:
raise ValueError(
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
(value, instance._meta.object_name)
@ -669,12 +669,6 @@ class ReverseSingleRelatedObjectDescriptor(object):
# Set the values of the related field.
else:
for lh_field, rh_field in self.field.related_fields:
pk = value._get_pk_val()
if not self.field.allow_unsaved_instance_assignment and pk is None:
raise ValueError(
'Cannot assign "%r": "%s" instance isn\'t saved in the database.' %
(value, self.field.rel.to._meta.object_name)
)
setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
# Since we already know what the related object is, seed the related
@ -1491,6 +1485,7 @@ class ForeignObject(RelatedField):
one_to_many = False
one_to_one = False
# For backwards compatibility; ignored as of Django 1.8.4.
allow_unsaved_instance_assignment = False
requires_unique_target = True
related_accessor_class = ForeignRelatedObjectsDescriptor

View File

@ -299,13 +299,6 @@ model:
is ``True``. This mirrors the ``for_concrete_model`` argument to
:meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`.
.. attribute:: GenericForeignKey.allow_unsaved_instance_assignment
.. versionadded:: 1.8
Works analogously to :attr:`ForeignKey.allow_unsaved_instance_assignment
<django.db.models.ForeignKey.allow_unsaved_instance_assignment>`.
.. deprecated:: 1.7
This class used to be defined in ``django.contrib.contenttypes.generic``.

View File

@ -1349,30 +1349,6 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
If in doubt, leave it to its default of ``True``.
.. attribute:: ForeignKey.allow_unsaved_instance_assignment
.. versionadded:: 1.8
This flag was added for backwards compatibility as older versions of
Django always allowed assigning unsaved model instances.
Django prevents unsaved model instances from being assigned to a
``ForeignKey`` field to prevent accidental data loss (unsaved foreign keys
are silently ignored when saving a model instance).
If you require allowing the assignment of unsaved instances and aren't
concerned about the data loss possibility (e.g. you never save the objects
to the database), you can disable this check by creating a subclass of the
field class and setting its ``allow_unsaved_instance_assignment`` attribute
to ``True``. For example::
class UnsavedForeignKey(models.ForeignKey):
# A ForeignKey which can point to an unsaved object
allow_unsaved_instance_assignment = True
class Book(models.Model):
author = UnsavedForeignKey(Author)
.. _ref-manytomany:
``ManyToManyField``
@ -1575,12 +1551,6 @@ that control how the relationship functions.
If in doubt, leave it to its default of ``True``.
.. attribute:: ManyToManyField.allow_unsaved_instance_assignment
.. versionadded:: 1.8
Works analogously to :attr:`ForeignKey.allow_unsaved_instance_assignment`.
:class:`ManyToManyField` does not support :attr:`~Field.validators`.
:attr:`~Field.null` has no effect since there is no way to require a

View File

@ -27,3 +27,7 @@ Bugfixes
* Fixed the recording of squashed migrations when running the ``migrate``
command (:ticket:`25231`).
* Moved the :ref:`unsaved model instance assignment data loss check
<unsaved-model-instance-check-18>` to ``Model.save()`` to allow easier usage
of in-memory models (:ticket:`25160`).

View File

@ -684,9 +684,23 @@ This has one backwards incompatible side effect, signal handlers triggered from
these methods are now executed within the method's transaction and any
exception in a signal handler will prevent the whole operation.
.. _unsaved-model-instance-check-18:
Assigning unsaved objects to relations raises an error
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note::
To more easily allow in-memory usage of models, this change was reverted in
Django 1.8.4 and replaced with a check during ``model.save()``. For example::
>>> book = Book.objects.create(name="Django")
>>> book.author = Author(name="John")
>>> book.save()
Traceback (most recent call last):
...
ValueError: save() prohibited to prevent data loss due to unsaved related object 'author'.
Assigning unsaved objects to a :class:`~django.db.models.ForeignKey`,
:class:`~django.contrib.contenttypes.fields.GenericForeignKey`, and
:class:`~django.db.models.OneToOneField` now raises a :exc:`ValueError`.
@ -713,8 +727,8 @@ Now, an error will be raised to prevent data loss::
If you require allowing the assignment of unsaved instances (the old behavior)
and aren't concerned about the data loss possibility (e.g. you never save the
objects to the database), you can disable this check by using the
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
attribute.
``ForeignKey.allow_unsaved_instance_assignment`` attribute. (This attribute was
removed in 1.8.4 as it's no longer relevant.)
Management commands that only accept positional arguments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -55,19 +55,17 @@ relationship. For example, creating an ``Article`` with unsaved ``Reporter``
raises ``ValueError``::
>>> r3 = Reporter(first_name='John', last_name='Smith', email='john@example.com')
>>> Article(headline="This is a test", pub_date=date(2005, 7, 27), reporter=r3)
>>> Article.objects.create(headline="This is a test", pub_date=date(2005, 7, 27), reporter=r3)
Traceback (most recent call last):
...
ValueError: 'Cannot assign "<Reporter: John Smith>": "Reporter" instance isn't saved in the database.'
ValueError: save() prohibited to prevent data loss due to unsaved related object 'reporter'.
If you want to disable the unsaved instance check, you can use the
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
attribute.
.. versionchanged:: 1.8.4
.. versionchanged:: 1.8
Previously, assigning unsaved objects did not raise an error and could
result in silent data loss.
Previously, saving an object with unsaved related objects did not raise an
error and could result in silent data loss. In 1.8-1.8.3, unsaved model
instances couldn't be assigned to related fields, but this restriction was
removed to allow easier usage of in-memory models.
Article objects have access to their related Reporter objects::

View File

@ -92,23 +92,17 @@ relationship. For example, creating an ``Restaurant`` with unsaved ``Place``
raises ``ValueError``::
>>> p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
>>> Restaurant(place=p3, serves_hot_dogs=True, serves_pizza=False)
>>> Restaurant.objects.create(place=p3, serves_hot_dogs=True, serves_pizza=False)
Traceback (most recent call last):
...
ValueError: 'Cannot assign "<Place: Demon Dogs>": "Place" instance isn't saved in the database.'
>>> p.restaurant = Restaurant(place=p, serves_hot_dogs=True, serves_pizza=False)
Traceback (most recent call last):
...
ValueError: 'Cannot assign "<Restaurant: Demon Dogs the restaurant>": "Restaurant" instance isn't saved in the database.'
ValueError: save() prohibited to prevent data loss due to unsaved related object 'place'.
If you want to disable the unsaved instance check, you can use the
:attr:`~django.db.models.ForeignKey.allow_unsaved_instance_assignment`
attribute.
.. versionchanged:: 1.8.4
.. versionchanged:: 1.8
Previously, assigning unsaved objects did not raise an error and could
result in silent data loss.
Previously, saving an object with unsaved related objects did not raise an
error and could result in silent data loss. In 1.8-1.8.3, unsaved model
instances couldn't be assigned to related fields, but this restriction was
removed to allow easier usage of in-memory models.
Restaurant.objects.all() just returns the Restaurants, not the Places. Note
that there are two restaurants - Ace Hardware the Restaurant was created in the

View File

@ -12,7 +12,7 @@ from django.contrib.admin.utils import (
)
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
from django.db import DEFAULT_DB_ALIAS, models
from django.test import TestCase
from django.test import SimpleTestCase, TestCase
from django.utils import six
from django.utils.formats import localize
from django.utils.safestring import mark_safe
@ -94,7 +94,8 @@ class NestedObjectsTests(TestCase):
n.collect([Vehicle.objects.first()])
class UtilsTests(TestCase):
class UtilsTests(SimpleTestCase):
def test_values_from_lookup_field(self):
"""
Regression test for #12654: lookup_field
@ -112,7 +113,7 @@ class UtilsTests(TestCase):
simple_function = lambda obj: SIMPLE_FUNCTION
site_obj = Site.objects.create(domain=SITE_NAME)
site_obj = Site(domain=SITE_NAME)
article = Article(
site=site_obj,
title=TITLE_TEXT,

View File

@ -212,54 +212,6 @@ class GenericForeignKeyTests(IsolatedModelsTestCase):
errors = checks.run_checks()
self.assertEqual(errors, ['performed!'])
def test_unsaved_instance_on_generic_foreign_key(self):
"""
#10811 -- Assigning an unsaved object to GenericForeignKey
should raise an exception.
"""
class Model(models.Model):
content_type = models.ForeignKey(ContentType, null=True)
object_id = models.PositiveIntegerField(null=True)
content_object = GenericForeignKey('content_type', 'object_id')
author = Author(name='Author')
model = Model()
model.content_object = None # no error here as content_type allows None
with self.assertRaisesMessage(ValueError,
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
% (author, author._meta.object_name)):
model.content_object = author # raised ValueError here as author is unsaved
author.save()
model.content_object = author # no error because the instance is saved
def test_unsaved_instance_on_generic_foreign_key_allowed_when_wanted(self):
"""
#24495 - Assigning an unsaved object to a GenericForeignKey
should be allowed when the allow_unsaved_instance_assignment
attribute has been set to True.
"""
class UnsavedGenericForeignKey(GenericForeignKey):
# A GenericForeignKey which can point to an unsaved object
allow_unsaved_instance_assignment = True
class Band(models.Model):
name = models.CharField(max_length=50)
class BandMember(models.Model):
band_ct = models.ForeignKey(ContentType)
band_id = models.PositiveIntegerField()
band = UnsavedGenericForeignKey('band_ct', 'band_id')
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
beatles = Band(name='The Beatles')
john = BandMember(first_name='John', last_name='Lennon')
# This should not raise an exception as the GenericForeignKey between
# member and band has allow_unsaved_instance_assignment=True.
john.band = beatles
self.assertEqual(john.band, beatles)
class GenericRelationshipTests(IsolatedModelsTestCase):

View File

@ -4,6 +4,7 @@ from django import forms
from django.contrib.contenttypes.forms import generic_inlineformset_factory
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.db import IntegrityError
from django.db.models import Q
from django.test import TestCase
from django.utils import six
@ -408,6 +409,15 @@ class GenericRelationsTests(TestCase):
with self.assertRaisesMessage(FieldError, msg):
TaggedItem.objects.get(content_object='')
def test_unsaved_instance_on_generic_foreign_key(self):
"""
Assigning an unsaved object to GenericForeignKey should raise an
exception on model.save().
"""
quartz = Mineral(name="Quartz", hardness=7)
with self.assertRaises(IntegrityError):
TaggedItem.objects.create(tag="shiny", content_object=quartz)
class CustomWidget(forms.TextInput):
pass

View File

@ -120,31 +120,6 @@ class ManyToOneTests(TestCase):
self.assertFalse(hasattr(self.r2.article_set, 'remove'))
self.assertFalse(hasattr(self.r2.article_set, 'clear'))
def test_assign_unsaved_check_override(self):
"""
#24495 - Assigning an unsaved object to a ForeignKey
should be allowed when the allow_unsaved_instance_assignment
attribute has been set to True.
"""
class UnsavedForeignKey(models.ForeignKey):
# A ForeignKey which can point to an unsaved object
allow_unsaved_instance_assignment = True
class Band(models.Model):
name = models.CharField(max_length=50)
class BandMember(models.Model):
band = UnsavedForeignKey(Band)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
beatles = Band(name='The Beatles')
john = BandMember(first_name='John', last_name='Lennon')
# This should not raise an exception as the ForeignKey between member
# and band has allow_unsaved_instance_assignment=True.
john.band = beatles
self.assertEqual(john.band, beatles)
def test_selects(self):
self.r.article_set.create(headline="John's second story",
pub_date=datetime.date(2005, 7, 29))
@ -524,15 +499,13 @@ class ManyToOneTests(TestCase):
# Creation using keyword argument and unsaved related instance (#8070).
p = Parent()
with self.assertRaisesMessage(ValueError,
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
% (p, Child.parent.field.rel.to._meta.object_name)):
Child(parent=p)
msg = "save() prohibited to prevent data loss due to unsaved related object 'parent'."
with self.assertRaisesMessage(ValueError, msg):
Child.objects.create(parent=p)
with self.assertRaisesMessage(ValueError,
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
% (p, Child.parent.field.rel.to._meta.object_name)):
ToFieldChild(parent=p)
msg = "save() prohibited to prevent data loss due to unsaved related object 'parent'."
with self.assertRaisesMessage(ValueError, msg):
ToFieldChild.objects.create(parent=p)
# Creation using attname keyword argument and an id will cause the
# related object to be fetched.

View File

@ -2,8 +2,8 @@ import json
import uuid
from django.core import exceptions, serializers
from django.db import models
from django.test import TestCase
from django.db import IntegrityError, models
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
from .models import (
NullableUUIDModel, PrimaryKeyUUIDModel, RelatedToUUIDModel, UUIDGrandchild,
@ -151,3 +151,14 @@ class TestAsPrimaryKey(TestCase):
def test_two_level_foreign_keys(self):
# exercises ForeignKey.get_db_prep_value()
UUIDGrandchild().save()
class TestAsPrimaryKeyTransactionTests(TransactionTestCase):
# Need a TransactionTestCase to avoid deferring FK constraint checking.
available_apps = ['model_fields']
@skipUnlessDBFeature('supports_foreign_keys')
def test_unsaved_fk(self):
u1 = PrimaryKeyUUIDModel()
with self.assertRaises(IntegrityError):
RelatedToUUIDModel.objects.create(uuid_fk=u1)

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals
from django.db import IntegrityError, connection, models, transaction
from django.db import IntegrityError, connection, transaction
from django.test import TestCase
from .models import (
@ -134,41 +134,9 @@ class OneToOneTests(TestCase):
should raise an exception.
"""
place = Place(name='User', address='London')
with self.assertRaisesMessage(ValueError,
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
% (place, Restaurant.place.field.rel.to._meta.object_name)):
msg = "save() prohibited to prevent data loss due to unsaved related object 'place'."
with self.assertRaisesMessage(ValueError, msg):
Restaurant.objects.create(place=place, serves_hot_dogs=True, serves_pizza=False)
bar = UndergroundBar()
p = Place(name='User', address='London')
with self.assertRaisesMessage(ValueError,
'Cannot assign "%r": "%s" instance isn\'t saved in the database.'
% (bar, p._meta.object_name)):
p.undergroundbar = bar
def test_unsaved_object_check_override(self):
"""
#24495 - Assigning an unsaved object to a OneToOneField
should be allowed when the allow_unsaved_instance_assignment
attribute has been set to True.
"""
class UnsavedOneToOneField(models.OneToOneField):
# A OneToOneField which can point to an unsaved object
allow_unsaved_instance_assignment = True
class Band(models.Model):
name = models.CharField(max_length=50)
class BandManager(models.Model):
band = UnsavedOneToOneField(Band)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
band = Band(name='The Beatles')
manager = BandManager(first_name='Brian', last_name='Epstein')
# This should not raise an exception as the OneToOneField between
# manager and band has allow_unsaved_instance_assignment=True.
manager.band = band
self.assertEqual(manager.band, band)
def test_reverse_relationship_cache_cascade(self):
"""
@ -249,6 +217,11 @@ class OneToOneTests(TestCase):
r = Restaurant(place=p)
self.assertIs(r.place, p)
# Creation using keyword argument and unsaved related instance (#8070).
p = Place()
r = Restaurant(place=p)
self.assertTrue(r.place is p)
# Creation using attname keyword argument and an id will cause the related
# object to be fetched.
p = Place.objects.get(name="Demon Dogs")
@ -392,8 +365,12 @@ class OneToOneTests(TestCase):
"""
p = Place()
b = UndergroundBar.objects.create()
msg = (
'Cannot assign "<UndergroundBar: UndergroundBar object>": "Place" '
'instance isn\'t saved in the database.'
)
with self.assertNumQueries(0):
with self.assertRaises(ValueError):
with self.assertRaisesMessage(ValueError, msg):
p.undergroundbar = b
def test_nullable_o2o_delete(self):