-
Notifications
You must be signed in to change notification settings - Fork 30
4.4 theory
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 thePool
. - 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
. ThePool
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
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 thePool
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
- Call the
- 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
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.
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 aMaker
model. Acar
has one and only onemaker
-
One2Many : A multiple relation which is usually used to modelize
ownership / non alterable relations. For instance a
Painter
-Painting
relation, where apainter
may have 0, 1, 2, etc...painting
. The relation of ownership means that thepainting
may only have onepainter
.One2Many
fields are not directly stored in the database, they represent a concept that is materialized by aMany2One
field on the owned model. So if apainter
has aOne2Many
ofpaintings
, this is only possible because apainting
has aMany2One
to itspainter
.
Warning : A
One2Many
requires aMany2One
field on the target model to be stored in the database since this is how it is effectively materialized. Technically, everyMany2One
field could have a relatedOne2Many
on the target Model, but it usually is irrelevant (as a realOne2Many
field). For instance, in the previous example, theMaker
Model could have aOne2Many
to theCar
Model, but it is not appropriate to do so, since thecars
are not really part of themakers
.-
Many2Many : A non exclusive multiple relation between two models. This
could be used to represent the relation between for instance
makers
anddealers
. Everydealer
works with many differentmakers
, and a givenmaker
will work with manydealers
as well.Many2Many
fields are stored with an extra matching table in the database.
-
Many2One : A relation between the current model and another one.
Typically, that would be the relation between a
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)
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')
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)]
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)
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 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 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.
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
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 ...')
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.