From 6c88333c2e0232aed9e0b3c9077306f09e36c65c Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 25 Apr 2012 15:53:50 +0200 Subject: Document polymorphism support --- doc/manual.xhtml | 549 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 543 insertions(+), 6 deletions(-) (limited to 'doc/manual.xhtml') diff --git a/doc/manual.xhtml b/doc/manual.xhtml index e14f2cf..cad568c 100644 --- a/doc/manual.xhtml +++ b/doc/manual.xhtml @@ -405,7 +405,14 @@ for consistency. 8Inheritance - + + + +
8.1Reuse Inheritance
8.2Polymorphism Inheritance
8.2Polymorphism Inheritance + + +
8.2.1Performance and Limitations
+
8.3Mixed Inheritance
@@ -449,6 +456,7 @@ for consistency. 12.1.6id 12.1.7callback 12.1.8schema + 12.1.9polymorphic @@ -3706,6 +3714,20 @@ namespace odb { }; + // Polymorphism support exceptions. + // + struct abstract_class: exception + { + virtual const char* + what () const throw (); + }; + + struct no_type_info: exception + { + virtual const char* + what () const throw (); + }; + // Schema catalog exceptions. // struct unknown_schema: exception @@ -3781,6 +3803,22 @@ namespace odb database system-specific runtime library. Refer to Part II, "Database Systems" for more information.

+

The abstract_class exception is thrown by the database + functions when we attempt to persist, update, load, or erase an + instance of a polymorphic abstract class. For more information + on abstract classes, refer to Section 12.1.3, + "abstract".

+ +

The no_type_info exception is thrown by the database + functions when we attempt to persist, update, load, or erase an + instance of a polymorphic class for which no type information + is present in the application. This normally means that the + generated database support code for this class has not been + linked (or dynamically loaded) into the application or the + discriminator value has not been mapped to a persistent + class. For more information on polymorphism support, refer to + Section 8.2, "Polymorphism Inheritance".

+

The unknown_schema exception is thrown by the odb::schema_catalog class if a schema with the specified name is not found. Refer to Section 3.3, "Database" @@ -6479,8 +6517,8 @@ class contractor: public person

A common trait of this inheritance style, referred to as reuse inheritance from now on, is the lack of virtual functions and a virtual destructor in the base class. Also with this style the - application code is normally written in terms of derived classes - instead of a base.

+ application code is normally written in terms of the derived classes + instead of the base.

The second way to utilize inheritance in C++ is to provide polymorphic behavior through a common interface. In this case the base class @@ -6665,14 +6703,501 @@ CREATE TABLE contractor (

The complete version of the code presented in this section is - available in the inheritance example in the + available in the inheritance/reuse example in the odb-examples package.

8.2 Polymorphism Inheritance

-

Polymorphism inheritance mapping is not yet implemented. Future - versions of ODB will add support for this functionality.

+

There are three general approaches to mapping a polymorphic + class hierarchy to a relational database. These are + table-per-hierarchy, table-per-difference, + and table-per-class. With the table-per-hierarchy + mapping, all the classes in a hierarchy are stored in a single, + "wide" table. NULL values are stored in columns + corresponding to data members of derived classes that are + not present in any particular instance.

+ +

In the table-per-difference mapping, each class is mapped + to a separate table. For a derived class, this table contains + only columns corresponding to the data members added by this + derived class.

+ +

Finally, in the table-per-class mapping, each class is mapped + to a separate table. For a derived class, this table contains + columns corresponding to all the data members, from this derived + class all the way down to the root of the hierarchy.

+ +

The table-per-difference mapping is generally considered as + having the best balance of flexibility, performance, and space + efficiency. It also results in a more canonical relational + database model compared to the other two approaches. As a + result, this is the mapping currently implemented in ODB. + Other mappings may be supported in the future.

+ +

A pointer or reference to an ordinary, non-polymorphic object + has just one type — the class type of that object. When we + start working with polymorphic objects, there are two types + to consider: the static type, or the declaration type + of a reference or pointer, and the object's actual or dynamic + type. An example will help illustrate the difference:

+ +
+class person {...};
+class employee: public person {...};
+
+person p;
+employee e;
+
+person& r1 (p);
+person& r2 (e);
+
+auto_ptr<person> p1 (new employee);
+  
+ +

In the above example, the r1 reference's both static + and dynamic types are person. + In contrast, the r2 reference's static type is + person while its dynamic type (the actual object + that it refers to) is employee. Similarly, + p1 points to the object of the person + static type but employee dynamic type.

+ +

In C++, the primary mechanisms for working with polymorphic objects + are virtual functions. We call a virtual function only knowing the + object's static type, but the version corresponding to the object's + dynamic type is automatically executed. This is the essence of + runtime polymorphism support in C++: we can operate in terms of a base + class interface but get the derived class' behavior. Similarly, the + essence of the runtime polymorphism support in ODB is to allow us to + persist, load, update, and query in terms of the base class interface + but have the derived class actually stored in the database.

+ +

To declare a persistent class as polymorphic we use the + db polymorphic pragma. We only need to + declare the root class of a hierarchy as polymorphic; ODB will + treat all the derived classes as polymorphic automatically. For + example:

+ +
+#pragma db object polymorphic
+class person
+{
+  ...
+
+  virtual
+  ~person () = 0; // Automatically abstract.
+
+  #pragma db id auto
+  unsigned long id_;
+
+  std::string first_;
+  std::string last_;
+};
 
+#pragma db object
+class employee: public person
+{
+  ...
+
+  bool temporary_;
+};
+
+#pragma db object
+class contractor: public person
+{
+
+  std::string email_;
+};
+  
+ +

A persistent class hierarchy declared polymorphic must also be + polymorphic in the C++ sense, that is, the root class must + declare or inherit at least one virtual function. It is + recommended that the root class also declares a virtual destructor. + The root class of the polymorphic hierarchy must contain + the data member designated as object id (a persistent class + without an object id cannot be polymorphic). Note also that, + unlike reuse inheritance, abstract polymorphic classes have + a table in the database, just like non-abstract classes.

+ +

Persistent classes in the same polymorphic hierarchy must use the + same kind of object pointer (Section 3.2, + "Object and View Pointers"). If the object pointer + for the root class is specified as a template or using the + special raw pointer syntax (*), then the ODB + compiler will automatically use the same object pointer + for all the derived classes. For example:

+ +
+#pragma db object polymorphic pointer(std::shared_ptr)
+class person
+{
+  ...
+};
+
+#pragma db object // Object pointer is std::shared_ptr<employee>.
+class employee: public person
+{
+  ...
+};
+
+#pragma db object // Object pointer is std::shared_ptr<contractor>.
+class contractor: public person
+{
+  ...
+};
+  
+ +

For polymorphic persistent classes, all the database operations can + be performed on objects with different static and dynamic types. + Similarly, operations that load persistent objects from the + database (load(), query(), etc.), can + return objects with different static and dynamic types. For + example:

+ +
+unsigned long id1, id2;
+
+// Persist.
+//
+{
+  shared_ptr<person> p1 (new employee (...));
+  shared_ptr<person> p2 (new contractor (...));
+
+  transaction t (db.begin ());
+  id1 = db.persist (p1); // Stores employee.
+  id2 = db.persist (p2); // Stores contractor.
+  t.commit ();
+}
+
+// Load.
+//
+{
+  shared_ptr<person> p;
+
+  transaction t (db.begin ());
+  p = db.load<person> (id1); // Loads employee.
+  p = db.load<person> (id2); // Loads contractor.
+  t.commit ();
+}
+
+// Query.
+//
+{
+  typedef odb::query<person> query;
+  typedef odb::result<person> result;
+
+  transaction t (db.begin ());
+
+  result r (db.query<person> (query::last == "Doe"));
+
+  for (result::iterator i (r.begin ()); i != r.end (); ++i)
+  {
+    person& p (*i); // Can be employee or contractor.
+  }
+
+  t.commit ();
+}
+
+// Update.
+//
+{
+  shared_ptr<person> p;
+  shared_ptr<employee> e;
+
+  transaction t (db.begin ());
+
+  e = db.load<employee> (id1);
+  e->temporary (false);
+  p = e;
+  db.update (p); // Updates employee.
+
+  t.commit ();
+}
+
+// Erase.
+//
+{
+  shared_ptr<person> p;
+
+  transaction t (db.begin ());
+  p = db.load<person> (id1); // Loads employee.
+  db.erase (p);              // Erases employee.
+  db.erase<person> (id2);    // Erases contractor.
+  t.commit ();
+}
+  
+ + +

The table-per-difference mapping, as supported by ODB, requires + two extra columns, in addition to those corresponding to the + data members. The first, called discriminator, is added + to the table corresponding to the root class of the hierarchy. + This column is used to determine the dynamic type of each + object. The second column is added to tables corresponding + to the derived classes and contains the object id. This + column is used to form a foreign key constraint referencing + the root class table.

+ +

When querying the database for polymorphic objects, it is + possible to obtain the discriminator value without + instantiating the object. For example:

+ +
+typedef odb::query<person> query;
+typedef odb::result<person> result;
+
+transaction t (db.begin ());
+
+result r (db.query<person> (query::last == "Doe"));
+
+for (result::iterator i (r.begin ()); i != r.end (); ++i)
+{
+  std::string d (i.discriminator ());
+  ...
+}
+
+t.commit ();
+  
+ +

In the current implementation, ODB has limited support for + customizing names, types, and values of the extra columns. + Currently, the discriminator column is always called + typeid and contains a namespace-qualified class + name (for example, "employee" or + "hr::employee"). The id column in the derived + class table has the same name as the object id column in + the root class table. Future versions of ODB will add support + for customizing these extra columns.

+ +

The sample database schema for the above polymorphic hierarchy + is shown below.

+ +
+CREATE TABLE person (
+  id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  typeid VARCHAR(255) NOT NULL,
+  first TEXT NOT NULL,
+  last TEXT NOT NULL);
+
+CREATE TABLE employee (
+  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
+  temporary TINYINT(1) NOT NULL,
+
+  CONSTRAINT employee_id_fk
+    FOREIGN KEY (id)
+    REFERENCES person (id)
+    ON DELETE CASCADE);
+
+CREATE TABLE contractor (
+  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
+  email TEXT NOT NULL,
+
+  CONSTRAINT contractor_id_fk
+    FOREIGN KEY (id)
+    REFERENCES person (id)
+    ON DELETE CASCADE);
+  
+ +

The complete version of the code presented in this section is + available in the inheritance/polymorphism example + in the odb-examples package.

+ +

8.2.1 Performance and Limitations

+ +

A database operation on a non-polymorphic object normally translates + to a single database statement execution (objects with containers + and eager object pointers can be the exception). Because polymorphic + objects have their data members + stored in multiple tables, some database operations on such objects + may result in multiple database statements being executed while others + may require more complex statements. There is also some functionality + that is not available to polymorphic objects.

+ +

The first part of this section discusses the performance implications + to keep in mind when designing and working with polymorphic hierarchies. + The second part talks about limitations of polymorphic objects.

+ +

The most important aspect of a polymorphic hierarchy that + affects database performance is its depth. The distance between + the root of the hierarchy and the derived class translates + directly to the number of database statements that will have to + be executed in order to persist, update, or erase this derived class. + It also translates directly to the number of SQL JOIN + clauses that will be needed to load or query the database for this + derived class. As a result, to achieve best performance, we should + try to keep our polymorphic hierarchies as flat as possible.

+ +

When loading an object or querying the database for objects, + ODB will need to execute two statements if this object's static + and dynamic types are different but only one statement if + they are the same. This example will help illustrate the + difference:

+ +
+unsigned long id;
+
+{
+  employee e (...);
+
+  transaction t (db.begin ());
+  id = db.persist (e);
+  t.commit ();
+}
+
+{
+  shared_ptr<person> p;
+
+  transaction t (db.begin ());
+  p = db.load<person> (id);   // Requires two statement.
+  p = db.load<employee> (id); // Requires only one statement.
+  t.commit ();
+}
+  
+ +

As a result, we should try to load and query using the most + derived class possible.

+ +

Finally, for polymorphic objects, erasing via the object instance + is faster than erasing via its object id. In the former case the + object's dynamic type can be determined locally in the application + while in the latter case an extra statement has to be executed to + achieve the same result. For example:

+ +
+shared_ptr<person> p = ...;
+
+transaction t (db.begin ());
+db.erase<person> (p.id ()); // Slower (executes extra statement).
+db.erase (p);               // Faster.
+t.commit ();
+  
+ +

Polymorphic objects can use all the mechanisms that are available + to ordinary objects. These include containers (Chapter 5, + "Containers"), object relationships, including to polymorphic + objects (Chapter 6, "Relationships"), views + (Chapter 9, "Views"), session (Chapter + 10, "Session"), and optimistic concurrency (Chapter + 11, "Optimistic Concurrency"). There are, however, a few + limitations, mainly due to the underlying use of SQL to access the + data.

+ +

When a polymorphic object is "joined" in a view, and the join + condition (either in the form of an object pointer or a custom + condition) comes from the object itself (as opposed to one of + the objects joined previously), then this condition must only + use data members from the derived class. For example, consider + the following polymorphic object hierarchy and a view:

+ + +
+#pragma db object polymorphic
+class employee
+{
+  ...
+};
+
+#pragma db object
+class permanent_employee: public employee
+{
+  ...
+};
+
+#pragma db object
+class temporary_employee: public employee
+{
+  ...
+
+  shared_ptr<permanent_employee> manager_;
+};
+
+#pragma db object
+class contractor: public temporary_employee
+{
+  shared_ptr<permanent_employee> manager_;
+};
+
+#pragma db view object(permanent_employee) \
+                object(contractor: contractor::manager_)
+struct contractor_manager
+{
+  ...
+};
+  
+ +

This view will not function correctly because the join condition + (manager_) comes from the base class + (temporary_employee) instead of the derived + (contractor). The reason for this limitation is the + JOIN clause order in the underlying SQL SELECT + statement. In the view presented above, the table corresponding + to the base class (temporary_employee) will have to + be joined first which will result in this view matching both + the temporary_employee and contractor + objects instead of just contractor. It is usually + possible to resolve this issue by reordering the objects in the + view. Our example, for instance, can be fixed by swapping the + two objects:

+ +
+#pragma db view object(contractor) \
+                object(permanent_employee: contractor::manager_)
+struct contractor_manager
+{
+  ...
+};
+  
+ +

The erase_query() database function (Section + 3.10, "Deleting Persistent Objects") also has limited functionality + when used on polymorphic objects. Because many database implementations + do not support JOIN clauses in the SQL DELETE + statement, only data members from the derived class being erased can + be used in the query condition. For example:

+ +
+typedef odb::query<employee> query;
+
+transaction t (db.begin ());
+db.erase_query<employee> (query::permanent);     // Ok.
+db.erase_query<employee> (query::last == "Doe"); // Error.
+t.commit ();
+  
+ +

8.3 Mixed Inheritance

+ +

It is possible to mix the reuse and polymorphism inheritance + styles in the same hierarchy. In this case, the reuse inheritance + must be used for the "bottom" (base) part of the hierarchy while + the polymorphism inheritance — for the "top" (derived) part. + For example:

+ +
+#pragma db object
+class person
+{
+  ...
+};
+
+#pragma db object polymorphic
+class employee: public person // Reuse inheritance.
+{
+  ...
+};
+
+#pragma db object
+class temporary_employee: public employee // Polymorphism inheritance.
+{
+  ...
+};
+
+#pragma db object
+class permanent_employee: public employee // Polymorphism inheritance.
+{
+  ...
+};
+  
@@ -8460,6 +8985,12 @@ class person 12.1.8 + + polymorphic + persistent class is polymorphic + 12.1.9 + +

12.1.1 table

@@ -9012,6 +9543,12 @@ class employee can be used as an alternative to database schemas if the target database system does not support schemas.

+

12.1.9 polymorphic

+ +

The polymorphic specifier specifies that a persistent + class is polymorphic. For more information on polymorphism support, + refer to Chapter 8, "Inheritance".

+

12.2 View Type Pragmas

A pragma with the view qualifier declares a C++ class -- cgit v1.1