Django models kind of multi-table inheritance not working - python

I want to make a hierarchy over a DB design as described in "Fundamentals of database systems" from Elmasri & Navathe.
This implies that when I have some info which is shared for many classes/tables, I can put it in a main parent table and use the main table id as foreign key in the child tables, kind of a weak entity.
I tried using abstract and multitable inheritance (this last one doesn't let me specify the OneToOneField, don't know where to find this at django docs).
My example is right down here (one table per class):
'''I would like this to be abstract, because I will never instantiate it,
but could be not if needed'''
class Person(models.Model):
personId = models.IntegerField(primary_key=True)
name = models.CharField(max_length=45)
surname = models.CharField(max_length=45, blank=True)
email = models.CharField(max_length=45, blank=True)
phone = models.CharField(max_length=15, blank=True)
class Meta:
managed = False
db_table = 'person'
class Alumn(Person):
# Maybe this one down should be OneToOne.
# alumnId == personId always true for the same real world guy
alumnId = models.ForeignKey('Person', db_column='alumnId', primary_key=True)
comments = models.CharField(max_length=255, blank=True)
class Meta:
managed = False
db_table = 'alumn'
# There are more child classes (Client, Professor, etc....)
# but for the example this is enough
My target is achieving to create an Alumn in DB just with two sentences like:
a = Alumn(personId=1,name='Joe', [...more params...] , alumnId=1, comments='Some comments' )
a.save()
and having these two lines insert two rows: one for Person and one for Alumn. The alumnId attribute in this snippet up here could be omitted, because it will always be the same as the personId (I told you, like a weak entity).
I'm quite a beginner at django but I have looked at the documentation and proved some things with abstract=True in Person and not having succeeded I guess now that I should mess with the init constructors for getting the superclass built and after that build the child class.
I don't know the right path to choose but definitely want not to alter the database design. Please help.
Thanks in advance.

You don't need to have ids in your models; Django handle it automatically. Also you're not supposed to use camel case. In other words: personId should be person_id and is not necessary anyway - just remove it.
In general I avoid non-abstract inheritance with an ORM.
I don't really understand what you want to achieve but I'd suggest 2 approaches (for Person, Alumni, Professor, etc.), depending on your needs:
1. Abstract inheritance:
class Person:
class Meta:
abstract = True
# here you put all the common columns
Then:
class Alumni(Person):
# the other columns - specific to alumn
etc.
By doing this you have one table per sub-type of Person: Alumn, Professor etc.
2. Use composition:
class Alumn:
person = models.ForeignKey(Person, null=True, related_name="alumni_at")
university = ...
class Professor:
person = models.ForeignKey(Person, null=True, related_name="professor_at")
university = ...
This way you can do:
bob = Person.objects.create(first_name="bob", ...)
Alumn.objects.create(person=bob, university="univ 1")
Professor.objects.create(person=bob, university="univ 2")
Alumn.objects.create(person=bob, university="univ 2")

Related

Is it possible to use a related field as a choice field in Django?

In Django, if I have something like this:
class Library(models.Model):
name = models.CharField(...)
address = models.CharField(...)
book_of_the_week = ?
class Book(models.Model):
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name="books")
name = models.CharField(...)
This gives me the ability to create multiple libraries, each with a large number of books.
For book_of_the_week, I want this to be a reference to a Book instance, so that in Django Admin, the field is represented as a dropdown that lets you select from the books in the current library, and in code, you can use .book_of_the_week to access a specific instance of Book.
Is this possible?
Sure, it's possible. But if you do this, you'll only ever be able to save the current book of the week. What happens if you want to show a library's book-of-the-week history? Seems like a likely scenario to me. Consider doing something like:
class Library(models.Model):
name = models.CharField(...)
address = models.CharField(...)
book_of_the_week = ?
class Book(models.Model):
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name="books")
name = models.CharField(...)
class BookOfTheWeek(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='book_of_the_week')
library = models.ForeignKey(Library, ...etc)
week_of = models.DateField()
In this manner every book of the week will be a discrete database record, and you'll be able to track a history of each library's book of the week over time.
However...
That said, if you do not need to do this and a single instance record is fine, you should be able to create a ForeignKey relation from Library to Book. However, you may run into some circular and/or hierarchical reference issues depending on how where your model classes are written. In the example you show above, you'd need to declare your ForeignKey model name as a string, like so:
book_of_the_week = models.ForeignKey('Book', on_delete ... etc)
instead of:
book_of_the_week = models.ForeignKey(Book, on_delete ... etc)
...otherwise Django will throw an error because the Book model class is referenced before it is defined. Making 'Book' a string will let Django parse the full models.py file, build the logic, and avoid the error.
Sidenote:
Depending on your specific case you may also consider a ForeignKey to self, like so:
class Book(models.Model):
library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name="books")
name = models.CharField(...)
book_of_the_week = models.ForeignKey('self', on_delete=models.SET_NULL, ...etc)
This would allow any Book on the database table to reference another single Book instance.

How to keep DRY while creating common models in Django?

For example I have 2 main models in my django app:
class Employee(Model):
name = CharField(max_length=50)
class Client(Model):
title = CharField(max_length=50)
Abstract base class for phones:
class Phone(Model):
number = CharField(max_length=10)
class Meta:
abstract = True
Inherited separate classes for Employee and for Client:
class EmployeePhone(Phone):
employee = ForeignKey(Employee, on_delete=CASCADE, related_name='employee_phones')
class ClientPhone(Phone):
client = ForeignKey(Client, on_delete=CASCADE, related_name='client_phones')
It works but I don't like it, I would prefer to keep just one Phone model instead of 3. I know I could use Generic-Models but unfortunately that's not longer an option, because my app is actually REST-API and it seems to be impossible to create Generic-Object while creating Parent-Object. So is there any solution to keep things clean and DRY ?
Other answers present good ideas how you can normalise the database, but if you'd like to keep the schema the same and just avoid repeating the same thing in the code, maybe a custom field subclass is what you're after?
Example:
# fields.py
class PhoneField(models.CharField):
def __init__(self, **kwargs):
kwargs.setdefault('max_length', 50)
...
super().__init__(**kwargs)
# models.py
class Employee(models.Model):
phone = PhoneField()
How about keeping 1 Phone class and linking Employee and Customer to Phone via a Foreign Key and removing the abstract :).
How about moving EmployeePhone (ClientPhone) as a ManyToManyField in Employee (Client) model related to Phone model?
class Employee(Model):
name = CharField(max_length=50)
phones = ManyToManyField(Phone, ...)

Get only one related object from Django ORM

Here are stripped down versions of the models I'm dealing with:
class Contact(models.Model):
contact_no = models.IntegerField(primary_key=True)
email_address = models.CharField(max_length=60, null=True)
class ContactRole(models.Model):
contact_no = models.ForeignKey(Contact, primary_key=True, db_column='contact_no')
role_code = models.CharField(primary_key=True, max_length=16)
role_scope_code = models.CharField(primary_key=True, max_length=16)
Contacts can and almost always do have many ContactRoles.
I want a list of Contacts where the role_scope_code of the related ContactRole is 'foo'. I know I can get this with:
Contact.objects.filter(contactrole__role_scope_code='foo')
What I also want, is for each Contact in the queryset to have a single .contactrole property. It would be the ContactRole with the role_scope_code of 'foo'. Instead I'm getting a set of all ContactRoles that match on contact_no, so that to get to properties of the ContactRole I have to do something like this:
contacts = Contact.objects.filter(contactrole__role_scope_code='foo')
for contact in contacts:
print contact.contactrole_set.filter(role_scope_code='foo')[0].role_code
I have to filter on role_scope_code twice! That doesn't seem DRY at all. What I'm looking for is a query that will allow me to have a set that works like this:
contacts = Contact.objects.filter(contactrole__role_scope_code='foo')
for contact in contacts:
print contact.contactrole.role_code
For the life of me I can't figure out how to tell Django to only return the related objects that match the filter I applied to the parent object.
A OneToOneField will solve this provided that a contact only have one contactrole. A OneToOneField gives you the api you are looking for. So instead of using a ForeignKey use a OneToOneField

Validating admin site save in Django

I have the following two classes defined in models.py
class Part(models.Model):
name = models.CharField(max_length=32)
value = models.CharField(max_length=32)
class Car(models.Model):
name = models.CharField(max_length=32)
parts = models.ManyToManyField(Part)
So my model allows for multiple parts, but I want the part names to be unique. For example, only one part with name "engine" should be allowed for a given car. How do I enforce this uniqueness?
Things I have looked into:
class save()
Overriding the default save() for Car doesn't help because parts isn't updated until save is hit.
class save_model()
This will not work because it always saves.
So what are my options for enforcing uniqueness of part names?
UPDATED:
Although I want only one part with name engine to be associated with a car, I still want the ability to define multiple parts with name engine, of course with different values.
So a car with can have a part (part.name=Engine and part.value=V6) and another car can have a part (part.name=Engine and part.value=V4, but a car can't have two parts that have part.name == engine.
EDIT:
class Part(models.Model):
name = models.CharField(max_length=32)
value = models.CharField(max_length=32)
class CarPart(models.Model):
car = models.ForeignKey(Car)
part_type = models.CharField(max_length=32, unique=true)
part = models.ForeignKey(Part)
class Car(models.Model):
name = models.CharField(max_length=32)
treat part_type as part type id (e.g. type='engine') for engines it will be unique
more about ForeignKey

In Django - Model Inheritance - Does it allow you to override a parent model's attribute?

I'm looking to do this:
class Place(models.Model):
name = models.CharField(max_length=20)
rating = models.DecimalField()
class LongNamedRestaurant(Place): # Subclassing `Place`.
name = models.CharField(max_length=255) # Notice, I'm overriding `Place.name` to give it a longer length.
food_type = models.CharField(max_length=25)
This is the version I would like to use (although I'm open to any suggestion):
http://docs.djangoproject.com/en/dev/topics/db/models/#id7
Is this supported in Django? If not, is there a way to achieve similar results?
Updated answer: as people noted in comments, the original answer wasn't properly answering the question. Indeed, only the LongNamedRestaurant model was created in database, Place was not.
A solution is to create an abstract model representing a "Place", eg. AbstractPlace, and inherit from it:
class AbstractPlace(models.Model):
name = models.CharField(max_length=20)
rating = models.DecimalField()
class Meta:
abstract = True
class Place(AbstractPlace):
pass
class LongNamedRestaurant(AbstractPlace):
name = models.CharField(max_length=255)
food_type = models.CharField(max_length=25)
Please also read #Mark answer, he gives a great explanation why you can't change attributes inherited from a non-abstract class.
(Note this is only possible since Django 1.10: before Django 1.10, modifying an attribute inherited from an abstract class wasn't possible.)
Original answer
Since Django 1.10 it's
possible!
You just have to do what you asked for:
class Place(models.Model):
name = models.CharField(max_length=20)
rating = models.DecimalField()
class Meta:
abstract = True
class LongNamedRestaurant(Place): # Subclassing `Place`.
name = models.CharField(max_length=255) # Notice, I'm overriding `Place.name` to give it a longer length.
food_type = models.CharField(max_length=25)
No, it is not:
Field name “hiding” is not permitted
In normal Python class inheritance, it is permissible for a child
class to override any attribute from the parent class. In Django, this
is not permitted for attributes that are Field instances (at least,
not at the moment). If a base class has a field called author, you
cannot create another model field called author in any class that
inherits from that base class.
That is not possible unless abstract, and here is why: LongNamedRestaurant is also a Place, not only as a class but also in the database. The place-table contains an entry for every pure Place and for every LongNamedRestaurant. LongNamedRestaurant just creates an extra table with the food_type and a reference to the place table.
If you do Place.objects.all(), you also get every place that is a LongNamedRestaurant, and it will be an instance of Place (without the food_type). So Place.name and LongNamedRestaurant.name share the same database column, and must therefore be of the same type.
I think this makes sense for normal models: every restaurant is a place, and should have at least everything that place has. Maybe this consistency is also why it was not possible for abstract models before 1.10, although it would not give database problems there. As #lampslave remarks, it was made possible in 1.10. I would personally recommend care: if Sub.x overrides Super.x, make sure Sub.x is a subclass of Super.x, otherwise Sub cannot be used in place of Super.
Workarounds: You can create a custom user model (AUTH_USER_MODEL) which involves quite a bit of code duplication if you only need to change the email field. Alternatively you can leave email as it is and make sure it's required in all forms. This doesn't guarantee database integrity if other applications use it, and doesn't work the other way around (if you want to make username not required).
See https://stackoverflow.com/a/6379556/15690:
class BaseMessage(models.Model):
is_public = models.BooleanField(default=False)
# some more fields...
class Meta:
abstract = True
class Message(BaseMessage):
# some fields...
Message._meta.get_field('is_public').default = True
My solution is as simple as next monkey patching, notice how I changed max_length attribute of name field in LongNamedRestaurant model:
class Place(models.Model):
name = models.CharField(max_length=20)
class LongNamedRestaurant(Place):
food_type = models.CharField(max_length=25)
Place._meta.get_field('name').max_length = 255
Pasted your code into a fresh app, added app to INSTALLED_APPS and ran syncdb:
django.core.exceptions.FieldError: Local field 'name' in class 'LongNamedRestaurant' clashes with field of similar name from base class 'Place'
Looks like Django does not support that.
This supercool piece of code allows you to 'override' fields in abstract parent classes.
def AbstractClassWithoutFieldsNamed(cls, *excl):
"""
Removes unwanted fields from abstract base classes.
Usage::
>>> from oscar.apps.address.abstract_models import AbstractBillingAddress
>>> from koe.meta import AbstractClassWithoutFieldsNamed as without
>>> class BillingAddress(without(AbstractBillingAddress, 'phone_number')):
... pass
"""
if cls._meta.abstract:
remove_fields = [f for f in cls._meta.local_fields if f.name in excl]
for f in remove_fields:
cls._meta.local_fields.remove(f)
return cls
else:
raise Exception("Not an abstract model")
When the fields have been removed from the abstract parent class you are free to redefine them as you need.
This is not my own work. Original code from here: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba
Maybe you could deal with contribute_to_class :
class LongNamedRestaurant(Place):
food_type = models.CharField(max_length=25)
def __init__(self, *args, **kwargs):
super(LongNamedRestaurant, self).__init__(*args, **kwargs)
name = models.CharField(max_length=255)
name.contribute_to_class(self, 'name')
Syncdb works fine. I dont tried this example, in my case I just override a constraint parameter so ... wait & see !
I know it's an old question, but i had a similar problem and found a workaround:
I had the following classes:
class CommonInfo(models.Model):
image = models.ImageField(blank=True, null=True, default="")
class Meta:
abstract = True
class Year(CommonInfo):
year = models.IntegerField()
But I wanted Year's inherited image-field to be required while keeping the image field of the superclass nullable. In the end I used ModelForms to enforce the image at the validation stage:
class YearForm(ModelForm):
class Meta:
model = Year
def clean(self):
if not self.cleaned_data['image'] or len(self.cleaned_data['image'])==0:
raise ValidationError("Please provide an image.")
return self.cleaned_data
admin.py:
class YearAdmin(admin.ModelAdmin):
form = YearForm
It seems this is only applicable for some situations (certainly where you need to enforce stricter rules on the subclass field).
Alternatively you can use the clean_<fieldname>() method instead of clean(), e.g. if a field town would be required to be filled in:
def clean_town(self):
town = self.cleaned_data["town"]
if not town or len(town) == 0:
raise forms.ValidationError("Please enter a town")
return town
You can not override Model fields, but its easily achieved by overriding/specifying clean() method. I had the issue with email field and wanted to make it unique on Model level and did it like this:
def clean(self):
"""
Make sure that email field is unique
"""
if MyUser.objects.filter(email=self.email):
raise ValidationError({'email': _('This email is already in use')})
The error message is then captured by Form field with name "email"

Categories