Skip to content

Models

Have you ever wondered how time consuming and sometimes how hard is to declare a simple table with SQLAlchemy where sometimes it can also be combersome?

What about the Django interface type for tables? Cleaner right? Well, Saffier although is on the top of SQLAlchemy core, it provides a Django like experience when it comes to create models.

Do you already have a database with tables and views you would simply would like to reflect them back instead of the opposite? Check the reflection section for more details.

What is a model

A model in Saffier is a python class with attributes that represents a database table.

In other words, it is what represents your SQL table in your codebase.

Declaring models

When declaring models by simply inheriting from saffier.Model object and define the attributes using the saffier Fields.

For each model defined you also need to set one mandatory field, the registry which is also an instance of Registry from Saffier.

There are more parameters you can use and pass into the model such as tablename and a few more but more on this in this document.

Since Saffier took inspiration from the interface of Django, that also means that a Meta class should be declared.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    """
    The user model representation
    """

    id = saffier.IntegerField(primary_key=True)
    name = saffier.CharField(max_length=255)
    age = saffier.IntegerField(minimum=18)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

Although this looks very simple, in fact Saffier is doing a lot of work for you behind the scenes.

Saffier models are a bit opinionated when it comes to ID and this is to maintain consistency within the SQL tables with field names and lookups.

Attention

If no id is declared in the model, Saffier will automatically generate an id of type BigIntegerField and automatically becoming the primary key.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    """
    The user model representation.

    The `id` is not provided and Saffier will automatically
    generate a primary_key `id` BigIntegerField.
    """

    name = saffier.CharField(max_length=255)
    age = saffier.IntegerField(minimum=18)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

Restrictions with primary keys

Primary keys should always be declared in an id field. If you create a different primary_key within the model in a different attribute, it will raise an ImproperlyConfigured.

Primary keys must be always declared inside an ID attribute.

Let us go through some examples.

What you should not do

The below examples are the practices you should avoid and it will raise an error. The examples are applied to any field available in Saffier.

Declaring a model primary key different from ID
import uuid

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255, primary_key=True, default=str(uuid.uuid4))
    age = saffier.IntegerField(minimum=18)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

This will raise ImproperlyConfigured as you cannot have two primary keys. One is the auto-generated id and the second is the name.

You can only have one and it should always be called id.

Declaring a model with ID and without default

When declaring an id, unless the field type is IntegerField or BigIntegerField, you should always declare a default or a ValueError is raised.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    id = saffier.CharField(max_length=255, primary_key=True)
    age = saffier.IntegerField(minimum=18)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

Since the deafult for a CharField is not provided, a ValueError is raised.

What you should do

When it comes to primary keys, it is actually very simple and clean.

Declaring a model primary key with different field type

This is for an explicit primary_key that you don't want to be the default, for example, a UUIDField.

import uuid

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    id = saffier.UUIDField(primary_key=True, default=uuid.uuid4)
    age = saffier.IntegerField(minimum=18)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
Declaring a model with default primary key

If you don't want to be bothered and you are happy with the defaults generated by Saffier, then you can simply declare the model without the id.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    age = saffier.IntegerField(minimum=18)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

The Meta class

When declaring a model, it is crucial having the Meta class declared. There is where you declare the metadata needed for your models.

Currently the available parameters for the meta are:

  • registry - The registry instance for where the model will be generated. This field is mandatory and it will raise an ImproperlyConfigured error if no registry is found.

  • tablename - The name of the table in the database, not the class name.

    Default: name of class pluralised

  • abstract - If the model is abstract or not. If is abstract, then it won't generate the database table.

    Default: False

  • unique_together - The unique constrainsts for your model.

    Default: None

  • indexes - The extra custom indexes you want to add to the model

Registry

Working with a registry is what makes Saffier dynamic and very flexible with the familiar interface we all love. Without the registry, the model doesn't know where it should get the data from.

Imagine a registry like a bridge because it does exactly that.

Let us see some examples in how to use the registry with simple design and with some more complex approaches.

In a nutshell

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

As you can see, when declaring the registry and assigning it to models, that same models is then used in the Meta of the model.

With inheritance

Yes, you can also use the model inheritance to help you out with your models and avoid repetition.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class BaseModel(saffier.Model):
    """
    The base model for all models using the `models` registry.
    """

    class Meta:
        registry = models


class User(BaseModel):
    name = saffier.CharField(max_length=255)
    is_active = saffier.BooleanField(default=True)


class Product(BaseModel):
    user = saffier.ForeignKey(User, null=False, on_delete=saffier.CASCADE)
    sku = saffier.CharField(max_length=255, null=False)

As you can see, the User and Product tables are inheriting from the BaseModel where the registry was already declared. This way you can avoid repeating yourself over and over again.

This can be particularly useful if you have more than one registry in your system and you want to split the bases by responsabilities.

With abstract classes

What if your class is abstract? Can you inherit the registry anyway?

Of course! That doesn't change anything with the registry.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class BaseModel(saffier.Model):
    """
    The base model for all models using the `models` registry.
    """

    class Meta:
        abstract = True
        registry = models


class User(BaseModel):
    name = saffier.CharField(max_length=255)
    is_active = saffier.BooleanField(default=True)


class Product(BaseModel):
    user = saffier.ForeignKey(User, null=False, on_delete=saffier.CASCADE)
    sku = saffier.CharField(max_length=255, null=False)

Table name

This is actually very simple and also comes with defaults. When creating a model if a tablename field in the Meta object is not declared, it will pluralise the python class.

Model without table name

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    """
    If the `tablename` is not declared in the `Meta`,
    saffier will pluralise the class name.

    This table will be called in the database `users`.
    """

    name = saffier.CharField(max_length=255)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

As mentioned in the example, because a tablename was not declared, Saffier will pluralise the python class name User and it will become users in your SQL Database.

Model with a table name

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        tablename = "users"
        registry = models

Here the tablename is being explicitly declared as users. Although it matches with a puralisation of the python class name, this could also be something else.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        tablename = "db_users"
        registry = models

In this example, the User class will be represented by a db_users mapping into the database.

Tip

Calling tablename with a different name than your class it doesn't change the behaviour in your codebase. The tablename is used solely for SQL internal purposes. You will still access the given table in your codebase via main class.

Abstract

As the name suggests, it is when you want to declare an abstract model.

Why do you need an abstract model in the first place? Well, for the same reason when you need to declare an abstract class in python but for this case you simply don't want to generate a table from that model declaration.

This can be useful if you want to hold common functionality across models and don't want to repeat yourself.

The way of declaring an abstract model in Saffier is by passing True to the abstract attribute in the meta class.

In a nutshell

In this document we already mentioned abstract models and how to use them but let us use some more examples to be even clear.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class BaseModel(saffier.Model):
    class Meta:
        abstract = True
        registry = models

This model itself does not do much alone. This simply creates a BaseModel and declares the registry as well as declares the abstract as True.

Use abstract models to hold common functionality

Taking advantage of the abstract models to hold common functionality is usually the common use case for these to be use in the first place.

Let us see a more complex example and how to use it.

import uuid

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class BaseModel(saffier.Model):
    id = saffier.UUIDField(primary_key=True, default=uuid.uuid4)
    name = saffier.CharField(max_length=255)

    class Meta:
        abstract = True
        registry = models

    def get_description(self):
        """
        Returns the description of a record
        """
        return getattr(self, "description", None)


class User(BaseModel):
    """
    Inheriting the fields from the abstract class
    as well as the Meta data.
    """

    phone_number = saffier.CharField(max_length=15)
    description = saffier.TextField()

    def transform_phone_number(self):
        # logic here for the phone number
        ...


class Product(BaseModel):
    """
    Inheriting the fields from the abstract class
    as well as the Meta data.
    """

    sku = saffier.CharField(max_length=255)
    description = saffier.TextField()

    def get_sku(self):
        # Logic to obtain the SKU
        ...

This is already quite a complex example where User and Product have both common functionality like the id and description as well the get_description() function.

Limitations

You can do almost everything with abstract models and emphasis in almost.

Abstract models do not allow you to:

This limitations are intentional as these operations should be done for models and not abstact models.

Unique together

This is a very powerful tool being used by almost every single SQL database out there and extremely useful for database design and integrity.

If you are not familiar with the concept, a unique together enforces records to be unique within those parameters when adding a record to a specific table.

Let us see some examples.

Simple unique together

The simplest and cleanest way of declaring a unique together. There are actually two ways of declaring this simple unique. Via saffier field directly or via unique_together in the meta class.

Within the saffier field
import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70, unique=True)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

In the field you can declare directly unique and that is about it.

With unique_together
import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = ["name"]  # or ("name",)

The unique_together expects one of the following:

  • List of strings.
  • List of tuple of strings.
  • List of tuples of strings.
  • List of tuples of strings as well as strings
  • A List of UniqueConstraint instances.

If none of these values are provided, it will raise a ValueError.

Complex unique together

Now, we all know that using simple uniques is easier if automatically declared within the saffier field an using the meta for only one field is overkill.

You take advantage of the unique_together when something more complex is needed and not limited to one database field only.

When you need more than one field, independently, to be unique
import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = ["name", "email"]

Now here is the tricky part. If you wanted to have together non-duplicate records with the same email and name, this is not doing that. This is in fact saying unique emails and unique names independent of each other.

This is useful but depends on each use case.

For this we used a list of strings.

When you need more than one field, together, to be unique
import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = [("name", "email")]

Did you notice the difference? In this case, when you add a new record to the database it will validate if the name and email together already exists. They are treated as one.

For this we used a list of tuple of strings.

When you need more than combined key, to be unique
import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    phone_number = saffier.CharField(max_length=15)
    address = saffier.CharField(max_length=500)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = [
            ("name", "email"),
            ("name", "email", "phone_number"),
            ("email", "address"),
        ]

Now here is where the things get complex and exciting. As you can see, you can add different variations of the fields combined and generate with whatever complexity you need for your cases.

For this we used a list of tuples of strings.

When you want to mix it all

There are also cases when you want to mix it all up and this is also possible.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    phone_number = saffier.CharField(max_length=15)
    address = saffier.CharField(max_length=500)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = [
            ("name", "email"),
            ("name", "email", "phone_number"),
            ("email", "address"),
            "is_active",
        ]

Did you notice the different compared to the previous example? This time we added a string is_active to the mix.

This will make sure that is_active is also unique (although in general, for this case would not make too much sense).

For this we used a list of tuples of strings as well as strings.

When you use UniqueConstraint instances

This is another clean way of adding the unique together constrainst. This can be used also with the other ways of adding unique together shown in the above examples.

import saffier
from saffier import Database, Registry, UniqueConstraint

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    phone_number = saffier.CharField(max_length=15)
    address = saffier.CharField(max_length=500)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = [
            UniqueConstraint(fields=["name", "email"]),
            UniqueConstraint(fields=["name", "email", "phone_number"]),
            UniqueConstraint(fields=["email", "address"]),
        ]

Or mixing both

import saffier
from saffier import Database, Registry, UniqueConstraint

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    phone_number = saffier.CharField(max_length=15)
    address = saffier.CharField(max_length=500)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        unique_together = [
            UniqueConstraint(fields=["name", "email"]),
            ("name", "email", "phone_number"),
            ("email", "address"),
        ]

Indexes

Sometimes you might want to add specific designed indexes to your models. Database indexes also somes with costs and you should always be careful when creating one.

If you are familiar with indexes you know what this means but if you are not, just have a quick read and get yourself familiar.

There are different ways of declaring an index.

Saffier provides an Index object that must be used when declaring models indexes or a ValueError is raised.

from saffier import Index

Parameters

The Index parameters are:

  • fields - List of model fields in a string format.
  • name - The name of the new index. If no name is provided, it will generate one, snake case with a suffix _idx in the end. Example: name_email_idx.
  • suffix - The suffix used to generate the index name when the name value is not provided.

Let us see some examples.

Simple index

The simplest and cleanest way of declaring an index with Saffier. You declare it directly in the model field.

import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70, index=True)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models

With indexes in the meta

import saffier
from saffier import Database, Index, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)

    class Meta:
        registry = models
        indexes = [Index(fields=["email"])]

With complex indexes in the meta

import saffier
from saffier import Database, Index, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
    name = saffier.CharField(max_length=255)
    email = saffier.EmailField(max_length=70)
    is_active = saffier.BooleanField(default=True)
    status = saffier.CharField(max_length=255)

    class Meta:
        registry = models
        indexes = [
            Index(fields=["name", "email"]),
            Index(fields=["is_active", "status"], name="active_status_idx"),
        ]