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:
- Declare managers.
- Declare unique together
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"),
]