【问题标题】:Django: Access primary key in models.filefield(upload_to) locationDjango:访问models.filefield(upload_to)位置中的主键
【发布时间】:2009-03-16 20:02:38
【问题描述】:

我想使用条目的主键保存我的文件。

这是我的代码:

def get_nzb_filename(instance, filename):
    if not instance.pk:
        instance.save() # Does not work.
    name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
    name_slug = re.sub('[-]+', '-', name_slug)
    return u'files/%s_%s.nzb' % (instance.pk, name_slug)

class File(models.Model):
    nzb = models.FileField(upload_to=get_nzb_filename)
    name = models.CharField(max_length=256)

我知道第一次保存对象时主键不可用,所以我愿意额外点击保存对象以获取主键,然后继续。

上面的代码不起作用。它抛出以下错误:

maximum recursion depth exceeded while calling a Python object

我假设这是一个无限循环。调用save 方法将调用get_nzb_filename 方法,这将再次调用save 方法,依此类推。

我正在使用最新版本的 Django 主干。

如何获取主键,以便使用它来保存上传的文件?


更新@muhuk:

我喜欢你的解决方案。你能帮我实现它吗?我已将代码更新为以下内容,错误为'File' object has no attribute 'create'。也许我在使用你写的断章取义的东西?

def create_with_pk(self):
    instance = self.create()
    instance.save()
    return instance

def get_nzb_filename(instance, filename):
    if not instance.pk:
        create_with_pk(instance)
    name_slug = re.sub('[^a-zA-Z0-9]', '-', instance.name).strip('-').lower()
    name_slug = re.sub('[-]+', '-', name_slug)
    return u'files/%s_%s.nzb' % (instance.pk, name_slug)

class File(models.Model):
    nzb = models.FileField(upload_to=get_nzb_filename, blank=True, null=True)
    name = models.CharField(max_length=256)

我不会在我的模型中强制执行必填字段,而是在我的 Form 类中执行此操作。没问题。

【问题讨论】:

    标签: python django file-io


    【解决方案1】:

    看来您需要先使用空文件字段预先生成 File 模型。然后拿起一个并将其与给定的文件对象一起保存。

    你可以有一个这样的自定义管理器方法;

    def create_with_pk(self):
        instance = self.create()
        instance.save()     # probably this line is unneeded
        return instance
    

    但是,如果您的任何一个字段都是必需的,这将很麻烦。因为您最初创建的是一个空对象,所以您不能在模型级别强制执行必填字段。

    编辑

    create_with_pk 应该是custom manager method,在您的代码中它只是一个常规方法。因此self 毫无意义。这一切都是正确的documented 示例。

    【讨论】:

    • instance = self.create()之后不需要instance.save()
    • 我无法推断此解决方案中涉及的任何额外步骤。我创建了一个自定义管理器,定义了上面提到的方法,但最终进行了两次保存,其中第一次保存创建了一个空对象,第二次未能修改正确的对象。我认为任何试图使用这种方法的人也必须修改他们的保存功能。最终切换到下面的@Giles 解决方案。
    【解决方案2】:

    您可以通过将upload_to 设置为临时位置并创建自定义保存方法来做到这一点。

    save 方法应该首先调用 super 来生成主键(这会将文件保存到临时位置)。然后您可以使用主键重命名文件并将其移动到正确的位置。再调用一次 super 以保存更改,您就可以开始了!当我遇到这个确切的问题时,这对我很有效。

    例如:

    class File( models.Model ):
        nzb = models.FileField( upload_to='temp' )
    
        def save( self, *args, **kwargs ):
            # Call save first, to create a primary key
            super( File, self ).save( *args, **kwargs )
    
            nzb = self.nzb
            if nzb:
                # Create new filename, using primary key and file extension
                oldfile = self.nzb.name
                dot = oldfile.rfind( '.' )
                newfile = str( self.pk ) + oldfile[dot:]
    
                # Create new file and remove old one
                if newfile != oldfile:
                    self.nzb.storage.delete( newfile )
                    self.nzb.storage.save( newfile, nzb )
                    self.nzb.name = newfile 
                    self.nzb.close()
                    self.nzb.storage.delete( oldfile )
    
            # Save again to keep changes
            super( File, self ).save( *args, **kwargs )
    

    【讨论】:

    • 我可以对其进行修改,以便我可以将主键作为所服务文件路径的一部分。谢谢。
    • @seanmus 效果好吗?我也想用pk来生成路径,但是有一些严重的牦牛剃须(编辑后的相对路径连接,交换具有相似名称的文件的问题,删除等)。你有一个优雅而健壮的例子的链接吗?
    【解决方案3】:

    上下文

    有同样的问题。 通过首先保存对象来解决它为当前对象分配一个id。

    方法

    1. 创建自定义 upload_to 函数
    2. 检测对象是否有pk
    3. 如果没有,请先保存实例,检索 pk 并将其分配给对象
    4. 用它生成你的路径

    示例工作代码:

    class Image(models.Model):
        def upload_path(self, filename):
            if not self.pk:
                i = Image.objects.create()
                self.id = self.pk = i.id
            return "my/path/%s" % str(self.id)
        file = models.ImageField(upload_to=upload_path)
    

    【讨论】:

    • i = Image.objects.create()之后不需要i.save()
    • 这不起作用,因为创建的新对象保持为空,而您正在保存的对象被保存为不同的对象。
    【解决方案4】:

    您可以创建 pre_save 和 post_save 信号。当 pk 已创建时,实际文件保存将在 post_save 中。 不要忘记在 app.py 中包含信号,以便它们工作。 这是一个例子:

    _UNSAVED_FILE_FIELD = 'unsaved_file'
    
    
    @receiver(pre_save, sender=File)
    def skip_saving_file_field(sender, instance: File, **kwargs):
        if not instance.pk and not hasattr(instance, _UNSAVED_FILE_FIELD):
            setattr(instance, _UNSAVED_FILE_FIELD, instance.image)
            instance.nzb = None
    
    
    @receiver(post_save, sender=File)
    def save_file_field(sender, instance: Icon, created, **kwargs):
        if created and hasattr(instance, _UNSAVED_FILE_FIELD):
            instance.nzb = getattr(instance, _UNSAVED_FILE_FIELD)
            instance.save()
    

    【讨论】:

      【解决方案5】:

      这里有两种可能的解决方案:

      在插入行之前检索id

      为简单起见,我使用 postgresql 数据库,尽管可以为您的数据库后端调整实现。

      默认情况下,django 创建idbigserial(或serial,取决于DEFAULT_AUTO_FIELD)。例如这个模型:

      class File(models.Model):
          nzb = models.FileField(upload_to=get_nzb_filename)
          name = models.CharField(max_length=256)
      

      产生以下 DDL:

      CREATE TABLE "example_file" ("id" bigserial NOT NULL PRIMARY KEY, "nzb" varchar(100) NOT NULL, "name" varchar(256) NOT NULL);
      

      没有明确的序列规范。默认情况下,bigserialtablename_colname_seq 的形式创建序列名称(在我们的例子中为example_file_id_seq

      解决方案是使用nextval 检索此id:

      def get_nextval(model, using=None):
          seq_name = f"{model._meta.db_table}_id_seq"
          if using is None:
              using = "default"
          with connections[using].cursor() as cursor:
              cursor.execute("select nextval(%s)", [seq_name])
              return cursor.fetchone()[0]
      

      并在保存模型之前设置:

      class File(models.Model):
          # fields definition
      
          def save(
              self, force_insert=False, force_update=False, using=None, update_fields=None
          ):
              if not self.pk:
                  self.pk = get_nextval(self, using=using)
                  force_insert = True
              super().save(
                  force_insert=force_insert,
                  force_update=force_update,
                  using=using,
                  update_fields=update_fields,
              )
      

      请注意,我们依赖 force_insert 行为,因此请确保 read documentation 并使用测试覆盖您的代码:

      from django.core.files.uploadedfile import SimpleUploadedFile
      from django.forms import ModelForm
      from django.test import TestCase
      
      from example import models
      
      
      class FileForm(ModelForm):
          class Meta:
              model = models.File
              fields = (
                  "nzb",
                  "name",
              )
      
      
      class FileTest(TestCase):
          def test(self):
              form = FileForm(
                  {
                      "name": "picture",
                  },
                  {
                      "nzb": SimpleUploadedFile("filename", b"content"),
                  },
              )
              self.assertTrue(form.is_valid())
              form.save()
      
              self.assertEqual(models.File.objects.count(), 1)
              f = models.File.objects.first()
              self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
      

      不带nzt 插入,然后用实际nzt 值更新

      这个想法是不言自明的——我们基本上在对象创建时弹出nzt,并在我们知道id之后再次保存对象:

      def save(
          self, force_insert=False, force_update=False, using=None, update_fields=None
      ):
          nzb = None
          if not self.pk:
              nzb = self.nzb
              self.nzb = None
      
          super().save(
              force_insert=force_insert,
              force_update=force_update,
              using=using,
              update_fields=update_fields,
          )
      
          if nzb:
              self.nzb = nzb
              super().save(
                  force_insert=False,
                  force_update=True,
                  using=using,
                  update_fields=["nzb"],
              )
      

      更新测试以检查实际查询:

      def test(self):
          form = FileForm(
              {
                  "name": "picture",
              },
              {
                  "nzb": SimpleUploadedFile("filename", b"content"),
              },
          )
          self.assertTrue(form.is_valid())
          with CaptureQueriesContext(connection) as ctx:
              form.save()
      
          self.assertEqual(models.File.objects.count(), 1)
          f = models.File.objects.first()
          self.assertRegexpMatches(f.nzb.name, rf"files/{f.pk}_picture(.*)\.nzb")
      
          self.assertEqual(len(ctx.captured_queries), 2)
          insert, update = ctx.captured_queries
          self.assertEqual(
              insert["sql"],
              '''INSERT INTO "example_file" ("nzb", "name") VALUES ('', 'picture') RETURNING "example_file"."id"''',
          )
          self.assertRegexpMatches(
              update["sql"],
              rf"""UPDATE "example_file" SET "nzb" = 'files/{f.pk}_picture(.*)\.nzb' WHERE "example_file"."id" = {f.pk}""",
          )
      

      【讨论】:

      • 谢谢。 get_nextval() 方法很酷。这行得通,我也使用 PostgreSQL。
      【解决方案6】:

      Ty,你推出自己的 slugify 过滤器有什么原因吗?

      Django 带有一个内置的slugify 过滤器,你可以这样使用它:

      from django.template.defaultfilters import slugify
      
      slug = slugify(some_string)
      

      不确定您是否知道它可以使用...

      【讨论】:

      • 我认为你的答案是正确的,但它不适合这个问题。还是我错过了什么?
      • 这已经很老了,但你是对的,它没有回答问题,而是指出作者可能不需要创建自己的 slugify 方法
      【解决方案7】:

      您可以使用下一个可用的主键 ID:

      class Document(models.Model):
          def upload_path(self, filename):
              if not self.pk:
                  document_next_id = Document.objects.order_by('-id').first().id + 1
                  self.id = self.pk = document_next_id
              return "my/path/document-%s" % str(self.pk)
          document = models.FileField(upload_to=upload_path)
      

      详情

      我的例子是对@vinyll's answer的修改,但是Giles mentioned in his comment的问题(正在创建两个对象)在这里解决了。

      我知道我的答案并不完美,并且“下一个可用 ID”可能存在问题,例如,当更多用户尝试同时提交多个表单时。 Giles 的答案更健壮,我的更简单(无需生成临时文件,然后移动文件并删除它们)。对于更简单的应用程序,这就足够了。

      还要感谢 Tjorriemorrie 在how to get the next available ID 对象上的清晰示例。

      【讨论】:

      • 如果同时有多个上传,像这样获取下一个id会失败。
      • 是的,我知道这一点,这就是我在回答中作为免责声明所写的内容。就我的程序而言,一次只能上传一个(单个帐户/用户),因此获取下一个这样的 id 就足够了。
      【解决方案8】:

      嗯,我不确定我的答案,但是-

      如果可以的话,使用嵌套模型 -

      class File(models.Model):
          name = models.CharField(max_length=256)
      
      class FileName(models.Model):
          def get_nzb_filename(instance, filename):
              return instance.name
          name = models.ForeignKey(File)
          nzb = models.FileField(upload_to=get_nzb_filename)
      

      在创建方法中-

      File_name = validated_data.pop(file_name_data)
      file = File.objects.create(validated_data)
      F = FileName.objects.create(name=file, **File_name)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-06-16
        • 2012-08-01
        • 2012-09-07
        • 1970-01-01
        • 1970-01-01
        • 2019-05-26
        • 1970-01-01
        • 2010-10-09
        相关资源
        最近更新 更多