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, Edgy 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 Edgy is a python class with attributes that represents a database table as well as a pydantic model which enables serialization and validation.
In other words, it is what represents your SQL table in your codebase.
Model dump¶
The model_dump
and model_dump_json
methods are here overloaded and supports an argument:
show_pk
which auto-includes the the primary key fields when set to True
. Default to the value of __show_pk__
of models.
Embedding¶
Models also doubles as embeddable; they can be used as a field template with optional prefix.
Declaring models¶
When declaring models by simply inheriting from edgy.Model
object and define the attributes
using the edgy Fields.
For each model defined you also need to set one mandatory field, the registry
which is also
an instance of Registry
from Edgy.
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 Edgy took inspiration from the interface of Django, that also means that a Meta class should be declared.
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
"""
The user model representation.
Note: For sqlite the BigIntegerField transforms in an IntegerField when autoincrement is set
because sqlite doesn't support BigInteger autoincrement fields.
"""
id: int = edgy.BigIntegerField(primary_key=True, autoincrement=True)
name: str = edgy.CharField(max_length=255)
age: int = edgy.IntegerField(minimum=18)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
Although this looks very simple, in fact Edgy is doing a lot of work for you behind the scenes.
Edgy 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.
Strict models¶
Sometimes you want more input validation. E.g. non-existing fields are not just ignored but raise an error or the input type is not coerced into the right type but raises an error instead.
For this the StrictModel
model can be used. Otherwise it behaves like a normal Model.
There is no strict version of a ReflectModel
because the laxness is required.
Loading models¶
You may have the models distributed among multiple files and packages.
To ensure the models can self-register we need to ensure the files load.
Previously this was done with model_apps
in the Migrate
object but this approach is deprecated and
didn't allow multiple registries.
Edgy has now a special setting preloads
in Settings. Here it is possible to provide the import pathes
and the corresponding files are loaded.
It is also possible to point to a function via module_path:fn
which is executed without argument and expected to load the modules.
Special attributes¶
There are few special attributes which can be set
database
: Different database/access current database of model. Should be in registry extra. It is set per instance when using queries.__using_schema__
: Different schema. The schema must be created first. Default is Undefined. It is set per instance for tenant models.- `` These attributes work on instances as well as classes.
There are a few more exotic ones:
__require_model_based_deletion__
: (Default False): Enforce a deletion in edgy instead of the database (Query iterates through models and deletes each explicitly). This is quite imperformant, so only set it if you really require that the delete method of a model is called.__reflected__
: Only read but don't set it. It shows if a model has the reflected state.
Attention¶
If no id
is declared in the model, Edgy will automatically generate an id
of type
BigIntegerField
and automatically becoming the primary key.
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
"""
The user model representation.
The `id` is not provided and Edgy will automatically
generate a primary_key `id` BigIntegerField.
"""
name: str = edgy.CharField(max_length=255)
age: int = edgy.IntegerField(minimum=18)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
Restrictions with primary keys¶
Earlier there were many restrictions. Now they were lifted
What you should not do¶
Declaring an IntegerField as primary key without autoincrement set¶
Because of backward compatibility we set for an IntegerField or BigIntegerField which is declared as primary key
the autoincrement option to True
. So you still need to explicitly set autoincrement=False
to turn this off.
This will change in future. The default will be False
.
Warning
For every model only one IntegerField/BigIntegerField can be set to autoincrement=True.
What you can do¶
The examples are applied to any field available in Edgy.
Declaring a model primary key different from ID¶
import uuid
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
non_default_id = edgy.BigIntegerField(primary_key=True, autoincrement=True)
name: str = edgy.CharField(max_length=255, primary_key=True)
age: int = edgy.IntegerField(minimum=18)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
Multiple primary keys¶
This is novel and maybe a bit buggy in combination with ForeignKeys.
Declaring a model with ID and without default and autoincrement¶
When declaring an id
, unless the field type is IntegerField or
BigIntegerField, you have to provide the primary key when creating the object.
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
id: int = edgy.CharField(max_length=255, primary_key=True)
age: int = edgy.IntegerField(minimum=18)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
obj = edgy.run_sync(User.query.create(id="edgy", age=19))
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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
id: int = edgy.UUIDField(primary_key=True, default=uuid.uuid4)
age: int = edgy.IntegerField(minimum=18)
is_active: bool = edgy.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 Edgy,
then you can simply declare the model without the id
.
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
age: int = edgy.IntegerField(minimum=18)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
Customizing the deletion variant¶
By default a database only deletion is used where possible when using the QuerySet delete method.
If you want to force the QuerySet calling the delete method of the model instead, you can set the class variable:
__require_model_based_deletion__ = True
Copying a model to a new registry¶
We have now a method:
model.copy_edgy_model(registry=None, name="")
to copy a model class and optionally add it to an other registry.
You can add it to a registry later by using:
model_class.add_to_registry(registry, name="", database=None, replace_related_field=False)
In fact the last method is called when the registry parameter of copy_edgy_model
is not None
.
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.
-
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 Edgy 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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class BaseModel(edgy.Model):
"""
The base model for all models using the `models` registry.
"""
class Meta:
registry = models
class User(BaseModel):
nametr = edgy.CharField(max_length=255)
is_active: bool = edgy.BooleanField(default=True)
class Product(BaseModel):
user: User = edgy.ForeignKey(User, null=False, on_delete=edgy.CASCADE)
sku: str = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class BaseModel(edgy.Model):
"""
The base model for all models using the `models` registry.
"""
class Meta:
abstract = True
registry = models
class User(BaseModel):
name: str = edgy.CharField(max_length=255)
is_active: bool = edgy.BooleanField(default=True)
class Product(BaseModel):
user: User = edgy.ForeignKey(User, null=False, on_delete=edgy.CASCADE)
sku: str = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
"""
If the `tablename` is not declared in the `Meta`,
edgy will pluralise the class name.
This table will be called in the database `users`.
"""
name: str = edgy.CharField(max_length=255)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
As mentioned in the example, because a tablename
was not declared, Edgy will pluralise
the python class name User
and it will become users
in your SQL Database.
Model with a table name¶
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
is_active: bool = edgy.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 Edgy is by passing True
to the abstract
attribute in the meta class.
Explicitly disable registry registration¶
Sometimes you want to add a model manually to the registry and not to retrieve a registry by the parents. This can be archived by setting Meta registry to False.
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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class BaseModel(edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class BaseModel(edgy.Model):
id: uuid.UUID = edgy.UUIDField(primary_key=True, default=uuid.uuid4)
name: str = edgy.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: str = edgy.CharField(max_length=15)
description: str = edgy.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: str = edgy.CharField(max_length=255)
description: str = edgy.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 edgy field directly or via
unique_together
in the meta class.
Within the edgy field¶
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70, unique=True)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
In the field you can declare directly unique
and that is about it.
With unique_together¶
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
is_active: bool = edgy.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 edgy 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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
phone_number: str = edgy.CharField(max_length=15)
address: str = edgy.CharField(max_length=500)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
phone_number: str = edgy.CharField(max_length=15)
address: str = edgy.CharField(max_length=500)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry, UniqueConstraint
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
phone_number: str = edgy.CharField(max_length=15)
address: str = edgy.CharField(max_length=500)
is_active: bool = edgy.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 edgy
from edgy import Database, Registry, UniqueConstraint
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
phone_number: str = edgy.CharField(max_length=15)
address: str = edgy.CharField(max_length=500)
is_active: bool = edgy.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.
Edgy provides an Index
object that must be used when declaring models indexes or a
ValueError
is raised.
from edgy 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 Edgy. You declare it directly in the model field.
import edgy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70, index=True)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
With indexes in the meta¶
import edgy
from edgy import Database, Index, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
is_active: bool = edgy.BooleanField(default=True)
class Meta:
registry = models
indexes = [Index(fields=["email"])]
With complex indexes in the meta¶
import edgy
from edgy import Database, Index, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name: str = edgy.CharField(max_length=255)
email: str = edgy.EmailField(max_length=70)
is_active: bool = edgy.BooleanField(default=True)
status: str = edgy.CharField(max_length=255)
class Meta:
registry = models
indexes = [
Index(fields=["name", "email"]),
Index(fields=["is_active", "status"], name="active_status_idx"),
]
Constraints¶
Plain sqlalchemy constraints can be passed via the meta.constraints parameter.
This is useful for CheckConstraints (despite pydantic can handle most cases better).
import edgy
import sqlalchemy
from edgy import Database, Registry
database = Database("sqlite:///db.sqlite")
models = Registry(database=database)
class User(edgy.Model):
name = edgy.fields.CharField(max_length=255)
is_admin = edgy.fields.BooleanField()
age = edgy.IntegerField(null=True)
class Meta:
registry = models
constraints = [sqlalchemy.CheckConstraint("age > 13 OR is_admin = true", name="user_age")]
Note
This operates on column level not on field level. The column key is relevant.
Meta info attributes¶
The metaclass also calculates following readonly attributes:
special_getter_fields
(set): Field names with__get__
function. This is used for the pseudo-descriptor__get__
protocol for fields.excluded_fields
(set): Field names of fields withexclude=True
set. They are excluded by default.secret_fields
(set): Field names of fields withsecret=True
set. They are excluded by default when usingexclude_secret
.input_modifying_fields
(set): Field names of fields with amodify_input
method. They are altering the input kwargs oftransform_input
(setting to model) andextract_column_values
.post_save_fields
(set): Field names of fields with apost_save_callback
method.pre_save_fields
(set): Field names of fields with apre_save_callback
method.post_delete_fields
(set): Field names of fields with apost_save_callback
method.foreign_key_fields
(set): Field names of ForeignKey fields. Note: this does not include ManyToMany fields but their internal ForeignKeys.relationship_fields
(set): Field names of fields inheriting from RelationshipField.field_to_columns
(pseudo dictionary): Maps a fieldname to its defined columns.field_to_column_names
(pseudo dictionary): Maps a fieldname to its defined column keys. Uses internallyfield_to_columns
.columns_to_field
(pseudo dictionary): Maps a column key to its defining field.
Some other interesting attributes are:
multi_related
(set[tuple[str, str]]): Holds foreign_keys used by ManyToManyFields when being used as a through model. Is empty for non-through models.fields
(pseudo dictionary): Holds fields. When setting/deleting fields it updates the attributes.model
(model_class): This attribute is special in it's way that it is not retrieved from a meta class. It must be explicitly set. This has implications for custom MetaInfo. You either replace the original one by passing meta_info_class as metaclass argument or set it in your overwrite manually.