Advanced¶
Schemas¶
Postgres and CoackroachDB have a concept called schemas.
It’s a way of grouping the tables in a database. To learn more:
To specify a table’s schema, do the following:
class Band(Table, schema="music"):
...
# The table will be created in the `music` schema.
# The music schema will also be created if it doesn't already exist.
>>> await Band.create_table()
If the schema
argument isn’t specified, then the table is created in the
public
schema.
Migration support¶
Schemas are fully supported in database migrations.
For example, if we change the schema
argument:
class Band(Table, schema="music_2"):
...
Then create an automatic migration and run it, then the table will be moved to the new schema:
>>> piccolo migrations new my_app --auto
>>> piccolo migrations forwards my_app
SchemaManager
¶
The SchemaManager
class is used
internally by Piccolo to interact with schemas. You may find it useful if you
want to write a script to interact with schemas (create / delete / list etc).
Readable¶
Sometimes Piccolo needs a succinct representation of a row - for example, when
displaying a link in the Piccolo Admin. Rather than
just displaying the row ID, we can specify something more user friendly using
Readable
.
# tables.py
from piccolo.table import Table
from piccolo.columns import Varchar
from piccolo.columns.readable import Readable
class Band(Table, tablename="music_band"):
name = Varchar(length=100)
@classmethod
def get_readable(cls):
return Readable(template="%s", columns=[cls.name])
Specifying the get_readable
classmethod isn’t just beneficial for Piccolo
tooling - you can also use it your own queries.
await Band.select(Band.get_readable())
Here is an example of a more complex Readable
.
class Band(Table, tablename="music_band"):
name = Varchar(length=100)
@classmethod
def get_readable(cls):
return Readable(template="Band %s - %s", columns=[cls.id, cls.name])
As you can see, the template can include multiple columns, and can contain your own text.
Mixins¶
If you’re frequently defining the same columns over and over again, you can use mixins to reduce the amount of repetition.
from piccolo.columns import Varchar, Boolean
from piccolo.table import Table
class FavouriteMixin:
favourite = Boolean(default=False)
class Manager(FavouriteMixin, Table):
name = Varchar()
Choices¶
You can specify choices for a column, using Python’s Enum
support.
from enum import Enum
from piccolo.columns import Varchar
from piccolo.table import Table
class Shirt(Table):
class Size(str, Enum):
small = 's'
medium = 'm'
large = 'l'
size = Varchar(length=1, choices=Size)
We can then use the Enum
in our queries.
>>> await Shirt(size=Shirt.Size.large).save()
>>> await Shirt.select()
[{'id': 1, 'size': 'l'}]
Note how the value stored in the database is the Enum
value (in this case 'l'
).
You can also use the Enum
in where
clauses, and in most other situations
where a query requires a value.
>>> await Shirt.insert(
... Shirt(size=Shirt.Size.small),
... Shirt(size=Shirt.Size.medium)
... )
>>> await Shirt.select().where(Shirt.size == Shirt.Size.small)
[{'id': 1, 'size': 's'}]
Advantages¶
By using choices, you get the following benefits:
Signalling to other programmers what values are acceptable for the column.
Improved storage efficiency (we can store
'l'
instead of'large'
).Piccolo Admin support
Array
columns¶
You can also use choices with Array
columns.
class Ticket(Table):
class Extras(str, enum.Enum):
drink = "drink"
snack = "snack"
program = "program"
extras = Array(Varchar(), choices=Extras)
Note how you pass choices
to Array
, and not the base_column
:
# CORRECT:
Array(Varchar(), choices=Extras)
# INCORRECT:
Array(Varchar(choices=Extras))
We can then use the Enum
in our queries:
>>> await Ticket.insert(
... Ticket(extras=[Extras.drink, Extras.snack]),
... Ticket(extras=[Extras.program]),
... )
Reflection¶
This is a very advanced feature, which is only required for specialist use cases. Currently, just Postgres is supported.
Instead of writing your Table
definitions in a tables.py
file, Piccolo
can dynamically create them at run time, by inspecting the database. These
Table
classes are then stored in memory, using a singleton object called
TableStorage
.
Some example use cases:
You have a very dynamic database, where new tables are being created constantly, so updating a
tables.py
is impractical.You use Piccolo on the command line to explore databases.
Full reflection¶
Here’s an example, where we reflect the entire schema:
from piccolo.table_reflection import TableStorage
storage = TableStorage()
await storage.reflect(schema_name="music")
Table
objects are accessible from TableStorage.tables
:
>>> storage.tables
{"music.Band": <class 'Band'>, ... }
>>> Band = storage.tables["music.Band"]
Then you can use them like your normal Table
classes:
>>> await Band.select()
[{'id': 1, 'name': 'Pythonistas', 'manager': 1}, ...]
Partial reflection¶
Full schema reflection can be a heavy process based on the size of your schema.
You can use include
, exclude
and keep_existing
parameters of
the reflect
method to limit the overhead dramatically.
Only reflect the needed table(s):
from piccolo.table_reflection import TableStorage
storage = TableStorage()
await storage.reflect(schema_name="music", include=['band', ...])
Exclude table(s):
await storage.reflect(schema_name="music", exclude=['band', ...])
If you set keep_existing=True
, only new tables on the database will be
reflected and the existing tables in TableStorage
will be left intact.
await storage.reflect(schema_name="music", keep_existing=True)
get_table¶
TableStorage
has a helper method named get_table
. If the table is
already present in the TableStorage
, this will return it and if the table
is not present, it will be reflected and returned.
Band = storage.get_table(tablename='band')
Hint
Reflection will automatically create Table
classes for referenced
tables too. For example, if Table1
references Table2
, then
Table2
will automatically be added to TableStorage
.
How to create custom column types¶
Sometimes, the column types shipped with Piccolo don’t meet your requirements, and you will need to define your own column types.
Generally there are two ways to define your own column types:
Create a subclass of an existing column type; or
Directly subclass the Column class.
Try to use the first method whenever possible because it is more straightforward and can often save you some work. Otherwise, subclass Column.
Example
In this example, we create a column type called MyColumn
, which is fundamentally
an Integer
type but has a custom attribute custom_attr
:
from piccolo.columns import Integer
class MyColumn(Integer):
def __init__(self, *args, custom_attr: str = '', **kwargs):
self.custom_attr = custom_attr
super().__init__(*args, **kwargs)
@property
def column_type(self):
return 'INTEGER'
Hint
It is important to specify the column_type
property, which
tells the database engine the actual storage type of the custom
column.
Now we can use MyColumn
in our table:
from piccolo.table import Table
class MyTable(Table):
my_col = MyColumn(custom_attr='foo')
...
And later we can retrieve the value of the attribute:
>>> MyTable.my_col.custom_attr
'foo'