From 6c88333c2e0232aed9e0b3c9077306f09e36c65c Mon Sep 17 00:00:00 2001
From: Boris Kolpackov 8 Inheritance
@@ -449,6 +456,7 @@ for consistency.
- 8.1 Reuse Inheritance
+ 8.2 Polymorphism Inheritance
+
+ 8.2 Polymorphism Inheritance
+
+
+
+
+ 8.2.1 Performance and Limitations 8.3 Mixed Inheritance 12.1.6 id
12.1.7 callback
+ 12.1.8 schema
@@ -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.12.1.9 polymorphic
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.
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.
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 (); ++ +
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
polymorphic
table
polymorphic
The polymorphic
specifier specifies that a persistent
+ class is polymorphic. For more information on polymorphism support,
+ refer to Chapter 8, "Inheritance".
A pragma with the view
qualifier declares a C++ class
--
cgit v1.1