5. 容器
-
ODB运行时库为所有常用的标准C++98/03容器提供了内置的持久性支持,即
std::vector
,std::list
,std::deque
,std::set
,std::multiset
,std::map
,和std::multimap
,以及C++11std::array
、std::forward_list
、std::unordered_set
、std::unsodered_multise
t、std::unordered_map
和std::unodered_multimap
。此外,可用于常用框架和库(如Boost和Qt)的ODB配置文件库为这些框架和库中的容器提供持久性支持(第三部分,“配置文件”)。ODB运行时库和配置文件库还提供了许多更改跟踪容器等效物,可用于最大限度地减少将容器状态与数据库同步所需的数据库操作次数(第5.4节,“更改跟踪容器”)。如第5.5节“使用自定义容器”所述,持久化自定义容器类型也很容易。 -
我们不需要做任何特殊的事情来声明持久类中容器类型的成员。例如:
#pragma db object
class person
{
...
private:
std::vector<std::string> nicknames_;
...
};
-
上述代码片段的完整版本和本章中提供的其他代码示例可以在
odb-examples
中的container
示例中找到。 -
容器类型的持久类中的数据成员的行为类似于值类型。也就是说,当一个对象被持久化时,容器的元素会存储在数据库中。同样,当从数据库加载持久对象时,容器的内容也会自动加载。容器类型的数据成员也可以使用智能指针,如第7.3节“指针和NULL值语义”所述。
-
当普通成员映射到对象表中的一个或多个列时,容器类型的成员映射到单独的表。这种表的确切模式取决于容器的类型。ODB定义了以下容器类型:有序、集合、多集合、映射和多映射。以下部分将详细讨论容器类型及其映射到的表的内容。
-
ODB中的容器可以包含简单值类型(第7.1节,“简单值类型”)、复合值类型(第一节,“复合值类型”,第7.2节)和指向对象的指针(第六章,“关系”)。不允许直接或通过复合值类型间接使用容器的容器。映射或多映射容器中的键可以是简单值类型或复合值类型,但不能是指向对象的指针。有序容器中的索引应该是简单的整数值类型。
-
有序容器、集合容器和映射容器中的值类型以及映射容器的键类型应该是默认可构造的。这些类型中的默认构造函数可以设置为私有,在这种情况下,
odb::access
类应该成为值或键类型的朋友。例如:
#pragma db value
class name
{
public:
name (const std::string&, const std::string&);
...
private:
friend class odb::access;
name ();
...
};
#pragma db object
class person
{
...
private:
std::vector<name> aliases_;
...
};
5.1 ordered容器
- 在ODB中,有序容器是指以整数索引的形式(显式或隐式)维护其元素顺序的任何容器。排序的标准C++容器包括
std::vector
std::list
、std::deque
以及C++11std::array
和std::forward_list
。虽然std::set
中的元素也以特定的顺序保存,但这种顺序不是基于整数索引,而是基于元素之间的关系。因此,出于持久性的目的,std::set
不被视为有序容器。
有序容器的数据库表至少由三列组成。第一列包含容器所属的持久类实例的对象id。第二列包含容器内的元素索引。最后一列包含元素值。如果对象id或元素值是复合的,那么它们可以占据多列,而不是单列。对于有序容器表,ODB编译器还定义了两个索引:一个用于对象id列,另一个用于索引列。有关如何自定义这些索引的更多信息,请参阅第14.7节“索引定义规范”。
以以下持久对象为例:
#pragma db object
class person
{
...
private:
#pragma db id auto
unsigned long id_;
std::vector<std::string> nicknames_;
...
};
-
生成的数据库表(称为
person_nicknames
)将包含unsigned long
类型的对象id列(称为object_id
)、整数类型的索引列(称为主索引)和std::string
类型的值列(称为辅值)。 -
许多ODB语法允许我们在每个容器和每个成员的基础上自定义有序容器的表名、列名和本机数据库类型。有关这些语法的更多信息,请参阅第14章“ODB语法语言”。以下示例显示了一些可能的自定义设置:
#pragma db object
class person
{
...
private:
#pragma db table("nicknames") \
id_column("person_id") \
index_type("SMALLINT UNSIGNED") \
index_column("nickname_number") \
value_type("VARCHAR(255)") \
value_column("nickname")
std::vector<std::string> nicknames_;
...
};
- 虽然持久类中使用的C++容器可能是有序的,但有时我们可能希望在没有顺序信息的情况下将这样的容器存储在数据库中。例如,在上面的例子中,人们昵称的顺序可能并不重要。为了指示ODB编译器忽略有序容器中的顺序,我们可以使用
db-ordered
pragma(第14.3.9节,“无序”,第14.4.19节,“有序”)。例如:
#pragma db object
class person
{
...
private:
#pragma db unordered
std::vector<std::string> nicknames_;
...
};
- 标记为无序的有序容器的表将没有索引列,从数据库检索元素的顺序可能与它们的存储顺序不同。
5.2 set和multiset容器
-
在ODB中,set和multiset容器(称为just-set容器)是关联容器,包含基于它们之间某种关系的元素。集合容器可能保证也可能不保证其存储的元素的特定顺序。为了持久化的目的,被认为是集合容器的标准C++容器包括
std::set
和std::multiset
,以及C++11std::unordered_set
和std::unodered_multiset
。 -
集合容器的数据库表至少由两列组成。第一列包含容器所属的持久类实例的对象id。第二列包含元素值。如果对象id或元素值是复合的,那么它们可以占据多列,而不是单列。ODB编译器还为对象id列在集合容器表上定义了一个索引。有关如何自定义此索引的更多信息,请参阅第14.7节“索引定义说明”。
-
以以下持久对象为例:
#pragma db object
class person
{
...
private:
#pragma db id auto
unsigned long id_;
std::set<std::string> emails_;
...
};
-
生成的数据库表(称为
person_emails
)将包含unsigned long
类型的object id列(称为object_id
)和std::string
类型的value列(称为value
)。 -
许多ODB语法允许我们在每个容器和每个成员的基础上自定义集合容器的表名、列名和本机数据库类型。有关这些语法的更多信息,请参阅第14章“ODB语法语言”。以下示例显示了一些可能的自定义设置:
#pragma db object
class person
{
...
private:
#pragma db table("emails") \
id_column("person_id") \
value_type("VARCHAR(255)") \
value_column("email")
std::set<std::string> emails_;
...
};
5.3 map和multimap容器
-
在ODB中,map和multimap容器(简称map容器)是关联容器,包含基于键之间某种关系的键值元素。地图容器可能保证也可能不保证它存储的元素的特定顺序。出于持久性目的,被认为是映射容器的标准C++容器包括
std::map
和std::multimap
,以及C++11std::unordered_map
和std::unoddered_multimap
。 -
映射容器的数据库表至少由三列组成。第一列包含容器所属的持久类实例的对象id。第二列包含元素键。最后一列包含元素值。如果对象id、元素键或元素值是复合的,那么它们可以占据多列,而不是单列。ODB编译器还在映射容器表上为对象id列定义了一个索引。有关如何自定义此索引的更多信息,请参阅第14.7节“索引定义说明”。
-
以以下持久对象为例:
#pragma db object
class person
{
...
private:
#pragma db id auto
unsigned long id_;
std::map<unsigned short, float> age_weight_map_;
...
};
-
生成的数据库表(称为
person_age_weight_map
)将包含类型为unsigned long
(称为object_id
)的对象id列、类型为unsigned short
(称为key
)的键列和类型为float
的值列(称为value
)。 -
许多ODB语法允许我们在每个容器和每个成员的基础上自定义映射容器的表名、列名和本机数据库类型。有关这些语法的更多信息,请参阅第14章“ODB语法语言”。以下示例显示了一些可能的自定义设置:
#pragma db object
class person
{
...
private:
#pragma db table("weight_map") \
id_column("person_id") \
key_type("INT UNSIGNED") \
key_column("age") \
value_type("DOUBLE") \
value_column("weight")
std::map<unsigned short, float> age_weight_map_;
...
};
5.4 Change-Tracking容器
-
当包含标准容器之一的持久对象在数据库中更新时,ODB不知道插入、删除或修改了哪些元素。因此,ODB别无选择,只能假设整个容器已经更改,并更新每个元素的状态。如果容器包含大量元素,而我们只更改了其中的一小部分,这可能会导致巨大的开销。
-
为了消除这种开销,ODB提供了一种更改跟踪容器的概念。变更跟踪容器除了包含其元素外,就像普通容器一样,还包括每个元素的变更状态。当需要更新数据库中的此类容器时,ODB可以使用此更改信息来执行将容器状态与数据库同步所需的最小数量的数据库操作。
-
当前版本的ODB运行时库提供了
std::vector
(第5.4.1节,“变更跟踪向量”)的变更跟踪等效项,并支持计划在未来版本中使用的其他标准容器等效项。ODB配置文件库还为相应框架和库中的一些容器提供了更改跟踪等效项(第三部分,“配置文件”)。 -
变更跟踪容器等效物通常可以用作普通容器的直接替代品,除了一些微小的接口差异(在相应的小节中讨论)。特别是,我们不需要做任何额外的事情来实现变更跟踪。必要时,ODB将自动启动、停止和重置更改跟踪。以下示例使用
odb::vector
作为std::vector
的替换来说明这一点。
#pragma db object
class person
{
...
odb::vector<std::string> names;
};
person p; // No change tracking (not persistent).
p.names.push_back ("John Doe");
{
transaction t (db.begin ());
db.persist (p); // Start change tracking (persistent).
t.commit ();
}
p.names.push_back ("Johnny Doo");
{
transaction t (db.begin ());
db.update (p); // One INSERT; reset change state.
t.commit ();
}
p.names.modify (0) = "Doe, John"; // Instead of operator[].
p.names.pop_back ();
{
transaction t (db.begin ());
db.update (p); // One UPDATE, one DELETE; reset change state.
t.commit ();
}
{
transaction t (db.begin ());
auto_ptr<person> p1 (db.load<person> (...)); // Start change tracking.
p1->names.insert (p1->names.begin (), "Joe Do");
db.update (*p1); // One UPDATE, one INSERT; reset change state.
t.commit ();
}
{
transaction t (db.begin ());
db.erase (p); // One DELETE; stop change tracking (not persistent).
t.commit ();
}
- 变更跟踪的一个有趣方面是,当包含更新的事务稍后回滚时会发生什么。在这种情况下,虽然更改跟踪容器重置了更改状态(更新后),但实际更改并未提交到数据库。更改跟踪容器通过自动注册回滚回调来处理这种情况,然后,如果调用了回滚回调,则将容器标记为“完全更改”。在这种状态下,容器不再跟踪单个元素的更改,并且在更新时,会像普通容器一样回退到完整的状态更新。以下示例说明了这一点:
person p;
p.names.push_back ("John Doe");
{
transaction t (db.begin ());
db.persist (p); // Start change tracking (persistent).
t.commit ();
}
p.names.push_back ("Johnny Doo");
for (;;)
{
try
{
transaction t (db.begin ());
// First try: one INSERT.
// Next try: one DELETE, two INSERTs.
//
db.update (p); // Reset change state.
t.commit (); // If throws (rollback), mark as completely changed.
break;
}
catch (const odb::recoverable&)
{
continue;
}
}
- 关于变更跟踪容器与变更更新对象部分的交互,请参阅第9.4节“部分和变更跟踪容器”。
5.5 Change-Tracking vector
-
类模板
odb::vector
在<odb/vector.hxx>
中定义,是std::vector
的变更跟踪等价物。它是根据std::vector
实现的,可以隐式转换为const std::vector&
,也可以隐式构造const std::vector&
。特别是,这意味着我们可以在任何需要const std::vector&
的地方使用odb::vector
实例。此外,odb::vector
常量迭代器(const_iterator
)与std::vector
的类型相同。 -
odb::vector
为了存储更改状态,每个元素产生2位开销。它不能无序地存储在数据库中(第14.4.19节“无序”),但可以用作关系的反面(6.2“双向关系”)。在这种情况下,由于数据库中没有存储此类容器的状态,因此不执行更改跟踪。 -
更新
odb::vector
状态所需的数据库操作数量与std::vector
函数的复杂性非常吻合。特别是,在向量后面添加或删除元素(例如,使用push_back()
和pop_back()
),只需要执行一个数据库语句。相反,在向量中间的某个位置插入或擦除元素将需要对其后面的每个元素使用数据库语句。 -
odb::vector
复制了C++98/03和C++11标准中定义的大部分std::vector
接口。然而,为了支持更改跟踪,必须更改或禁用提供对元素的直接写入访问的功能和运算符。还添加了用于与std::vector
接口和控制更改跟踪状态的其他功能。以下列表总结了odb::vector
和std::vector
接口之间的差异。本清单中未提及的任何std::vector
函数或运算符在odb::vector
中都具有完全相同的签名和语义。禁用的函数和运算符显示为注释掉,后面是替换它们的函数/运算符。
namespace odb
{
template <class T, class A = std::allocator<T> >
class vector
{
...
// Element access.
//
//reference operator[] (size_type);
reference modify (size_type);
//reference at (size_type);
reference modify_at (size_type);
//reference front ();
reference modify_front ();
//reference back ();
reference modify_back ();
//T* data () noexcept;
T* modify_data () noexcept; // C++11 only.
// Iterators.
//
typedef typename std::vector<T, A>::const_iterator const_iterator;
class iterator
{
...
// Element Access.
//
//reference operator* () const;
const_reference operator* () const;
reference modify () const;
//pointer operator-> () const;
const_pointer operator-> () const;
//reference operator[] (difference_type);
const_reference operator[] (difference_type);
reference modify (difference_type) const;
// Interfacing with std::vector::iterator.
//
typename std::vector<T, A>::iterator base () const;
};
// Return std::vector iterators. The begin() functions mark
// all the elements as modified.
//
typename std::vector<T, A>::iterator mbegin ();
typename std::vector<T, A>::iterator mend ();
typename std::vector<T, A>::reverse_iterator mrbegin ();
typename std::vector<T, A>::reverse_iterator mrend ();
// Interfacing with std::vector.
//
vector (const std::vector<T, A>&);
vector (std::vector<T, A>&&); // C++11 only.
vector& operator= (const std::vector<T, A>&);
vector& operator= (std::vector<T, A>&&); // C++11 only.
operator const std::vector<T, A>& () const;
std::vector<T, A>& base ();
const std::vector<T, A>& base ();
// Change tracking.
//
bool _tracking () const;
void _start () const;
void _stop () const;
void _arm (transaction&) const;
};
}
- 以下示例突出了这两个接口之间的一些差异。
std::vector
版本被注释掉了。
#include <vector>
#include <odb/vector.hxx>
void f (const std::vector<int>&);
odb::vector<int> v ({1, 2, 3});
f (v); // Ok, implicit conversion.
if (v[1] == 2) // Ok, const access.
//v[1]++;
v.modify (1)++;
//v.back () = 4;
v.modify_back () = 4;
for (auto i (v.begin ()); i != v.end (); ++i)
{
if (*i != 0) // Ok, const access.
//*i += 10;
i.modify () += 10;
}
std::sort (v.mbegin (), v.mend ());
- 还要注意
odb::vector
实例的复制/移动构造和复制/移动赋值之间的细微区别。虽然复制/移动构造函数会复制/移动元素及其更改状态,但与此相反,分配会被跟踪为向量内容的任何其他更改。
5.6 使用自定义容器
-
虽然ODB运行时和配置文件库提供了对各种容器的支持,但也很容易持久化自定义容器类型或从其中创建更改跟踪版本。
-
为了实现这一点,您需要为您的容器实现
container_traits
类模板专门化。首先,确定容器类型的容器类型(有序、集合、多集合、映射或多映射)。然后使用公共ODB运行时库(libodb
)中的一个标准C++容器的专门化作为您自己实现的基础。 -
一旦容器的容器特征专门化准备就绪,您将需要使用
--ODB-epilogue
选项将其包含在ODB编译过程中,并使用--hxx-prologue
选项将其包括在生成的头文件中。例如,假设我们有一个哈希表容器,我们在hashtable-traits.hxx
文件中实现了特征专门化。然后,我们可以为此容器创建一个ODB编译器选项文件,并将其保存到hashtable.options
中:
# Options file for the hash table container.
#
--odb-epilogue '#include "hashtable-traits.hxx"'
--hxx-prologue '#include "hashtable-traits.hxx"'
- 现在,每当我们编译使用哈希表容器的头文件时,我们都可以指定以下命令行选项,以确保它被ODB编译器识别为容器,并且特征文件包含在生成的代码中:
--options-file hashtable.options