Quickstart

After Installation, you can use django-composite-foreignkey in your models.

the django-composite-foreignkey give you two fields : CompositeForeignKey and CompositeOneToOneField. each one has the same behavior: it don’t create a field on the database, but use a/some existings ones.

Example simple composite ForeignKey models

CompositeForeignKey

CompositeOneToOneField

these 2 models is linked by either a CompositeForeignKey or a CompositeOneToOneField on Contact.customer. there is no customer_id field, but it use instead the shared fields company and customer_id. CompositeForeignKey support a advanced mapping which allow the fields from both models to no beeing named identicaly.

in the prevous exemple, the folowing fields is linked :

Contact Customer
company_code company
customer_code company_code

where a «normal» ForeignKey shoud be :

Contact Customer
customer_id pk

Note

you can provide the to_fields attribute as a set instead of a dict if ALL fields is a simple linke to the related model and no special value is required.

to_fields={"company", "customer_id"}

is equivalent to

to_fields={"company": "company", "customer_id": "customer_id"}
Extra Customer
company
customer_id

Example advanced composite ForeignKey models

in this exemple, the Address Model can be used by either Supplier OR Customer. the linked fields is for Customer :

Customer Address
company company
customer_id customer_id
RawFieldValue(“C”) type_tiers

The model Address have a field named «type_tiers» that allow to dinstinguish if the «tiers_id» is for a Supplier or a Customer. si the Customer model will always have an address with «S» in the «type_tiers» field. so be it via the RawFieldValue which tel exactly that : don’t search on the table, the value is always «C».

for convenience, a oposit version of RawFieldValue exists and mean «search on the table field X». it is LocalFieldValue(“X”).

so the class Supplier could be wrote:

We also can refer by CompositeForeignKey in more flexible way using FunctionBasedFieldValue instead of RawFieldValue:

from django.conf import global_settings
from django.utils import translation

from compositefk.fields import CompositeForeignKey

class Supplier(models.Model):
    company = models.IntegerField()
    supplier_id = models.IntegerField()


class SupplierTranslations(models.Model):
    master = models.ForeignKey(
        Supplier,
        on_delete=CASCADE,
        related_name='translations',
        null=True,
    )
    language_code = models.CharField(max_length=255, choices=global_settings.LANGUAGES)
    name = models.CharField(max_length=255)
    title = models.CharField(max_length=255)

    class Meta:
        unique_together = ('language_code', 'master')


active_translations = CompositeForeignKey(
    SupplierTranslations,
    on_delete=DO_NOTHING,
    to_fields={
        'master_id': 'id',
        'language_code': FunctionBasedFieldValue(translation.get_language)
    })


active_translations.contribute_to_class(Supplier, 'active_translations')

in this example, the Supplier Model joins with SupplierTranslations in current active language and supplier_instance.active_translations.name will return different names depend on which language was activated by translation.activate(..):

translation.activate('en')
print Supplier.objects.get(id=1).active_translations.name
translation.activate('your_language_code')
print Supplier.objects.get(id=1).active_translations.name
output should be:
  • ‘en_language_name’
  • ‘your_language_name’

Treate specific values as None

sometimes, some database is broken and some values should be treated as None to make sur no query will be made. ie if company code is «-1» instead of None, the query shall not seach for related model with company = -1 since this is an old aplicative exception.

you just have one thing to do that : null_if_equal

in this exemple, if company is -1, OR customer_id is -1 too, no query will be made and custome.address will be equal to None. it is the same behavior as if a normal foreignkey address had address_id = None.

Note

you must allow null value to permit that (which will not have any impact on database).

Note

these cases should not be possible on database that use ForeignKey constraint. but with some legacy database that won’t, this feathure is mandatory to bypass the headarch comming with broken logic on special values.

Set Specific attribute to None

Sometimes, all fields used in the composite relation is not only used for this one. in our Contact class, the company can be used in other fields. you can use the arguments nullable_fields to give the list of fields to set to null in case you wante to remove the link. since if one of the composite field is resolved to None, the field will return None.

so Contact.customer = None is equal to Contact.customer_code = None if nullable_fields=[“customer_code”]

nullable_fields can be a dict, which provide the value to put instead of None of each updated fields, which can synergize well with null_if_equal

Test application

The test application provides a number of useful examples.

https://github.com/onysos/django-composite-foreignkey/tree/master/testapp/