📣 Add code to your data with Model Classes

When you’re building an app with Data Tables, you sometimes want to attach logic, server functions, and validation to the data you’re storing. Spreading that code among forms, modules and server functions can get messy.

Today, we’re releasing several new features in Accelerated Tables to make database-backed apps with Anvil much cleaner:

  • Model Classes, where you can specify your own class to use for rows from a particular table.

  • Buffered Changes, where you can make temporary changes to a row, and either save them or roll them back. This means you can use data bindings with your model classes.

  • Client-writable models, where you can write code as though you’re writing to data tables directly from client Form code, but all changes must go via your model code (running on the server) before they can be applied.

Note 1: This is the first version of this functionality. It’s fully documented and supported, but it will have some rough edges – and we want to hear your feedback about what is easy and hard with this new model. We look forward to your posts in Q&A or Show&Tell!

You can click on those links and jump right into the docs, or read on for a bit more detail about each of them.

Note 2: Massive props to @stucork, who implemented the lion’s share of this!

1. Model Classes

A model class is a subclass of the Data Tables Row object. If you define a model class for a particular table, Anvil will instantiate your class for every row of that table, instead of the generic Row object. You can add properties, methods, and even server-side methods (like server functions, but in your class). Anywhere that Anvil would give you a row object (search iterators, the get() method, following links from other tables), it will give you a model object instead if one exists.

You can jump straight into the docs, but here’s a simple example to get you started. It works with a people table that has columns for name and date_of_birth:

class Person(app_tables.people.Row):
  @property
  def age(self):
    days = (date.today() - self["date_of_birth"])).days
    return days // 365

for person in app_tables.people.search():
  print(f"{person['name']} is {person.age} years old")

You can do a lot more than this with Model Classes – check out the docs!

2. Buffered changes

We often want to make temporary changes to row objects, or create “drafts” of records that have not yet been saved in the database. Right now we usually do that with dictionaries, but they don’t have all that model-class goodness! So now, you can buffer changes to a particular row.

For example, when you’re popping up an edit dialog, with “Save” and “Cancel” buttons, you can now do:

person = # ...get it from somewhere

with person.buffer_changes():
  # the EditPerson form receives a Person instance, with
  # all its properties and methods, not just a dict!
  if alert(EditPerson(item=person),
           buttons=[("Save", True), ("Cancel", False)]):
    person.save() # otherwise changes are discarded

There are a few ways to invoke buffering – you can switch it on and off for individual rows, or specify that every instance of a model is always buffered.

Read all about it in the docs!

You can even create “draft rows”, which are instances of the row/model class that you use before they are saved to the database. For example, to create a new Person record:

  person = Person()
  # Again, the EditPerson form receives a Person instance,
  #  with all its properties and methods, not just a dict!
  if alert(EditPerson(item=person),
           buttons=[("Save", True), ("Cancel", False)]):
    person.save()
    # now "person" is a real row saved in the database.

Of course, that snippet on its own would require your table to be client-writable, which has worrying security implications. But what if we had…

3. Client-writable models

The usual way to build web apps is to build a front-end UI to perform some operation, then build some back-end function to apply those changes to the database, then call the back-end from the front end. This is how we do things in Anvil, but it spreads your logic out across your app.

What if you could write a Model Class, specifying the rules for what changes are allowed to a particular piece of data, and then just…use that model class everywhere? Look ma, no Server Modules!

You’ll definitely want to read the docs for this one, but here’s a taste of what you can do. This snippet creates a model class that can be edited directly from client code, but only if you’re logged in, and timestamps every edit:

class Person(app_tables.people.Row,
             client_updatable=True):
  def _do_update(self, updates, from_client):
     if not anvil.users.get_user():
       raise Exception("You must be logged in")
     updates['last_edited'] = datetime.now()
     super()._do_update(updates, from_client)

Tell us what you think!

We’re really excited to get this into your hands. We’ve developed these features by talking a lot to people who are already using Anvil for substantial database-backed applications, but the proof is in the pudding – we want to see how you use it! We’re going to let it shake down a bit before we go rewriting our tutorials, but we think this is going to make database-backed apps much, much easier to write.

As ever, please ask questions and discuss these new features in the Q&A section, and show us what you’ve built in Show&Tell!

9 Likes

All of this looks really cool. Definitely some new things to learn but I can already see so many uses for it.

Very exciting! Great set of features, I’m looking forward to exploring them.

Ooh, does this mean we can have server functions akin to nosql dbs now?:tada:

1 Like

Nice! This addresses some common pain points that lead to awkward code.

1 Like

:exploding_head: :nerd_face: :partying_face:

I’m looking forward to trying these features out!

_do_create, _do_update, _do_delete all mesh with ideas I’ve had about implementing referential integrity, as these are the events that can lead to breaking such integrity rules.

This might also be a good place to use mixin-style multiple inheritance, so that some of the obvious helper functions land right in the classes that need them. In that case, I would expect the mixin base class names to appear before app_tables.table_name.Row. Is that expected to work?

Looks like the anvil-extras persistence module just became redundant.

Nice!

1 Like

We have played with ideas like these (there’s a cute little “record every operation in an audit log” mixin we’ve been playing with), and they are both super neat and a normal, natural consequence of Python method overriding. It’s one of the things I’m really proud of about this design (and why it was worth taking time to get it right).

Not without being extensively stolen borrowed from! Seeing some of the ergonomics you and others were shooting for (including some non-public work) really helped us with working out what this design wanted to be.

(Now to find out whether we missed anything big :wink: )

3 Likes

It’s going to be very interesting to see how this meshes with Portable Classes!

How might I handle a linked row? Can I also have that as a model class instance somehow?

1 Like

If you try it, you’ll find that what you get by following a link is already an instance of the appropriate model class!

If there’s a model class registered for a particular table, the Data Tables code will use that model class whenever it’s generating row objects. This means that you’ll get model instances from lazy search iterators, when following links, when calling get()…the whole nine yards.

4 Likes

Very nice. Thank you!

All works via views too, I take it?

(on the clock for clients for a day or two, so I can’t play around with this stuff just yet).

To answer @p.colbert above - models are already portable classes, because that’s how Accelerated Tables is implemented! They’re a bit special, though - because they can be instantiated out of things like lazy search iterators, models aren’t allowed to have any state that isn’t in the DB. (They’re also registered automatically as part of the process that registers models with Data Tables.)

The flipside of these limitations is that model classes can be transmitted just as efficiently as raw table rows (which have some quite optimised ways of serialising their state, deduplicating data, etc). It also makes it easier to write @server_methods securely, because we know that everything in self comes straight from the database and is therefore trustworthy (docs here).

We even automatically convert rows between representations – if there’s some code you want to hide from the client, you can have a “server version” and “client version” of a particular model, and Anvil will automagically* convert between them when you transmit them in server calls. (See docs here.)

(* via judicious use of __new_deserialized__(), controlling what portable-class names the model classes are registered under, and similar shenanigans.)

1 Like

Yes it does!

Although you should be careful to avoid client_writable() views – for backwards compatibility reasons, that method grants unconditional write access, and overrides any checks provided in the model. (I think the plan is to make it an error to combine those two things, just to be sure!). Stick to client_readable() views, then use models to offer client writability. (See the big honking red warnings on the docs page)

2 Likes

Actually, I was thinking of making a SimpleObject column value into a Portable Class. The other column types are usually smart enough on their own to not need individual wrapper classes.

But I haven’t studied the infrastructure to see whether that’s feasible with Anvil Models.

1 Like

Is there a model equivalent to a batch update with the buffered models? Something to update a lot of models with a single server interaction, rather than a server interaction per call to save()?

1 Like

You can probably do that with the custom property feature that models allow. And even make a portable class that you then instantiate in the custom property there.

1 Like

Just starting a new largish project and I’m very excited about this!

Not without being extensively stolen borrowed from! Seeing some of the ergonomics you and others were shooting for (including some non-public work) really helped us with working out what this design wanted to be.

(Now to find out whether we missed anything big :wink: )

I suspect this is a deliberate design decision rather than ‘missing’…

In persistence, if you create an attribute with a leading underscore, it behaves like any other attribute but never gets transmitted between client and server.

I sometimes use that for properties that are ‘expensive’ to compute - the getter checks the ‘internal’ property and returns it if set. If not, it computes and sets the internal property.

Something like:


@persisted_class
class Thing:
    def __init__(self):
        self._computed = None

    @property
    def computed(self):
        if self._computed is None:
            self._computed = do_heavy_computation()
        return self._computed

With model classes, I’d presumably have to use an external class or closure to achieve the same effect.