7 值类型
- 在第3.1节“概念和术语”中,我们已经讨论了值和值类型的概念,以及简单值和复合值之间的区别。本章更详细地介绍了简单值类型和复合值类型。
7.1 简单值类型
- 简单值类型是一种基本的C++类型或映射到单个数据库列的类类型。对于每个支持的数据库系统,ODB编译器为大多数基本的C++类型(如
int
或float
)以及某些类类型(如std::string
)提供了一个到合适数据库类型的默认映射。有关每个数据库系统的默认映射的更多信息,请参阅第二部分“数据库系统”。我们还可以使用db-type
pragma为这些或我们自己的值类型提供自定义映射(第14.3.1节,“type”)。
7.2 复合值类型
- 复合值类型是映射到多个数据库列的类或结构类型。要声明复合值类型,我们使用
db-value
pragma,例如:
#pragma db value
class basic_name
{
...
std::string first_;
std::string last_;
};
-
上述代码片段的完整版本和本节中提供的其他代码示例可以在
odb-examples
中的composite
示例中找到。 -
复合值类型不必定义默认构造函数,除非它被用作容器的元素。在这种情况下,默认构造函数可以设置为私有,前提是我们还将
<odb/core.hxx>
标头中定义的odb::access
类设置为该值类型的朋友。例如:
#include <odb/core.hxx>
#pragma db value
class basic_name
{
public:
basic_name (const std::string& first, const std::string& last);
...
private:
friend class odb::access;
basic_name () {} // Needed for storing basic_name in containers.
...
};
-
ODB编译器还需要访问复合值类型的非瞬态(第14.4.11节,“瞬态”)数据成员。它使用与第3.2节“声明持久对象和值”中讨论的持久类相同的机制。
-
复合值的成员可以是其他值类型(简单或复合)、容器(第5章,“容器”)和指向对象的指针(第6章,“关系”)。同样,复合值类型可以在对象成员中使用,作为容器的元素,也可以作为另一种复合值类型的基础。特别是,复合值类型可以用作集合容器中的元素类型(第5.2节,“集合和多集合容器”),也可以用作映射容器中的键类型(第5.3节,“映射和多映射容器”)。用作容器元素的复合值类型不能包含其他容器,因为不允许使用容器的容器。以下示例说明了一些可能的用例:
#pragma db value
class basic_name
{
...
std::string first_;
std::string last_;
};
typedef std::vector<basic_name> basic_names;
#pragma db value
class name_extras
{
...
std::string nickname_;
basic_names aliases_;
};
#pragma db value
class name: public basic_name
{
...
std::string title_;
name_extras extras_;
};
#pragma db object
class person
{
...
name name_;
};
- 复合值类型可以在持久类、视图或其他复合值中定义,甚至可以设置为私有,前提是我们将
odb::access
设置为包含类的朋友,例如:
#pragma db object
class person
{
...
#pragma db value
class name
{
...
std::string first_;
std::string last_;
};
name name_;
};
- 复合值类型也可以定义为C++类模板的实例化,例如:
template <typename T>
struct point
{
T x;
T y;
T z;
};
typedef point<int> int_point;
#pragma db value(int_point)
#pragma db object
class object
{
...
int_point center_;
};
- 请注意,这种复合值类型的数据库支持代码是在编译包含
db value
pragma的标头时生成的,而不是在编译包含模板定义或typedef名称的标头时。这允许我们使用在其他文件中定义的模板,例如在utility
标准头文件中定义了std::pair
:
#include <utility> // std::pair
typedef std::pair<std::string, std::string> phone_numbers;
#pragma db value(phone_numbers)
#pragma db object
class person
{
...
phone_numbers phone_;
};
- 我们还可以在数据库查询中使用复合值类型的数据成员(第4章,“查询数据库”)。对于持久类中的每个复合值,查询类定义了一个嵌套成员,其中包含与值类型中的数据成员对应的成员。然后,我们可以使用成员访问语法(.)来引用值类型中的数据成员。例如,上面显示的
person
对象的查询类包含name
成员(其名称来源于name_data
成员),而name
成员又包含extras
成员(其姓名来源于复合值类型的name::extras-data
成员)。对于嵌套的复合值类型,此过程会递归进行,因此,我们可以在数据库中查询person
对象时使用query::name.extras.nickname
表达式。例如:
typedef odb::query<person> query;
typedef odb::result<person> result;
transaction t (db.begin ());
result r (db.query<person> (
query::name.extras.nickname == "Squeaky"));
...
t.commit ();
7.2.1 复合对象ID
- 对象id可以是复合值类型,例如:
#pragma db value
class name
{
...
std::string first_;
std::string last_;
};
#pragma db object
class person
{
...
#pragma db id
name name_;
};
- 但是,可用作对象
id
的值类型有许多限制。这样的值类型不能有容器、对象指针或只读数据成员。它还必须是默认可构造的、可复制可构造的和可复制可分配的。此外,如果将此复合值类型用作对象id的持久类启用了会话支持(第11章,“会话”),则它还必须实现小于比较运算符(operator<
)。
7.2.2 复合值列和表名称
- 为简单值类型的数据成员自定义列名很简单:我们只需使用
db-column
pragma指定所需的名称(第14.4.9节,“column”)。对于复合值类型,事情稍微复杂一些,因为它们被映射到多列。考虑以下示例:
#pragma db value
class name
{
...
std::string first_;
std::string last_;
};
#pragma db object
class person
{
...
#pragma db id auto
unsigned long id_;
name name_;
};
first_
和last_
成员的列名是通过使用经过净化的person::name_
成员的名称作为前缀,使用值类型(first_
和last_
)中的成员名称作为后缀来构造的。因此,上述类的数据库模式如下:
CREATE TABLE person (
id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
name_first TEXT NOT NULL,
name_last TEXT NOT NULL);
- 我们可以使用
db column
pragma自定义前缀和后缀,如下例所示:
#pragma db value
class name
{
...
#pragma db column("first_name")
std::string first_;
#pragma db column("last_name")
std::string last_;
};
#pragma db object
class person
{
...
#pragma db column("person_")
name name_;
};
- 数据库架构更改如下:
CREATE TABLE person (
id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
person_first_name TEXT NOT NULL,
person_last_name TEXT NOT NULL);
- 我们还可以将列前缀设为空,例如:
#pragma db object
class person
{
...
#pragma db column("")
name name_;
};
- 这将导致以下架构:
CREATE TABLE person (
id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL);
-
当复合值类型用作容器的元素时,同样的原则也适用,除了使用
db-value_column
(第14.4.36节,“value_column”)或db-key_column
(第14.4.45节,“key_column。 -
当复合值类型包含容器时,会使用额外的表来存储其元素(第5章,“容器”)。此类表的名称的构造方式类似于列名,除了默认情况下对象名和成员名都用作前缀。例如:
#pragma db value
class name
{
...
std::string first_;
std::string last_;
std::vector<std::string> nicknames_;
};
#pragma db object
class person
{
...
name name_;
};
- 相应的数据库架构如下:
CREATE TABLE person_name_nicknames (
object_id BIGINT UNSIGNED NOT NULL,
index BIGINT UNSIGNED NOT NULL,
value TEXT NOT NULL)
CREATE TABLE person (
id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
name_first TEXT NOT NULL,
name_last TEXT NOT NULL);
- 要自定义容器表名,我们可以使用
db-table
pragma(第14.4.20节,“table”),例如:
#pragma db value
class name
{
...
#pragma db table("nickname")
std::vector<std::string> nicknames_;
};
#pragma db object
class person
{
...
#pragma db table("person_")
name name_;
};
- 这将导致以下架构更改:
CREATE TABLE person_nickname (
object_id BIGINT UNSIGNED NOT NULL,
index BIGINT UNSIGNED NOT NULL,
value TEXT NOT NULL)
- 与列类似,我们可以将表前缀设为空。
7.3 指针和NULL
值语义
-
关系数据库系统有一个特殊的NULL值的概念,用于表示列中没有有效值。虽然默认情况下ODB将值映射到不允许NULL值的列,但可以使用
db-NULL
pragma(第14.4.6节,“NULL/not_NULL”)进行更改。 -
为了正确支持
NULL
语义,C++值类型必须具有NULL
值的概念或类似的特殊状态概念。大多数基本的C++类型,如int或std::string
,都没有这个概念,因此不能直接用于启用NULL
的数据成员(在从数据库加载NULL
值的情况下,这些数据成员将被默认初始化)。 -
为了允许将不支持
NULL
语义的值类型轻松转换为支持NULL
语义,ODB提供了ODB::NULL
类模板。它允许我们将现有的C++类型包装到一个类似容器的类中,该类可以是NULL,也可以包含包装类型的值。ODB还会自动为ODB::NULL
类型的数据成员启用NULL
值。例如:
#include <odb/nullable.hxx>
#pragma db object
class person
{
...
std::string first_; // TEXT NOT NULL
odb::nullable<std::string> middle_; // TEXT NULL
std::string last_; // TEXT NOT NULL
};
odb::null
类模板在<odb/nullable.hxx>
头文件中定义,并具有以下接口:
namespace odb
{
template <typename T>
class nullable
{
public:
typedef T value_type;
nullable ();
nullable (const T&);
nullable (const nullable&);
template <typename Y> explicit nullable (const nullable<Y>&);
nullable& operator= (const T&);
nullable& operator= (const nullable&);
template <typename Y> nullable& operator= (const nullable<Y>&);
void swap (nullable&);
// Accessor interface.
//
bool null () const;
T& get ();
const T& get () const;
// Pointer interface.
//
operator bool_convertible () const;
T* operator-> ();
const T* operator-> () const;
T& operator* ();
const T& operator* () const;
// Reset to the NULL state.
//
void reset ();
};
}
- 以下示例显示了如何使用此接口:
nullable<string> ns;
// Using the accessor interface.
//
if (ns.null ())
{
s = "abc";
}
else
{
string s (ns.get ());
ns.reset ();
}
// The same using the pointer interface.
//
if (ns)
{
s = "abc";
}
else
{
string s (*ns);
ns.reset ();
}
-
odb::null
类模板要求包装类型具有公共默认构造函数和复制构造函数以及复制赋值运算符。还要注意,odb::null
实现并不是最有效的,因为它总是包含包装类型的完全构造值。对于C++基本类型或std::string
等简单类型,这通常不是问题。然而,对于更复杂的类型,这可能会成为一个问题。在这种情况下,您可能需要考虑使用可选值概念的更有效实现,例如Boost中的可选类模板(第23.4节,“可选库”)。 -
另一个常见的C++值表示形式是指针,它可以是
NULL
。ODB将自动处理指向值的指针数据成员,但是,它不会像ODB::nullable
那样自动为这些数据成员启用NULL
值。相反,如果需要NULL
值,我们需要使用db-NULL
pragma显式启用它。例如:
#pragma db object
class person
{
...
std::string first_;
#pragma db null
std::auto_ptr<std::string> middle_;
std::string last_;
};
-
ODB编译器内置了对使用
std::auto_ptr
、std::unique_ptr
(C++11)和shared_ptr
(TR1或C++11)作为值指针的支持。此外,ODB配置文件库可用于常用的框架和库(如Boost和Qt),为这些框架和库中的智能指针提供支持(第三部分,“配置文件”)。 -
ODB还支持复合值的
NULL
语义。在关系数据库中,对于该复合值的所有简单数据成员,NULL
复合值被转换为NULL
值。例如:
#pragma db value
struct name
{
std::string first_;
odb::nullable<std::string> middle_;
std::string last_;
};
#pragma db object
class person
{
...
odb::nullable<name> name_;
};
- ODB不支持容器的
NULL
语义。这也意味着包含容器的复合值不能为NULL
。考虑到这一限制,我们仍然可以在容器类型的数据成员中使用智能指针。唯一的限制是这些指针不能为NULL
。例如:
#pragma db object
class person
{
...
std::auto_ptr<std::vector<std::string> > aliases_;
};