10 视图
-
ODB视图是一个C++类或结构类型,它体现了一个或多个持久对象或数据库表或本机SQL查询执行结果的轻量级只读投影。
-
视图的一些常见应用包括从数据库表的对象或列加载数据成员的子集,执行和处理任意SQL查询的结果,包括聚合查询和存储过程调用,以及使用对象关系或自定义连接条件连接多个对象和/或数据库表。
-
许多关系数据库也定义了视图的概念。但是请注意,ODB视图不会映射到数据库视图。相反,默认情况下,ODB视图被映射到SQL SELECT查询。但是,如果需要,可以很容易地创建基于数据库视图的ODB视图。
-
通常,视图是根据其他持久实体定义的,如持久对象、数据库表、序列等。因此,在我们检查第一个视图之前,我们需要定义一些持久对象和一个数据库表。我们将在本章的示例中使用此模型。在这里,我们假设您熟悉ODB对象关系支持(第6章,“关系”)。
#pragma db object
class country
{
...
#pragma db id
std::string code_; // ISO 2-letter country code.
std::string name_;
};
#pragma db object
class employer
{
...
#pragma db id
unsigned long id_;
std::string name_;
};
#pragma db object
class employee
{
...
#pragma db id
unsigned long id_;
std::string first_;
std::string last_;
unsigned short age_;
shared_ptr<country> residence_;
shared_ptr<country> nationality_;
shared_ptr<employer> employed_by_;
};
- 除了这些对象,我们还有一个未映射到任何持久类的遗留
employee_extra
表。其定义如下:
CREATE TABLE employee_extra(
employee_id INTEGER NOT NULL,
vacation_days INTEGER NOT NULL,
previous_employer_id INTEGER)
-
上述持久对象和数据库表以及本章中显示的许多视图都是基于odb发行版的
odb-examples
中的view
示例。 -
为了声明一个视图,我们使用
db-view
pragma,例如:
#pragma db view object(employee)
struct employee_name
{
std::string first;
std::string last;
};
-
上面的示例显示了我们可以创建的最简单的视图之一。它有一个关联的对象(
employee
),其目的是提取员工的名字和姓氏,而不加载任何其他数据,如引用的country
和employer
对象。 -
视图使用与持久对象相同的查询工具(第4章,“查询数据库”)。因为对查询的支持是可选的,没有这种支持就无法使用视图,所以您需要使用
--generate-query
ODB编译器选项编译任何定义视图的标头。 -
为了查询数据库中的视图,我们使用
database::query()
、database::query_one()
或database::query_value()
函数,其方式与我们使用它们查询数据库中对象的方式完全相同。例如,以下代码片段显示了如何查找所有31岁以下员工的姓名:
typedef odb::query<employee_name> query;
typedef odb::result<employee_name> result;
transaction t (db.begin ());
result r (db.query<employee_name> (query::age < 31));
for (result::iterator i (r.begin ()); i != r.end (); ++i)
{
const employee_name& en (*i);
cout << en.first << " " << en.last << endl;
}
t.commit ();
- 视图可以定义为一个或多个对象、一个或更多个表、对象和表的组合的投影,也可以是自定义SQL查询的结果。以下部分将更详细地讨论每种视图。
10.1 对象视图
- 为了将一个或多个对象与视图相关联,我们使用
db object
pragma(第14.2.1节,“对象”)。在本章的引言中,我们已经看到了一个简单的单对象视图。为了关联第二个和后续的对象,我们对每个额外的对象重复db object
pragma,例如:
#pragma db view object(employee) object(employer)
struct employee_employer
{
std::string first;
std::string last;
std::string name;
};
db object
pragma的完整语法如下所示:
object(name [= alias] [join-type] [: join-condition])
-
名称部分是之前定义的潜在限定持久类名。可选的别名部分为该对象提供别名。如果提供了别名,则该别名将在多个上下文中使用,而不是对象的非限定名称。我们将在下面介绍每种情况时进一步讨论别名。可选的连接类型部分指定了此对象的关联方式。它可以是左、右、全、内和交叉,默认为左。最后,可选的连接条件部分提供了将此对象与任何先前关联的对象相关联的标准,或者如我们将在第10.4节“混合视图”中看到的表格。请注意,虽然第一个关联的对象可以有别名,但它不能有联接类型或条件。
-
对于每个后续的关联对象,ODB编译器都需要一个连接条件,有几种方法可以指定它。最简单的方法是完全省略它,让ODB编译器尝试自动提出连接条件。为此,ODB编译器将检查每个先前关联的对象,以查找这些对象与关联对象之间可能存在的对象关系(第6章,“关系”)。如果这样的关系存在并且是明确的,也就是说只有一个这样的关系,那么ODB编译器将自动使用它来为这个对象提出连接条件。这正是前一个示例中发生的情况:
employee
和employer
对象之间只有一个关系(employee::employed_by
)。 -
另一方面,考虑一下这种视图:
#pragma db view object(employee) object(country)
struct employee_residence
{
std::string first;
std::string last;
std::string name;
};
- 虽然
country
和employee
之间存在关系,但这种关系是模糊的。它可以是employee::residence_
(这是我们想要的),也可以是employee::nationality_
(我们不想要)。因此,在编译上述视图时,ODB编译器将发出一个错误,指示对象关系不明确。为了解决这种歧义,我们可以明确地将用于创建连接条件的对象关系指定为相应数据成员的名称。以下是我们如何修复employee_residence
视图:
#pragma db view object(employee) object(country: employee::residence_)
struct employee_residence
{
std::string first;
std::string last;
std::string name;
};
- 可以使用不同的连接条件多次将同一对象与单个视图相关联。然而,在这种情况下,我们必须使用别名为每个关联分配不同的名称。例如:
#pragma db view object(employee) \
object(country = res_country: employee::residence_) \
object(country = nat_country: employee::nationality_)
struct employee_country
{
...
};
-
请注意,在此视图中正确定义数据成员需要使用我们尚未涉及的机制。我们很快就会看到如何做到这一点。
-
如果我们为一个对象分配一个别名,并在其中一个连接条件中引用该对象的数据成员,我们必须使用非限定别名而不是潜在的限定对象名。例如:
#pragma db view object(employee = ee) object(country: ee::residence_)
struct employee_residence
{
...
};
- 指定联接条件的最后一种方法是提供自定义查询表达式。如果您想使用不涉及对象关系的条件来关联对象,则此方法非常有用。例如,考虑从本章开头添加出生国成员的修改后的
employee
对象。出于某种原因,我们决定不使用与country
对象的关系,就像我们在居住地和国籍方面所做的那样。
#pragma db object
class employee
{
...
std::string birth_place_; // Country name.
};
- 如果我们现在想创建一个返回员工出生国家代码的视图,那么在关联
country
对象时,我们必须使用自定义连接条件。例如:
#pragma db view object(employee) \
object(country: employee::birth_place_ == country::name_)
struct employee_birth_code
{
std::string first;
std::string last;
std::string code;
};
-
自定义连接条件中的查询表达式的语法与用于查询数据库对象的查询工具中的语法相同(第4章,“查询数据库”),除了对于查询成员,我们直接引用对象成员,而不是使用
odb::query<object>::member
。 -
查看我们迄今为止定义的视图,您可能想知道ODB编译器如何知道哪些视图数据成员对应于哪些对象数据成员。虽然名称相似,但并不完全相同,例如
employee_name::first
和employee::first_
。 -
与连接条件一样,当涉及到关联数据成员时,ODB编译器会尝试自动执行此操作。它首先搜索所有关联的对象以查找精确的名称匹配。如果没有找到匹配项,则ODB编译器会比较所谓的公共名称。通过删除常见的成员名称修饰(如前导和尾随下划线、
m_
前缀等)来获得成员的公共名称。在这两个搜索中,ODB编译器还确保两个成员的类型相同或兼容。 -
如果上述搜索之一返回了匹配,并且是明确的,即只有一个匹配,则ODB编译器将自动关联这两个成员。另一方面,如果找不到匹配项或匹配项不明确,ODB编译器将发出错误。为了关联两个不同名称的成员或解决歧义,我们可以使用
db column
pragma显式指定成员关联(第14.4.9节,“列”)。例如:
#pragma db view object(employee) object(employer)
struct employee_employer
{
std::string first;
std::string last;
#pragma db column(employer::name_)
std::string employer_name;
};
-
如果对象数据成员使用
db type
pragma指定SQL类型(第14.4.3节,“type”),则此类型也用于关联的视图数据成员。 -
还要注意,与连接条件类似,如果我们为对象分配一个别名,并在
db column
语法中引用此对象的数据成员,那么我们必须使用非限定别名而不是潜在的限定对象名称。例如:
#pragma db view object(employee) \
object(country = res_country: employee::residence_) \
object(country = nat_country: employee::nationality_)
struct employee_country
{
std::string first;
std::string last;
#pragma db column(res_country::name_)
std::string res_country_name;
#pragma db column(nat_country::name_)
std::string nat_country_name;
};
- 除了只指定对象成员外,我们还可以在
db column
pragma中指定一个+表达式。+表达式由字符串文字和使用+运算符连接的对象成员引用组成。它主要用于基于SQL聚合函数定义聚合视图,例如:
#pragma db view object(employee)
struct employee_count
{
#pragma db column("count(" + employee::id_ + ")")
std::size_t count;
};
- 在数据库中查询视图时,我们可能希望根据与此视图关联的对象提供额外的查询条件。为了支持这一点,视图为所有关联的对象定义了查询成员,这允许我们使用
odb::query<view>::member
表达式引用这些对象的成员。这类似于在数据库中查询对象时,我们如何使用odb::query<object>::member
表达式引用对象成员。例如:
typedef odb::query<employee_count> query;
transaction t (db.begin ());
// Find the number of employees with the Doe last name. Result of this
// aggregate query contains only one element so use the query_value()
// shortcut function.
//
employee_count ec (
db.query_value<employee_count> (query::last == "Doe"));
cout << ec.count << endl;
t.commit ();
-
在上述查询中,我们使用关联
employee
对象中的姓氏数据成员,仅计算具有特定姓名的员工。 -
当一个视图只有一个关联对象时,与此对象对应的查询成员直接在
odb::query<view>
作用域中定义。例如,在上面的示例中,我们将姓氏成员称为odb::query<employee_count>::last
。但是,如果视图有多个关联对象,则与每个此类对象对应的查询成员将在以该对象命名的嵌套范围内定义。例如,再次考虑employee_employer
视图:
#pragma db view object(employee) object(employer)
struct employee_employer
{
std::string first;
std::string last;
#pragma db column(employer::name_)
std::string employer_name;
};
- 现在,为了从
employee
对象中引用姓氏数据成员,我们使用odb::query<...>::employee::last
一句话。同样,为了引用雇主名称,我们使用odb::query<...>::employer::name
表达。例如:
typedef odb::result<employee_employer> result;
typedef odb::query<employee_employer> query;
transaction t (db.begin ());
result r (db.query<employee_employer> (
query::employee::last == "Doe" &&
query::employer::name == "Simple Tech Ltd"));
for (result::iterator i (r.begin ()); i != r.end (); ++i)
cout << i->first << " " << i->last << " " << i->employer_name << endl;
t.commit ();
- 如果我们为对象分配一个别名,那么这个别名将用于命名查询成员范围,而不是对象名称。例如,再次考虑
employee_country
视图:
#pragma db view object(employee) \
object(country = res_country: employee::residence_) \
object(country = nat_country: employee::nationality_)
struct employee_country
{
...
};
- 以及一个返回具有相同居住国和国籍的所有员工的查询:
typedef odb::query<employee_country> query;
typedef odb::result<employee_country> result;
transaction t (db.begin ());
result r (db.query<employee_country> (
query::res_country::name == query::nat_country::name));
for (result::iterator i (r.begin ()); i != r.end (); ++i)
cout << i->first << " " << i->last << " " << i->res_country_name << endl;
t.commit ();
- 还要注意,与对象查询成员不同,视图查询成员不支持引用相关对象中的成员。例如,以下查询无效:
typedef odb::query<employee_name> query;
typedef odb::result<employee_name> result;
transaction t (db.begin ());
result r (db.query<employee_name> (
query::employed_by->name == "Simple Tech Ltd"));
t.commit ();
-
为了获得这种行为,我们需要将
employer
对象与此视图相关联,然后使用query::employer::name
表达式,而不是query::employed_by->name
。 -
正如我们上面讨论的那样,如果指定了对象别名,则在连接条件中使用对象别名而不是对象名称,在
db column
pragma中使用数据成员引用,以及命名查询成员范围。对象别名也用作ODB编译器生成的底层SELECT语句中的表名别名。通常,您不会直接在对象视图中使用表别名。但是,如果出于某种原因,您需要直接引用表列,例如作为本机查询表达式的一部分,并且需要用表限定该列,那么您将需要使用表别名。
10.2 对象加载视图
-
对象视图的一个特殊变体是对象加载视图。对象加载视图允许我们加载一个或多个完整的对象,而不是数据成员的子集,或者除了数据成员的一个子集之外,还可以加载一个或者多个完整对象。虽然我们通常可以通过调用
database::load()
来实现相同的最终结果,但使用视图有几个优点。 -
如果我们需要加载多个对象,那么使用视图允许我们通过执行单个SELECT语句来实现这一点,而不是在
load()
的情况下对每个对象执行一个SELECT语句。如果我们要使用的查询条件涉及其他可能无关的对象,则视图也可用于仅加载单个对象。我们将在本节的其余部分研究这些和其他场景的具体示例。 -
为了将完整的对象作为视图的一部分加载,我们使用指向对象类型的指针的数据成员,就像对象关系一样(第6章,“关系”)。例如,我们可以用一条语句加载上一节中的
employee
和employer
对象:
#pragma db view object(employee) object(employer)
struct employee_employer
{
shared_ptr<employee> ee;
shared_ptr<employer> er;
};
- 我们使用对象加载视图,就像使用其他视图一样。在查询结果中,正如我们所料,指针数据成员指向加载的对象。例如:
typedef odb::query<employee_employer> query;
transaction t (db.begin ());
for (const employee_employer& r:
db.query<employee_employer> (query::employee::age < 31))
{
cout << r.ee->age () << " " << r.er->name () << endl;
}
t.commit ();
- 再举一个例子,考虑一个使用基于其员工的某些条件加载
employer
对象的查询。例如,我们想找到所有雇用65岁以上员工的雇主。我们可以使用此对象加载视图来实现这样的查询(请注意第10.5节“查看查询条件”中稍后讨论的不同结果修饰符):
#pragma db view object(employer) object(employee) query(distinct)
struct employer_view
{
shared_ptr<employer> er;
};
- 这就是我们如何利用这一观点来找到所有雇用老年人的雇主:
typedef odb::query<employer_view> query;
db.query<employer_view> (query::employee::age > 65)
- 我们甚至可以使用对象加载视图来加载完全无关的(从ODB对象关系的角度来看)对象。例如,以下视图将加载与国家名称相同的所有雇主(请注意
inner
联接类型):
#pragma db view object(employer) \
object(country inner: employer::name == country::name)
struct employer_named_country
{
shared_ptr<employer> e;
shared_ptr<country> c;
};
- 除了对象指针之外,对象加载视图还可以包含普通数据成员。例如,如果我们只对上述视图中的国家代码感兴趣,那么我们可以像这样重新实现它:
#pragma db view object(employer) \
object(country inner: employer::name == country::name)
struct employer_named_country
{
shared_ptr<employer> e;
std::string code;
};
- 对象加载视图也有一些规则和限制。首先,数据成员中指向的对象必须与视图相关联。此外,如果关联的对象有别名,则数据成员名称必须与别名相同(更确切地说,从数据成员派生的公共名称必须与该别名匹配;这意味着我们可以使用普通的数据成员装饰,如尾随下划线等,有关公共名称的更多信息,请参阅上一节)。以下视图说明了别名作为数据成员名称的使用:
#pragma db view object(employee) \
object(country = res: employee::residence_) \
object(country = nat: employee::nationality_)
struct employee_country
{
shared_ptr<country> res;
shared_ptr<country> nat_;
};
- 最后,对象指针必须是视图的直接数据成员。例如,不支持将包含指针的复合值用作视图数据成员。还要注意,根据您使用的连接类型,一些结果指针可能为NULL。
到目前为止,我们一直在视图中使用shared_ptr
作为对象指针。我们可以使用其他指针吗,比如unique_ptr
或原始指针?为了回答这个问题,我们首先需要讨论可能位于视图加载的对象内部的对象指针会发生什么。作为一个具体的例子,让我们从本节开始重新审视employee_employer
视图:
#pragma db view object(employee) object(employer)
struct employee_employer
{
shared_ptr<employee> ee;
shared_ptr<employer> er;
};
-
此视图加载两个对象:
employee
和employer
。然而,employee
对象还包含一个指向雇主的指针(请参阅employee_by_
数据成员)。事实上,这是视图加载的同一对象,因为雇主使用这种相同的关系与视图相关联(ODB会自动使用它,因为它是唯一的一个)。加载这样一个视图的正确结果就是清除:er
和er->employee_by_
都必须指向(或共享)同一个实例。 -
就像通过
database
类函数加载对象一样,视图在会话的对象缓存的帮助下实现了只加载同一对象的单个实例的正确行为(第11章,“会话”)。事实上,如果没有当前会话,并且视图加载的对象也由其他对象之一间接加载,则对象加载视图通过抛出session_required异常来强制执行此操作。如果此类对象禁用了会话支持,ODB编译器也会发出诊断(第14.1.10节,“会话”)。 -
有了这种理解,我们现在可以提供使用
employee_employer
视图的事务的正确实现:
typedef odb::query<employee_employer> query;
transaction t (db.begin ());
odb::session s;
for (const employee_employer& r:
db.query<employee_employer> (query::employee::age < 31))
{
assert (r.ee->employed_by_ == r.er);
cout << r.ee->age () << " " << r.er->name () << endl;
}
t.commit ();
-
因此,始终从与视图的所有渴望关系中加载所有对象似乎是合乎逻辑的。毕竟,这将导致它们都加载一条语句。虽然这在理论上是正确的,但现实情况却稍显微妙。如果对象很可能已经加载并位于缓存中,那么不将对象作为视图的一部分加载(因此不从数据库中获取其所有数据)可能会带来更好的性能。
-
现在,我们还可以回答在对象加载视图中可以使用哪些指针的问题。从上面的讨论中可以清楚地看出,如果我们正在加载的一个对象也是我们在加载的另一个对象内部关系的一部分,那么我们应该使用某种形式的共享所有权指针。然而,如果不涉及任何关系,例如在上面的
employee_named_country
和employee_country
视图中,那么我们可以使用唯一的所有权指针,如unique_ptr
。 -
还要注意,您对指针类型的选择可能会受到分配给对象的“官方”对象指针类型的限制(第3.3节,“对象和视图指针”)。例如,如果对象指针类型为
shared_ptr
,则无法使用unique_ptr
将此类对象加载到视图中,因为从shared_ptr
初始化unique_prt
将是一个错误。 -
除非您想执行自己的对象清理,否则视图中的原始对象指针并不是特别有用。然而,它们确实有一个特殊的语义:如果原始指针用作视图成员,那么在创建新实例之前,实现将检查该成员是否为NULL。如果不是,那么假设它指向一个现有的实例,实现将把数据加载到其中,而不是创建一个新的实例。此特殊功能的主要用途是通过值加载实现检测NULL值的能力。
-
为了说明此功能,请考虑以下按值加载员工居住国家的视图:
#pragma db view object(employee) \
object(country = res: employee::residence_) transient
struct employee_res_country
{
typedef country* country_ptr;
#pragma db member(res_) virtual(country_ptr) get(&this.res) \
set(this.res_null = ((?) == nullptr))
country res;
bool res_null;
};
-
在这里,我们使用虚拟数据成员(第14.4.13节,“虚拟”)向视图添加对象指针成员。它的访问器表达式返回指向
res
成员的指针,以便实现可以将数据加载到其中。修饰符表达式检查传递的指针以初始化NULL值指示符。这里,可以传递给修饰符表达式的两个可能值是我们之前从访问器返回的res成员的地址和NULL(严格来说,还有第三种可能性:在会话缓存中找到的对象的地址)。 -
如果我们对NULL指示符不感兴趣,那么上述视图可以简化为:
#pragma db view object(employee) \
object(country = res: employee::residence_) transient
struct employee_res_country
{
typedef country* country_ptr;
#pragma db member(res_) virtual(country_ptr) get(&this.res) set()
country res;
};
-
也就是说,我们指定了一个空的修饰符表达式,导致值被忽略。
-
作为按值加载的另一个例子,考虑一个允许我们将对象加载到已在视图外部分配的现有实例中的视图:
#pragma db view object(employee) \
object(country = res: employee::residence_) \
object(country = nat: employee::nationality_)
struct employee_country
{
employee_country (country& r, country& n): res (&r), nat (&n) {}
country* res;
country* nat;
};
- 以下是我们如何使用此视图:
typedef odb::result<employee_country> result;
transaction t (db.begin ());
result r (db.query<employee_country> (...);
for (result::iterator i (r.begin ()); i != r.end (); ++i)
{
country res, nat;
employee_country v (res, nat);
i.load (v);
if (v.res != nullptr)
... // Result is in res.
if (v.nat != nullptr)
... // Result is in nat.
}
t.commit ();
- 作为按值加载的最后一个示例,考虑以下视图,该视图实现了稍微更高级的逻辑:如果对象已经在会话缓存中,则它将视图中的指针数据成员(
er_p
)设置为该值。否则,它会将数据加载到by-value实例(er
)中。我们还可以检查指针数据成员是否指向实例,以区分这两种结果。我们可以检查它的nullptr
以检测NULL值。
#pragma db view object(employer)
struct employer_view
{
// Since we may be getting the pointer as both smart and raw, we
// need to create a bit of support code to use in the modifier
// expression.
//
void set_er (employer* p) {er_p = p;} // &er or NULL.
void set_er (shared_ptr<employer> p) {er_p = p.get ();} // From cache.
#pragma db get(&this.er) set(set_er(?))
employer* er_p;
#pragma db transient
employer er;
// Return-by-value support (e.g., query_value()).
//
employer_view (): er_p (0) {}
employer_view (const employer_view& x)
: er_p (x.er_p == &x.er ? &er : x.er_p), er (x.er) {}
};
-
我们可以使用带有多态对象的对象加载视图(第8.2节,“多态继承”)。但是请注意,当通过视图中的基指针加载派生对象时,将执行单独的语句来加载对象的动态部分。多态对象不支持按值加载。
-
我们还可以使用没有id的对象加载视图(第14.1.6节,“no_id”)。但是,请注意,对于此类对象,不会自动检测NULL值(因为没有主键,否则保证不为NULL,因此可能没有一列作为此检测的基础)。解决此限制的方法是在对象旁边加载一个非NULL列,该列将用作指示器。例如:
#pragma db object no_id
class object
{
...
int n; // NOT NULL
std::string s;
};
#include <odb/nullable.hxx>
#pragma db view object(object)
struct view
{
odb::nullable<int> n; // If 'n' is NULL, then, logically, so is 'o'.
unique_ptr<object> o;
};
10.3 表视图
- 表视图类似于对象视图,除了它基于一个或多个数据库表而不是持久对象。表视图主要在处理未映射到持久类的ad-hoc表时非常有用。
为了将一个或多个表与视图相关联,我们使用db table
pragma(第14.2.2节,“table”)。为了关联第二个表和后续表,我们对每个额外的表重复db table
pragma。例如,以下视图基于我们在本章开头定义的employee_extra
遗留表。
#pragma db view table("employee_extra")
struct employee_vacation
{
#pragma db column("employee_id") type("INTEGER")
unsigned long employee_id;
#pragma db column("vacation_days") type("INTEGER")
unsigned short vacation_days;
};
- 除了
db table
pragma中的表名外,我们还必须为每个视图数据成员指定列名。请注意,与对象视图不同,ODB编译器不会尝试自动为表视图生成列名。此外,我们也不能使用对对象成员的引用,因为表视图中没有关联的对象。相反,实际的列名或列表达式必须指定为字符串文字。列名也可以用“table.column
”形式中的表名限定,或者如果表或列名包含句点,则可以用“table”.“column”
形式限定。以下示例说明了列表达式的使用:
#pragma db view table("employee_extra")
struct employee_max_vacation
{
#pragma db column("max(vacation_days)") type("INTEGER")
unsigned short max_vacation_days;
};
- 关联的表名和列名都可以用数据库模式限定,例如:
#pragma db view table("hr.employee_extra")
struct employee_max_vacation
{
#pragma db column("hr.employee_extra.vacation_days") type("INTEGER")
unsigned short vacation_days;
};
-
有关数据库模式和限定名格式的更多信息,请参阅第14.1.8节“模式”。
-
还要注意,在上述示例中,我们为每个列指定了SQL类型,以确保ODB编译器了解数据库模式中指定的实际类型。这是获得正确和最佳生成代码所必需的。
-
db table
pragma的完整语法类似于db object
pragma,如下所示:
table("name" [= "alias"] [join-type] [: join-condition])
-
名称部分是数据库表名。可选的别名部分为该表提供了一个别名。如果提供了别名,则每当使用对表的引用时,都必须使用别名而不是表。可能需要这种引用的上下文包括连接条件(下面讨论)、列名和查询表达式。可选的联接类型部分指定了此表的关联方式。它可以是
left
,right
,full
,inner
, 和cross
,默认为left
。最后,可选的连接条件部分提供了将此表与任何先前关联的表或对象相关联的标准,正如我们将在第10.4节“混合视图”中看到的那样。请注意,虽然第一个关联的表可以有别名,但它不能有联接类型或条件。 -
与对象视图类似,对于每个后续的关联表,ODB编译器都需要一个连接条件。然而,与对象视图不同,对于表视图,ODB编译器不会尝试自动生成一个。此外,我们也不能使用对与对象关系对应的对象成员的引用,因为表视图中没有关联的对象。相反,对于每个后续的关联表,必须将联接条件指定为自定义查询表达式。虽然查询表达式的语法与用于在数据库中查询对象的查询工具中的语法相同(第4章,“查询数据库”),但表的连接条件通常指定为包含本机SQL查询表达式的单个字符串文字。
-
作为多表视图的一个示例,考虑我们除了
employee_extra
之外定义的employee_health
表:
CREATE TABLE employee_health(
employee_id INTEGER NOT NULL,
sick_leave_days INTEGER NOT NULL)
- 给定这两个表,我们现在可以定义一个视图,返回每个员工的假期和病假信息:
#pragma db view table("employee_extra" = "extra") \
table("employee_health" = "health": \
"extra.employee_id = health.employee_id")
struct employee_leave
{
#pragma db column("extra.employee_id") type("INTEGER")
unsigned long employee_id;
#pragma db column("vacation_days") type("INTEGER")
unsigned short vacation_days;
#pragma db column("sick_leave_days") type("INTEGER")
unsigned short sick_leave_days;
};
- 查询数据库中的表视图与对象视图相同,只是我们只能使用本机查询表达式。例如:
typedef odb::query<employee_leave> query;
typedef odb::result<employee_leave> result;
transaction t (db.begin ());
unsigned short v_min = ...
unsigned short l_min = ...
result r (db.query<employee_leave> (
"vacation_days > " + query::_val(v_min) + "AND"
"sick_leave_days > " + query::_val(l_min)));
t.commit ();
10.4 混合视图
- 混合视图具有关联的对象和表。作为混合视图的第一个示例,让我们改进上一节中的
employee_vcation
,返回员工的名字和姓氏,而不是员工id。为了实现这一点,我们必须将employee
对象和employee_extra
表与视图相关联:
#pragma db view object(employee) \
table("employee_extra" = "extra": "extra.employee_id = " + employee::id_)
struct employee_vacation
{
std::string first;
std::string last;
#pragma db column("extra.vacation_days") type("INTEGER")
unsigned short vacation_days;
};
- 在查询数据库以获取混合视图时,我们可以对查询表达式中涉及对象成员的部分使用查询成员,但必须对涉及表列的部分使用本机语法。例如:
typedef odb::query<employee_vacation> query;
typedef odb::result<employee_vacation> result;
transaction t (db.begin ());
result r (db.query<employee_vacation> (
(query::last == "Doe") + "AND extra.vacation_days <> 0"));
for (result::iterator i (r.begin ()); i != r.end (); ++i)
cout << i->first << " " << i->last << " " << i->vacation_days << endl;
t.commit ();
- 再举一个例子,考虑一个更高级的视图,它通过一个遗留表将两个对象关联起来。此视图允许我们查找每个员工的前雇主名称:
#pragma db view object(employee) \
table("employee_extra" = "extra": "extra.employee_id = " + employee::id_) \
object(employer: "extra.previous_employer_id = " + employer::id_)
struct employee_prev_employer
{
std::string first;
std::string last;
// If previous_employer_id is NULL, then the name will be NULL as well.
// We use the odb::nullable wrapper to handle this.
//
#pragma db column(employer::name_)
odb::nullable<std::string> prev_employer_name;
};
10.5 视图查询条件
-
对象、表和混合视图还可以指定一个可选的查询条件,每当为此视图查询数据库时都应该使用该查询条件。为了指定查询条件,我们使用
db query pragma
(第14.2.3节,“query”)。 -
例如,考虑一个返回所有超过预定义退休年龄的员工的一些信息的视图。实现这一点的一种方法是定义一个标准的对象视图,就像我们在前面的部分中所做的那样,然后使用这样的查询:
result r (db.query<employee_retirement> (query::age > 50));
- 上述方法的问题在于,每次执行查询时,我们都必须不断重复
query::age > 50
表达式,即使这个表达式始终保持不变。查看查询条件允许我们解决此问题。例如:
#pragma db view object(employee) query(employee::age > 50)
struct employee_retirement
{
std::string first;
std::string last;
unsigned short age;
};
- 有了这个改进,我们可以像这样重写我们的查询:
result r (db.query<employee_retirement> ());
- 但是,如果我们可能还需要根据一些不同的标准(如员工的姓氏)限制结果集呢?或者,换句话说,我们可能需要将
db query
pragma中指定的常量查询表达式与查询执行时指定的变量表达式组合在一起。为了实现这一点,db query
pragma语法支持使用特殊的(?)占位符,该占位符指示应在常量查询表达式中插入运行时表达式的位置。例如:
#pragma db view object(employee) query(employee::age > 50 && (?))
struct employee_retirement
{
std::string first;
std::string last;
unsigned short name;
};
- 通过此更改,我们现在可以在视图中使用其他查询条件:
result r (db.query<employee_retirement> (query::last == "Doe"));
- 查询条件中表达式的语法与用于在数据库中查询对象的查询工具中的语法相同(第4章,“查询数据库”),除了两个区别。首先,对于查询成员,我们不使用
odb::query<object>::member
名称,而是直接引用对象成员,如果分配了别名,则使用对象别名而不是对象名称。其次,查询条件支持特殊的(?)占位符,它既可以在如上所示的C++集成查询表达式中使用,也可以在指定为字符串文字的本机SQL表达式中使用。以下观点是后一种情况的一个例子:
#pragma db view table("employee_extra") \
query("vacation_days <> 0 AND (?)")
struct employee_vacation
{
...
};
- 查询条件的另一个常见用例是具有
ORDER BY
或GROUP BY
子句的视图。在涉及此类视图的每个查询中,此类子句通常以相同的形式出现。例如,考虑一个计算每个雇主员工最低和最高年龄的汇总视图:
#pragma db view object(employee) object(employer) \
query((?) + "GROUP BY" + employer::name_)
struct employer_age
{
#pragma db column(employer::name_)
std::string employer_name;
#pragma db column("min(" + employee::age_ + ")")
unsigned short min_age;
#pragma db column("max(" + employee::age_ + ")")
unsigned short max_age;
};
- 查询条件后面可以有一个或多个结果修饰符(如果不需要常量查询表达式,则可以替换)。目前支持的结果修饰符是distinct(转换为
SELECT DISTINCT
)和for_update
(转换为FOR UPDATE
或支持它的数据库系统的等效值)。例如,考虑一个视图,它允许我们按对象id顺序获取有关雇主的一些信息,并且没有任何重复项:
#pragma db view object(employer) object(employee) \
query((?) + "ORDER BY" + employer::name_, distinct)
struct employer_info
{
...
};
- 如果我们不需要排序,那么这个视图可以像这样重新实现:
#pragma db view object(employer) object(employee) query(distinct)
struct employer_info
{
...
};
10.6 原生视图
- ODB支持的最后一种视图是原生视图。原生视图是一种低级机制,用于捕获本机SQL查询、存储过程调用等的结果。原生视图没有关联的表或对象。相反,我们使用
db query
pragma来指定本机SQL查询,该查询通常应包括select列表,如果适用,还应包括from列表。例如,以下是我们如何将上述第10.3节中的employee_vcation
表视图重新实现为原生视图:
#pragma db view query("SELECT employee_id, vacation_days " \
"FROM employee_extra")
struct employee_vacation
{
#pragma db type("INTEGER")
unsigned long employee_id;
#pragma db type("INTEGER")
unsigned short vacation_days;
};
-
在原生视图中,查询选择列表中的列按照指定的顺序与视图数据成员相关联。也就是说,第一列存储在第一成员中,第二列存储在第二成员中,以此类推。ODB编译器在此关联中不执行任何错误检查。因此,您必须确保查询选择列表中的列数和顺序与视图中的数据成员数和顺序匹配。这也是为什么我们不需要像对象和表视图那样在本机视图中为每个数据成员提供列名的原因。
-
还要注意,虽然总是可以将表视图实现为原生视图,但必须首选表视图,因为它们更安全。在本机视图中,如果在不更新查询中的列列表的情况下添加、删除或重新排列数据成员,或者反之亦然,充其量只会导致运行时错误。相比之下,在表视图中,此类更改将导致查询自动更新。
-
与对象和表视图类似,为本机视图指定的查询可以包含特殊的(?)占位符,该占位符将被查询执行时指定的查询表达式替换。如果本机查询不包含占位符,如上例所示,则在查询执行时指定的任何查询表达式都将与
WHERE
关键字一起附加到查询文本中(如果需要)。以下示例显示了占位符的用法:
#pragma db view query("SELECT employee_id, vacation_days " \
"FROM employee_extra " \
"WHERE vacation_days <> 0 AND (?)")
struct employee_vacation
{
...
};
- 再举一个例子,考虑一个返回数据库序列下一个值的视图:
#pragma db view query("SELECT nextval('my_seq')")
struct sequence_value
{
unsigned long long value;
};
-
虽然这种实现在某些情况下是可以接受的,但它有许多缺点。首先,序列的名称在视图中是固定的,这意味着如果我们有第二个序列,我们将不得不定义另一个几乎相同的视图。同样,我们对序列执行的操作也是固定的。在某些情况下,我们可能需要最后一个值,而不是返回下一个值。
-
请注意,我们不能使用占位符机制来解决这些问题,因为占位符只能在
WHERE
、GROUP BY
和类似的子句中使用。换句话说,以下方法不起作用:
#pragma db view query("SELECT nextval('(?)')")
struct sequence_value
{
unsigned long long value;
};
result r (db.query<sequence_value> ("my_seq"));
- 为了支持这些用例,ODB允许我们在运行时而不是在视图定义时为本机视图指定完整的查询。为了表明本机视图具有运行时查询,我们可以指定空的
db query
pragma,也可以完全省略pragma。例如:
#pragma db view
struct sequence_value
{
unsigned long long value;
};
Given this view, we can perform the following queries:
typedef odb::query<sequence_value> query;
typedef odb::result<sequence_value> result;
string seq_name = ...
result l (db.query<sequence_value> (
"SELECT lastval('" + seq_name + "')"));
result n (db.query<sequence_value> (
"SELECT nextval('" + seq_name + "')"));
- 原生视图也可用于调用和处理存储过程的结果。存储过程的语义和限制在数据库系统之间差异很大,而有些系统根本不支持此功能。因此,在第二部分“数据库系统”中,为每个数据库系统描述了使用本机视图调用存储过程的支持。
10.7 其他视图功能和限制
-
视图不能从其他视图导出。但是,您可以从瞬态C++类中派生视图。视图数据成员不能是对象指针。如果需要从指向的对象访问数据,则需要将此类对象与视图相关联。同样,视图数据成员不能是容器。这两个限制也适用于包含对象指针或容器的复合值类型。此类复合值不能用作视图数据成员。
-
另一方面,可以在视图中使用不包含对象指针或容器的复合值。例如,考虑员工持久类的修改版本,该类将人员的姓名存储为复合值:
#pragma db value
class person_name
{
std::string first_;
std::string last_;
};
#pragma db object
class employee
{
...
person_name name_;
...
};
- 鉴于这一变化,我们可以重新实现
employee_name
视图,如下所示:
#pragma db view object(employee)
struct employee_name
{
person_name name;
};
- 还可以将复合值的部分或全部嵌套成员提取到单个视图数据成员中。如果我们想保持其原始结构,我们可以这样定义
employee_name
视图:
#pragma db view object(employee)
struct employee_name
{
#pragma db column(employee::name.first_)
std::string first;
#pragma db column(employee::name.last_)
std::string last;
};