DB Tables¶
When using the default SQLAlchemy backend, defining your tables as SQLAlchemy tables gives you the ability to use Uvicore's query builder, or SQLAlchemy's query builder. If you do not define any tables, then RAW SQL may be the only way to interact with your data.
Uvicore provides two methods to define your SQLAlchemy tables.
Tables as Separate Files¶
Tables may be stored in separate files located in database/tables/*
. You may use the Uvicore schematic generator to create this table automatically, or create it by hand.
./uvicore gen table --help
./uvicore gen table posts
init file
Be sure to add your new table to the database/tables/__init__.py
The schematic includes many commented examples of how to use the table. A form of quick inline documentation!
Tip
In general, tables should be plural (posts) while their corresponding ORM model (if you decide to use the ORM) would be singular (post). This is a convention rather than a rule.
A basic "wiki" posts
table looks like this
import uvicore
import sqlalchemy as sa
from uvicore.database import Table
from uvicore.support.dumper import dump
# Get related table names with proper prefixes
users = uvicore.db.tablename('auth.users')
@uvicore.table()
class Posts(Table):
# Actual database table name
# Plural table names and singular ORM model names are encouraged
# Do not add a package prefix, leave that to the connection config
name = 'posts'
# Connection for this table from your config/database.py file
connection = 'wiki'
# SQLAlchemy Table definition
# This will be converted into an actual SQLAlchemy Table() instance
# and automatically associated with the proper SQLAlchemy Metadata
# See https://uvicore.io/database/db-tables/
# See https://docs.sqlalchemy.org/en/20/core/schema.html
schema = [
# Defaults: nullable=True, index=False, unique=False, primary_key=False
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('unique_slug', sa.String(length=100), unique=True),
sa.Column('title', sa.String(length=100)),
sa.Column('body', sa.Text()),
sa.Column('other', sa.String(length=100), nullable=True),
sa.Column('creator_id', sa.Integer, sa.ForeignKey(f"{users}.id"), nullable=False),
sa.Column('owner_id', sa.Integer, sa.ForeignKey(f"{users}.id"), nullable=False),
sa.Column('created_at', sa.DateTime(), default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(), default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
]
# Optional SQLAlchemy Table() instance kwargs
schema_kwargs = {
# Enable SQLite autoincrements (this is OK even when not using SQLite)
'sqlite_autoincrement': True,
}
Notice
Notice the uvicore.db.tablename('auth.users')
above. This gets the users
table name from the auth
package and obeys adding additional table prefixes defined in the configs. Try not to use table names manually or the prefix config will be ignored!
If you are using the Uvicore ORM (optional), and you are defining your table in a separate file, simply point the __tableclass__
to the proper table class.
# ...
from acme.wiki.database.tables import posts as table
@uvicore.model()
class Post(Model['Post'], metaclass=ModelMetaclass):
"""Wiki Posts"""
# Database table definition
# Optional as some models have no database table
__tableclass__ = table.Posts
#...
Tables as Inline ORM¶
Some folks don't like having the table in separate files from the ORM models. Uvicore also lets you define the table inline!
If you are using the Uvicore ORM (optional), and you want to define your tables inline instead of in a separate file, you may do so like this:
# ...
from acme.wiki.database.tables import posts as table
# Get related table names with proper prefixes
users = uvicore.db.tablename('auth.users')
@uvicore.model()
class Post(Model['Post'], metaclass=ModelMetaclass):
"""Wiki Posts"""
# Database table definition
# Optional as some models have no database table
__connection__ = 'wiki'
__tablename__ = 'posts'
__table__ = [
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('unique_slug', sa.String(length=100), unique=True),
sa.Column('title', sa.String(length=100)),
]
#...
# Model fields go here
# ...
See the ORM documentation for more ORM specific details.
Base Table¶
Notice all tables inherit from the uvicore.database.Table
base class. This class turns your simple schema
table definition defined in schame
into a full SQLAlchemy Table and attaches that table to the proper connections SQLAlchemy metadata found in uvicore.db.metadatas
. It does this using the __init__(self)
constructor.
Checkout the database/table.py file below to see how the Base class build the sa.Table()
import uvicore
import sqlalchemy as sa
from abc import ABCMeta
from typing import Dict, List
from uvicore.support.dumper import dd, dump
@uvicore.service()
class Table:
@property
def table(self):
return self.schema
def __init__(self):
self.metadata = uvicore.db.metadata(self.connection)
prefix = uvicore.db.connection(self.connection).prefix
if prefix is not None:
self.name = str(prefix) + self.name
# Only enhance schema if connection string backend is 'sqlalchemy'
if uvicore.db.connection(self.connection).backend == 'sqlalchemy':
self.schema = sa.Table(
self.name,
self.metadata,
*self.schema,
**self.schema_kwargs
)
If you want to modify the actual SQLAlchemy table after it is created, simply extend this parent base __init__(self)
method. See #composite-indexes below for an example.
Note
Notice that any schema_kwargs
you defined in your table are passed into the SQLAlchemy Table() method giving you extra control!
Table Examples¶
Below are some quick examples on some tricky to remember SQLAlchemy table definitions. For brevity we are only showing the schema
List.
See https://docs.sqlalchemy.org/en/20/core/constraints.html for more complex definitions.
Multiple Unique Constraints¶
Most SQL dialects allow for multiple unique constraints.
schema = [
sa.Column('id', sa.Integer, primary_key=True),
# Polymorphic Relations
sa.Column('addressable_type', sa.String(length=50)),
sa.Column('addressable_id', sa.Integer),
sa.Column('address_id', sa.Integer, sa.ForeignKey(f"{addresses}.id"), nullable=False),
sa.UniqueConstraint('addressable_type', 'addressable_id', 'address_id')
#sa.UniqueConstraint('addressable_type', 'addressable_id', 'address_id', name='uix_1')
]
Multiple Primary Keys¶
Most SQL dialects allow for multiple primary key constraints. Useful when you won't want an auto incrementing PK but don't have uniqueness of a single field.
schema = [
sa.Column('price_list_id', sa.Integer, sa.ForeignKey(f"{price_lists}.id"), nullable=False),
sa.Column('stock_item_id', sa.Integer, sa.ForeignKey(f"{items}.id"), nullable=False),
sa.Column('value', sa.DECIMAL(precision=8, scale=2)),
sa.Column('disabled', sa.Boolean(), default=False),
sa.PrimaryKeyConstraint('price_list_id', 'stock_item_id')
]
You may also define multiple primary keys by using the primary_key=True
parameter twice
sa.Column('price_list_id', sa.Integer, sa.primary_key=True, sa.ForeignKey(f"{price_lists}.id"), nullable=False),
sa.Column('stock_item_id', sa.Integer, sa.primary_key=True, sa.ForeignKey(f"{items}.id"), nullable=False),
Multiple Foreign Key Constraints¶
The invoices
table has a 2 column primary key constraint
schema = [
sa.Column("invoice_id", sa.Integer, primary_key=True),
sa.Column("ref_num", sa.Integer, primary_key=True),
sa.Column("description", sa.String(60), nullable=False),
]
And the invoice_items
table with dual foreign key constraints
invoices = uvicore.db.tablename('wiki.invoices')
schema = [
sa.Column('item_id', sa.Integer, primary_key=True),
sa.Column('item_name', sa.String(60), nullable=False),
sa.Column('invoice_id', sa.Integer, nullable=False),
sa.Column('ref_num', sa.Integer, nullable=False),
sa.ForeignKeyConstraint(
['invoice_id', 'ref_num'], [f"{invoices}.invoice_id", f"{invoices}.ref_num"],
onupdate="CASCADE",
ondelete="SET NULL",
)
]
Composite Indexes¶
The base Table
class is what turns your schema
property into an actual sa.Table()
. That full table is saved back to your Class as self.schema
. To add composite indexes to a table you must add it AFTER the actual SQLAlchemy table is created. To achieve this in Uvicore, simply overwrite the __init__
and after calling super()
, modify self.schema
(which is the actual sa.Table).
import uvicore
import sqlalchemy as sa
from uvicore.database import Table
@uvicore.table()
class Posts(Table):
name = 'posts'
connection = 'wiki'
schema = [
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('unique_slug', sa.String(length=100), unique=True),
sa.Column('title', sa.String(length=100), index=True),
sa.Column('val_a', sa.String(length=100), nullable=False),
sa.Column('val_b', sa.String(length=100), nullable=False),
]
schema_kwargs = {}
def __init__(self):
# Call parent from uvicore.database.Table which sets self.schema
# as the fully built SQLAlchemy Table object.
super().__init__()
# Add additional attributes to our SQLAlchemy table
sa.Index('idx_1', self.schema.c.val_a, self.schema.c.val_b)
Cascades¶
schema = [
sa.Column('id', sa.Integer, primary_key=True),
sa.Column(
'address_id',
sa.Integer,
sa.ForeignKey(f"{addresses}.id"),
nullable=False,
onupdate='CASCADE',
ondelete='CASCADE'
),
]