= S3Model = [[TOC]] See also: - [wiki:S3/S3Model/ComponentResources Component Resources] - [wiki:S3/S3Model/SuperEntities Super-Entities] == Purpose == The {{{S3Model}}} class defines the framework for data models in Eden. It extends the ''web2py'' concept by: - implicit and lazy model loading - an extensible, per-table configuration pattern - a meta-model for projected entities (the so-called "resource component model") - a meta-model for multi-table keys (the so-called "super-entity model") Data models can be implemented as subclasses of S3Model, and then loaded on demand - with automatic resolution of cross-model dependencies. This saves processing time as it will always only load those models which are actually needed to process the request. == Concepts == === Dynamic Model Loading === While web2py executes ''all'' models during every request cycle, {{{S3Model}}} only executes the models needed to process the request. That means though that all names (e.g. tables, variables, functions) which are defined in S3Models need to be requested explicitly before they can be used. To facilitate that, a global {{{S3Model}}} instance [#current.s3db s3db] serves as model loader. In web2py, database tables can be accessed from the global '''db''' object: {{{#!python table = db.my_table }}} In S3, tables must be requested from '''s3db''': {{{#!python table = s3db.my_table }}} Model loading happens ''only'' when a name from this model is requested, i.e. s3db loads no models whatsoever unless names are requested. If the model defining the table (or function, variable etc) has not yet been executed during the request, s3db will do so ''at this point''. S3Models can request other S3Models to be loaded, thereby automatically resolving dependencies. Note that for models where the Eden module is deactivated in the current template, s3db runs the defaults()-method instead of the model()-method (except for mandatory modules). If dependencies between S3Models are circular (i.e. X requesting Y and Y requesting X), S3Model will raise an exception. It is wise to split Eden modules into multiple S3Models - so that dynamic model loading always only loads the necessary minimum for a requested name. However, too excessive splitting can increase loader overheads and the risk of bugs, so tables which are used together most of the time should be kept in the same S3Model. == Defining Models == === Modules === S3 data models reside in {{{modules/s3db}}}. The file name of each ''Python module'' in {{{modules/s3db}}} corresponds to the ''Eden module'' prefix. All names with this prefix will be looked up from this file. '''Example:''' - ''Tablename'': org_office => ''Module Prefix'': "org" => ''Looked up from'': modules/s3db/org.py All S3 data models need to be imported in models/00_tables.py: {{{#!python import s3db.assess import s3db.asset import s3db.auth import s3db.cap ... }}} Every Python module in modules/s3db must have an {{{__all__}}} statement declaring the classes and functions to be imported: {{{#!python __all__ = ["S3DeploymentModel", "S3DeploymentAlertModel", "deploy_rheader", "deploy_apply", "deploy_alert_select_recipients", "deploy_response_select_mission", ] }}} '''Important:''' Undeclared classes or functions are not available to the model loader! All names in {{{__all__}}} starting with the module prefix (e.g. {{{deploy_}}}) can be accessed globally with {{{current.s3db.}}} (e.g. {{{current.s3db.deploy_apply}}}), without need to import them explicitly. The {{{modules/s3db/skeleton.py}}} module is a well-commented skeleton module to explain how things should look like inside an S3 model. '̈́''Note:''' it is possible (and sometimes even recommendable) to split an s3db module into sub-modules, and then import the names from the sub-modules into the main module. === Model Classes === Every data model is defined as a subclass of S3Model: {{{#!python class S3DeploymentModel(S3Model): }}} ==== names ==== All names from the model which shall be globally accessible (i.e. tables, functions, variables, classes) must be declared in the {{{names}}} class variable: {{{#!python class S3DeploymentModel(S3Model): names = ["deploy_event_type", "deploy_mission", "deploy_mission_id", "deploy_mission_document", "deploy_application", "deploy_assignment", "deploy_assignment_appraisal", "deploy_assignment_experience", ] }}} These names can then be accessed via {{{current.s3db.}}} (e.g. {{{current.s3db.deploy_mission_id}}}). '''Important:''' All table names and names which are returned from a model class '''must''' use the module prefix (otherwise they can't be found). ==== model() function ==== Every {{{S3Model}}} subclass '''must''' implement the model() function. This function defines all tables, functions and variables of the model: {{{#!python class S3DeploymentModel(S3Model): ... def model(self): }}} To define a table, the model() function must use {{{self.define_table}}} (instead of {{{current.db.define_table}}}): {{{#!python def model(self): tablename = "deploy_mission" table = self.define_table(tablename, ...) }}} The model function '''must''' return a dict with the definitions of all names as declared in the {{{names}}} class-variable ('''except''' table names): {{{#!python class MyModel(S3Model): names = ["my_function", "my_variable"] def model(self): my_variable = "example" return dict(my_own_function = self.my_function my_variable = my_variable ) @staticmethod def my function(): ... return }}} Ideally, custom functions in model classes which are returned from model() should be declared @staticmethod or @classmethod to allow the instance to be garbage-collected (i.e. release the thread-global pointer to the instance from current.s3db). ==== defaults() function ==== Every model class ''should'' define a {{{defaults()}}} function which returns safe defaults for the declared names in case the Eden module has been disabled per deployment-settings. This is particularly important for re-usable fields holding foreign keys to tables defined in this model: {{{#!python class S3DeploymentModel(S3Model): names = [... "deploy_mission_id", ] def model(self): ... mission_id = S3ReusableField("mission_id", table, ... ) return dict(deploy_mission_id = mission_id) def defaults(self): # Module disabled, define a safe default for "mission_id": mission_id = S3ReusableField("mission_id", "integer", readable=False, writable=False) return dict(deploy_mission_id = mission_id) }}} ==== Defining additional functions ==== The {{{S3Model}}} base class implements a number of useful helper functions to implement models, among others: - {{{super_entity}}} and {{{super_link}}} to define or reference super entities - {{{add_component}}} to define resource components - {{{configure}}} to define table configuration settings These functions should not be overwritten in the subclass. To avoid name collisions, names of additional functions (i.e. besides {{{model()}}} and {{{defaults()}}}) should always start with the module prefix: {{{#!python class MyModel(S3Model): def model(self): ... def defaults(self): # Use the module prefix in function names to avoid accidental overrides of superclass functions: def my_additional_function(self, param1, param2): ... }}} == current.s3db == {{{current.s3db}}} is a global, empty instance of the S3Model class that loads tables, functions and variables from other models on demand. === Loading Tables, Functions and Variables from Models === {{{current.s3db}}} allows easy access to names using the ''attribute''-notation: Loading a table: {{{#!python table = current.s3db.my_table }}} Loading a function: {{{#!python my_function = current.s3db.my_function }}} If you have the name in a variable, you can use the ''item''-notation instead: {{{#!python tablename = "my_table" table = current.s3db[tablename] }}} '''Note:''' The attribute- or item-notations will raise an {{{AttributeError}}} if the name can not be found (e.g. when the respective module is disabled). To avoid exceptions due to disabled modules, one can use the {{{table()}}} function to access tables: {{{#!python # Returns None if "my_table" is not found table = current.s3db.table("my_table") }}} '''Note:''' {{{s3db.table()}}} will also return functions or variables with the specified name. To limit the lookup to tables, use {{{db_only}}}: {{{#!python # Returns only tables, but not functions or variables table = current.s3db.table("my_table", db_only=True) }}} To only lookup functions and variables, but not tables, you can use the {{{get()}}} function: {{{#!python # Returns only functions or variables, but not tables my_function = current.s3db.get("my_function") }}} === Table Configuration Settings === {{{S3Model}}} provides a key-value store for each table which is used to store per-table configuration settings. The following functions can be used to manage table configuration settings: ||= '''Function''' =||= '''Use-Case''' =|| ||{{{configure(, =, =, ...)}}}||to add or change configuration settings for a table|| ||{{{get_config(, , )}}}||to retrieve a configuration setting|| ||{{{clear_config(, )}}}||to remove a configuration setting for a table|| '''Note:''' In {{{get_config()}}}, the parameter is optional (default: {{{None}}}), it will be returned if is not configured for this table. '''Note:''' These functions are available both in S3Model instances (self.* or current.s3db.*) and as class methods (S3Model.*). '''Important:''' {{{get_config()}}} does ''not'' load the respective model class. Settings configured inside the model class are therefore only available outside of it (e.g. with {{{s3db.get_config()}}}) after the model has been loaded (e.g. with {{{s3db.}}})! Example: {{{#!python # Configure list_fields for the org_organisation table s3db.configure("org_organisation", list_fields=["id", "name", "acronym", "organisation_type_id", "country", "website"]) }}} === Constructing Resources === {{{current.s3db}}} also provides a method to define a resource (see [wiki:S3/S3Resource]): {{{#!python # Define a resource resource = current.s3db.resource("my_table") # Limit to certain record IDs resource = current.s3db.resource("my_table", id=[1, 3, 7]) # Use a filter from s3 import S3FieldSelector resource = current.s3db.resource("my_table", filter=S3FieldSelector("id").belongs([1, 3, 7])) # Apply a URL filter resource = current.s3db.resource("my_table", vars={"~.id__belongs": "1, 3, 7"}) }}} ---- DeveloperGuidelines