wiki:S3/S3Model/ComponentResources

Component Resources

Introduction

Component Resources (more commonly referred to as components) are an S3 framework concept to simplify the access to sub-entities related to a master entity.

Typically, a master entity has a number of associated sub-entities ("have"-relationships), e.g.:

  • organisations = have => offices, projects, teams
  • persons = have => addresses, identities
  • groups = have => members, tasks

These sub-entities are called components of the master entity.

A component relationship constitutes a join between the master entity and the sub-entity. The S3 framework provides functionality to pre-define such joins and then use them as pseudo-attributes of the master entity.

In URLs, component relationships can be utilized to construct implicit queries (also called "projections"):

    # Offices of organisation 5
    # "office" is a component of "org/organisation"
    # Join: org_organisation <-- id/organisation_id --> org_office

    /org/organisation/5/office

...likewise in URL filter queries:

    # Persons with the email address user@example.com
    # "contact" is a component of "pr/person"
    # Join: pr_person <-- pe_id/pe_id --> pr_contact

    /pr/person?contact.contact_method=EMAIL&contact.value=user@example.com

Besides simplified construction of queries and views, it also facilitates implicit (=keyless) constraints e.g. in CRUD forms and XML imports:

<s3xml>
    <!-- an organisation record -->
    <resource name="org_organisation">
        <data>...</data>

        <!-- an office "of this organisation" (=keyless constraint) -->
        <resource name="org_office">
            <data>...</data>
        </resource>

    </resource>
</s3xml>

Component Types

The following diagram describes the joins which are currently supported:

Note that, for efficiency reasons, "components" can not be nested in queries.

Component Definition

Method

Components are defined using the s3db.add_components method.

s3db.add_components(<master_name>, 
                    <component_name> = <join>, 
                    <component_name> = <join>, 
                    ...
                    )

Important: Component definitions in dynamically loaded models must be in the class which defines the master table! (because otherwise the component definition would not be found when the model is loaded).

Simple Components

For simple components, it is sufficient to specify the foreign key as join:

s3db.add_components("org_organisation",
                    # Component URL: /org/organisation/<id>/office
                    org_office = "organisation_id",
                    )

This assumes that the component alias is the same as the name of the component table without prefix, e.g. tablename = "org_office" => component alias = "office".

If you want set the alias explicitly, you can instead describe the join in a dict:

s3db.add_components("org_organisation", 
                    # Component URL: /org/organisation/<id>/headquarter
                    org_office = {"name": "headquarter",       # the component alias
                                  "joinby": "organisation_id", # the foreign key
                                  },
                    )

Filtered Components

Component joins can be filtered to a subset of the records in the component table:

s3db.add_components("org_organisation",
                    # Component URL: /org/organisation/<id>/headquarter
                    org_office = {"name": "headquarter",        # the component alias
                                  "joinby": "organisation_id",  # the foreign key
                                  "filterby": "office_type_id", # the name of the field in the component table to filter by
                                  "filterfor": [4],             # the value(s) to filter for
                                  },
                    )

Currently only inclusive filters are supported.

It is possible to link the same component table to the same master table more than once using different aliases with joins and/or filters:

s3db.add_components("org_organisation", 
                    org_office = (# Component URL: /org/organisation/<id>/headquarter
                                  {"name": "headquarter",
                                   "joinby": "organisation_id",
                                   "filterby": "office_type_id",
                                   "filterfor": [4],
                                   },
                                  # Component URL: /org/organisation/<id>/fieldoffice
                                  {"name": "fieldoffice",
                                   "joinby": "organisation_id",
                                   "filterby": "office_type_id",
                                   "filterfor": [5],
                                   },
                                  ),
                    )

Single / Multiple Component Records

Sometimes it may be necessary to move fields out of the master table into a component in order to have better control over access permissions to these fields, or to hold the same fields for multiple master tables in a single table. In these cases the a record in the master table can only have exactly one corresponding record in the component.

This requirement is often called "singe-record component" or "subtable", and can be defined in the join dict:

s3db.add_components("org_organisation", 
                    org_office = {"name": "headquarter",        # the component alias
                                  "joinby": "organisation_id"   # the foreign key
                                  "filterby": "office_type_id", # the name of the field in the component table to filter by
                                  "filterfor": 4,               # the value(s) to filter for
                                  "multiple": False             # there can be only one component record per master record
                                  })

Most Eden modules respect the multiple-setting and enforce a single component record per master record.

IMPORTANT: Note that it is possible, but not recommendable, to change the multiple-setting dynamically in the controller environment: setting multiple=False does not allow to choose a particular component record, but simply selects the first (by ID) that matches the query. Thus, where the query can change (e.g. due to different authorization levels), then it is difficult to predict which record that would be, and different users may see different records despite multiple=False (which can be deliberate at times, though).

Components can be linked to their master records via link-tables.

In such cases, the foreign key constraints for the component link are stored in a separate link-table:

  master (primary key) <==== (foreign key) link table (foreign key) ====> (primary key) component
                  pkey <==== joinby                             key ====> fkey

Link-table component links have some advantages over simple foreign key constraints:

  • link tables can carry attributes of their own (attributed link)
  • they provide the option to link the same component record to multiple master records (many-to-many)
  • there are several different ways to actuate such links
  • link-table links work both ways (i.e. with master/component exchanged, can be declared both ways at the same time)

However, they do have disadvantages too:

  • overhead to maintain a separate database table (processing time, migration issues etc.)
  • increased complexity to access and query resources (3 tables instead of 2)
  • increased complexity to handle such links in CRUD and XML/XSLT

Declaration

The basic syntax of a link-table component link declaration is:

s3db.add_components("my_master",                    # Tablename of the master table
                    my_component = {                # Tablename of the component
                        name = "alias",             # Use this 'alias' as the component name
                        link = "my_linktable",      # Tablename of the link table
                        joinby = "fieldname",       # FK of the master table (=left key constraint)
                        key = "fieldname",          # FK of the component table (=right key constraint)
                        actuate = "replace",        # Actuation option (see above, optional, defaults to "link")
                        autodelete = False          # Delete the component record together with the last link (optional, default is False)
                        widget = Widget,            # Widget to use for embedding (optional, defaults to S3EmbedComponentWidget)
                        autocomplete = "fieldname", # Field in the component to use for autocomplete when embedding the component
                        })                          

If no field is defined for autocomplete, then no autocomplete-widget will be used, but a standard SELECT of options for key (default behavior).

Important: if you specify a widget for embedding (e.g. S3AddPersonWidget), then you must ensure that the foreign key in the link-table doesn't also use either this widget or any other widget validator!

Link Actuation Options

S3CRUD can handle the link-table in a number of different ways, depending on the method and the configured option:

  • replace: hides the link table and always operates on the component table
  • hide: hides the component table and always operates on the link table
  • link: operates on the component table for single-record requests, and on the link table for summary requests (=without record ID) and delete
  • embed: operates on the link table, embeds the component record in single-record requests

The following table gives an overview of link actuation in S3CRUD:

CRUD Method Link Actuation Option
replacehidelinkembed
create create-form for component create-form for link create-form for link create-form for link with component embedded
read read-view of component read-view of link read-view of component read-view of link (with component embedded2)
update update-form for component update-form for link update-form for component update-form for link with component embedded
delete deletes both, component and link deletes the link deletes the link1 deletes the link1
list list view of component list view of link list view of link list view of link (with component embedded2)
  • 1 = deletes the component together with the last link if autodelete option is set
  • 2 = not implemented yet

DeveloperGuidelines

Last modified 9 years ago Last modified on 02/22/16 18:18:24

Attachments (1)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.