Creación de un modelo de perfil con un InlineAdmin y una señal de post_save en Django

Creé un modelo de “perfil” (con una relación de 1 a 1 con el modelo de usuario) como se describe en Ampliación del modelo de usuario existente . El modelo de perfil tiene una relación opcional de muchos a uno con otro modelo:

class Profile(models.Model): user = models.OneToOneField(User, primary_key=True) account = models.ForeignKey(Account, blank=True, null=True, on_delete=models.SET_NULL) 

Como se documenta allí, también creé un administrador en línea:

 class ProfileInline(admin.StackedInline): model = Profile can_delete = False verbose_name_plural = 'profiles' # UserAdmin and unregister()/register() calls omitted, they are straight copies from the Django docs 

Ahora, si no selecciono una account en el administrador al crear al usuario, no se creará el modelo de perfil. Así que me conecto a la señal post_save , de nuevo solo siguiendo la documentación:

 @receiver(post_save, sender=User) def create_profile_for_new_user(sender, created, instance, **kwargs): if created: profile = Profile(user=instance) profile.save() 

Esto funciona bien siempre y cuando no seleccione una account en el administrador, pero si lo hago, obtendré una excepción IntegrityError , que me dice que el duplicate key value violates unique constraint "app_profile_user_id_key" DETAIL: Key (user_id)=(15) already exists.

Aparentemente, el administrador en línea intenta crear la instancia del profile , pero mi post_save señales post_save ya lo creó en ese momento.

¿Cómo soluciono este problema, manteniendo todos los requisitos siguientes?

  1. No importa cómo se cree el nuevo usuario, siempre habrá un modelo de profile que se vincule con él también después.
  2. Si el usuario selecciona una account en el administrador durante la creación del usuario, esta account se configurará en el nuevo modelo de profile posteriormente. Si no, el campo es null.

Ambiente: Django 1.5, Python 2.7

Preguntas relacionadas:

  • Creación de un perfil de usuario extendido (síntomas similares, pero la causa resultó ser diferente)

El problema se puede evitar configurando primary_key=True en el OneToOneField apunta al modelo de User , tal como lo ha descubierto usted mismo.

La razón por la que esto funciona parece ser bastante simple.

Cuando intenta crear una instancia de modelo y establecer pk manualmente antes de guardarla, Django intentará encontrar un registro en la base de datos con ese pk y actualizarlo en lugar de intentar crear uno nuevo a ciegas. Si no existe ninguno, crea el nuevo registro como se esperaba.

Cuando configura OneToOneField como la clave principal y Django Admin configura ese campo con el ID del modelo de User relacionado, eso significa que pk ya está establecido y Django intentará encontrar un registro existente primero.

Esto es lo que sucede con el conjunto OneToOneField como clave principal:

  1. Django Admin crea la nueva instancia de User , sin id .
  2. Django Admin guarda la instancia del User .
    1. Debido a que pk (en este caso, id ) no está establecido, Django intenta crear un nuevo registro.
    2. La id del nuevo registro se establece automáticamente por la base de datos.
    3. El gancho post_save crea una nueva instancia de Profile para esa instancia de User .
  3. Django Admin crea la nueva instancia de Profile , con su user establecido en el id del usuario.
  4. Django Admin guarda la instancia del Profile .
    1. Debido a que el pk (en este caso, el user ) ya está establecido, Django intenta recuperar un registro existente con ese pk .
    2. Django encuentra el registro existente y lo actualiza.

Si no establece la clave principal explícitamente, Django agrega un campo que usa la funcionalidad de auto_increment la base de datos: la base de datos establece el pk al siguiente valor más grande que no existe. Esto significa que el campo se dejará en blanco a menos que lo establezca manualmente y, por lo tanto, Django siempre intentará insertar un nuevo registro, lo que OneToOneField un conflicto con la restricción de unicidad en el OneToOneField .

Esto es lo que causa el problema original:

  1. Django Admin crea la nueva instancia de User , sin id .
  2. Django Admin guarda la instancia de User , el post_save creando una nueva instancia de Profile como antes.
  3. Django Admin crea la nueva instancia de Profile , sin id (el campo pk agregado automáticamente).
  4. Django Admin guarda la instancia del Profile .
    1. Debido a que pk (en este caso, id ) no está establecido, Django intenta crear un nuevo registro.
    2. La base de datos informa de una violación de la restricción de unicidad de la tabla en el campo de user .
    3. Django lanza una excepción. No irás al espacio hoy.

Parece que establecer primary_key=True en el OneToOneField conecta el modelo de perfil con el modelo de User soluciona este problema. Sin embargo, no creo que entienda todas las implicaciones de eso y por qué ayuda.

Dejaré esto aquí como una sugerencia, pero si esa es la mejor solución y alguien podría tener una explicación bien escrita, lo votaría / aceptaría y posiblemente eliminaría la mía.