Models (Model)¶
Overview¶
In the Grit SDK, data structures are defined using Models.
Model definitions reside in models.py files within each application module.
A Model serves as the authoritative schema for your business data, encapsulating both field definitions and associated behaviors. Each Model corresponds to a dedicated database table.
Core Principles¶
- Inheritance: All Models extend
BaseModel, which provides essential fields including a UUID primary key, timestamps (created_at,updated_at), ownership tracking, and a flexible metadata JSON field. - Field Mapping: Each Model attribute defines a corresponding database column.
- Data Access: The SDK provides a comprehensive query interface for data retrieval and manipulation.
Example: Lead Model¶
The following example demonstrates a typical business Model implementation:
from grit.core.db.models import BaseModel
from django.db import models
class Lead(BaseModel):
first_name = models.CharField(max_length=255, blank=True)
last_name = models.CharField(max_length=255, blank=True)
email = models.EmailField(max_length=255, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
This Lead Model automatically inherits UUID identification, audit timestamps, and metadata capabilities from BaseModel.
Registering Models¶
After defining a Model, register it with the SDK's metadata system to enable automatic URL generation and administrative interfaces.
Create a metadata.py file in your application module and use the registration decorator:
from grit.core import metadata
from .models import Lead
@metadata.register(Lead)
class LeadMetadata(metadata.ModelMetadata):
list_display = ('first_name', 'last_name', 'email')
The SDK automatically discovers and registers metadata definitions during application initialization.
Fields¶
The primary component of a model is the collection of database fields it defines. Fields are declared as class attributes. Avoid selecting field names that conflict with the model API, such as clean, save, or delete.
Example:
from grit.core.db.models import BaseModel
from django.db import models
class Company(BaseModel):
name = models.CharField(max_length=100)
industry = models.CharField(max_length=100)
website = models.URLField(blank=True)
class Employee(BaseModel):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
hire_date = models.DateField()
department = models.CharField(max_length=100)
Field types¶
Each field in your model should be an instance of the appropriate Field class. The SDK uses field class types to determine:
-
The column type, which specifies what kind of data the database stores (e.g., INTEGER, VARCHAR, TEXT).
-
The default HTML widget for rendering form fields (e.g.,
<input type="text">,<select>). -
The validation requirements applied in forms and administrative interfaces.
The SDK provides numerous built-in field types. You can also create custom fields to address specific requirements; refer to the custom model fields documentation.
Field options¶
Each field accepts a set of field-specific arguments. For example, CharField (and its subclasses) requires a max_length argument that specifies the size of the VARCHAR database field used to store the data.
A set of common arguments is available to all field types. All are optional. The following summarizes the most frequently used options:
null
When set to True, the SDK stores empty values as NULL in the database. Default is False.
blank
When set to True, the field is permitted to be blank. Default is False.
Note the distinction between null and blank: null is database-related, whereas blank pertains to validation. If a field has blank=True, form validation permits empty values. If a field has blank=False, the field is required.
choices A sequence of 2-value tuples, a mapping, an enumeration type, or a callable that returns any of these formats. When specified, the default form widget renders as a select box with the provided choices.
A choices list example:
LEAD_STATUS_CHOICES = [
("NEW", "New"),
("CONTACTED", "Contacted"),
("QUALIFIED", "Qualified"),
("CONVERTED", "Converted"),
("CLOSED", "Closed"),
]
Note: A new migration is created each time the order of choices changes.
The first element in each tuple is the value stored in the database. The second element is the display label rendered in form widgets.
To access the display value for a field with choices, use the get_FOO_display() method:
from grit.core.db.models import BaseModel
from django.db import models
class Lead(BaseModel):
STATUS_CHOICES = {
"NEW": "New",
"QUALIFIED": "Qualified",
"CONVERTED": "Converted",
}
name = models.CharField(max_length=100)
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
>>> lead = Lead(name="Acme Corporation", status="QUALIFIED")
>>> lead.save()
>>> lead.status
'QUALIFIED'
>>> lead.get_status_display()
'Qualified'
Enumeration classes provide a concise method for defining choices:
from grit.core.db.models import BaseModel
from django.db import models
class Opportunity(BaseModel):
StageType = models.TextChoices("StageType", "PROSPECTING QUALIFICATION PROPOSAL NEGOTIATION CLOSED_WON CLOSED_LOST")
name = models.CharField(max_length=100)
stage = models.CharField(blank=True, choices=StageType, max_length=20)
Refer to the model field reference for additional examples.
default The default value for the field. This can be a value or a callable object. If callable, it is invoked each time a new object is created.
db_default The database-computed default value for the field. This can be a literal value or a database function.
When both db_default and Field.default are configured, default takes precedence when creating instances in Python code. The db_default value is set at the database level and applies when inserting rows outside of the ORM or when adding a new field in a migration.
help_text Supplementary text displayed with the form widget. This is valuable for documentation purposes even when the field is not used in a form.
primary_key
When set to True, this field serves as the primary key for the model.
When extending BaseModel, a UUID primary key is automatically provided. You only need to specify primary_key=True if you want to override this default behavior. Refer to Automatic primary key fields for details.
The primary key field is read-only. Modifying the primary key value on an existing object and saving it creates a new object alongside the original:
from grit.core.db.models import BaseModel
from django.db import models
class ProductCategory(BaseModel):
code = models.CharField(max_length=50, primary_key=True)
name = models.CharField(max_length=100)
>>> category = ProductCategory.objects.create(code="ELEC", name="Electronics")
>>> category.code = "TECH"
>>> category.save()
>>> ProductCategory.objects.values_list("code", flat=True)
<QuerySet ['ELEC', 'TECH']>
unique
When set to True, this field must contain unique values throughout the table.
These descriptions cover the most common field options. Refer to the model field option reference for comprehensive details.
Automatic primary key fields¶
When extending BaseModel, the SDK automatically provides a UUID primary key field:
To specify a custom primary key, set primary_key=True on one of your fields. When an explicit primary key is defined, the automatic id column is not added.
Each model requires exactly one field with primary_key=True (either explicitly declared or automatically provided by BaseModel).
Verbose field names¶
Each field type, except for ForeignKey, ManyToManyField, and OneToOneField, accepts an optional first positional argument: a verbose name. When not provided, the SDK automatically generates the verbose name from the field's attribute name, converting underscores to spaces.
In this example, the verbose name is "contact email address":
In this example, the verbose name is automatically set to "contact email":
ForeignKey, ManyToManyField, and OneToOneField require the first argument to be a model class. Use the verbose_name keyword argument instead:
account = models.ForeignKey(
Account,
on_delete=models.CASCADE,
verbose_name="associated account",
)
contacts = models.ManyToManyField(Contact, verbose_name="assigned contacts")
billing_address = models.OneToOneField(
Address,
on_delete=models.CASCADE,
verbose_name="billing address",
)
By convention, the first letter of verbose_name should not be capitalized. The SDK automatically capitalizes it where appropriate in the user interface.
Relationships¶
The strength of relational databases lies in establishing relationships between tables. The SDK provides methods to define the three most common types of database relationships: many-to-one, many-to-many, and one-to-one.
Many-to-one relationships¶
To define a many-to-one relationship, use ForeignKey. Include it as a class attribute of your model, like any other field type.
ForeignKey requires a positional argument: the class to which the model is related.
For example, if a Contact model belongs to an Account—that is, an Account has multiple Contacts but each Contact belongs to only one Account—use the following definitions:
from grit.core.db.models import BaseModel
from django.db import models
class Account(BaseModel):
name = models.CharField(max_length=255)
class Contact(BaseModel):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
Recursive relationships (an object with a many-to-one relationship to itself) and relationships to models not yet defined are also supported. Refer to the model field reference for details.
The recommended convention is to name the ForeignKey field after the related model in lowercase (account in the example above), though this is not required:
class Contact(BaseModel):
parent_organization = models.ForeignKey(
Account,
on_delete=models.CASCADE,
)
# ...
ForeignKey fields accept additional arguments as documented in the model field reference. These options define relationship behavior and are all optional.
Many-to-many relationships¶
To define a many-to-many relationship, use ManyToManyField. Include it as a class attribute of your model, like any other field type.
ManyToManyField requires a positional argument: the class to which the model is related.
For example, if a Project has multiple Employee objects—that is, an Employee can work on multiple Projects and each Project has multiple Employees—the implementation is as follows:
from grit.core.db.models import BaseModel
from django.db import models
class Employee(BaseModel):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
class Project(BaseModel):
name = models.CharField(max_length=255)
team_members = models.ManyToManyField(Employee)
As with ForeignKey, recursive relationships (an object with a many-to-many relationship to itself) and relationships to models not yet defined are supported.
The recommended convention is to name the ManyToManyField with a plural that describes the set of related model objects (team_members in the example above).
The ManyToManyField should be placed in only one of the models, not both. Generally, place it in the model that will be edited on a form. In the example above, team_members is in Project (rather than Employee having a projects field) because it is more intuitive to assign employees to a project than to assign projects to an employee. This approach allows the Project form to display employee selection options.
ManyToManyField accepts additional arguments as documented in the model field reference. These options define relationship behavior and are all optional.
Extra fields on many-to-many relationships¶
For straightforward many-to-many relationships, a standard ManyToManyField is sufficient. However, some scenarios require storing additional data about the relationship between two models.
Consider an application that tracks which employees are assigned to which projects. While a many-to-many relationship exists between employees and projects, you may need to capture additional details such as the assignment date, role, or allocation percentage.
For these scenarios, the SDK allows you to specify an intermediate model to govern the many-to-many relationship. The intermediate model is associated with the ManyToManyField using the through argument. Here is a project assignment example:
from grit.core.db.models import BaseModel
from django.db import models
class Employee(BaseModel):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Project(BaseModel):
name = models.CharField(max_length=128)
team_members = models.ManyToManyField(Employee, through="ProjectAssignment")
def __str__(self):
return self.name
class ProjectAssignment(BaseModel):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
date_assigned = models.DateField()
role = models.CharField(max_length=64)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["employee", "project"], name="unique_employee_project"
)
]
When configuring the intermediary model, explicitly specify foreign keys to the models involved in the many-to-many relationship. This explicit declaration defines how the two models are related.
To prevent multiple associations between the same instances, add a UniqueConstraint including the source and target fields. Automatically generated many-to-many tables include such a constraint by default.
The intermediate model has the following restrictions:
The intermediate model must contain exactly one foreign key to the source model (Project in this example), or you must explicitly specify the foreign keys using ManyToManyField.through_fields. Multiple foreign keys without through_fields specified raises a validation error. The same restriction applies to the foreign key to the target model (Employee in this example).
For models with a many-to-many relationship to themselves through an intermediary model, two foreign keys to the same model are permitted but are treated as the two different sides of the relationship. Without through_fields specified, the first foreign key represents the source side and the second represents the target side. More than two foreign keys requires explicit through_fields specification.
With the ManyToManyField configured to use the intermediary model (ProjectAssignment), create many-to-many relationships by instantiating the intermediate model:
>>> sarah = Employee.objects.create(name="Sarah Chen")
>>> michael = Employee.objects.create(name="Michael Torres")
>>> crm_upgrade = Project.objects.create(name="CRM Platform Upgrade")
>>> a1 = ProjectAssignment(
... employee=sarah,
... project=crm_upgrade,
... date_assigned=date(2024, 1, 15),
... role="Project Lead",
... )
>>> a1.save()
>>> crm_upgrade.team_members.all()
<QuerySet [<Employee: Sarah Chen>]>
>>> sarah.project_set.all()
<QuerySet [<Project: CRM Platform Upgrade>]>
>>> a2 = ProjectAssignment.objects.create(
... employee=michael,
... project=crm_upgrade,
... date_assigned=date(2024, 2, 1),
... role="Developer",
... )
>>> crm_upgrade.team_members.all()
<QuerySet [<Employee: Sarah Chen>, <Employee: Michael Torres>]>
The add(), create(), and set() methods can also create relationships when you specify through_defaults for required fields:
>>> crm_upgrade.team_members.add(david, through_defaults={"date_assigned": date(2024, 3, 1), "role": "QA Engineer"})
>>> crm_upgrade.team_members.create(
... name="Emily Watson", through_defaults={"date_assigned": date(2024, 3, 15), "role": "Designer"}
... )
>>> crm_upgrade.team_members.set(
... [sarah, michael, david], through_defaults={"date_assigned": date(2024, 1, 1), "role": "Team Member"}
... )
Creating instances of the intermediate model directly is also a valid approach.
When the intermediate model does not enforce uniqueness on the model pair, the remove() method removes all matching intermediate model instances:
>>> ProjectAssignment.objects.create(
... employee=sarah,
... project=crm_upgrade,
... date_assigned=date(2024, 6, 1),
... role="Technical Advisor",
... )
>>> crm_upgrade.team_members.all()
<QuerySet [<Employee: Sarah Chen>, <Employee: Michael Torres>, <Employee: Sarah Chen>]>
>>> # This removes all ProjectAssignment instances for Sarah Chen
>>> crm_upgrade.team_members.remove(sarah)
>>> crm_upgrade.team_members.all()
<QuerySet [<Employee: Michael Torres>]>
The clear() method removes all many-to-many relationships for an instance:
>>> # Project has been completed
>>> crm_upgrade.team_members.clear()
>>> # This deletes all intermediate model instances
>>> ProjectAssignment.objects.all()
<QuerySet []>
After establishing many-to-many relationships, you can perform queries. Query using attributes of the many-to-many-related model:
# Find all projects with a team member whose name contains 'Sarah'
>>> Project.objects.filter(team_members__name__icontains="Sarah")
<QuerySet [<Project: CRM Platform Upgrade>]>
Query on intermediate model attributes:
# Find all employees assigned to CRM Platform Upgrade after January 2024
>>> Employee.objects.filter(
... project__name="CRM Platform Upgrade", projectassignment__date_assigned__gt=date(2024, 1, 31)
... )
<QuerySet [<Employee: Michael Torres>]>
Access assignment information by querying the ProjectAssignment model directly:
>>> sarahs_assignment = ProjectAssignment.objects.get(project=crm_upgrade, employee=sarah)
>>> sarahs_assignment.date_assigned
datetime.date(2024, 1, 15)
>>> sarahs_assignment.role
'Project Lead'
Alternatively, access the same information via the many-to-many reverse relationship:
>>> sarahs_assignment = sarah.projectassignment_set.get(project=crm_upgrade)
>>> sarahs_assignment.date_assigned
datetime.date(2024, 1, 15)
>>> sarahs_assignment.role
'Project Lead'
One-to-one relationships¶
To define a one-to-one relationship, use OneToOneField. Include it as a class attribute of your model, like any other field type.
This is particularly useful when one object extends another object in some way.
OneToOneField requires a positional argument: the class to which the model is related.
For example, consider a system with a Contact model containing standard information such as name and email. To extend contacts with additional employee-specific data without duplicating fields, create an EmployeeProfile model with a OneToOneField to Contact (because an employee profile "is a" contact extension):
from grit.core.db.models import BaseModel
from django.db import models
class Contact(BaseModel):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField()
class EmployeeProfile(BaseModel):
contact = models.OneToOneField(Contact, on_delete=models.CASCADE)
employee_id = models.CharField(max_length=20)
department = models.CharField(max_length=100)
hire_date = models.DateField()
As with ForeignKey, recursive relationships and references to models not yet defined are supported.
OneToOneField accepts an optional parent_link argument.
OneToOneField no longer automatically becomes the primary key on a model (though you can manually specify primary_key=True). Multiple OneToOneField fields on a single model are supported.
Models across files¶
Models can reference models from other application modules. Import the related model at the top of the file where your model is defined:
from grit.core.db.models import BaseModel
from django.db import models
from grit.auth.models import CustomUser
class Contact(BaseModel):
user = models.OneToOneField(
CustomUser,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
Alternatively, use a lazy reference specified as a string in the format "app_label.ModelName". This approach does not require importing the related model:
from grit.core.db.models import BaseModel
from django.db import models
class Opportunity(BaseModel):
name = models.CharField(max_length=255)
account = models.ForeignKey(
"sales.Account",
on_delete=models.SET_NULL,
blank=True,
null=True,
)
Field name restrictions¶
The SDK enforces the following restrictions on model field names:
- A field name cannot be a Python reserved word, as this causes a syntax error:
- A field name cannot contain consecutive underscores, due to the query lookup syntax:
-
A field name cannot end with an underscore, for similar reasons.
-
A field name cannot be
check, as this would override theModel.check()method.
These limitations can be circumvented by using the db_column option to specify a different database column name.
SQL reserved words such as join, where, or select are permitted as model field names. The SDK escapes all database table names and column names in SQL queries using the appropriate quoting syntax for the database engine.
Custom field types¶
When the built-in model fields do not meet your requirements, or when you need to utilize specialized database column types, you can create custom field classes.
Meta options¶
Configure model metadata by defining an inner Meta class within your model definition:
from grit.core.db.models import BaseModel
from django.db import models
class Product(BaseModel):
inventory_count = models.IntegerField()
class Meta:
ordering = ["inventory_count"]
verbose_name_plural = "products"
Model metadata encompasses configuration options beyond field definitions, including record ordering (ordering), database table naming (db_table), and display labels (verbose_name and verbose_name_plural). These options are optional and can be added incrementally as requirements evolve.
Model Attributes¶
objects¶
The Manager is the primary interface for executing database queries on Grit SDK models. Every model includes a default Manager accessible via the objects attribute, which provides methods to retrieve, filter, and manipulate database records.
Organizations can define custom Managers to encapsulate frequently used query logic. This pattern promotes code reusability and maintains consistent data access patterns across the application.
from grit.core.db.models import BaseModel
class ActiveProductManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
class Product(BaseModel):
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
# Managers
objects = models.Manager() # Default manager - all records
active = ActiveProductManager() # Custom manager - active records only
With this configuration, Product.objects.all() returns all products, while Product.active.all() returns only active products. Managers are accessible exclusively through model classes, not through individual instances.
Model Methods¶
Custom methods can be defined on a model to implement instance-level functionality. While Manager methods operate at the table level, model methods execute operations on individual model instances.
This approach centralizes business logic within the model, promoting maintainability and consistency across the application.
The following example demonstrates custom method implementation:
from datetime import date
from grit.core.db.models import BaseModel
from django.db import models
class Employee(BaseModel):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
hire_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
def get_employment_status(self):
"""Returns the employee's current employment status based on end date."""
if self.end_date is None:
return "Active"
elif self.end_date > date.today():
return "Notice Period"
else:
return "Inactive"
@property
def full_name(self):
"""Returns the employee's full name."""
return f"{self.first_name} {self.last_name}"
The full_name method in this example is implemented as a property.
The model instance reference provides a complete list of methods automatically available on each model. Most of these can be overridden (see Overriding Predefined Model Methods below), but the following two methods should be defined in most cases:
str() A Python "magic method" that returns a string representation of any object. This method is invoked whenever a model instance needs to be displayed as a plain string, such as in an interactive console or the admin interface.
Defining this method is recommended, as the default implementation provides limited useful information.
get_absolute_url() This method defines how to calculate the URL for an object. The framework uses this in the admin interface and whenever a URL for an object needs to be determined.
Any object that has a URL that uniquely identifies it should implement this method.
Overriding Predefined Model Methods¶
Models include a set of methods that encapsulate database behavior. These methods, particularly save() and delete(), can be overridden to customize their functionality.
Any model method can be overridden to modify its default behavior.
A common use case for overriding built-in methods is executing additional logic when saving an object. The following example demonstrates this pattern (refer to save() documentation for accepted parameters):
from grit.core.db.models import BaseModel
from django.db import models
class Project(BaseModel):
name = models.CharField(max_length=100)
description = models.TextField()
def save(self, **kwargs):
self.perform_pre_save_validation()
super().save(**kwargs) # Call the parent save() method
self.notify_stakeholders()
Conditional save logic can also be implemented to enforce business rules:
from decimal import Decimal
from grit.core.db.models import BaseModel
from django.db import models
class Project(BaseModel):
name = models.CharField(max_length=100)
budget = models.DecimalField(max_digits=12, decimal_places=2)
def save(self, **kwargs):
if self.budget > Decimal("1000000.00"):
raise ValueError("Projects exceeding budget threshold require executive approval.")
super().save(**kwargs) # Call the parent save() method
Invoking the superclass method (super().save(**kwargs)) is essential to ensure the object is persisted to the database. Omitting this call will prevent the default save behavior from executing.
Additionally, passing **kwargs through to the superclass method ensures forward compatibility. As the framework evolves and introduces new keyword arguments to built-in methods, using **kwargs guarantees that custom implementations will automatically support these additions.
When modifying a field value within the save() method, ensure the field is included in the update_fields keyword argument if specified. This guarantees the field is persisted when update_fields is provided:
from grit.core.db.models import BaseModel
from django.db import models
from django.utils.text import slugify
class Project(BaseModel):
name = models.CharField(max_length=100)
slug = models.TextField()
def save(self, **kwargs):
self.slug = slugify(self.name)
if (
update_fields := kwargs.get("update_fields")
) is not None and "name" in update_fields:
kwargs["update_fields"] = {"slug"}.union(update_fields)
super().save(**kwargs)
Important: Overridden Model Methods Are Not Called on Bulk Operations
The delete() method for an object is not invoked when deleting objects in bulk using a QuerySet or as a result of a cascading delete. To ensure custom delete logic executes in these scenarios, implement pre_delete and/or post_delete signals.
Note that there is no equivalent workaround for bulk create or update operations, as save(), pre_save, and post_save are not triggered during these operations.
Executing custom SQL¶
Another common pattern is writing custom SQL statements in model methods and module-level methods.
Model Inheritance¶
Model inheritance in the Grit SDK operates similarly to standard Python class inheritance, while adhering to the foundational principles outlined earlier in this documentation. All base classes should extend from BaseModel provided by the SDK.
When designing your data architecture, the primary consideration is whether parent models should maintain their own database tables or serve solely as containers for shared attributes accessible through child models.
The Grit SDK supports three inheritance patterns:
-
Abstract Base Classes — Ideal for consolidating common fields and behaviors that should not exist as standalone database entities. This approach is recommended when multiple models share identical attributes.
-
Multi-table Inheritance — Appropriate when extending an existing model while requiring separate database tables for each model in the hierarchy. This pattern maintains referential integrity across related entities.
-
Proxy Models — Suitable for modifying Python-level behavior without altering the underlying data schema. This pattern enables custom business logic while preserving the original table structure.
Abstract Base Classes¶
Abstract base classes provide a mechanism for defining shared fields and behaviors across multiple models without creating a corresponding database table. To designate a model as abstract, set abstract=True within the Meta class. Child models that inherit from an abstract base class will incorporate all parent fields into their own schema.
Example: Shared Contact Information
from grit.core.db.models import BaseModel
from django.db import models
class BaseContact(BaseModel):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
email = models.EmailField(max_length=255)
class Meta:
abstract = True
class Employee(BaseContact):
department = models.CharField(max_length=100)
employee_id = models.CharField(max_length=20)
The Employee model contains five fields: first_name, last_name, email, department, and employee_id. The BaseContact model functions exclusively as an abstract base class—it does not generate a database table, cannot be instantiated directly, and does not include a manager.
Fields inherited from abstract base classes may be overridden with alternative field definitions or removed by assigning None.
This inheritance pattern is particularly effective for enterprise applications where multiple entities share common attributes. It enables centralized field definitions at the code level while generating only one database table per concrete model.
Meta Inheritance¶
When defining an abstract base class, the Meta inner class becomes available as an inheritable attribute. Child classes that do not declare their own Meta class will automatically inherit the parent's configuration. To extend the parent's Meta options, the child class can subclass it directly.
Example: Extending Meta Configuration
from grit.core.db.models import BaseModel
from django.db import models
class BaseContact(BaseModel):
# ...
class Meta:
abstract = True
ordering = ["last_name"]
class Employee(BaseContact):
# ...
class Meta(BaseContact.Meta):
db_table = "employee_records"
The framework automatically sets abstract=False on child classes, ensuring that descendants of abstract base classes become concrete models by default. To create an abstract class that inherits from another abstract class, you must explicitly declare abstract=True in the child's Meta class.
Certain Meta attributes are inappropriate for abstract base classes. Specifying db_table in an abstract class would cause all child models to reference the same database table—an undesirable outcome in most enterprise scenarios.
When inheriting from multiple abstract base classes, Python's method resolution order applies only the Meta options from the first listed parent by default. To consolidate Meta options from multiple abstract parents, explicit declaration is required.
Example: Multiple Abstract Base Class Inheritance
from grit.core.db.models import BaseModel
from django.db import models
class BaseContact(BaseModel):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
class Meta:
abstract = True
ordering = ["last_name"]
class AuditableMixin(BaseModel):
class Meta:
abstract = True
managed = False
class Employee(BaseContact, AuditableMixin):
department = models.CharField(max_length=100)
class Meta(BaseContact.Meta, AuditableMixin.Meta):
pass
Configuring related_name and related_query_name¶
When defining ForeignKey or ManyToManyField relationships with related_name or related_query_name attributes, each field must have a unique reverse name and query name. This requirement presents a challenge in abstract base classes, where fields are replicated across all child classes with identical attribute values.
To address this constraint, abstract base classes should incorporate dynamic placeholders: '%(app_label)s' and '%(class)s'.
-
'%(class)s'resolves to the lowercase name of the child class where the field is instantiated. -
'%(app_label)s'resolves to the lowercase name of the application containing the child class. Since application names and model class names must be unique within an application, the resulting identifier will be distinct.
Example: CRM Module Relationships
Given an application crm/models.py:
from grit.core.db.models import BaseModel
from django.db import models
class BaseEntity(BaseModel):
contacts = models.ManyToManyField(
Contact,
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss",
)
class Meta:
abstract = True
class Account(BaseEntity):
pass
class Opportunity(BaseEntity):
pass
With an additional application sales/models.py:
The reverse relationship names resolve as follows: crm.Account.contacts yields crm_account_related, crm.Opportunity.contacts yields crm_opportunity_related, and sales.Lead.contacts yields sales_lead_related. The corresponding query names follow the same pattern with the plural suffix.
Omitting the related_name attribute defaults the reverse name to the child class name followed by '_set' (e.g., account_set, opportunity_set). System validation will raise errors during migrations if dynamic placeholders are not used when required.
Multi-table Inheritance¶
Multi-table inheritance enables each model in a hierarchy to function as an independent entity with its own database table while maintaining inheritance relationships. The framework automatically establishes links between child and parent models via an implicitly created OneToOneField.
Example: Organization Hierarchy
from grit.core.db.models import BaseModel
from django.db import models
class Organization(BaseModel):
name = models.CharField(max_length=100)
address = models.CharField(max_length=255)
class Supplier(Organization):
payment_terms = models.CharField(max_length=50)
is_preferred = models.BooleanField(default=False)
All fields from Organization are accessible through Supplier, though the data persists in separate database tables. Both query patterns are valid:
>>> Organization.objects.filter(name="Acme Corporation")
>>> Supplier.objects.filter(name="Acme Corporation")
When an Organization instance is also a Supplier, navigation to the child class uses the lowercase model name:
>>> org = Organization.objects.get(id=12)
# If org is a Supplier instance, access the child class:
>>> org.supplier
<Supplier: ...>
Accessing org.supplier on an Organization that is not a Supplier raises a Supplier.DoesNotExist exception.
The implicit OneToOneField linking Supplier to Organization is structured as follows:
organization_ptr = models.OneToOneField(
Organization,
on_delete=models.CASCADE,
parent_link=True,
primary_key=True,
)
This field can be overridden by declaring a custom OneToOneField with parent_link=True.
Meta Configuration in Multi-table Inheritance¶
In multi-table inheritance scenarios, child classes do not inherit their parent's Meta class, as the parent model exists as a concrete entity with its own configuration. Reapplying Meta options would create conflicting behavior.
Child models do inherit certain limited behaviors: ordering and get_latest_by attributes propagate from parent to child when not explicitly defined on the child.
To disable inherited ordering:
Managing Reverse Relations¶
Multi-table inheritance consumes the default related_name for the implicit OneToOneField linkage. When defining ForeignKey or ManyToManyField relationships on child models, explicit related_name attributes are mandatory to prevent naming conflicts.
Example: Supplier with Customer Relationships
This configuration generates a validation error:
Reverse query name for 'Vendor.customers' clashes with reverse query
name for 'Vendor.organization_ptr'.
HINT: Add or change a related_name argument to the definition for
'Vendor.customers' or 'Vendor.organization_ptr'.
Resolution requires specifying related_name: models.ManyToManyField(Organization, related_name='vendors').
Customizing the Parent Link Field¶
The framework automatically generates a OneToOneField linking child classes to non-abstract parent models. To control the attribute name for this relationship, declare a custom OneToOneField with parent_link=True to designate it as the parent reference.
Proxy Models¶
Multi-table inheritance creates a new database table for each subclass, which is appropriate when child models require additional fields. However, certain business requirements only necessitate modified Python-level behavior—such as custom managers or specialized methods—without schema changes.
Proxy model inheritance addresses this requirement by creating a proxy for the original model. All CRUD operations on the proxy model persist data to the original table. This pattern enables customization of default ordering, managers, or business logic without modifying the base model.
Proxy models are declared by setting proxy = True in the Meta class.
Example: Account with Business Logic Extensions
from grit.core.db.models import BaseModel
from django.db import models
class Account(BaseModel):
name = models.CharField(max_length=100)
tier = models.CharField(max_length=20)
class PremiumAccount(Account):
class Meta:
proxy = True
def calculate_discount(self):
# Premium tier business logic
return 0.15
The PremiumAccount class operates on the same database table as Account. Records created through either model are accessible via both:
>>> account = Account.objects.create(name="Enterprise Corp", tier="premium")
>>> PremiumAccount.objects.get(name="Enterprise Corp")
<PremiumAccount: Enterprise Corp>
Proxy models also support customized ordering configurations. This is useful when different views of the same data require different sort orders:
Standard Account queries return unordered results, while AccountByRevenue queries are sorted by revenue in descending order.
Proxy models inherit Meta attributes following the same rules as standard model inheritance.
QuerySet Behavior¶
QuerySets return instances of the requested model type. Querying Account returns Account objects; proxy models do not automatically substitute for parent models in query results. This design ensures backward compatibility—existing code using the parent model continues to function, while specialized code can leverage proxy model extensions.
Inheritance Constraints¶
Proxy models must inherit from exactly one non-abstract model class, as they cannot establish relationships across multiple database tables. However, proxy models may inherit from any number of abstract model classes (provided those abstract classes define no fields) or from other proxy models sharing a common concrete ancestor.
Custom Managers for Proxy Models¶
Proxy models without explicit managers inherit from their parent. Defining a manager on a proxy model establishes it as the default, while parent managers remain accessible.
Example: Custom Manager for Account Filtering
from django.db import models
class ActiveAccountManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
class ActiveAccount(Account):
objects = ActiveAccountManager()
class Meta:
proxy = True
To add supplementary managers without replacing the default, create an abstract class containing the additional managers and include it in the inheritance chain:
# Abstract class for additional managers
class ReportingManagers(BaseModel):
reporting = ActiveAccountManager()
class Meta:
abstract = True
class ManagedAccount(Account, ReportingManagers):
class Meta:
proxy = True
Proxy Models vs. Unmanaged Models¶
Proxy model inheritance differs from unmanaged models (Meta.managed=False) in maintenance requirements and use cases.
Unmanaged models with manually configured Meta.db_table can shadow existing tables and add Python methods. However, this approach requires manual synchronization between model definitions—a maintenance burden that increases error risk.
Proxy models maintain automatic synchronization with their parent, inheriting all fields and managers directly.
Recommended Guidelines:
-
Use
Meta.managed=Falsewhen interfacing with database views, legacy tables, or schemas not managed by the application's migrations. -
Use
Meta.proxy=Truewhen extending Python-level behavior while preserving the original schema. Proxy models ensure data storage consistency with the parent model.
Multiple Inheritance¶
The Grit SDK supports multiple inheritance following standard Python method resolution order (MRO). When multiple parent classes define the same attribute (e.g., Meta), only the first parent's definition takes precedence; subsequent definitions are disregarded.
In practice, multiple inheritance is primarily useful for mixin classes—adding standardized fields or methods across multiple models. To maintain code clarity and reduce debugging complexity, inheritance hierarchies should remain as straightforward as possible.
Primary Key Considerations
Inheriting from multiple models with conflicting id primary key fields raises an error. Two approaches resolve this constraint:
Approach 1: Explicit Primary Keys
Define explicit primary key fields in each base model:
from grit.core.db.models import BaseModel
from django.db import models
class Document(BaseModel):
document_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=255)
...
class Contract(BaseModel):
contract_id = models.AutoField(primary_key=True)
effective_date = models.DateField()
...
class SignedContract(Contract, Document):
signature_date = models.DateField()
Approach 2: Common Ancestor
Establish a shared ancestor containing the primary key, with explicit OneToOneField parent links from each intermediate model:
from grit.core.db.models import BaseModel
from django.db import models
class BusinessRecord(BaseModel):
pass
class Document(BusinessRecord):
document_record = models.OneToOneField(
BusinessRecord, on_delete=models.CASCADE, parent_link=True
)
title = models.CharField(max_length=255)
...
class Contract(BusinessRecord):
contract_record = models.OneToOneField(
BusinessRecord, on_delete=models.CASCADE, parent_link=True
)
effective_date = models.DateField()
...
class SignedContract(Contract, Document):
signature_date = models.DateField()
This pattern prevents field conflicts by centralizing the primary key in the common ancestor while maintaining explicit relationships.
Field Name Shadowing Restrictions¶
While standard Python inheritance permits child classes to override parent attributes, the Grit SDK enforces restrictions on model field overriding. If a concrete (non-abstract) base class defines a field named owner, child classes cannot redefine or shadow that field with another model field or attribute.
This constraint does not apply to fields inherited from abstract base classes. Abstract-inherited fields may be overridden with alternative field definitions or removed by assigning field_name = None.
Important Considerations
Model managers are inherited from abstract base classes. Overriding an inherited field that is referenced by an inherited manager may introduce subtle runtime issues that are difficult to diagnose.
Certain field types create auxiliary attributes on models. For example, ForeignKey fields generate an additional attribute with _id appended to the field name, along with related_name and related_query_name on the referenced model. These auxiliary attributes cannot be overridden unless the defining field is modified or removed.
Technical Rationale
Field overriding in parent models creates complications during instance initialization (specifically, determining which field is being initialized in Model.__init__) and serialization. These challenges are unique to ORM model inheritance and do not arise in standard Python class hierarchies. The restriction exists to ensure predictable behavior across enterprise applications.
This constraint applies exclusively to Field instances. Standard Python attributes may be overridden without restriction. Additionally, the constraint applies only to Python attribute names—manually specifying database column names allows identical column names across parent and child models in multi-table inheritance scenarios, as they reference separate database tables.
The framework raises a FieldError when field overriding is detected in any ancestor model.
Field Resolution Order
Fields inherited from multiple abstract parent models resolve using strict depth-first order during class definition. This differs from Python's standard method resolution order (MRO), which uses breadth-first resolution in diamond inheritance patterns. This distinction affects only complex inheritance hierarchies, which should be avoided in favor of simpler, more maintainable designs.
Organizing Models in a Package¶
As applications grow in complexity, organizing models across multiple files improves maintainability and team collaboration. Rather than maintaining a single models.py file, consider structuring models as a package.
To implement this pattern, replace the models.py file with a models/ directory containing an __init__.py file and separate module files for each domain area. All models must be imported in the __init__.py file to ensure proper registration.
For example, a sales application might organize models as follows:
Explicitly importing each model rather than using from .models import * ensures a clean namespace, enhances code readability, and maintains compatibility with static analysis tools.