Kaydet (Commit) 134ca4d4 authored tarafından Alex Hill's avatar Alex Hill Kaydeden (comit) Josh Smeaton

Fixed #24509 -- Added Expression support to SQLInsertCompiler

üst 6e51d5d0
...@@ -263,11 +263,10 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): ...@@ -263,11 +263,10 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys
return OracleSpatialRefSys return OracleSpatialRefSys
def modify_insert_params(self, placeholders, params): def modify_insert_params(self, placeholder, params):
"""Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial """Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
backend due to #10888 backend due to #10888.
""" """
# This code doesn't work for bulk insert cases. if placeholder == 'NULL':
assert len(placeholders) == 1 return []
return [[param for pholder, param return super(OracleOperations, self).modify_insert_params(placeholder, params)
in six.moves.zip(placeholders[0], params[0]) if pholder != 'NULL'], ]
...@@ -576,7 +576,7 @@ class BaseDatabaseOperations(object): ...@@ -576,7 +576,7 @@ class BaseDatabaseOperations(object):
def combine_duration_expression(self, connector, sub_expressions): def combine_duration_expression(self, connector, sub_expressions):
return self.combine_expression(connector, sub_expressions) return self.combine_expression(connector, sub_expressions)
def modify_insert_params(self, placeholders, params): def modify_insert_params(self, placeholder, params):
"""Allow modification of insert parameters. Needed for Oracle Spatial """Allow modification of insert parameters. Needed for Oracle Spatial
backend due to #10888. backend due to #10888.
""" """
......
...@@ -166,9 +166,10 @@ class DatabaseOperations(BaseDatabaseOperations): ...@@ -166,9 +166,10 @@ class DatabaseOperations(BaseDatabaseOperations):
def max_name_length(self): def max_name_length(self):
return 64 return 64
def bulk_insert_sql(self, fields, num_values): def bulk_insert_sql(self, fields, placeholder_rows):
items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
return "VALUES " + ", ".join([items_sql] * num_values) values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
return "VALUES " + values_sql
def combine_expression(self, connector, sub_expressions): def combine_expression(self, connector, sub_expressions):
""" """
......
...@@ -439,6 +439,8 @@ WHEN (new.%(col_name)s IS NULL) ...@@ -439,6 +439,8 @@ WHEN (new.%(col_name)s IS NULL)
name_length = self.max_name_length() - 3 name_length = self.max_name_length() - 3
return '%s_TR' % truncate_name(table, name_length).upper() return '%s_TR' % truncate_name(table, name_length).upper()
def bulk_insert_sql(self, fields, num_values): def bulk_insert_sql(self, fields, placeholder_rows):
items_sql = "SELECT %s FROM DUAL" % ", ".join(["%s"] * len(fields)) return " UNION ALL ".join(
return " UNION ALL ".join([items_sql] * num_values) "SELECT %s FROM DUAL" % ", ".join(row)
for row in placeholder_rows
)
...@@ -221,9 +221,10 @@ class DatabaseOperations(BaseDatabaseOperations): ...@@ -221,9 +221,10 @@ class DatabaseOperations(BaseDatabaseOperations):
def return_insert_id(self): def return_insert_id(self):
return "RETURNING %s", () return "RETURNING %s", ()
def bulk_insert_sql(self, fields, num_values): def bulk_insert_sql(self, fields, placeholder_rows):
items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
return "VALUES " + ", ".join([items_sql] * num_values) values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
return "VALUES " + values_sql
def adapt_datefield_value(self, value): def adapt_datefield_value(self, value):
return value return value
......
...@@ -226,13 +226,11 @@ class DatabaseOperations(BaseDatabaseOperations): ...@@ -226,13 +226,11 @@ class DatabaseOperations(BaseDatabaseOperations):
value = uuid.UUID(value) value = uuid.UUID(value)
return value return value
def bulk_insert_sql(self, fields, num_values): def bulk_insert_sql(self, fields, placeholder_rows):
res = [] return " UNION ALL ".join(
res.append("SELECT %s" % ", ".join( "SELECT %s" % ", ".join(row)
"%%s AS %s" % self.quote_name(f.column) for f in fields for row in placeholder_rows
)) )
res.extend(["UNION ALL SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1))
return " ".join(res)
def combine_expression(self, connector, sub_expressions): def combine_expression(self, connector, sub_expressions):
# SQLite doesn't have a power function, so we fake it with a # SQLite doesn't have a power function, so we fake it with a
......
...@@ -180,6 +180,13 @@ class BaseExpression(object): ...@@ -180,6 +180,13 @@ class BaseExpression(object):
return True return True
return False return False
@cached_property
def contains_column_references(self):
for expr in self.get_source_expressions():
if expr and expr.contains_column_references:
return True
return False
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False): def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
""" """
Provides the chance to do any preprocessing or validation before being Provides the chance to do any preprocessing or validation before being
...@@ -339,6 +346,17 @@ class BaseExpression(object): ...@@ -339,6 +346,17 @@ class BaseExpression(object):
def reverse_ordering(self): def reverse_ordering(self):
return self return self
def flatten(self):
"""
Recursively yield this expression and all subexpressions, in
depth-first order.
"""
yield self
for expr in self.get_source_expressions():
if expr:
for inner_expr in expr.flatten():
yield inner_expr
class Expression(BaseExpression, Combinable): class Expression(BaseExpression, Combinable):
""" """
...@@ -613,6 +631,9 @@ class Random(Expression): ...@@ -613,6 +631,9 @@ class Random(Expression):
class Col(Expression): class Col(Expression):
contains_column_references = True
def __init__(self, alias, target, output_field=None): def __init__(self, alias, target, output_field=None):
if output_field is None: if output_field is None:
output_field = target output_field = target
......
...@@ -458,6 +458,8 @@ class QuerySet(object): ...@@ -458,6 +458,8 @@ class QuerySet(object):
specifying whether an object was created. specifying whether an object was created.
""" """
lookup, params = self._extract_model_params(defaults, **kwargs) lookup, params = self._extract_model_params(defaults, **kwargs)
# The get() needs to be targeted at the write database in order
# to avoid potential transaction consistency problems.
self._for_write = True self._for_write = True
try: try:
return self.get(**lookup), False return self.get(**lookup), False
......
...@@ -909,17 +909,102 @@ class SQLInsertCompiler(SQLCompiler): ...@@ -909,17 +909,102 @@ class SQLInsertCompiler(SQLCompiler):
self.return_id = False self.return_id = False
super(SQLInsertCompiler, self).__init__(*args, **kwargs) super(SQLInsertCompiler, self).__init__(*args, **kwargs)
def placeholder(self, field, val): def field_as_sql(self, field, val):
"""
Take a field and a value intended to be saved on that field, and
return placeholder SQL and accompanying params. Checks for raw values,
expressions and fields with get_placeholder() defined in that order.
When field is None, the value is considered raw and is used as the
placeholder, with no corresponding parameters returned.
"""
if field is None: if field is None:
# A field value of None means the value is raw. # A field value of None means the value is raw.
return val sql, params = val, []
elif hasattr(val, 'as_sql'):
# This is an expression, let's compile it.
sql, params = self.compile(val)
elif hasattr(field, 'get_placeholder'): elif hasattr(field, 'get_placeholder'):
# Some fields (e.g. geo fields) need special munging before # Some fields (e.g. geo fields) need special munging before
# they can be inserted. # they can be inserted.
return field.get_placeholder(val, self, self.connection) sql, params = field.get_placeholder(val, self, self.connection), [val]
else: else:
# Return the common case for the placeholder # Return the common case for the placeholder
return '%s' sql, params = '%s', [val]
# The following hook is only used by Oracle Spatial, which sometimes
# needs to yield 'NULL' and [] as its placeholder and params instead
# of '%s' and [None]. The 'NULL' placeholder is produced earlier by
# OracleOperations.get_geom_placeholder(). The following line removes
# the corresponding None parameter. See ticket #10888.
params = self.connection.ops.modify_insert_params(sql, params)
return sql, params
def prepare_value(self, field, value):
"""
Prepare a value to be used in a query by resolving it if it is an
expression and otherwise calling the field's get_db_prep_save().
"""
if hasattr(value, 'resolve_expression'):
value = value.resolve_expression(self.query, allow_joins=False, for_save=True)
# Don't allow values containing Col expressions. They refer to
# existing columns on a row, but in the case of insert the row
# doesn't exist yet.
if value.contains_column_references:
raise ValueError(
'Failed to insert expression "%s" on %s. F() expressions '
'can only be used to update, not to insert.' % (value, field)
)
if value.contains_aggregate:
raise FieldError("Aggregate functions are not allowed in this query")
else:
value = field.get_db_prep_save(value, connection=self.connection)
return value
def pre_save_val(self, field, obj):
"""
Get the given field's value off the given obj. pre_save() is used for
things like auto_now on DateTimeField. Skip it if this is a raw query.
"""
if self.query.raw:
return getattr(obj, field.attname)
return field.pre_save(obj, add=True)
def assemble_as_sql(self, fields, value_rows):
"""
Take a sequence of N fields and a sequence of M rows of values,
generate placeholder SQL and parameters for each field and value, and
return a pair containing:
* a sequence of M rows of N SQL placeholder strings, and
* a sequence of M rows of corresponding parameter values.
Each placeholder string may contain any number of '%s' interpolation
strings, and each parameter row will contain exactly as many params
as the total number of '%s's in the corresponding placeholder row.
"""
if not value_rows:
return [], []
# list of (sql, [params]) tuples for each object to be saved
# Shape: [n_objs][n_fields][2]
rows_of_fields_as_sql = (
(self.field_as_sql(field, v) for field, v in zip(fields, row))
for row in value_rows
)
# tuple like ([sqls], [[params]s]) for each object to be saved
# Shape: [n_objs][2][n_fields]
sql_and_param_pair_rows = (zip(*row) for row in rows_of_fields_as_sql)
# Extract separate lists for placeholders and params.
# Each of these has shape [n_objs][n_fields]
placeholder_rows, param_rows = zip(*sql_and_param_pair_rows)
# Params for each field are still lists, and need to be flattened.
param_rows = [[p for ps in row for p in ps] for row in param_rows]
return placeholder_rows, param_rows
def as_sql(self): def as_sql(self):
# We don't need quote_name_unless_alias() here, since these are all # We don't need quote_name_unless_alias() here, since these are all
...@@ -933,35 +1018,27 @@ class SQLInsertCompiler(SQLCompiler): ...@@ -933,35 +1018,27 @@ class SQLInsertCompiler(SQLCompiler):
result.append('(%s)' % ', '.join(qn(f.column) for f in fields)) result.append('(%s)' % ', '.join(qn(f.column) for f in fields))
if has_fields: if has_fields:
params = values = [ value_rows = [
[ [self.prepare_value(field, self.pre_save_val(field, obj)) for field in fields]
f.get_db_prep_save(
getattr(obj, f.attname) if self.query.raw else f.pre_save(obj, True),
connection=self.connection
) for f in fields
]
for obj in self.query.objs for obj in self.query.objs
] ]
else: else:
values = [[self.connection.ops.pk_default_value()] for obj in self.query.objs] # An empty object.
params = [[]] value_rows = [[self.connection.ops.pk_default_value()] for _ in self.query.objs]
fields = [None] fields = [None]
can_bulk = (not any(hasattr(field, "get_placeholder") for field in fields) and
not self.return_id and self.connection.features.has_bulk_insert)
if can_bulk: # Currently the backends just accept values when generating bulk
placeholders = [["%s"] * len(fields)] # queries and generate their own placeholders. Doing that isn't
else: # necessary and it should be possible to use placeholders and
placeholders = [ # expressions in bulk inserts too.
[self.placeholder(field, v) for field, v in zip(fields, val)] can_bulk = (not self.return_id and self.connection.features.has_bulk_insert)
for val in values
] placeholder_rows, param_rows = self.assemble_as_sql(fields, value_rows)
# Oracle Spatial needs to remove some values due to #10888
params = self.connection.ops.modify_insert_params(placeholders, params)
if self.return_id and self.connection.features.can_return_id_from_insert: if self.return_id and self.connection.features.can_return_id_from_insert:
params = params[0] params = param_rows[0]
col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column)) col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column))
result.append("VALUES (%s)" % ", ".join(placeholders[0])) result.append("VALUES (%s)" % ", ".join(placeholder_rows[0]))
r_fmt, r_params = self.connection.ops.return_insert_id() r_fmt, r_params = self.connection.ops.return_insert_id()
# Skip empty r_fmt to allow subclasses to customize behavior for # Skip empty r_fmt to allow subclasses to customize behavior for
# 3rd party backends. Refs #19096. # 3rd party backends. Refs #19096.
...@@ -969,13 +1046,14 @@ class SQLInsertCompiler(SQLCompiler): ...@@ -969,13 +1046,14 @@ class SQLInsertCompiler(SQLCompiler):
result.append(r_fmt % col) result.append(r_fmt % col)
params += r_params params += r_params
return [(" ".join(result), tuple(params))] return [(" ".join(result), tuple(params))]
if can_bulk: if can_bulk:
result.append(self.connection.ops.bulk_insert_sql(fields, len(values))) result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows))
return [(" ".join(result), tuple(v for val in values for v in val))] return [(" ".join(result), tuple(p for ps in param_rows for p in ps))]
else: else:
return [ return [
(" ".join(result + ["VALUES (%s)" % ", ".join(p)]), vals) (" ".join(result + ["VALUES (%s)" % ", ".join(p)]), vals)
for p, vals in zip(placeholders, params) for p, vals in zip(placeholder_rows, param_rows)
] ]
def execute_sql(self, return_id=False): def execute_sql(self, return_id=False):
...@@ -1034,10 +1112,11 @@ class SQLUpdateCompiler(SQLCompiler): ...@@ -1034,10 +1112,11 @@ class SQLUpdateCompiler(SQLCompiler):
connection=self.connection, connection=self.connection,
) )
else: else:
raise TypeError("Database is trying to update a relational field " raise TypeError(
"of type %s with a value of type %s. Make sure " "Tried to update field %s with a model instance, %r. "
"you are setting the correct relations" % "Use a value compatible with %s."
(field.__class__.__name__, val.__class__.__name__)) % (field, val, field.__class__.__name__)
)
else: else:
val = field.get_db_prep_save(val, connection=self.connection) val = field.get_db_prep_save(val, connection=self.connection)
......
...@@ -139,9 +139,9 @@ class UpdateQuery(Query): ...@@ -139,9 +139,9 @@ class UpdateQuery(Query):
def add_update_fields(self, values_seq): def add_update_fields(self, values_seq):
""" """
Turn a sequence of (field, model, value) triples into an update query. Append a sequence of (field, model, value) triples to the internal list
Used by add_update_values() as well as the "fast" update path when that will be used to generate the UPDATE query. Might be more usefully
saving models. called add_update_targets() to hint at the extra information here.
""" """
self.values.extend(values_seq) self.values.extend(values_seq)
......
...@@ -5,10 +5,14 @@ Query Expressions ...@@ -5,10 +5,14 @@ Query Expressions
.. currentmodule:: django.db.models .. currentmodule:: django.db.models
Query expressions describe a value or a computation that can be used as part of Query expressions describe a value or a computation that can be used as part of
a filter, order by, annotation, or aggregate. There are a number of built-in an update, create, filter, order by, annotation, or aggregate. There are a
expressions (documented below) that can be used to help you write queries. number of built-in expressions (documented below) that can be used to help you
Expressions can be combined, or in some cases nested, to form more complex write queries. Expressions can be combined, or in some cases nested, to form
computations. more complex computations.
.. versionchanged:: 1.9
Support for using expressions when creating new model instances was added.
Supported arithmetic Supported arithmetic
==================== ====================
...@@ -27,7 +31,7 @@ Some examples ...@@ -27,7 +31,7 @@ Some examples
.. code-block:: python .. code-block:: python
from django.db.models import F, Count from django.db.models import F, Count
from django.db.models.functions import Length from django.db.models.functions import Length, Upper, Value
# Find companies that have more employees than chairs. # Find companies that have more employees than chairs.
Company.objects.filter(num_employees__gt=F('num_chairs')) Company.objects.filter(num_employees__gt=F('num_chairs'))
...@@ -49,6 +53,13 @@ Some examples ...@@ -49,6 +53,13 @@ Some examples
>>> company.chairs_needed >>> company.chairs_needed
70 70
# Create a new company using expressions.
>>> company = Company.objects.create(name='Google', ticker=Upper(Value('goog')))
# Be sure to refresh it if you need to access the field.
>>> company.refresh_from_db()
>>> company.ticker
'GOOG'
# Annotate models with an aggregated value. Both forms # Annotate models with an aggregated value. Both forms
# below are equivalent. # below are equivalent.
Company.objects.annotate(num_products=Count('products')) Company.objects.annotate(num_products=Count('products'))
...@@ -122,6 +133,8 @@ and describe the operation. ...@@ -122,6 +133,8 @@ and describe the operation.
will need to be reloaded:: will need to be reloaded::
reporter = Reporters.objects.get(pk=reporter.pk) reporter = Reporters.objects.get(pk=reporter.pk)
# Or, more succinctly:
reporter.refresh_from_db()
As well as being used in operations on single instances as above, ``F()`` can As well as being used in operations on single instances as above, ``F()`` can
be used on ``QuerySets`` of object instances, with ``update()``. This reduces be used on ``QuerySets`` of object instances, with ``update()``. This reduces
...@@ -356,7 +369,10 @@ boolean, or string within an expression, you can wrap that value within a ...@@ -356,7 +369,10 @@ boolean, or string within an expression, you can wrap that value within a
You will rarely need to use ``Value()`` directly. When you write the expression You will rarely need to use ``Value()`` directly. When you write the expression
``F('field') + 1``, Django implicitly wraps the ``1`` in a ``Value()``, ``F('field') + 1``, Django implicitly wraps the ``1`` in a ``Value()``,
allowing simple values to be used in more complex expressions. allowing simple values to be used in more complex expressions. You will need to
use ``Value()`` when you want to pass a string to an expression. Most
expressions interpret a string argument as the name of a field, like
``Lower('name')``.
The ``value`` argument describes the value to be included in the expression, The ``value`` argument describes the value to be included in the expression,
such as ``1``, ``True``, or ``None``. Django knows how to convert these Python such as ``1``, ``True``, or ``None``. Django knows how to convert these Python
......
...@@ -542,6 +542,10 @@ Models ...@@ -542,6 +542,10 @@ Models
* Added a new model field check that makes sure * Added a new model field check that makes sure
:attr:`~django.db.models.Field.default` is a valid value. :attr:`~django.db.models.Field.default` is a valid value.
* :doc:`Query expressions </ref/models/expressions>` can now be used when
creating new model instances using ``save()``, ``create()``, and
``bulk_create()``.
Requests and Responses Requests and Responses
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
......
...@@ -3,6 +3,8 @@ from __future__ import unicode_literals ...@@ -3,6 +3,8 @@ from __future__ import unicode_literals
from operator import attrgetter from operator import attrgetter
from django.db import connection from django.db import connection
from django.db.models import Value
from django.db.models.functions import Lower
from django.test import ( from django.test import (
TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature, TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature,
) )
...@@ -183,3 +185,12 @@ class BulkCreateTests(TestCase): ...@@ -183,3 +185,12 @@ class BulkCreateTests(TestCase):
TwoFields.objects.all().delete() TwoFields.objects.all().delete()
with self.assertNumQueries(1): with self.assertNumQueries(1):
TwoFields.objects.bulk_create(objs, len(objs)) TwoFields.objects.bulk_create(objs, len(objs))
@skipUnlessDBFeature('has_bulk_insert')
def test_bulk_insert_expressions(self):
Restaurant.objects.bulk_create([
Restaurant(name="Sam's Shake Shack"),
Restaurant(name=Lower(Value("Betty's Beetroot Bar")))
])
bbb = Restaurant.objects.filter(name="betty's beetroot bar")
self.assertEqual(bbb.count(), 1)
...@@ -249,6 +249,32 @@ class BasicExpressionsTests(TestCase): ...@@ -249,6 +249,32 @@ class BasicExpressionsTests(TestCase):
test_gmbh = Company.objects.get(pk=test_gmbh.pk) test_gmbh = Company.objects.get(pk=test_gmbh.pk)
self.assertEqual(test_gmbh.num_employees, 36) self.assertEqual(test_gmbh.num_employees, 36)
def test_new_object_save(self):
# We should be able to use Funcs when inserting new data
test_co = Company(
name=Lower(Value("UPPER")), num_employees=32, num_chairs=1,
ceo=Employee.objects.create(firstname="Just", lastname="Doit", salary=30),
)
test_co.save()
test_co.refresh_from_db()
self.assertEqual(test_co.name, "upper")
def test_new_object_create(self):
test_co = Company.objects.create(
name=Lower(Value("UPPER")), num_employees=32, num_chairs=1,
ceo=Employee.objects.create(firstname="Just", lastname="Doit", salary=30),
)
test_co.refresh_from_db()
self.assertEqual(test_co.name, "upper")
def test_object_create_with_aggregate(self):
# Aggregates are not allowed when inserting new data
with self.assertRaisesMessage(FieldError, 'Aggregate functions are not allowed in this query'):
Company.objects.create(
name='Company', num_employees=Max(Value(1)), num_chairs=1,
ceo=Employee.objects.create(firstname="Just", lastname="Doit", salary=30),
)
def test_object_update_fk(self): def test_object_update_fk(self):
# F expressions cannot be used to update attributes which are foreign # F expressions cannot be used to update attributes which are foreign
# keys, or attributes which involve joins. # keys, or attributes which involve joins.
...@@ -272,7 +298,22 @@ class BasicExpressionsTests(TestCase): ...@@ -272,7 +298,22 @@ class BasicExpressionsTests(TestCase):
ceo=test_gmbh.ceo ceo=test_gmbh.ceo
) )
acme.num_employees = F("num_employees") + 16 acme.num_employees = F("num_employees") + 16
self.assertRaises(TypeError, acme.save) msg = (
'Failed to insert expression "Col(expressions_company, '
'expressions.Company.num_employees) + Value(16)" on '
'expressions.Company.num_employees. F() expressions can only be '
'used to update, not to insert.'
)
self.assertRaisesMessage(ValueError, msg, acme.save)
acme.num_employees = 12
acme.name = Lower(F('name'))
msg = (
'Failed to insert expression "Lower(Col(expressions_company, '
'expressions.Company.name))" on expressions.Company.name. F() '
'expressions can only be used to update, not to insert.'
)
self.assertRaisesMessage(ValueError, msg, acme.save)
def test_ticket_11722_iexact_lookup(self): def test_ticket_11722_iexact_lookup(self):
Employee.objects.create(firstname="John", lastname="Doe") Employee.objects.create(firstname="John", lastname="Doe")
......
...@@ -98,8 +98,13 @@ class BasicFieldTests(test.TestCase): ...@@ -98,8 +98,13 @@ class BasicFieldTests(test.TestCase):
self.assertTrue(instance.id) self.assertTrue(instance.id)
# Set field to object on saved instance # Set field to object on saved instance
instance.size = instance instance.size = instance
msg = (
"Tried to update field model_fields.FloatModel.size with a model "
"instance, <FloatModel: FloatModel object>. Use a value "
"compatible with FloatField."
)
with transaction.atomic(): with transaction.atomic():
with self.assertRaises(TypeError): with self.assertRaisesMessage(TypeError, msg):
instance.save() instance.save()
# Try setting field to object on retrieved object # Try setting field to object on retrieved object
obj = FloatModel.objects.get(pk=instance.id) obj = FloatModel.objects.get(pk=instance.id)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment