Supongamos que mi models.py es así:
class Character(models.Model):
name = models.CharField(max_length=255)
is_the_chosen_one = models.BooleanField()
Quiero que solo una de mis Character
instancias tenga is_the_chosen_one == True
y todas las demás is_the_chosen_one == False
. ¿Cuál es la mejor manera de garantizar que se respete esta restricción de unicidad?
¡Las mejores calificaciones para las respuestas que tienen en cuenta la importancia de respetar la restricción en los niveles de base de datos, modelo y formulario (administrador)!
database
django
django-models
django-admin
django-forms
sampablokuper
fuente
fuente
through
tablaManyToManyField
que necesita unaunique_together
restricción.Respuestas:
Siempre que he necesitado realizar esta tarea, lo que he hecho es anular el método de guardado para el modelo y hacer que verifique si algún otro modelo ya tiene el indicador configurado (y apagarlo).
class Character(models.Model): name = models.CharField(max_length=255) is_the_chosen_one = models.BooleanField() def save(self, *args, **kwargs): if self.is_the_chosen_one: try: temp = Character.objects.get(is_the_chosen_one=True) if self != temp: temp.is_the_chosen_one = False temp.save() except Character.DoesNotExist: pass super(Character, self).save(*args, **kwargs)
fuente
save(self)
asave(self, *args, **kwargs)
pero la edición fue rechazado. ¿Podría alguno de los revisores tomarse el tiempo para explicar por qué, ya que esto parece ser coherente con las mejores prácticas de Django?get()
usar el objeto Carácter y luego volversave()
a hacerlo, solo necesita filtrar y actualizar, lo que produce solo una consulta SQL y ayuda a mantener la base de datos consistente:if self.is_the_chosen_one:
<newline>Character.objects.filter(is_the_chosen_one=True).update(is_the_chosen_one=False)
<newline>super(Character, self).save(*args, **kwargs)
transaction.atomic
que es importante aquí. También es más eficiente usar una sola consulta.Anularía el método de guardado del modelo y si ha establecido el booleano en Verdadero, asegúrese de que todos los demás estén configurados en Falso.
from django.db import transaction class Character(models.Model): name = models.CharField(max_length=255) is_the_chosen_one = models.BooleanField() def save(self, *args, **kwargs): if not self.is_the_chosen_one: return super(Character, self).save(*args, **kwargs) with transaction.atomic(): Character.objects.filter( is_the_chosen_one=True).update(is_the_chosen_one=False) return super(Character, self).save(*args, **kwargs)
Intenté editar la respuesta similar de Adam, pero fue rechazada por cambiar demasiado la respuesta original. De esta forma es más conciso y eficiente ya que la verificación de otras entradas se realiza en una sola consulta.
fuente
save
en una@transaction.atomic
transacción. Porque podría suceder que elimine todas las banderas, pero luego el guardado falle y termine con todos los caracteres no elegidos.@transaction.atomic
también protege de la condición de carrera.with transaction.atomic:
dentro de la declaración if junto con guardar dentro de if. Luego agregue un bloque else y también guarde en el bloque else.En lugar de usar limpieza / guardado de modelos personalizados, creé un campo personalizado que anula el
pre_save
método endjango.db.models.BooleanField
. En lugar de generar un error si había otro campoTrue
, hice todos los demás camposFalse
si lo estabaTrue
. Además, en lugar de generar un error si el campo estabaFalse
y ningún otro campo lo estabaTrue
, lo guardé comoTrue
fields.py
from django.db.models import BooleanField class UniqueBooleanField(BooleanField): def pre_save(self, model_instance, add): objects = model_instance.__class__.objects # If True then set all others as False if getattr(model_instance, self.attname): objects.update(**{self.attname: False}) # If no true object exists that isnt saved model, save as True elif not objects.exclude(id=model_instance.id)\ .filter(**{self.attname: True}): return True return getattr(model_instance, self.attname) # To use with South from south.modelsinspector import add_introspection_rules add_introspection_rules([], ["^project\.apps\.fields\.UniqueBooleanField"])
modelos.py
from django.db import models from project.apps.fields import UniqueBooleanField class UniqueBooleanModel(models.Model): unique_boolean = UniqueBooleanField() def __unicode__(self): return str(self.unique_boolean)
fuente
Return True
asetattr(model_instance, self.attname, True)
true
si eliminara la únicatrue
fila.La siguiente solución es un poco fea pero podría funcionar:
class MyModel(models.Model): is_the_chosen_one = models.NullBooleanField(default=None, unique=True) def save(self, *args, **kwargs): if self.is_the_chosen_one is False: self.is_the_chosen_one = None super(MyModel, self).save(*args, **kwargs)
Si establece is_the_chosen_one en False o None, siempre será NULL. Puede tener NULL tanto como desee, pero solo puede tener uno True.
fuente
Tratando de llegar a fin de mes con las respuestas aquí, encuentro que algunos de ellos abordan el mismo problema con éxito y cada uno es adecuado en diferentes situaciones:
Yo elegiría:
@semente : respeta la restricción en los niveles de la base de datos, el modelo y el formulario de administración, mientras que anula Django ORM lo menos posible. Además puede
probablementeser utilizado dentro de unathrough
mesa de aManyToManyField
en ununique_together
situación.(Lo comprobaré e informaré)class MyModel(models.Model): is_the_chosen_one = models.NullBooleanField(default=None, unique=True) def save(self, *args, **kwargs): if self.is_the_chosen_one is False: self.is_the_chosen_one = None super(MyModel, self).save(*args, **kwargs)
@Ellis Percival : llega a la base de datos solo una vez más y acepta la entrada actual como la elegida. Limpio y elegante.
from django.db import transaction class Character(models.Model): name = models.CharField(max_length=255) is_the_chosen_one = models.BooleanField() def save(self, *args, **kwargs): if not self.is_the_chosen_one: # The use of return is explained in the comments return super(Character, self).save(*args, **kwargs) with transaction.atomic(): Character.objects.filter( is_the_chosen_one=True).update(is_the_chosen_one=False) # The use of return is explained in the comments return super(Character, self).save(*args, **kwargs)
Otras soluciones no adecuadas para mi caso pero viables:
@nemocorp está anulando el
clean
método para realizar una validación. Sin embargo, no informa qué modelo es "el indicado" y esto no es fácil de usar. A pesar de eso, es un enfoque muy agradable, especialmente si alguien no tiene la intención de ser tan agresivo como @Flyte.@ saul.shanabrook y @Thierry J. crearían un campo personalizado que cambiaría cualquier otra entrada "is_the_one"
False
o generaría unValidationError
. Simplemente soy reacio a implementar nuevas funciones en mi instalación de Django a menos que sea absolutamente necesario.@daigorocub : usa señales de Django. Lo encuentro un enfoque único y da una pista de cómo usar Django Signals . Sin embargo, no estoy seguro de si esto es un uso -estricto- "adecuado" de las señales, ya que no puedo considerar este procedimiento como parte de una "aplicación desacoplada".
fuente
save()
que falle la operación.class Character(models.Model): name = models.CharField(max_length=255) is_the_chosen_one = models.BooleanField() def save(self, *args, **kwargs): if self.is_the_chosen_one: qs = Character.objects.filter(is_the_chosen_one=True) if self.pk: qs = qs.exclude(pk=self.pk) if qs.count() != 0: # choose ONE of the next two lines self.is_the_chosen_one = False # keep the existing "chosen one" #qs.update(is_the_chosen_one=False) # make this obj "the chosen one" super(Character, self).save(*args, **kwargs) class CharacterForm(forms.ModelForm): class Meta: model = Character # if you want to use the new obj as the chosen one and remove others, then # be sure to use the second line in the model save() above and DO NOT USE # the following clean method def clean_is_the_chosen_one(self): chosen = self.cleaned_data.get('is_the_chosen_one') if chosen: qs = Character.objects.filter(is_the_chosen_one=True) if self.instance.pk: qs = qs.exclude(pk=self.instance.pk) if qs.count() != 0: raise forms.ValidationError("A Chosen One already exists! You will pay for your insolence!") return chosen
También puede usar el formulario anterior para administrador, solo use
class CharacterAdmin(admin.ModelAdmin): form = CharacterForm admin.site.register(Character, CharacterAdmin)
fuente
class Character(models.Model): name = models.CharField(max_length=255) is_the_chosen_one = models.BooleanField() def clean(self): from django.core.exceptions import ValidationError c = Character.objects.filter(is_the_chosen_one__exact=True) if c and self.is_the_chosen: raise ValidationError("The chosen one is already here! Too late")
Al hacer esto, la validación estuvo disponible en el formulario de administración básico
fuente
Es más sencillo agregar este tipo de restricción a su modelo después de la versión 2.2 de Django. Puede usar directamente
UniqueConstraint.condition
. Documentos de DjangoSimplemente anule sus modelos de
class Meta
esta manera:class Meta: constraints = [ UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one') ]
fuente
Y eso es todo.
def save(self, *args, **kwargs): if self.default_dp: DownloadPageOrder.objects.all().update(**{'default_dp': False}) super(DownloadPageOrder, self).save(*args, **kwargs)
fuente
Usando un enfoque similar al de Saul, pero con un propósito ligeramente diferente:
class TrueUniqueBooleanField(BooleanField): def __init__(self, unique_for=None, *args, **kwargs): self.unique_for = unique_for super(BooleanField, self).__init__(*args, **kwargs) def pre_save(self, model_instance, add): value = super(TrueUniqueBooleanField, self).pre_save(model_instance, add) objects = model_instance.__class__.objects if self.unique_for: objects = objects.filter(**{self.unique_for: getattr(model_instance, self.unique_for)}) if value and objects.exclude(id=model_instance.id).filter(**{self.attname: True}): msg = 'Only one instance of {} can have its field {} set to True'.format(model_instance.__class__, self.attname) if self.unique_for: msg += ' for each different {}'.format(self.unique_for) raise ValidationError(msg) return value
Esta implementación generará un
ValidationError
al intentar guardar otro registro con un valor de Verdadero.Además, he agregado el
unique_for
argumento que se puede establecer en cualquier otro campo en el modelo, para verificar la unicidad verdadera solo para registros con el mismo valor, como:class Phone(models.Model): user = models.ForeignKey(User) main = TrueUniqueBooleanField(unique_for='user', default=False)
fuente
¿Obtengo puntos por responder a mi pregunta?
El problema era que se encontraba en el bucle, solucionado por:
# is this the testimonial image, if so, unselect other images if self.testimonial_image is True: others = Photograph.objects.filter(project=self.project).filter(testimonial_image=True) pdb.set_trace() for o in others: if o != self: ### important line o.testimonial_image = False o.save()
fuente
Probé algunas de estas soluciones y terminé con otra, solo en aras de la brevedad del código (no tengo que anular formularios o guardar el método). Para que esto funcione, el campo no puede ser único en su definición, pero la señal se asegura de que eso suceda.
# making default_number True unique @receiver(post_save, sender=Character) def unique_is_the_chosen_one(sender, instance, **kwargs): if instance.is_the_chosen_one: Character.objects.all().exclude(pk=instance.pk).update(is_the_chosen_one=False)
fuente
Actualización 2020 para hacer las cosas menos complicadas para los principiantes:
class Character(models.Model): name = models.CharField(max_length=255) is_the_chosen_one = models.BooleanField(blank=False, null=False, default=False) def save(self): if self.is_the_chosen_one == True: items = Character.objects.filter(is_the_chosen_one = True) for x in items: x.is_the_chosen_one = False x.save() super().save()
Por supuesto, si desea que el booleano único sea False, simplemente intercambiaría todas las instancias de True con False y viceversa.
fuente