Data Model


Using Data Models

Data models are a core concept of pylite-orm, defined by inheriting from the DbModel class and used during CRUD operations.

💡 Almost all modern ORMs use data models. Beyond being tagged as "object-oriented," it indeed greatly simplifies data access operations.

Let's look at a simple data model definition first:

from pylite_orm import DbModel, DbField, DbType

class User(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    name = DbField(db_type = DbType.TEXT)
    age = DbField(db_type = DbType.INT)
    created_at = DbField(db_type = DbType.TEXT, default_factory = datetime.now)

Does it have a SQLAlchemy/Tortoise-ORM/Peewee style? (So there's no learning curve when using pylite-orm)

DbField is used to set parameters for model fields (these parameters will be introduced in detail later). If you use the data migration tool, corresponding tables will be created in the database. If you modify the model definition, the tables in the database will be updated synchronously when you re-run the migration command.

A more complex data model looks like this:

from pylite_orm import DbModel, DbField, RelationField, OnDelete, DbType

class Article(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    title = DbField(db_type = DbType.TEXT)
    content = DbField(db_type = DbType.TEXT)
    user_id = DbField(db_type = DbType.INT, relation = 'user.id', on_delete = OnDelete.CASCADE)
    author:User = RelationField(fk = 'user_id')

    class Meta:
        table = 'art'
        indexes = ['user_id']

This time an article table is defined, not only with a foreign key user_id but also with a relation field user , representing the article's author information.

Another new element is the Meta configuration, where you can specify the actual table name in the database and define indexes. The current example has only one regular index. If needed, you can define more indexes, including composite indexes.

DbModel is not just a mapping of database tables (used in CRUD), you can treat it as a special data type, especially in desktop software projects, where it can conveniently store and pass data, and persist to the database at any time.


Fields in Data Models

DbModel has two types of fields: one is DbField , which can be called a regular field; the other is RelationField, which can be called a relation field.

DbField

Contains 8 optional configuration parameters:

Parameter Type Description
db_column str Custom database field name
If not specified, the model field name becomes the database field name. For example, if the model field name is uname, the database field name will also be uname
db_type DbType Defines the field type
Through DbType enumeration, defaults to DbType.TEXT
pk bool Whether it is a primary key
Each data model allows only 1 primary key, and it can only be set on integer type (DbType.INT) fields, which automatically becomes an auto-increment field
default any Specifies a default value for the current field
Note: This is the default value for the data model and will not be set on the corresponding database field, but the effect is actually the same
default_factory callable Default value generator
If the default value is a function, it must be set here. If it exists together with the default setting, the default shall prevail
nullable bool Whether the current field allows NULL
Defaults to True, meaning NULL is allowed
relation str Specifies the foreign key relationship
Format is "main_table.field_name". Once specified, the current field becomes a foreign key field
on_delete OnDelete Association deletion strategy
Defined through OnDelete enumeration, defaults to OnDelete.CASCADE

Model fields defined through DbField will create corresponding table fields in the database. If the model definition is modified, the table in the database will be updated synchronously when the migration command is re-run.

RelationField

Has only 1 configuration parameter (and must be configured):

Parameter Type Description
fk str Specifies the foreign key field name
Note this is the actual field name in the data table, not the data model field name (be careful about the difference). Example: fk='user_id' (this is the database field name)

Model fields defined through RelationField will not create corresponding table fields in the database. It is just a virtual field, or rather, it is just another data model object embedded in the current data model, used to carry associated data.


Meta Conf in Data Models

Currently there are only 2 optional configuration items:

Configuration Type Description
table str Custom data table name
If not customized, the snake_case form of the data model name is used as the actual data table name by default. For example: User --> user, UserRole --> user_role
indexes list Index list
A single-field index is just a field name string, format is ["field_name", "field_name", ...]. If it's a composite index, it needs to be placed in a tuple. A setting with both single-field indexes and composite indexes looks like this: indexes = ["user_id", ("org_id", "created_at")]

pylite-orm does not support unique indexes for a simple reason: the purpose of indexes is to accelerate queries, not to limit data uniqueness. Data validation and constraints should be done at the application layer.


Enum in Data Models

DbType

Data type enumeration, used for the db_type parameter of DbField.

Enum Value Description
DbType.INT Integer type
DbType.TEXT Text type
DbType.REAL Floating-point type
DbType.BLOB Binary type


OnDelete

Association deletion strategy enumeration, used for the on_delete parameter of DbField .

Enum Value Description
OnDelete.CASCADE Cascade delete. When the main table record is deleted, the related table record will also be deleted
SET_NULL Set to NULL. When the main table record is deleted, the association field of the related table record will be set to NULL
OnDelete.SET_DEFAULT Set to default value. When the main table record is deleted, the association field of the related table record will be set to the default value
OnDelete.RESTRICT Restrict deletion. When the related table record exists, the main table record is not allowed to be deleted
OnDelete.NO_ACTION Restrict deletion. When the related table record exists, nothing happens when deleting the main table record

About One-to-One

Let's look at the example first:

class User(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    name = DbField(db_type = DbType.TEXT)
    profile:'Profile' = RelationField(fk='user_id')

class Profile(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    user_id = DbField(db_type = DbType.INT, relation = 'user.id')
    age = DbField(db_type = DbType.INT)
    sex = DbField(db_type = DbType.INT, default = 1)
    weight = DbField(db_type = DbType.REAL)
    author:User = RelationField(fk = 'user_id')

    class Meta:
        indexes = ['user_id']

💡 No need to explain why the Profile type annotation in the User class is quoted, right?

User and Profile have a typical one-to-one relationship. By creating a user_id field in Profile and specifying relation = "user.id" (main_table_name.field_name), the relationship between the two is established. And user_id thus becomes a foreign key.

In the current example, user_id does not specify an on_delete strategy, so it defaults to OnDelete.CASCADE , meaning when the main table (user) record is deleted, the related table (profile) record will also be deleted.

The profile object in User and the author object in Profile are optional. The benefit of creating such RelationField virtual fields is that you don't need to consider join operations during queries, and you can directly access related objects when accessing the current object. For example:

# Assume a user object has been queried and returned earlier
print(user.profile.age)

In relationship settings, the most easily confused part is fk = user_id and relation = user.id . fk refers to the foreign key field name, while relation refers to the main table's primary key field name.

They are both database field names, not model field names.

💡 Usually there's no need to specifically distinguish between database field names and model field names because they are consistent. But when you customize database field names to be different from model field names, you need to be careful to distinguish them.


About One-to-Many

Let's look at the example first:

class User(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    name = DbField(db_type = DbType.TEXT)
    arts:list['Article'] = RelationField(fk='user_id')

class Article(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    title = DbField(db_type = DbType.TEXT)
    content = DbField(db_type = DbType.TEXT)
    user_id = DbField(db_type = DbType.INT, relation = 'user.id', on_delete = OnDelete.SET_NULL)
    author:User = RelationField(fk = 'user_id')

User and Article have a typical one-to-many relationship. By creating a user_id field in Article and specifying relation = "user.id" , the relationship between the two is established. And user_id thus becomes a foreign key.

To preserve articles, user_id specifies the on_delete strategy as OnDelete.SET_NULL , meaning when the main table (user) record is deleted, the user_id field of the related table (article) record will be set to NULL, rather than synchronously deleting the article.

Unlike before, a user may have multiple articles, so arts in User is an object list.


About Many-to-Many

pylite-orm doesn't have a dedicated many-to-many setting syntax, but implements many-to-many relationships through 2 one-to-many relationships. Project practice has proven that dedicated many-to-many syntax in ORMs is used in less than 10% of scenarios.

Implementing many-to-many with 2 one-to-many:

class User(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    name = DbField(db_type = DbType.TEXT)

class Role(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    code = DbField(db_type = DbType.TEXT)

class UserRole(DbModel):
    id = DbField(db_type = DbType.INT, pk = True)
    user_id = DbField(db_type = DbType.INT, relation = 'user.id')
    role_id = DbField(db_type = DbType.INT, relation = 'role.id')
    user:User = RelationField(fk = 'user_id')
    role:Role = RelationField(fk = 'role_id')

The actual database table is essentially many-to-many (of course, it is indeed 2 one-to-many relationships, hope you're not dizzy).


Methods in Data Models

DbModel has only 3 methods:

Method Type Description
table_name() Class Method Returns the mapped database table name
pk_name() Class Method Returns the primary key field name
asdict() Instance Method Converts the current object to a dictionary.
The exc_unset parameter specifies whether to exclude unset fields