DEV Community

loading...
Cover image for Model Field - Django ORM Working - Part 2

Model Field - Django ORM Working - Part 2

Kracekumar
I'm interested in building maintainable software with a special eye on backend solutions for web-based software, data-intense applications, machine learning using Open Source Softwares.
・8 min read

The last post covered the structure of Django Model. This post covers how the model field works, what are the some important methods and functionality and properties of the field.

Object-Relational Mapper is a technique of declaring, querying the database tables using Object relationship in the programming language. Here is a sample model declaration in Django.


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
Enter fullscreen mode Exit fullscreen mode

Each class inherits from models.Model becomes a table inside the SQL database unless explicitly marked as abstract. The Question model becomes <app_name>_question table in the database. question_text and pub_date become columns in the table. The properties of the each field are declared by instantiating the respective class. Below is the method resolution order for CharField.

In [5]: models.CharField.mro()
Out[5]:
[django.db.models.fields.CharField,
 django.db.models.fields.Field,
 django.db.models.query_utils.RegisterLookupMixin,
 object]
Enter fullscreen mode Exit fullscreen mode

CharField inherits Field and Field inherits RegisterLookUpMixin.

High-level role of Field class

  1. The role of field class is to map type of the field to SQL database type.

  2. Serialization - to convert the Python object into relevant SQL database value.

  3. DeSerialization - to convert the SQL database value into Python object.

  4. Check declared validations at the field level and built-in checks before serializing the data. For example, in a PositiveIntegerField the value should be greater than zero - built-in constraint.

Structure of Field Class

# Find out all the classes inheriting the Field

In [7]: models.Field.__subclasses__()
Out[7]:
[django.db.models.fields.BooleanField,
 django.db.models.fields.CharField,
 django.db.models.fields.DateField,
 django.db.models.fields.DecimalField,
 django.db.models.fields.DurationField,
 django.db.models.fields.FilePathField,
 django.db.models.fields.FloatField,
 django.db.models.fields.IntegerField,
 django.db.models.fields.IPAddressField,
 django.db.models.fields.GenericIPAddressField,
 django.db.models.fields.TextField,
 django.db.models.fields.TimeField,
 django.db.models.fields.BinaryField,
 django.db.models.fields.UUIDField,
 django.db.models.fields.json.JSONField,
 django.db.models.fields.files.FileField,
 django.db.models.fields.related.RelatedField,
 django.contrib.postgres.search.SearchVectorField,
 django.contrib.postgres.search.SearchQueryField,
 fernet_fields.fields.EncryptedField,
 enumchoicefield.fields.EnumChoiceField,
 django.contrib.postgres.fields.array.ArrayField,
 django.contrib.postgres.fields.hstore.HStoreField,
 django.contrib.postgres.fields.ranges.RangeField]
Enter fullscreen mode Exit fullscreen mode

Here fernet_fields is a third-party library which implements the EncryptedField by inheriting the Field class. Also these are high level fields. For example, Django implements other high-level fields which inherit the above fields.

For example, EmailField inherits the CharField.

In [10]: models.CharField.__subclasses__()
Out[10]:
[django.db.models.fields.CommaSeparatedIntegerField,
 django.db.models.fields.EmailField,
 django.db.models.fields.SlugField,
 django.db.models.fields.URLField,
 django.contrib.postgres.fields.citext.CICharField,
 django_extensions.db.fields.RandomCharField,
 django_extensions.db.fields.ShortUUIDField]
Enter fullscreen mode Exit fullscreen mode

Here is the Field class initializer signature

In [11]: models.Field?
Init signature:
models.Field(
    verbose_name=None,
    name=None,
    primary_key=False,
    max_length=None,
    unique=False,
    blank=False,
    null=False,
    db_index=False,
    rel=None,
    default=<class 'django.db.models.fields.NOT_PROVIDED'>,
    editable=True,
    serialize=True,
    unique_for_date=None,
    unique_for_month=None,
    unique_for_year=None,
    choices=None,
    help_text='',
    db_column=None,
    db_tablespace=None,
    auto_created=False,
    validators=(),
    error_messages=None,
)
Enter fullscreen mode Exit fullscreen mode

The Field initializer contains 22 arguments. Most of the arguments are related to SQL database column properties and rest of the arguments are for Django admin and model forms.

For example, Django provides Admin interface to browse the database records and allows you to edit. blank parameter determines whether the field is required while filling up data in the admin interface and custom form. help_text field is used while display the form.

The most commonly used fields are max_length, unique, blank, null, db_index, validators, default, auto_created. null attribute is a boolean type when set to True, the allows null value while saving to the database. db_index=True created a B-Tree index on the column. default attribute stores the default value passed on to the database, when the value for the field is missing.

validators attribute contains list of validators passed on by the user and Django's internal validators. The function of the validator is to determine the value is valid or not. For example, in our question_text field declaration max_length is set to 200. When the field value is greater than 200, Django raises ValidationError. max_length attribute is useful only for text field and MaxLengthValidator will be missing in non-text fields.

In [29]: from django.core.exceptions import ValidationError

In [30]: def allow_odd_validator(value):
    ...:     if value % 2 == 0:
    ...:         raise ValidationError(f'{value} is not odd number')
    ...:

In [31]: int_field = models.IntegerField(validators=[allow_odd_validator])

In [32]: int_field.validators
Out[32]:
[<function __main__.allow_odd_validator(value)>,
 <django.core.validators.MinValueValidator at 0x1305fdac0>,
 <django.core.validators.MaxValueValidator at 0x1305fda30>]

In [33]: # let's look into question_text field validators

In [38]: question_text.validators
Out[38]: [<django.core.validators.MaxLengthValidator at 0x12e767fa0>]
Enter fullscreen mode Exit fullscreen mode

As long as the validator function or custom class doesn't raise exception, the value is considered as valid.

The details of each field can found in the Django model field reference

Field Methods

In [41]: import inspect

In [44]: len(inspect.getmembers(models.Field, predicate=inspect.isfunction))
Out[44]: 59

In [45]: len(inspect.getmembers(models.Field, predicate=inspect.ismethod))
Out[45]: 6
Enter fullscreen mode Exit fullscreen mode

The Field class consists of (along with inherited ones) 65 methods. Let's look at some of the important ones.

to_python

to_python method is responsible to convert the value passed on to the model during intialization. For example, to_python for IntegerField will convert the value to Python integer. The original value could be string or float. Every field will override to_python method. Here is an example of to_python method invocation on an IntegerField.

In [46]: int_field.to_python
Out[46]: <bound method IntegerField.to_python of <django.db.models.fields.IntegerField>>

In [47]: int_field.to_python('23')
Out[47]: 23

In [48]: int_field.to_python(23)
Out[48]: 23

In [49]: int_field.to_python(23.56)
Out[49]: 23
Enter fullscreen mode Exit fullscreen mode

get_db_prep_value

get_db_prep_value method is responsible to convert Python value to SQL database specific value. Each field may have a different implementation depending on field type. For example, Postgres has a native UUID type, whereas in SQLite and MySQL Django uses varchar(32). Here is the implementation for get_db_prep_value from UUIDField.

def get_db_prep_value(self, value, connection, prepared=False):
    if value is None:
    return None
  if not isinstance(value, uuid.UUID):
    value = self.to_python(value)

  if connection.features.has_native_uuid_field:
    return value
  return value.hex
Enter fullscreen mode Exit fullscreen mode

connection is a Database Connection or Wrapper object of underlying database. Below is an example output from a Postgres Connection and SQLite Connection for uuid field check.

In [50]: from django.db import connection
    ...:

In [51]: connection
Out[51]: <django.utils.connection.ConnectionProxy at 0x10e3c8970>

In [52]: connection.features
Out[52]: <django.db.backends.postgresql.features.DatabaseFeatures at 0x1236a6a00>

In [53]: connection.features.has_native_uuid_field
Out[53]: True
Enter fullscreen mode Exit fullscreen mode
In [1]: from django.db import connection

In [2]: connection
Out[2]: <django.utils.connection.ConnectionProxy at 0x10fe3b4f0>

In [3]: connection.features
Out[3]: <django.db.backends.sqlite3.features.DatabaseFeatures at 0x110ba5d90>

In [4]: connection.features.has_native_uuid_field
Out[4]: False
Enter fullscreen mode Exit fullscreen mode

One thing to note, Django uses psycopg2 driver for Postgres and it will take care of handling UUID specific to Postgres because UUID Python object needs to be converted to string or bytes before sending to the Postgres server.

Similar to get_db_prep_value, get_prep_value which converts Python value to query value.

formfield

Django supports ModelForm which is one to one mapping of HTML form to Django model. The Django admin uses ModelForm . The form consists of several fields. Each field in the form maps to field in the model. So Django can automatically construct the form with a list of validators from the model field.

Here is the implementation for the UUIDField.

def formfield(self, **kwargs):
    return super().formfield(**{
    'form_class': forms.UUIDField,
    **kwargs,
   })
Enter fullscreen mode Exit fullscreen mode

When you create a custom database field, you need to create a custom form field to work with Django admin and pass it as an argument to super class method.

deconstruct

deconstruct method returns value for creating an exact copy of the field. The method returns a tuple with 4 values.

  • The first value is the name of the field passed during initialisation. The default value is None.
  • The import path of the field.
  • The list of positonal arguments passed during the field creation.
  • The dictionary of keyword arguments passed during the field creation.
In [62]: # Let's see the question_text deconstruct method return value

In [63]: question_text.deconstruct()
Out[63]: (None, 'django.db.models.CharField', 
[], {'max_length': 200})

In [65]: # let's create a new integer field with a name

In [66]: int_field = models.IntegerField(name='int_field', validators=[allow_odd_validator])

In [67]: int_field.deconstruct()
Out[67]:
('int_field',
 'django.db.models.IntegerField',
 [],
 {'validators': [<function __main__.allow_odd_validator(value)>]})

In [68]: models.IntegerField(**int_field.deconstruct()[-1])
Out[68]: <django.db.models.fields.IntegerField>

In [69]: int_2_field = models.IntegerField(default=2)

In [70]: int_2_field.deconstruct()
Out[70]: (None, 'django.db.models.IntegerField', 
[], {'default': 2})

Enter fullscreen mode Exit fullscreen mode

Also when you implement a custom field, you can override the deconstruct method. Here is the deconstruct implementation for UUIDField.

def deconstruct(self):
    name, path, args, kwargs = super().deconstruct()
    del kwargs['max_length']
    return name, path, args, kwargs
Enter fullscreen mode Exit fullscreen mode

init

__init__ method is a good place to override some of the default values. For example, UUIDField max_length should always be 32 irrespective of the value passed on. In the decimal field, max_digits can be modified during initialization.

Here is the UUIDField initializer method implementation.

def __init__(self, verbose_name=None, **kwargs):
    kwargs['max_length'] = 32
    super().__init__(verbose_name, **kwargs)
Enter fullscreen mode Exit fullscreen mode

db_type

db_type method takes Django connection as an argument and returns the database specific implementation type for this field. The method takes connection as an argument. Here is the output of db_type for Postgres and SQLite.

In [72]: # Postgres

In [73]: uuid_field = models.UUIDField()

In [74]: uuid_field.db_type(connection)
Out[74]: 'uuid'
Enter fullscreen mode Exit fullscreen mode
In [8]: # Sqlite

In [9]: uuid_field = models.UUIDField()

In [10]: uuid_field.rel_db_type(connection)
Out[10]: 'char(32)'
Enter fullscreen mode Exit fullscreen mode

get_internal_type method returns internal Python type which is companion to the db_type method. In practice, Django fields type and database field mapping is maintained as class variable in DatabaseWrapper. You can find, Django fields and Postgres fields mapping in backends module. Below is the mapping taken from source code.

class DatabaseWrapper(BaseDatabaseWrapper):
    vendor = 'postgresql'
    display_name = 'PostgreSQL'
    # This dictionary maps Field objects to their associated PostgreSQL column
    # types, as strings. Column-type strings can contain format strings; they'll
    # be interpolated against the values of Field.__dict__ before being output.
    # If a column type is set to None, it won't be included in the output.
    data_types = {
        'AutoField': 'serial',
        'BigAutoField': 'bigserial',
        'BinaryField': 'bytea',
        'BooleanField': 'boolean',
        'CharField': 'varchar(%(max_length)s)',
        'DateField': 'date',
        'DateTimeField': 'timestamp with time zone',
        'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
        'DurationField': 'interval',
        'FileField': 'varchar(%(max_length)s)',
        'FilePathField': 'varchar(%(max_length)s)',
        'FloatField': 'double precision',
        'IntegerField': 'integer',
        'BigIntegerField': 'bigint',
        'IPAddressField': 'inet',
        'GenericIPAddressField': 'inet',
        'JSONField': 'jsonb',
        'OneToOneField': 'integer',
        'PositiveBigIntegerField': 'bigint',
        'PositiveIntegerField': 'integer',
        'PositiveSmallIntegerField': 'smallint',
        'SlugField': 'varchar(%(max_length)s)',
        'SmallAutoField': 'smallserial',
        'SmallIntegerField': 'smallint',
        'TextField': 'text',
        'TimeField': 'time',
        'UUIDField': 'uuid',
    }
Enter fullscreen mode Exit fullscreen mode

get_internal_type values are keys and values are Postgres field names.

I have skipped the implementation of the rest of the methods like __reduce__ and check . You can go through the source code of Django fields in GitHub and also you will find class variables and private methods usages.

Django documentation has an excellent page on how-to write custom model field.

Summary

  1. models.Field is the root of all the model fields.
  2. Field initializer takes configuration details like name, default, db_index, null for the database columns and blank, help_text for non-column features like Django model form and Django admin.
  3. __init__ method in the child class can override the user passed value and(or) set custom default value.
  4. validators attribute in the field contains the user-defined validators and default validators specific to the field.
  5. Every field needs to implement a few methods to work with the specific databases. Some of the useful methods are to_python, get_db_prep_value, get_prep_value, deconstruct, formfield, db_type.
  6. Django Connection object or wrapper contains details and features of the underlying the database.

Notes

Discussion (0)