 |
About Relationships |
A relationship is a special type of property that defines an association
between two objects of a specific type. For example, a Company
class may define a relationship with an Employee class:
In this case, there may be zero or more Employee objects
associated with each Company object.
Caché relationships have the following characteristics:
- Relationships are binary, that is a relationship must be
defined between two, and only two, classes (which may be the same class; a class
may have a relationship with itself).
- Relationships can only be defined between two persistent classes.
- Relationships must be bi-directional, that is both sides of a
relationship must be defined.
- Currently, only two types of relationship are supported:
one-to-many (independent) and parent-to-children (dependent).
One-to-one and many-to-many relationships are not supported at this time.
- Relationships automatically provide referential integrity; unlike a
reference (or object-valued) property, the value of relationship is
constrained to be correct (i.e., there are no dangling
references).
- Relationships automatically manage their in-memory and on-disk behavior.
- Relationships provide superior scaling and concurrency over object collections.
- Relationships are visible to SQL as foreign keys.
Defining a Relationship
A relationship is defined a special type of property. There are three
keywords or modifiers required for a relationship definition:
TYPE |
The type (class name) of the related class. This must be a persistent
class.
|
INVERSE |
The name of the corresponding relationship property in the related class (the
"other side" of the relationship).
|
CARDINALITY |
The cardinality of this side of the relationship. This is either ONE,
MANY, PARENT, or CHILDREN.
|
Other property keywords such as DESCRIPTION, FINAL, REQUIRED,
SQLFIELDNAME and PRIVATE are optional. Some property keywords, such
as MULTIDIMENSIONAL, do not apply.
The value of CARDINALITY defines how the relationship "appears"
from this side as well as whether it is an independent or dependent relationship.
For example, suppose we have a Book object that is related to MANY
Chapter objects:
Book | Chapters : Chapter (MANY) |
|
|
Chapter | TheBook : Book (ONE) |
|
The relationship Book.Chapters has cardinality of MANY. Each Chapter
is related to ONE Book so the Chapter.Book relationship has cardinality of ONE.
In the case of a dependent relationship, such as an Invoice object
with dependent LineItem objects,
Invoice | Items : LineItem (CHILDREN) |
|
|
LineItem | TheInvoice : Invoice (PARENT) |
|
the relationship Invoice.Items has cardinality of CHILDREN because
Items is a collection of children from the point of view of Invoice.
LineItem.TheInvoice has cardinality of PARENT because TheInvoice
refers to the parent of this LineItem.
The cardinality value of a relationship must correspond to the cardinality value of the
inverse relationship as defined in the following table:
Relationship Cardinality |
Inverse Relationship Cardinality |
ONE | MANY |
MANY | ONE |
PARENT | CHILDREN |
CHILDREN | PARENT |
In the Object
Architect you can define a relationship by:
- creating a new property,
- setting its type to the persistent class you wish to associate with,
- clicking the relationship checkbox,
- entering the name of the inverse property in the associated class,
- and, selecting the type of relationship.
When you define a relationship, the Object Architect will automatically
create the inverse relationship in the associated class.
You can also define a relationship using CDL. The syntax is:
relationship <relationship> { type = <related_class>
[ <type_parameters> ] ; INVERSE = <inverse_relationship> ;
CARDINALITY = ONE | PARENT | MANY | CHILDREN ;
<other property keywords> }
TYPE, INVERSE and CARDINALITY are required.
<related_class> must be a persistent class.
<inverse_relationship> must be the name of a relationship in
<related_class> whose <related_class> is the class containing <relationship>
and <inverse_relationship> is <relationship>.
For example, here is the CDL for two related classes, Company and
Employee:
class Company {
super = %Persistent;
persistent;
attribute Name { type = %String; }
// a Company has MANY Employees
relationship Employees { type = Employee; inverse = TheCompany; cardinality = MANY;}
}
class Employee {
super = %Persistent;
persistent;
attribute Name { type = %String; }
attribute Title { type = %String; }
// an Employee has ONE Company
relationship TheCompany { type = Company; inverse = Employees; cardinality = ONE;}
}
Dependent Relationships
A dependent relationship, defined by having cardinality of PARENT on
one side and CHILDREN on the other, has the following additional
characteristics:
- The existence of the child objects is dependent on the parent; if
a parent object is deleted then all of its children are automatically
deleted.
- Once associated with a particular parent object, a child object can
never be associated with a different parent. This is because the persistent ID
value of the child object is based in part on the parent's persistent
ID.
- As much as possible, instances of child objects are clustered with
parent objects on disk making disk access as optimal as possible (few
page accesses are required to retreive the children for a parent).
- A dependent relationship is projected to SQL as a parent-child
table.
For example, here is the CDL for two related dependent classes, Invoice and
LineItem:
class Invoice {
super = %Persistent;
persistent;
attribute CustomerName { type = %String; }
// an Invoice has CHILDREN that are LineItems
relationship Items { type = LineItem; inverse = TheInvoice; cardinality = CHILDREN;}
}
class LineItem {
super = %Persistent;
persistent;
attribute Product { type = %String; }
attribute Quantity { type = %Integer; }
// a LineItem has a PARENT that is an Invoice
relationship TheInvoice { type = Invoice; inverse = Items; cardinality = PARENT;}
}
In-memory Behavior of Relationships
Programmatically, relationships behave as properties. Single-valued relationships
(cardinality of ONE or PARENT) behave like atomic (non-collection)
reference attributes. Multi-valued relationships (cardinality of MANY or CHILDREN)
are instances of the %RelationshipObject class which has a collection-like interface.
For example, you could use the Company and Employee
objects defined above in the following way:
// create a new instance of Company
Set company = ##class(Company).%New()
Set company.Name = "Chiaroscuro LLC"
// create a new instance of Employee
Set emp = ##class(Employee).%New()
Set emp.Name = "Weiss,Melanie"
Set emp.Title = "CEO"
// Now associate Employee with Company
Set emp.TheCompany = company
// Save the Company (this will save emp as well)
Do company.%Save()
// Close the objects we have New'd
Do company.%Close()
Do employee.%Close()
Relationships are fully bi-directional in memory; any operation on one side
is immediately visible on the other side. We could have
performed the association from the other side:
Do company.Employees.Insert(emp)
Write emp.TheCompany.Name // this will print out "Chiaroscuro LLC"
We can load relationships from disk and use them as we would any other
property. When you refer to a related object from the ONE side, the
related object is automatically swizzled into memory in the same way as
a reference (object-valued) property.
When you refer to a related object from the MANY side, the related
objects are not swizzled immediately. Instead a transient
%RelationshipObject collection object is created. As
soon as any methods are called on this collection, it builds a list
containing the ID values of the objects within the relationship. It is
only when you refer to one of the objects within this collection that
the actual related object is swizzled into memory.
Here is an example that displays all Employee objects related to
a specific Company:
// open an instance of Company
Set company = ##class(Company).%OpenId(id)
// iterate over the employees; print their names
Set key = ""
Do {
Set employee = company.Employees.GetNext(.key)
If (employee '= "") {
Write employee.Name,!
}
} while (key '= "")
Do company.%Close()
In this example, closing company will remove the
Company object and all of its related Employee objects
from memory. Note, however, that every Employee object
contained in the relationship will be swizzled into memory by the time
the loop completes. If you are concerned about thisperhaps there are
thousands of Employee objectsthen we can modify the loop to
"unswizzle" the Employee objects as we go by calling
the %UnSwizzleAt method:
Do {
Set employee = company.Employees.GetNext(.key)
If (employee '= "") {
Write employee.Name,!
Do company.Employees.%UnSwizzleAt(key) // remove employee from memory
}
} while (key '= "")
Persistent Behavior of Relationships
Only single-valued relationships have a persistent state. The state of multi-valued
relationships is determined in memory by methods (Execute, Fetch, Close) of the
single-valued relationship. The persistent state of a single-valued
relationship is the same as a simple reference. No additional structures
(indices) are created automatically. However, to improve the performance of
Execute, Fetch, and Close an index is recommended if the size of the extent is
large (When defining a relationship, the Object Architect will ask you if you want such an index).
Relationships, when made persistent, also maintain referential integrity by enforcing
constraints and invoking referential actions. When a relationship is saved, the
target of its reference is checked for existence. If it does not exist an error
is returned and the save operation fails. When an object is deleted and there
are objects related to it, the related objects are deleted (parent/child
cardinality) or the delete operation fails (one/many cardinality).
As with IDENTIFIEDBY classes, a class containing a relationship with PARENT
cardinality can store instances subordinate to the parent instance.
With IDENTIFIEDBY this storage structure required explicit declaration
in the class' storage definition: datalocation = {%%PARENT}("someliteral")
.
With parent/child relationships this storage structure is provided by default.
The literal subscript is the name of the relationship in the parent
class. In the Invoice/LineItem example above the compiled datalocation for
LineItem would be ^ooInvoiceD("Items")
. Of course, this datalocation can be
defined explicitly, either to make the storage independent or to simply change
the literal subscript.
Relationships are affected by a deep save in much the same way as simple references. The
major difference is the list of ‘terminated relationships’ that is maintained
in the multi-valued relationship. Objects in that list are also ‘deep-saved’.
For example, consider these actions on the Company and Employee classes as shown
in the previous examples.
Set company1 = ##class(Company).%OpenId(10)
Set company2 = ##class(Company).%OpenId(12)
Set emp = company1.Employees.GetAt(1) ; Assume that there is at least one Employee related to company1
Set emp.TheCompany = company2
Set sc = customer1.%Save()
The call to %Save() will result in company1, emp, and company2
being saved. Even though company1 is no longer related to order,
order will be saved because it is still referenced by the list of terminated
relationships in company1. Also, company2 will be saved because it is
currently related to emp.
Using Relationships with SQL
The SQL projection of a relationship depends on its cardinality value:
The single-valued side (ONE or PARENT) is projected as a simple
designative reference field. For example, the
Employee table will
have a field called
TheCompany whose value is the ID of the
related
Company:
// display Employees ordered by Name
// Get company name using reference field
SELECT ID,Name,Title,TheCompany->Name
FROM Employee
ORDER BY Name
The multi-valued side is not projected as a field (Multi-valued relationships are
state-less on disk and SQL does not deal with in-memory objects).
Instead, you must perform a simple join based on the multi-valued side's ID value
and the single-valued side's reference field:
// Display all Employees by Company
SELECT c.Name, e.Name
FROM Company c, Employee e
WHERE e.TheCompany = c.ID
ORDER By 1,2
A single-valued relationship with cardinality of ONE is projected to SQL
as a FOREIGNKEY with NOACTION specified for UPDATE and DELETE.
If the cardinality of the single-valued relationship is PARENT then the table
projected from the class containing the relationship is "adopted" as a child
table by the table projected from the type class of the single-valued
relationship.