Skip to content

5.0 theory

Jean Cavallo edited this page Jul 22, 2019 · 1 revision

Trytond Server Behaviour

Server startup

When the server starts, it does the following:

  • Check the configuration
  • Parse all modules (whether they are installed or not is not important, it will try to parse all modules that are plugged in on the server). That means that a module that cannot parse will crash the server even though it is not installed.
  • Connect to the database server

If a database name was given, or when a user tries to connect to a particular database, it will :

  • Create a class Pool for the database
  • Search the database for the list of activated modules
  • Compare it to the modules which are available (i.e. which were imported when the server started)
  • Build a dependency graph between module, and chose a resolution order. This order may change depending on the installation since sister modules (i.e. module depending on the same parent but with no particular dependency between them) will not always be ordered the same way
  • Register every module in the Pool

Registering a module basically consist in calling the register method which is defined in its __init__.py module. All classes that are selected will go through the following process :

  • Check if its __name__ was already registerd in the Pool.
  • If no, the current module class for the __name__ is the original definition of the model, and is added in the Pool
  • If yes, the current module class is modified to inherit from the one already in the Pool. The Pool is then updated to use this new class for the related __name__.

Once all modules are registered in the pool, for each model in the Pool, the matching class will be constituted of all the python classes with the matching __name__ which were found in the activated module for the database. Then,

  • For every model in the Pool, the __setup__ method is called on the matching class.
  • Then the __post_setup__ method is called as well

The server is then ready to process incoming connections.

Module Inheritance Graph

                    +-------------+
                    |   Module A  |
                    +-------------+
                      /         \
                     /           \
                    /             \
            +------------+      +------------+
            |  Module B  |      |  Module C  |
            +------------+      +------------+

Module Code

# Module A

class MyModelA(Model):
    __name__ = 'my.model'

# Module B

class MyModelB:
    __name__ = 'my.model'

# Module C

class MyModelC:
    __name__ = 'my.model'

Post start classes

# The order between classes B and C may change depending on the installation
# since there aren't any explicit dependencies between modules B and C

#    +-------------+
#    |   Module A  |
#    +-------------+
#           ^
#           |
#    +-------------+
#    |   Module B  |
#    +-------------+
#           ^
#           |
#    +-------------+
#    |   Module C  |
#    +-------------+

class MyModelA(Model):
    __name__ = 'my.model'

class MyModelB(MyModelA):
    pass

class MyModelC(MyModelB):
    pass

Pool().get('my.model') == MyModelC

Module Update

Activating or Updating a module on a server does a lot of things :

  • Rebuild a Pool which includes the modules to activate (if any)
  • For each module, in their resolution order :
    • Call the __register__ method on each model in the Pool which was overriden in the module
    • Read all xml files declared in the tryton.cfg file of the module, and create or update the associated entries in the database
  • Create the __history tables for the models which required it
  • Remove all entries which were at one point in an xml file, but are not in the current version

Model

Models are the building blocks of Tryton. A model may be stored in the database and / or displayed in a view. It is identified by its __name__ which is must be a unique identifier.

They are bound to python classes and may be overriden in different modules simply by creating a class with the same __name__ than that of the target model. The Class name itself is irrelevant, what matters is the __name__ attribute on the class.

# Module 1

class MyModel(Model):
    'My Model'
    __name__ = 'my.model'

# Module 2

class SomeTotallyIrrelevantName:
    __metaclass__ = PoolMeta  # Tryton magic
    __name__ = 'my.model'     # This is an override of the model defined in
                              # Module 1


class MyModel:                # Even though the class name is the same, this
    __metaclass__ = PoolMeta  # class has no relation with the model in
    __name__ = 'oops'         # Module 1

Depending on what is expected from the model, it will inherit from Tryton basic components (ModelSQL for database persistency, ModelView for displaying in the client, etc...) which will enrich its behaviour and features.

Fields

Note : See tryton documentation for basic informations on the different fields

The basic component of a Model are its fields. The Tryton fields are classified as :

  • Basic fields : Integer, Char, Numeric, Date, Binary, etc.

  • Relational fields :

    • Many2One : A relation between the current model and another one. Typically, that would be the relation between a Car model and a Maker model. A car has one and only one maker
    • One2Many : A multiple relation which is usually used to modelize ownership / non alterable relations. For instance a Painter - Painting relation, where a painter may have 0, 1, 2, etc... painting. The relation of ownership means that the painting may only have one painter. One2Many fields are not directly stored in the database, they represent a concept that is materialized by a Many2One field on the owned model. So if a painter has a One2Many of paintings, this is only possible because a painting has a Many2One to its painter.

    Warning : A One2Many requires a Many2One field on the target model to be stored in the database since this is how it is effectively materialized. Technically, every Many2One field could have a related One2Many on the target Model, but it usually is irrelevant (as a real One2Many field). For instance, in the previous example, the Maker Model could have a One2Many to the Car Model, but it is not appropriate to do so, since the cars are not really part of the makers.

    • Many2Many : A non exclusive multiple relation between two models. This could be used to represent the relation between for instance makers and dealers. Every dealer works with many different makers, and a given maker will work with many dealers as well. Many2Many fields are stored with an extra matching table in the database.

Sample Code

class Car(Model):
    __name__ = 'car'

    # The owner field is a Many2One relation since one person may own multiple
    # cars, but a given car has one and only owner : Many to One
    owner = fields.Many2One('party.party', 'Owner')

    # The drivers field is a Many2Many relation because a car may have multiple
    # drivers, and a driver may drive multiple cars : Many to Many
    drivers = fields.Many2Many('party-car', 'car', 'party', 'Drivers')

    # Wheels are part of the car, a wheel may be part of only one car, and a
    # car will (usually) have multiple wheels : One to Many
    wheels = fields.One2Many('car.wheel', 'car', 'Wheels')


class Wheel(Model):
    __name__ = 'car.wheel'

    # The Many2One car field represents how the Car.wheels field will be
    # effectively stored in the database. In almost all cases, the reverse
    # Many2One field of a One2Many will be required, selected (i.e. the column
    # will be indexed in the database), and ondelete 'CASCADE', so that it will
    # automatically be deleted when its parent is deleted.
    car = fields.Many2One('car', 'Car', required=True, ondelete='CASCADE',
        select=True)


# This class is required to materialize the Many2Many field. Since both
# relations (car and party) may have multiple links to the other (one car can
# have multiple drivers and one driver may drive multiple cars), it is not
# possible to store the relation directly on the car / party table. The PartyCar
# model is a technical way to do so. When we want to store the fact that party1
# drives car1, we create an entry in the PartyCar model which will reference
# party1 and car1
class PartyCar(Model):
    __name__ = 'party-car'

    car = fields.Many2One('car', 'Car', required=True)
    party = fields.Many2One('party.party', 'Party', required=True)

__setup__

The __setup__ method on a model serves the purpose of modifying basic model data from inherited modules. If one want in an overriding module to change the string / domain / etc. of a field of a model, the one and only way to do so is to do it in the __setup__ of the overriding class. For instance, to modify the string of the foo field :

@classmethod
def __setup__(cls):
    super(MyModel, cls).__setup__()  # Should almost always be the first line
    cls.foo.string = 'Not foo'

It is also used to modify other model attributes :

@classmethod
def __setup__(cls):
    super(MyModel, cls).__setup__()  # Should almost always be the first line
    cls._error_messages.update({
            'my_error_key': 'My very detailed error message',
            })
    cls._sql_constraints.update({
            ...
            })
    cls._buttons.update({
            'my_button': {'readonly': Eval('answer') != 42},
            })
    cls._order.insert(0, ['my_order_field', 'ASC'])

It is the perfect place to check if a class is loaded or not when the server connects to the database :

@classmethod
def __setup__(cls):
    super(MyModel, cls).__setup__()  # Should almost always be the first line
    raise Exception('My model was loaded')

__post_setup__

The __post_setup__ method is called once the setup is done. All the model fields should be in their definitive states, and they must not be modified. This method is used internally by tryton to (for instance) detect all on_change(_with) depends and properly decorate the associated methods.

It can be used to build constants that can be useful later. Also, the Pool is accessible, so you can access other models typically to get the "final" values of a selection field.

@classmethod
def __post_setup__(cls):
    super(MyModel, cls).__post_setup__()  # Should almost always be the first line
    cls._my_int_fields = [x for x in cls._fields
        if isinstance(cls._fields[x], fields.Integer)]

__register__

Registering a model will usually modify the database. Calling the __register__ method on a model will basically sync its data with the database :

  • Create a ir.model instance for the model
  • Create a ir.model.field entry for each of the model's fields
  • If the model inherits from ModelSQL the current model data will be synced in the database. So a table will be created if needed, and all "basic" (i.e. non Function, non XXX2Many) fields will have a column created to store their data
  • All related translations will be created or updated accordingly

Warning : Tryton will never automatically delete a table corresponding to a model which is not declared anymore. It will not delete a removed field either.

Warning bis : The __register__ method is called "per module". This means that if you update "module 1", when there is an installed "module 2" which inherits from "module 1", the overrides of __register__ defined in "module 2" will not be used.

The __register__ method is the usual place in which will be written migration code. For instance, if we assume that the field foo was renamed to bar, we might want to override the __register__ method of the model to perform the migration :

@classmethod
def __register__(cls, module):
    table = backend.get('TableHandler')(cls, module)
    if table.column_exists('foo')
        table.column_rename('foo', 'bar')

    # We migrate before the super call because it will create the bar column,
    # and renaming is easier than copying the data
    super(MyModel, cls).__register__(module)

default methods

All field of all models may have a default method declared. The standard syntax for doing so is the following (for field foo) :

@classmethod
def default_foo(cls):
    return 'my default value'

# Alternate

@staticmethod
def default_foo():
    return 'my default value'

Default values are per field, there is no direct way to set all the values at once for a given model. Default methods are used in two cases :

  • When the user starts the creation of a new instance with the client. The default methods are then called for every field which is displayed to the client, while the record is still not saved.
  • When a record is created in any way (that may be through the client, directly from a RPC call to the server, or because some code in a module did it so). Basically, every time the create method is called on a model. In that case, the only fields for which the method will be called are those for which there aren't any value in the dictionary values.
Party.create([{'name': 'Doe'}])  # The default_name method will not be called
                                 # on the new party because there is a value
                                 # in the new party parameters

Party.create([{'name': None}])   # The default will not be called here either,
                                 # since None is an acceptable value

Party.create([{'ssn': '12345'}]) # The default_name method will be called since
                                 # 'name' does not appear in the data values

on_change_with methods

on_change_with methods are used, and should only be used, to customize the client behaviour in order to help the user input. They can be viewed as default values depending on other fields. The main difference with the default is that they are called every time a modification is made on one of the fields on which it depends.

Warning : on_change_with methods are not called when modifying fields server side !

# Make it so that if the user sets the 'name' field and the 'code' field is
# empty, the 'code' field is initialized with the lower-cased 'name' field

@fields.depends('code', 'name')       # Modifying those fields will trigger the
def on_change_with_code(self):        # call to the on_change_with method
    if self.code:
        return code
    return (self.name or '').lower()  # on_change_with return the final value

In order to reduce the data exchange between the server and the client, the client will only send the depending field values to the server when calling on_change_with methods. So the following will crash :

@fields.depends('code')
def on_change_with_code(self):
    return self.name                  # Server crash because the 'name' field
                                      # will not be initialized

It is usually considered bad practise to manually call an on_change_with method directly in server code, since its original purpose is to be called by the client, for user input only. If there is some logic / algorithm that is needed outside, it is usually better to write it in a separate function.

Typical use case for on_change_with methods would be to set a default value for the age field of a car when the building_date field is modified.

Warning : Obviously, naming a field with_... may cause problems since the server will not be able to decide whether the on_change_with_foo should trigger the on_change method of the with_foo field or the on_change_with method of the foo field.

on_change methods

on_change methods are used to trigger more complex modifications than simple on_change_with. They have similar purposes, that is user guidance when creating or modifying records, and may sometime be exchanged (i.e. a similar behaviour can be obtained through the use of one or the other). If the use cases for on_change_with are one field depending on many others, the typical use of on_change methods is one field whose modification triggers modifications on a lot of others.

Warning : on_change methods are not called when modifying fields server side !

# Update the 'maker', 'weight', 'number_of_seats' fields when modifying the
# 'model' attribute of a car

@fields.depends('maker', 'model', 'number_of_seats', 'weight') # Only those
def on_change_model(self):          # fields will be available and updated by
    if not self.model:              # the on_change
        self.maker = None
        self.number_of_seats = 4    # In an on_change, we just need to modify
        self.weight = 500           # the fields that we want to update
        return
    self.maker = self.model.maker
    self.number_of_seats = self.model.number_of_seats
    self.weight = self.model.weight

Warning : XXX2Many and Dict fields require some special handling

@fields.depends('new_driver', 'drivers')
def on_change_new_driver(self):
    if not self.new_driver:
        return
    if self.new_driver not in self.drivers:
        self.drivers.append(self.new_driver)

The previous code will not behave as expected, since the on_change mechanism only checks the fields which are effetcively modified on the main object. Here we do not modify 'self', but an element of the list. The working solution is :

@fields.depends('new_driver', 'drivers')
def on_change_new_driver(self):
    if not self.new_driver:
        return
    if self.new_driver not in self.drivers:
        self.drivers = list(self.new_drivers) + [self.new_driver]

on_change methods are particularly useful to initialize many fields from a particularly important other field. A special use case is to initialize the fields of the target of a One2Many field from its parent. This can be done by declaring an on_change method on the Many2One field toward the parent.

@fields.depends('father', 'name', 'nationality')
def on_change_father(self):
    self.name = self.father.name
    self.nationality = self.father.nationality

This allows for more context-dependant initialization.

Caching

Tryton has many caching mechanics that serve different purposes :

  • High level caching is used to store frequently accessed data accross RPC calls. The typical use case would be getting a configuration value from the database, which is required many times per transaction. We can save the database query time by using this sort of cache :
class MyModel:
    __name__ = 'my.model'

    _my_config_value_cache = Cache('my_config_value_name')

    @classmethod
    def get_my_config_value_name(cls):
        cached_value = cls._my_config_value_cache.get(
            'my_value_name', -1)
        if cached_value != -1:
            return cached_value
        # So some very hard work and get the value
        value = cls.search('...')
        cls._my_config_value_cache.set('my_value_name', value)
        return value

Warning : If the cached value depends on some models, the cache must be cleared manually by overriding the create / write / delete methods on those models :

@classmethod
def create(cls, vlist):
    values = super(MyClass, cls).create(vlist)
    Pool().get('my_cached_model')._my_config_value_cache.clear()
    return values

The high level cache can be either in memory or in a separate tool (for instance redis).

  • Medium level caching on model attributes inside a transaction. When a record is read from the database, the associated values are stored in a memory cache which is then used for later instanciation of the same record as long as there has not been any saved modification on the record.
party = Party(10)       # Fetch the party with id 10
party.name == 'Lucy'    # Read the 'name' field in the database

party_1 = Party(10)     # Create another instance mapped to the party with id 10
party_1.name            # The name field is in the cache, no database call

party.name = 'John'     # No save, the cache is still valid
party.save()            # Cache invalidation !

party_1.name == 'Lucy'  # The already created instance are not modified

party_2 = Party(10)     # Create another instance mapped to the party with id 10
party_2.name            # Will not use the cache and read from the database
  • Low level caching is used when accessing a field which was already read on the current record. This is particularly useful for Function fields which may be expensive to compute :
party = Party(10)
party.very_expensive_field  # Calls the getter for the field and cache it
party.very_expensive_field  # Very quick, the value is already computed

party_1 = Party(10)
party.very_expensive_field  # Very long again, this cache is not shared among
                            # differente instances

Transaction

The Transaction in tryton represents a transaction between the tryton server and the database. Everytime a RPC call is made, a new transaction is created, with different parameters. For instance, an on_change / on_change_with / default call (most of the client made calls) are executed in a readonly transaction. So it is not possible to mistakingly modify (and save) records while in one of those methods.

Everytime a new non-standard method is exposed through the tryton api, it should be registered. This is where the type of transaction can be customized :

@classmethod
def __setup__(cls):
    super(MyModel, cls).__setup__()
    cls.__rpc__.update({
            'my_readonly': RPC(readonly=True),  # Will be readonly
            'my_non_readonly': RPC(),           # Non readonly by default
            })

Accessing the current transaction can be done by using the Transaction() syntax. It is also possible to manually create a new transaction. It is necessary to be extra careful when doing so to avoid bad behaviour.

with Transaction().new_transaction() as transaction:
    try:
        # Do things
        transaction.cursor.commit()
    except:
        # Always rollback !
        transaction.cursor.rollback()
        raise

The transaction holds many contextual data, usually sent by the client when calling a RPC method. For instance, it is possible to access :

  • The current user : Useful to check for access rights, or check its language to adapt generated strings.
  • The current database connection : provides new cursors to manually execute queries in the database :
cursor = Transaction().connection.cursor()
cursor.execute('SELECT * FROM ...')

Context

The basic use case of the context is to pass data in another way than function parameters. There are some keys that are managed by the client (for instance the active_id and active_model keys), but it can also be modified manually in server code. The problem it solves is when it is needed to send some information in a deeply nested call.

def function_1(x, some_parameter):
    function_2(x + 1, some_parameter)

def function_2(x, some_parameter):
    function_3(x + 1, some_parameter)

def function_3(x, some_parameter):
    function_4(x + 1, some_parameter)

def function_4(x, some_parameter):
    print some_parameter
    return x

function_1(10, 'foo')

In the previous example, some_parameter has to be defined as a parameter for each of the function definitions to go from the function_1 call to the deeply nested function_4. The context allows to perform differently :

def function_1(x):
    function_2(x + 1)

def function_2(x):
    function_3(x + 1)

def function_3(x):
    function_4(x + 1)

def function_4(x):
    print Transaction().context.get('some_parameter', '')
    return x

with Transaction().set_context(some_parameter='foo'):
    function_1(10)

There is one big problem with using the context this way : Modifying the context resets the field cache on every record. The root cause for this is that some functions may depend on certain values of the context to get their value. For instance, a computed string field will depend on the language of the user :

def say_hello(self):
    if Transaction().context.get('language', '') == 'fr':
        print 'Bonjour'
    else:
        print 'Hello'

In the previous example, we could imagine that the say_hello method is used to calculate the value of a Function field, so obviously, we want it to be recalculated if we change the context.

Extra Dependencies

Apart from the depends and xml flags (in the tryton.cfg file, there is an extra, optional section, named extras_depend (not a typo!), which list modules on which the current module, according to the documentation, may depend on.

Here is an example.

Let's assume we are working on a module which does not necessarily requires that the party module be installed, but who wants to add a new field on the party.party model if it is.

The python code for this is rather simple:

class Party(metaclass=PoolMeta):
    __name__ = 'party.party'

    my_new_field = fields.Boolean('Hello')

Then in __init__.py:

from . import my_module
from . import party

def register():
    Pool.register(
        my_module.Model1,
        my_module.Model2,
        my_module.Model3,
        my_module.Model4,
        party.Party,
        module='my_module', type_='model')

The problem with this is that if the party module is not installed, there will be a crash, because the party.party model that we defined does not already exists in the Pool, and we used the metaclassattribute, so Tryton expects it to be there. This is logical, because the party.party model is actually defined in the party module, that we assumed was not installed.

So what we need is a way to tell Tryton that we want it to register some classes only if another module is installed (though we do not want a hard dependency):

[tryton]
depends:
    ir
extras_depend:
    party
from . import party

def register():
    Pool.register(
        my_module.Model1,
        my_module.Model2,
        my_module.Model3,
        my_module.Model4,
        module='my_module', type_='model')

    Pool.register(
        party.Party,
        module='my_module', type_='model', depends=['party'])

Note the depends parameter that we added in the call to register for the Party class.

Doing this will make the following:

  • Ensure that my_module will be loaded after the party module (if it is activated)
  • If the party module is activated, and only in this case, register the Party class

With this, all will behave as expected. Our modification to the party.party model will only be applied if the party module is actually activated.

There is the equivalent for loading xml data:

<?xml version="1.0"?>
<tryton>
    <data>
        <record model="ir.ui.view" id="my_model_view_form">
            <field name="model">my_model</field>
            <field name="type">form</form>
            <field name="name">my_model_form</field>
        </record>
    </data>
    <data depends="party">
        <record model="ir.ui.view" id="party_view_form">
            <field name="model">party.party</field>
            <field name="inherit" ref="party.party_view_form"/>
            <field name="name">party_form</field>
        </record>
    </data>
</tryton>

The second data tag contents will only be loaded if the party module is activated.