8 继承
- 在C++中,继承可以用来实现两个不同的目标。我们可以使用继承在多个类中重用公共数据和功能。例如:
class person
{
public:
const std::string& first () const;
const std::string& last () const;
private:
std::string first_;
std::string last_;
};
class employee: public person
{
...
};
class contractor: public person
{
...
};
-
在上面的例子中,
employee
和contractor
类都继承了person
基类的first_
和last_
数据成员以及first()
和last()
访问器。 -
这种继承风格的一个共同特征,从现在开始称为重用继承,是基类中缺少虚拟函数和虚拟析构函数。同样,采用这种风格,应用程序代码通常是根据派生类而不是基类编写的。
-
在C++中利用继承的第二种方法是通过公共接口提供多态行为。在这种情况下,基类定义了许多虚拟函数,通常还有一个虚拟析构函数,而派生类则提供了这些虚拟函数的特定实现。例如:
class person
{
public:
enum employment_status
{
unemployed,
temporary,
permanent,
self_employed
};
virtual employment_status
employment () const = 0;
virtual
~person ();
};
class employee: public person
{
public:
virtual employment_status
employment () const
{
return temporary_ ? temporary : permanent;
}
private:
bool temporary_;
};
class contractor: public person
{
public:
virtual employment_status
employment () const
{
return self_employed;
}
};
-
使用这种继承风格,我们称之为多态继承,应用程序代码通常通过基类接口与派生类一起工作。还要注意,在同一层次结构中混合这两种风格是很常见的。例如,可以组合上述两个代码片段,以便人员基类提供公共数据成员和函数,并定义多态接口。
-
以下部分描述了将重用和多态继承样式映射到关系数据模型的可用策略。还要注意,这两种风格之间的区别是概念上的,而不是形式上的。例如,如果这会导致所需的数据库映射和语义,则可以将定义虚拟函数的类层次结构视为重用继承的情况。
-
通常,采用重用继承的类被映射到数据库中完全独立的实体。它们使用不同的对象id空间,应该始终作为指向派生类型的指针或引用传递给数据库操作并从数据库操作返回。换句话说,从持久性的角度来看,这些类的行为就像基类中的数据成员被逐字复制到派生类中一样。
-
相比之下,采用多态继承的类共享对象id空间,可以作为指向基类的指针或引用以多态方式传递给数据库操作并从数据库操作返回。
-
对于这两种继承样式,有时需要防止基类的实例存储在数据库中。为了实现这一点,可以使用
db-stract
pragma(第14.1.3节,“abstract”)将持久类声明为抽象类。请注意,C++抽象类,或具有一个或多个纯虚函数且因此无法实例化的类,也是数据库抽象类。然而,数据库抽象类不一定是C++抽象类。ODB编译器自动将C++抽象类视为数据库抽象。
8.1 重用继承
- 重用继承层次结构中的每个非抽象类都映射到一个单独的数据库表,该表包含其所有数据成员,包括从基类继承的数据成员。抽象持久类不必定义对象id,也不必定义默认构造函数,也不需要相应的数据库表。抽象类不能是关系中的指向对象。只要每个基类只继承一次,就支持多重继承。以下示例显示了采用重用继承的持久类层次结构:
// Abstract person class. Note that it does not declare the
// object id.
//
#pragma db object abstract
class person
{
...
std::string first_;
std::string last_;
};
// Abstract employee class. It derives from the person class and
// declares the object id for all the concrete employee types.
//
#pragma db object abstract
class employee: public person
{
...
#pragma db id auto
unsigned long id_;
};
// Concrete permanent_employee class. Note that it doesn't define
// any data members of its own.
//
#pragma db object
class permanent_employee: public employee
{
...
};
// Concrete temporary_employee class. It adds the employment
// duration in months.
//
#pragma db object
class temporary_employee: public employee
{
...
unsigned long duration_;
};
// Concrete contractor class. It derives from the person class
// (and not employee; an independent contractor is not considered
// an employee). We use the contractor's external email address
// as the object id.
//
#pragma db object
class contractor: public person
{
...
#pragma db id
std::string email_;
};
- 此层次结构的示例数据库架构如下所示。
CREATE TABLE permanent_employee (
first TEXT NOT NULL,
last TEXT NOT NULL,
id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT);
CREATE TABLE temporary_employee (
first TEXT NOT NULL,
last TEXT NOT NULL,
id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
duration BIGINT UNSIGNED NOT NULL);
CREATE TABLE contractor (
first TEXT NOT NULL,
last TEXT NOT NULL,
email VARCHAR (255) NOT NULL PRIMARY KEY);
- 本节中提供的代码的完整版本可在
odb-examples
中的inheritance/reuse
示例中获得。
8.2 多态性继承
-
将多态类层次结构映射到关系数据库有三种通用方法。这些是每个层次结构的表、每个差异的表和每个类的表。通过按层次映射表,层次结构中的所有类都存储在一个“宽”表中。NULL值存储在与派生类的数据成员对应的列中,这些派生类在任何特定实例中都不存在。
-
在每个差异映射表中,每个类都映射到一个单独的表。对于派生类,此表仅包含与此派生类添加的数据成员对应的列。
-
最后,在表-类映射中,每个类都映射到一个单独的表。对于派生类,此表包含与所有数据成员对应的列,从该派生类一直到层次结构的根。
-
每个差异映射的表通常被认为具有灵活性、性能和空间效率的最佳平衡。与其他两种方法相比,它还产生了更规范的关系数据库模型。因此,这就是目前在ODB中实现的映射。未来可能会支持其他映射。
-
指向普通非多态对象的指针或引用只有一种类型——该对象的类类型。当我们开始使用多态对象时,有两种类型需要考虑:静态类型,或引用或指针的声明类型,以及对象的实际或动态类型。一个例子将有助于说明这种差异:
class person {...};
class employee: public person {...};
person p;
employee e;
person& r1 (p);
person& r2 (e);
auto_ptr<person> p1 (new employee);
-
在上面的示例中,
r1
引用的静态和动态类型都是person
。相比之下,r2
引用的静态类型是person
,而其动态类型(它引用的实际对象)是employee
。同样,p1
指向人员静态类型但员工动态类型的对象。 -
在C++中,处理多态对象的主要机制是虚函数。我们调用虚函数时只知道对象的静态类型,但会自动执行与对象的动态类型对应的版本。这是C++中运行时多态支持的本质:我们可以根据基类接口进行操作,但可以获得派生类的行为。同样,ODB中运行时多态性支持的本质是允许我们根据基类接口进行持久化、加载、更新和查询,但将派生类实际存储在数据库中。
-
为了将持久类声明为多态类,我们使用
db-polymorphic
pragma。我们只需要将层次结构的根类声明为多态类;ODB会自动将所有派生类视为多态类。例如:
#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_;
};
-
声明多态的持久类层次结构在C++意义上也必须是多态的,也就是说,根类必须声明或继承至少一个虚函数。建议根类也声明一个虚拟析构函数。多态层次结构的根类必须包含指定为对象id的数据成员(没有对象id的持久类不能是多态的)。还要注意,与重用继承不同,抽象多态类在数据库中有一个表,就像非抽象类一样。
-
同一多态层次结构中的持久类必须使用相同类型的对象指针(第3.3节,“对象和视图指针”)。如果根类的对象指针被指定为模板或使用特殊的原始指针语法(
*
),则ODB编译器将自动对所有派生类使用相同的对象指针。例如:
#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
{
...
};
-
同样,如果我们为根类启用或禁用会话支持(第11章,“会话”),则ODB编译器将自动为所有派生类启用或停用它。
-
对于多态持久类,所有数据库操作都可以在具有不同静态和动态类型的对象上执行。同样,从数据库加载持久对象的操作(
load()
、query()
等)可以返回具有不同静态和动态类型的对象。例如:
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 ();
-
ODB支持的每个差异映射的表除了与数据成员对应的列外,还需要两个额外的列。第一个被称为鉴别器,被添加到与层次结构的根类对应的表中。此列用于确定每个对象的动态类型。第二列添加到与派生类对应的表中,并包含对象id。此列用于形成引用根类表的外键约束。
-
在数据库中查询多态对象时,可以在不实例化对象的情况下获得鉴别器值。例如:
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 ();
-
在当前实现中,ODB对自定义额外列的名称、类型和值的支持有限。目前,鉴别器列始终称为
typeid
,并包含一个命名空间限定的类名(例如,“employee
”或“hr::employee
”)。派生类表中的id列与根类表中对象id列的名称相同。未来版本的ODB将添加对自定义这些额外列的支持。 -
下面显示了上述多态层次结构的示例数据库模式。
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);
- 本节中提供的代码的完整版本可在
odb-examples
中的继承/多态示例中获得。
8.2.1 性能及限制
-
对非多态对象的数据库操作通常转换为单个数据库语句执行(具有容器和热切对象指针的对象可能是例外)。由于多态对象的数据成员存储在多个表中,因此对这些对象的某些数据库操作可能会导致执行多个数据库语句,而其他操作可能需要更复杂的语句。还有一些功能是多态对象不可用的。
-
本节的第一部分讨论了在设计和使用多态层次结构时需要记住的性能影响。第二部分讨论了多态对象的局限性。
-
影响数据库性能的多态层次结构最重要的方面是它的深度。层次结构的根和派生类之间的距离直接转换为必须执行的数据库语句的数量,以便持久化、更新或擦除此派生类。它还直接转换为加载或查询此派生类的数据库所需的SQL JOIN子句的数量。因此,为了获得最佳性能,我们应该尽量保持多态层次结构尽可能平坦。
-
加载对象或在数据库中查询对象时,如果此对象的静态和动态类型不同,ODB将需要执行两条语句,但如果它们相同,则只需要执行一条语句。这个例子将有助于说明区别:
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 ();
}
-
因此,我们应该尝试使用尽可能多的派生类进行加载和查询。
-
最后,对于多态对象,通过对象实例擦除比通过其对象id擦除更快。在前一种情况下,可以在应用程序中本地确定对象的动态类型,而在后一种情况中,必须执行额外的语句才能达到相同的结果。例如:
shared_ptr<person> p = ...;
transaction t (db.begin ());
db.erase<person> (p.id ()); // Slower (executes extra statement).
db.erase (p); // Faster.
t.commit ();
-
多态对象可以使用普通对象可用的所有机制。这些包括容器(第5章,“容器”)、对象关系,包括多态对象(第6章,“关系”)、视图(第10章,“视图”)、会话(第11章,“会话”)和乐观并发(第12章,“乐观并发”)。然而,存在一些局限性,主要是由于底层使用SQL来访问数据。
-
当一个多态对象在视图中被“连接”,并且连接条件(以对象指针或自定义条件的形式)来自对象本身(与之前连接的对象之一相反)时,则此条件必须只使用派生类中的数据成员。例如,考虑以下多态对象层次结构和视图:
#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
{
...
};
- 此视图将无法正常工作,因为连接条件(
manager_
)来自基类(temporary_employee
)而不是派生类(contractor
)。这种限制的原因是底层SQL SELECT语句中的JOIN子句顺序。在上述视图中,必须首先连接与基类(temporary_employee
)对应的表,这将导致此视图匹配temporary_employee
和contractor
对象,而不仅仅是contractor
。通常可以通过重新排序视图中的对象来解决此问题。例如,我们的例子可以通过交换两个对象来修复:
#pragma db view object(contractor) \
object(permanent_employee: contractor::manager_)
struct contractor_manager
{
...
};
erase_query()
数据库函数(第3.11节,“删除持久对象”)在用于多态对象时也具有有限的功能。因为许多数据库实现不支持SQLDELETE语句中的JOIN子句,所以查询条件中只能使用被擦除的派生类中的数据成员。例如:
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 混合继承
- 可以在同一层次结构中混合重用和多态继承样式。在这种情况下,重用继承必须用于层次结构的“底部”(基础)部分,而多态继承必须用于“顶部”(派生)部分。例如:
#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.
{
...
};