Relationships in OOFILE, like other ODBMS, are managed by "traversal paths" between tables.
OOFILE traversal paths are declared with two macros, the difference being a Ref being a 1-ary and a Set being an n-ary relationship.
REF_TABLE and SET_TABLE
eg:
REF_TABLE(dbPeople)
SET_TABLE(dbVisits)
CLASS_TABLE(dbPeople) {
...
dbVisitsSet Visits;
...
};
CLASS_TABLE(dbVisits) {
...
dbPeopleRef Person;
...
};
2) DEFINING THE CONCRETE RELATIONSHIP
A link is defined between two concrete dbTable classes representing real tables (notthe table class definitions). The link may be named and consists of equal left and right sides defining:
- the table being related
- the traversal path in that table to its related table
- optionally the name of the link
- a join field, if the relationship is a runtime join
A simpler form of the above that can be used in a declaration - the only parameters are to the constructor and there's no further setup required. Thus, you could have a series of dbTable declarations and then dbRelationship declarations (see ooftest2.inc).
A related field expression starts with a traversal path and involves the normal pointer operator->. This ends in a field and may go through intermediate traversal paths eg:
People.Visits->VisitDate() // 1 link away
People.Visits->Doctor->Name() // 2 links away
Each operator-> along the path creates a link in a temporary chain.
The operator() evaluated at the end of the expression picks up the temporary chain and returns a reference to a field with the chain attached.
The same mechanism is used when operations are called on the related tables, eg:
People.Visits.count();
** all fields in the related table are pointed to by the same relationship chain object, including the related table itself.
NOTE: Whether it is People.Visits->... or People->Visits->... is totally up to you as to whether People is a pointer or not. The -> operator is only overloaded for the related files.
4) LOADING RELATED VALUES
Loading the related context is delayed until needed. This could be prompted by a related table command (count, newRecord, start...) or accessing a related field.
Relationship chains are marked as valid by the starting point - if you change its context then the current chains are invalidated.
When a related value must be loaded, the chain may already be valid - implying a valid table at the end of the chain. This means the field just loads its value as normal.
If the chain is invalid, the links in the chain are followed from the left, loading the related context for each linked table. eg
People.Visits->Doctor->Name() // 2 links away
loads a related context in Visits and again in Doctor.
This loading related context is independent of *how* the relationship is stored - it may be a runtime join over key fields, be stored pointers, physically aggregated records or some other mechanism.
NOTE ON NAMES
There is no restriction on naming, the use of Visits as a table object is purely a convention for the table dbVisits. Similary the traversal path People.Visits can be named anything you like.
DETAILS (with a c-tree bias)
(You may want to skip to the CONCRETE EXAMPLEs at the end)
Note: in the following, the "prototypical" table and fields referred to are the originals, being the objects you would use in directly acessing that table in the database.
1) DECLARING THE TRAVERSAL PATHS
The REF_TABLE and SET_TABLE macros create very simple class definitions that are subclasses of dbRelRefBase. These classes include an operator-> which returns a pointer to a table. Thus a dbVisitsSet returns a dbVisits*. In the following description, dbRelRef can be assumed to stand in for one of these macro-generated subclasses.
2) DEFINING
By the time all the values have been supplied to the dbRelationship, it contains everything necessary to establish the relationship but has not actually done so. The dbRelationship constructor registers itself with the current connection, for later processing.
For join relationships, the default requires you to set the join field in the destination (rhs of relationship). You can set globally that join fields are updated by
dbRelationship::sAllJoinsUpdateJoinField = true;
or, for a given relationship theRel:
theRel.joinsUpdateFields(true);
dbConnect_ctree::SetupConnection sends dbRelationship::buildRelationship to all the relationships. This in turn invokes dbRelHalf::buildRelationship for the left & right sides. The dbRelHalf is the true manager of the relationship, and it sets the dbRelRef (eg: People.Visits which is a dbVisitsSet.)
3a) RELATED FIELD EXPRESSIONS
dbRelRef::LogTransition is called for each -> in the expression.
LogTransition simply appends a dbRelHalf* to a static list.
Each -> also uses dbRelRef::RefersToTable to return a pointer to the prototypical table at the end of the link.
The second part of a related field expression is the parens on the final field, causing its operator() to be invoked. This is essential and, to cope with people forgetting these parens, the OOF_Debug setting will trap this side effect.
Each field type has an operator() which returns a field reference, either to itself or a special cloned field that's already been setup in the related table.
This includes a call to dbRelChain::buildChain as described in 3c).
3b) RELATED TABLE EXPRESSIONS
For a related table, the operator-> will only be called if there is more than one link in the relationship eg:
People.Visits->Doctor.newRecord();
dbRelRef defines newRecord, count and all the other basic operations performed on a table. These versions all call dbRelRefBase::BuildRelChainToTable and then pass on the operation to the relationship chain.
BuildRelChainToTable performs all the steps described in 3a, calling LogTransition and ConsumePossibleRelationshipChain.
It concludes by calling dbRelChain::buildChain as described in step 3c), the same as for fields.
3c) BUILDING THE CONCRETE RELATIONSHIP CHAINS
dbRelChain::buildChain calls dbTable::askTableToBuildValidChain from the first table in the chain. This factory will either:
- return an already built chain with the same list of nodes, or
- ask the candidate chain to convert itself into a real chain, by building a parallel list of cloned tables.
This sounds more complex than it really is. We start with a list of tables through which the relationship chain passes (in most cases, just the single end point, for a 1-link chain). However, the tables in this list can't be used to manage the actual relating process as they are the "prototypical" tables - they already have a current selection and should NOT be affected as a side-effect of relationships.
Thus, we copy each table along the chain to give us a "clone" table that *can* have its context modified.
The cloning process will end up copying:
- the complete table as defined by the user
- the field dictionary
- the backend
- alloc new buffer space in the backend
The only thing not copied or re-created is the current selection in the table - we will create our own selections as we validate relationships.
4) LOADING RELATED VALUES
Now that the relationship chain is built, we have a list of cloned tables (and backends) sitting waiting to be given a search command to load the related selections. This is (again) a delayed evaluation until the last possible minute. Thus, we may create the relationship chains and (eg: if the user cancelled a dialog) never actually load the related data.
dbField::validateContextInCaseRelated is called by every operation that either assigns to or reads from a dbField descendant. This is one area where you see the need for each field in the related context to use the same relationship chain - we validate the chain once and, provided it is not marked invalid, the subsequent calls (by other related fields) do nothing.
Validating a context simply walks along the chain, calling dbRelHalf::refreshContext at each node, with a parameter of the cloned table at that node. The dbRelHalf provides the knowledge on how to load the related context and the cloned dbTable provides a context into which to load.
The actual relating mechanism varies. In the simple dynamic joins, it is a search carried out on the target field, based on the value in the join field.
For OID-based relationships a runtime search may be replaced by saved pointer links, depending on the specific class, the backend and the number of related records.
As mentioned above, chains have a valid flag which saves us traversing the relationship for each field. The valid flag is reset by the starting table - each time the current record changes. This is by newRecord, a search or an iteration function.
5) CONCRETE EXAMPLE - FIELD
Let's illustrate the sequence with:
People.Visits->Why() = "Flu";
People.Visits->
invokes a dbRelRefBase::operator->, adding a link to the
(empty) temporary chain and returning a Visits*
Visits->Why()
invokes dbChar::operator() returning a reference to a clone of the
prototypical field, via dbField::GetRelatedFieldOrUs.
In copying the field, the dbField copy constructor calls
OOF_mixRelChainEndPoint::ConsumePossibleRelationshipChain and picks up the
temporary relationship chain
= "Flu";
invokes dbChar::operator=(char*) on the cloned field.
6) CONCRETE EXAMPLE - TABLE
In this example, we are going to create a new related record, assume assignment of related values has occurred as shown in step 5), and show the saving of the related data.