I have a ManyToMany relationship that indicates a Doctor can have many specialties, but only one of them is the PRIMARY one.
I've designed a custom M2M class as follows:
class Doctor(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
specialty = models.ManyToManyField(Specialty, through='DoctorSpecialty')
.....
class Specialty(models.Model):
title = models.CharField(max_length=45)
.....
class DoctorSpecialty(models.Model):
doctor = models.ForeignKey(Doctor, on_delete=models.CASCADE)
specialty = models.ForeignKey(Specialty, on_delete=models.CASCADE)
default = models.BooleanField(default=True)
The doctor can have many specialties, but only one of them can be the default one. He or she can have many specialties with the default field set as False, but cannot have more than one with the default field set as True
I wanted to do something like this:
class Meta:
constraints = [
models.UniqueConstraint(fields=['doctor', 'specialty', 'default'], name='unique specialty')
]
But this will mean that the doctor can have only one specialty as a default one, and only one other as a non default one.
How can we achieve this with the minimum of code?
PS: I could leave it without constraints and try to validate adding new entries by checking if another default specialty exists, but this will add a lot of overhead and exception raising.
I think there is no way we can achieve this with built-in functions. So I came up with this solution (since no one else answered):
I created another ForeignKey for the Primary Specialty, and ditched the DoctorSpecialty custom M2M class and left the M2M relationship with Specialty. One doctor can have only one primary specialty, and can also choose additional specialties as secondary. Later on in the views, I can put in place an algorithm to remove the primary specialty from the list of specialties when entering additional ones in case there is an existing primary specialty.
Related
Can you explain the difference between related_name and related_query_name attributes for the Field object in Django ? When I use them, How to use them? Thanks!
related_name will be the attribute of the related object that allows you to go 'backwards' to the model with the foreign key on it. For example, if ModelA has a field like: model_b = ForeignKeyField(ModelB, related_name='model_as'), this would enable you to access the ModelA instances that are related to your ModelB instance by going model_b_instance.model_as.all(). Note that this is generally written with a plural for a Foreign Key, because a foreign key is a one to many relationship, and the many side of that equation is the model with the Foreign Key field declared on it.
The further explanation linked to in the docs is helpful. https://docs.djangoproject.com/en/dev/topics/db/queries/#backwards-related-objects
related_query_name is for use in Django querysets. It allows you to filter on the reverse relationship of a foreign key related field. To continue our example - having a field on Model A like:
model_b = ForeignKeyField(ModelB, related_query_name='model_a') would enable you to use model_a as a lookup parameter in a queryset, like: ModelB.objects.filter(model_a=whatever). It is more common to use a singular form for the related_query_name. As the docs say, it isn't necessary to specify both (or either of) related_name and related_query_name. Django has sensible defaults.
class Musician(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
Here foreign key forward relation is Album to musician and backward relation is musician to album. This means one album instance can have relation with only one musician instance(forward relation), and one musician instance can relate to multiple album instance(backward).
Forward query will be like this
Album_instance.artist
note here forward query done by Album_instance followed by artist(field name). and backward would be
Musician_instance.album_set.all()
here for backward query modelname_set is used .
now if you specifies the related_name like
artist = models.ForeignKey(Musician, on_delete=models.CASCADE, related_name='back')
then backward query syntax will be change modelname_set(artist.set) will be replace by back.
now backward query
Musician_instance.back.all()
If you’d prefer Django not to create a backwards relation, set related_name to '+' or end it with '+'.
and related_query_name to use for the reverse filter name from the target model
class Basket:
name = models.CharField(max_length=50, blank=True, null=True)
class Apple:
name = models.CharField(max_length=50, blank=True, null=True)
basket = models.ForeignKey(Basket, on_delete=models.PROTECT)
...
myapple = new Apple(name="my")
myapple.save()
...
auto_created_basket = myapple.basket
myapple.basket = existing_basket
auto_created_basket.delete()
I try to swap out the auto_created_basket to another one, but I get an error when I try to delete it.
"Cannot delete some instances of model 'Basket' because they are referenced through a protected foreign key: 'Apple.basket'", [<Apple: My apple>])
In your Apple model the basket field is a foreing key
basket = models.ForeignKey(Basket, on_delete=models.PROTECT)
whose on_delete attribute value specifically states to protect the apples by preventing the deletion of the basket.
As the official docs say
When an object referenced by a ForeignKey is deleted, Django by
default emulates the behavior of the SQL constraint ON DELETE CASCADE
and also deletes the object containing the ForeignKey.
and
the PROTECT parameter prevents deletion of the referenced object by raising
ProtectedError
So, the easiest step should be to remove the on_delete parameter and go with the default behaviour
basket = models.ForeignKey(Basket)
However, please have a look at all the possible parameters to a ForeignKey model field and choose the combination that suits the requirements of your application/scenario.
UPDATE:
Recent Django versions require on_delete. Just don't remove it and add the parameter you want (like on_delete=models.CASCADE).
I hate to answer my question, but my example was too over simplified. The answers are very good points.
In the real product there are post_save signals involved, responsible for the creation of the auto_created_basket for example.
The problem is that when I say myapple.basket = existing_basket Django's layer is fine, but the DB is still holding the reference to older relation. The solution in my case was to move the auto_created_basket.delete() after I save myapple once more.
You can try moving apples from auto_created_basket into existing_basket before deleting the basket first:
>>> auto_created_basket.apple_set.update(basket=existing_basket)
>>> auto_created_basket.delete()
or
>>> myapple.basket = existing_basket
>>> myapple.save()
>>> auto_created_basket.delete()
Alternatively, if you want to collect apples that are from deleted baskets in a single basket, you can assign a function to on_delete property as follows:
def get_sentinel_basket():
basket, created = Basket.objects.get_or_create(name='DELETED')
return basket
--
basket = models.ForeignKey(Basket, on_delete=models.SET(get_sentinel_basket))
so when a basket is deleted, the .basket attribute of apples in that basket will automatically be set to Basket(name='DELETED').
I've a Django model
class Person(models.Model):
name = models.CharField(max_length=50)
team = models.ForeignKey(Team)
And a team model
class Team(models.Model):
name = models.CharField(max_length=50)
Then, I would like to add a 'coach' property which is a one to one relationship to person. If I am not wrong, I have two ways of doing it.
The first approach would be adding the field to Team:
class Team(models.Model):
name = models.CharField(max_length=50)
coach = models.OneToOneField(Person, related_name='master')
The second one would be creating a new model:
class TeamCoach(models.Model):
team = models.OneToOneField(Team)
coach = models.OneToOneField(Person)
Is this right ? is there a big difference for practical purposes ? which are the pro and cons of each approach ?
I will say NEITHER, as every Person has a Team and if every Team has a Coach, it's rather redundant circulation and somewhat unnecessary.
Better to add a field in Person called type directly is more clean and direct, something like:
class Person(models.Model):
# use _ if you care about i18n
TYPES = ('member', 'member',
'coach', 'coach',)
name = models.CharField(max_length=50)
team = models.ForeignKey(Team)
type = models.CharField(max_length=20, choices=TYPES)
Although I would seriously consider refactoring Person to be more generic and get Team to have a ManyToMany to Person... in that case, you can re-use Person in other areas, like Cheerleaders.
class Person(models.Model):
# use _ if you care about i18n
TYPES = ('member', 'member',
'coach', 'coach',)
name = models.CharField(max_length=50)
type = models.CharField(max_length=20, choices=TYPES)
class Team(models.Model):
name = models.CharField(max_length=50)
member = models.ManyToManyField(Person, related_name='master')
Make your models more generic and DRY, they should be easily manageable and not tightly coupled to certain fields (unless absolutely necessary), then the models are more future proof and will not fall under migration nightmare that easily.
Hope this helps.
I can't agree so easy with #Anzel, and since the name of the question is
What are the benefits of having two models instead of one?
I'll try to give my two cents. But before i start i want to place some quotes from the docs.
It doesn’t matter which model has the ManyToManyField, but you should
only put it in one of the models – not both.
Generally, ManyToManyField instances should go in the object that’s
going to be edited on a form. In the above example, toppings is in
Pizza (rather than Topping having a pizzas ManyToManyField ) because
it’s more natural to think about a pizza having toppings than a
topping being on multiple pizzas. The way it’s set up above, the Pizza
form would let users select the toppings.
Basically that's the first thing you should have in mind when creating a M2M relation (your TeamCoach model is that, but more on that in a second) which one is the object holding the relation. What would be more suitable for your problem - choosing a coach for a team when you create it, or choosing a team for a person when you create it? IF you ask me i would prefer the second variant and keep the teams inside of the Person class.
Now lets go to the next section of the docs
Extra fields on many-to-many relationships
When you’re only dealing with simple many-to-many relationships such
as mixing and matching pizzas and toppings, a standard ManyToManyField
is all you need. However, sometimes you may need to associate data
with the relationship between two models.
For example, consider the case of an application tracking the musical
groups which musicians belong to. There is a many-to-many relationship
between a person and the groups of which they are a member, so you
could use a ManyToManyField to represent this relationship. However,
there is a lot of detail about the membership that you might want to
collect, such as the date at which the person joined the group.
For these situations, Django allows you to specify the model that will
be used to govern the many-to-many relationship. You can then put
extra fields on the intermediate model. The intermediate model is
associated with the ManyToManyField using the through argument to
point to the model that will act as an intermediary.
That's actually the answer of your question, having an intermediate model give you the ability to store additional data about the collection. Consider the situation where a coach moves to another team next season, if you just update the M2M relation, you will loose the track of his past teams where he was coaching. Or you will never be able to answer the question who was the coach of that team at year XXX. So if you need more data, go with intermediate model. This is also were #Anzel going wrong, the type field is an additional data of that intermediate model, it's place must be inside it.
Now here is how i would probably create the relations:
class Person(models.Model):
name = models.CharField(max_length=50)
teams = models.ManyToManyField('Team', through='TeamRole')
class Team(models.Model):
name = models.CharField(max_length=50)
class TeamRole(models.Model):
COACH = 1
PLAYER = 2
CHEERLEADER = 3
ROLES = (
(COACH, 'Coach'),
(PLAYER, 'Player'),
(CHEERLEADER, 'Cheerleader'),
)
team = models.ForeignKey(Team)
person = models.ForeignKey(Person)
role = models.IntegerField(choices=ROLES)
date_joined = models.DateField()
date_left = models.DateField(blank=True, null=True, default=None)
How will I query this? Well, I can use the role to get what type of persons I'm looking for, and I can also use the date_left field to get the current persons participating in that team right now. Here are a few example methods:
class Person(models.Model):
#...
def get_current_team(self):
return self.teams.filter(teamrole__date_left__isnull=True).get()
class Team(models.Model):
#...
def _get_persons_by_role(self, role, only_active):
persons = self.person_set.filter(teamrole__role=role)
if only_active:
return persons.filter(teamrole__date_left__isnull=True)
return persons
def get_coaches(self, only_active=True):
return self._get_persons_by_role(TeamRole.COACH, only_active)
def get_players(self, only_active=True):
return self._get_persons_by_role(TeamRole.PLAYER, only_active)
I am creating a web application to manage robotics teams for our area. In the application I have a django model that looks like this:
class TeamFormNote(models.Model):
team = models.ForeignKey(Team, blank=True, null=True)
member = models.ForeignKey(TeamMember, blank=True, null=True)
notes = models.TextField()
def __unicode__(self):
if self.team:
return "Team Form Record: " + unicode(self.team)
if self.member:
return "Member Form Record: " + unicode(self.member)
Essentially, I want it to have a relationship with team or a relationship with member, but not both. Is there a way to enforce this?
I can only see two viable solutions. First is actually the same as #mariodev suggested in the comment which is to use Genetic foreign key. That will look something like:
# make sure to change the app name
ALLOWED_RELATIONSHIPS = models.Q(app_label = 'app_name', model = 'team') | models.Q(app_label = 'app_name', model = 'teammember')
class TeamFormNote(models.Model):
content_type = models.ForeignKey(ContentType, limit_choices_to=ALLOWED_RELATIONSHIPS)
relation_id = models.PositiveIntegerField()
relation = generic.GenericForeignKey('content_type', 'relation_id')
What that does is it sets up a generic foreign key which will allow you to link to any other model within your project. Since it can link to any other model, to restrict it to only the models you need, I use the limit_choices_to parameter of the ForeignKey. This will solve your problem since there is only one generic foreign key hence there is no way multiple relationships will be created. The disadvantage is that you cannot easily apply joins to generic foreign keys so you will not be able to do things like:
Team.objects.filter(teamformnote_set__notes__contains='foo')
The second approach is to leave the model as it and manually go into the database backend and add a db constaint. For example in postgres:
ALTER TABLE foo ADD CONSTRAINT bar CHECK ...;
This will work however it will not be transparent to your code.
This sounds like a malformed object model under the hood...
How about an abstract class which defines all common elements and two dreived classes, one for team and one for member?
If you are running into trouble with this because you want to have both referenced in the same table, you can use Generic Relations.
Let's supposed I created two models:
class Car(models.Model):
name = models.CharField(max_length=50)
size = models.IntegerField()
class Manufacturer(models.Model):
name = models.CharField(max_length=50)
country = models.CharField(max_length=50)
car = models.ManyToManyField(Car)
I added entries to both models, then I realized that each Car was only related to a unique Manufacturer. So, I should convert my ManyToManyField to a ForeignKey:
class Car(models.Model):
name = models.CharField(max_length=50)
size = models.IntegerField()
manufacturer = models.ForeignKey(Manufacturer)
class Manufacturer(models.Model):
name = models.CharField(max_length=50)
country = models.CharField(max_length=50)
How can I do that without losing my entries? I tried to look in South documentation but I did not found this way of conversion...
This is nontrivial, I think you will need three migrations:
Add the ForeignKey.
Convert the ManyToMany to ForeignKey (using the forwards method).
Remove the ManyToMany.
You could possibly merge 1 and 2 or 2 and 3 together, but I wouldn't recommend it.
Additionally, you should also implement a backwards method for 2.
An example for 2.'s forwards would be:
class Migration(SchemaMigration):
def forwards(self, orm):
for manufacturer in orm.Manufacturer.objects.all():
for car in manufacturer.car.all():
car.manufacturer = manufacturer
car.save()
Please note that:
There is no backwards method here yet.
This needs to be tested extensively: a migration is something you should be extra careful about.
In case a car has two manufacturers, the last one will be kept.
This is very inefficient, we do a Query per car per manufacturer!
You will also need to update the code that uses those relationships in step 2. / 3.
Based on the excellent answer provided by Thomas Orozco, I'd like to provide the solution for Django>=1.7 (basically the point 2 Convert the ManyToMany to ForeignKey is what varies in newer versions of Django). So here the code for the second migration:
class Migration(migrations.Migration):
def migrate_m2m_to_fk(apps, schema_editor):
Manufacturer = apps.get_model("app", "Manufacturer")
for manufacturer in Manufacturer.objects.all():
for car in manufacturer.car.all():
car.manufacturer = manufacturer
car.save()
def migrate_fk_to_m2m(apps, schema_editor):
Car = apps.get_model("app", "Car")
for c in Car.objects.all():
if c.manufacturer:
c.manufacturer.car.add(c)
c.manufacturer.save()
operations = [
migrations.RunPython(migrate_m2m_to_fk, migrate_fk_to_m2m)
]
Where "app" is the Django app where the models live. Both forward and reverse migration code is shown (as Thomas mentions, running this migrations may incur in loss of data, if more than one relationship was present beforehand, so please take care).