最終的に、生のSQLクエリを作成して実行し、単一のデータベーストランザクションで自動インクリメントと挿入を行うことで問題を解決しました。 Djangoのソースコードをじっくりと調べて、デフォルトのモデルの保存方法がどのように機能するかを理解し、これを可能な限り堅牢に実行できるようにしました。ただし、MySQL以外のバックエンドではこれを変更する必要があると完全に予想しています。
最初に、ObjectLogが派生する抽象クラスを作成しました。このクラスは、この新しい保存メソッドを備えています。
class AutoIncrementModel(models.Model):
"""
An abstract class used as a base for classes which need the
autoincrementing save method described below.
"""
class Meta:
abstract = True
def save(self, auto_field, auto_fk, *args, **kwargs):
"""
Arguments:
auto_field: name of field which acts as an autoincrement field.
auto_fk: name of ForeignKey to which the auto_field is relative.
"""
# Do normal save if this is not an insert (i.e., the instance has a
# primary key already).
meta = self.__class__._meta
pk_set = self._get_pk_val(meta) is not None
if pk_set:
super(ObjectLog, self).save(*args, **kwargs)
return
# Otherwise, we'll generate some raw SQL to do the
# insert and auto-increment.
# Get model fields, except for primary key field.
fields = meta.local_concrete_fields
if not pk_set:
fields = [f for f in fields if not
isinstance(f, models.fields.AutoField)]
# Setup for generating base SQL query for doing an INSERT.
query = models.sql.InsertQuery(self.__class__._base_manager.model)
query.insert_values(fields, objs=[self])
compiler = query.get_compiler(using=self.__class__._base_manager.db)
compiler.return_id = meta.has_auto_field and not pk_set
fk_name = meta.get_field(auto_fk).column
with compiler.connection.cursor() as cursor:
# Get base SQL query as string.
for sql, params in compiler.as_sql():
# compiler.as_sql() looks like:
# INSERT INTO `table_objectlog` VALUES (%s,...,%s)
# We modify this to do:
# INSERT INTO `table_objectlog` SELECT %s,...,%s FROM
# `table_objectlog` WHERE `object_id`=id
# NOTE: it's unlikely that the following will generate
# a functional database query for non-MySQL backends.
# Replace VALUES (%s, %s, ..., %s) with
# SELECT %s, %s, ..., %s
sql = re.sub(r"VALUES \((.*)\)", r"SELECT \1", sql)
# Add table to SELECT from and ForeignKey id corresponding to
# our autoincrement field.
sql += " FROM `{tbl_name}` WHERE `{fk_name}`={fk_id}".format(
tbl_name=meta.db_table,
fk_name=fk_name,
fk_id=getattr(self, fk_name)
)
# Get index corresponding to auto_field.
af_idx = [f.name for f in fields].index(auto_field)
# Put this directly in the SQL. If we use parameter
# substitution with cursor.execute, it gets quoted
# as a literal, which causes the SQL command to fail.
# We shouldn't have issues with SQL injection because
# auto_field should never be a user-defined parameter.
del params[af_idx]
sql = re.sub(r"((%s, ){{{0}}})%s".format(af_idx),
r"\1IFNULL(MAX({af}),0)+1", sql, 1).format(af=auto_field)
# IFNULL(MAX({af}),0)+1 is the autoincrement SQL command,
# {af} is substituted as the column name.
# Execute SQL command.
cursor.execute(sql, params)
# Get primary key from database and set it in memory.
if compiler.connection.features.can_return_id_from_insert:
id = compiler.connection.ops.fetch_returned_insert_id(cursor)
else:
id = compiler.connection.ops.last_insert_id(cursor,
meta.db_table, meta.pk.column)
self._set_pk_val(id)
# Refresh object in memory in order to get auto_field value.
self.refresh_from_db()
次に、ObjectLogモデルは次のように使用します:
class ObjectLog(AutoIncrementModel):
class Meta:
ordering = ['-created','-N']
unique_together = ("object","N")
object = models.ForeignKey(Object, null=False)
created = models.DateTimeField(auto_now_add=True)
issuer = models.ForeignKey(User)
N = models.IntegerField(null=False)
def save(self, *args, **kwargs):
# Set up to call save method of the base class (AutoIncrementModel)
kwargs.update({'auto_field': 'N', 'auto_fk': 'event'})
super(EventLog, self).save(*args, **kwargs)
これにより、ObjectLog.save()の呼び出しを引き続き期待どおりに機能させることができます。