Skip to content

Define Model

Danio use python's dataclasses as model layer.A danio model is basically a dataclass instance with special method and variable.

Field

Danio use danio.field function to define a field in model

def field(
    field_cls=Field,
    type="",
    name="",
    comment="",
    default=Field.FieldDefault,
    primary=False,
    auto_increment=False,
    not_null=True,
    enum: typing.Optional[typing.Type[enum.Enum]] = None,
) -> typing.Any

eg:

import danio

@dataclasses.dataclass
class Cat(danio.Model):
    id: int = danio.field(IntField, primary=True, auto_increment=True)
    name: str = danio.field(danio.CharField, comment="cat name")
    age: int = danio.field(danio.IntField)

There are the corresponding database table schema:

CREATE TABLE `cat` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '',
`name` varchar(255) NOT NULL  COMMENT 'cat name',
`age` int NOT NULL  COMMENT '',
PRIMARY KEY (`id`),
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;;

We can define more detail by danio.field params:

  • Define a different name in database:

    name: str = danio.filed(danio.ChareField, name="name_in_database")

  • Define a custom type:

    name: str = danio.filed(danio.ChareField, type="varchar(16)")

  • Define default value(Only affect model layer, will not set database field default'):

    age: int = danio.field(danio.IntField, default=1)

  • Define auto increment:

    id: int = danio.field(danio.IntField, auto_increment=True)

  • Define enum class(Only affect model layer, danio will load database value to defined enum):

    gender: Gender = danio.field(danio.IntField, enum=Gender, default=Gender.FEMALE)

Danio provide fields for now(It's easy to define custom field too):

  • TinyField, SmallIntField, IntField, BigIntField
  • BoolField(actually use tinyint in database by default), FloatField, DecimalField
  • CharField, TextField
  • TimeField, DateField, DateTimeField
  • JsonField(actually use varchar in database by default)

By typing.Annotated(Required in python3.11)

eg:

import typing
import dataclasses
import danio

@dataclasses.dataclass
class Cat(danio.Model):
    id: typing.Annotated[int, danio.IntField(primary=True, auto_increment=True)] = 0
    name: typing.Annotated[int, danio.CharField(comment="cat name")] = 0
    age: typing.Annotated[int, danio.IntField] = 0

Class Attribute and Instance Attribute

By dataclasses we can access model field by class attribute, eg:

user = User()
User.id  # IntField(...)
user.id  # 1

and danio will also generate a upcase class atrribute for distinguish with instance atrribute, eg:

User.ID  # IntField(...)
User.ID == User.id  # True

event more, you can use danio to auto write model type hints in code, like:

await danio.manage.write_model_hints(database, User)

Then the user model file will be updated like:

class User(danio.Model):
    # --------------------Danio Hints--------------------
    # TABLE NAME: user
    # TABLE IS MIGRATED!
    ID: typing.ClassVar[danio.Field]  # "id" serial PRIMARY KEY NOT NULL
    NAME: typing.ClassVar[danio.Field]  # "name" varchar(255)  NOT NULL
    AGE: typing.ClassVar[danio.Field]  # "age" int  NOT NULL
    CREATED_AT: typing.ClassVar[
        danio.Field
    ]  # "created_at" timestamp without time zone  NOT NULL
    UPDATED_AT: typing.ClassVar[
        danio.Field
    ]  # "updated_at" timestamp without time zone  NOT NULL
    GENDER: typing.ClassVar[danio.Field]  # "gender" int  NOT NULL
    # --------------------Danio Hints--------------------

    class Gender(enum.Enum):
        MALE = 0
        FEMALE = 1
        OTHER = 2

    id: typing.Annotated[int, danio.IntField(primary=True, type="serial")] = 0
    name: typing.Annotated[str, danio.CharField(comment="User name")] = ""
    age: typing.Annotated[int, danio.IntField] = 0
    ...

Index

Danio store index information in model's classvar _table_*_keys, eg:

@dataclasses.dataclass
class UserProfile(danio.Model):
    user_id: int = danio.field(danio.IntField)
    level: int = danio.field(danio.IntField)

    _table_index_keys: typing.ClassVar[
        typing.Tuple[typing.Tuple[typing.Union[Field, str], ...], ...]
    ] = ((level, user_id,),)
    _table_unique_keys: typing.ClassVar[
        typing.Tuple[typing.Tuple[typing.Union[Field, str], ...], ...]
    ] = (("user_id",),)

or

@dataclasses.dataclass
class UserProfile(danio.Model):
    user_id: typing.Annotated[int, danio.IntField] = 0
    level: typing.Annotated[int, danio.IntField] = 0

    @classmethod
    def get_index_keys(cls) -> typing.Tuple[typing.Tuple[typing.Union[Field, str], ...], ...]:
        return ((level, user_id,),)

    @classmethod
    def get_unique_keys(cls) -> typing.Tuple[typing.Tuple[typing.Union[Field, str], ...], ...]:
        return (("user_id",),)

There are the corresponding database table schema:

...
UNIQUE KEY `user_id_471_uiq` (`user_id`),
KEY `level_user_id_231_idx` (`level`, `user_id`)
...

Model Inherit

Danio use dataclasses's way to inherit, we can define a base model first:

@dataclasses.dataclass
class Pet(danio.Model):
    name: str = danio.field(danio.CharField)
    age: int = danio.field(danio.IntField)

    _table_abstracted: typing.ClassVar[bool] = True
    _table_index_keys = ((age,),)

_table_abstracted=True means no pet table in database. Then we inherit Pet:

@dataclasses.dataclass
class Cat(Pet):
    weight: int = danio.field(danio.IntField)

So Cat has 4 field now: id, name, age and weight.We can disable or redefine a field:

@dataclasses.dataclass
class Dog(Pet):
    name: str = ""
    age: int = danio.field(danio.SmallIntField)

Now Cat got 3 fields: id, age, weight.We can still use name variable as a normal dataclass's variable.And all Cat and Dog got same one index by field age.We can change this index too:

@dataclasses.dataclass
class Fish(Pet):
    _table_index_keys = ((Pet.name,),)

Or add new one to the original index:

@dataclasses.dataclass
class Fish(Pet):
    _table_index_keys = Pet._table_index_keys + ((Pet.name,),)

Config database

Table name

Danio obtain model schema's table name by get_table_name method, just join table prefix and model name by default:

@classmethod
def get_table_name(cls) -> str:
    return cls._table_name_prefix + cls.__name__.lower()

Database

For model's database instance, defined by get_database method:

def get_database(
    cls, operation: Operation, table: str, *args, **kwargs
) -> Database

We can define this at a base model:

db = danio.Database(
    "mysql://root:letmein@server:3306/test",
    maxsize=3,
    charset="utf8mb4",
    use_unicode=True,
    connect_timeout=60,
)

@dataclasses.dataclass
class BaseModel(danio.Model):
    @classmethod
    def get_database(
        cls, operation: danio.Operation, table: str, *args, **kwargs
    ) -> danio.Database:
        return db

Now any model inherit BaseModel will get db as database.

And we can set more than one database instance:

# read_db = ...
# config_db = ...
# write_db = ...
@classmethod
def get_database(
    cls, operation: danio.Operation, table: str, *args, **kwargs
) -> danio.Database:
    if operation == danio.Operation.READ:
        return read_db
    elif table.startswith("config_"):
        return config_db
    else:
        return write_db