高级技术和机制

15 高级技术和机制

15.1 Transaction Callbacks  事务回调

The ODB transaction class (odb::transaction) allows an application to register a callback that will be called after the transaction is finalized, that is, committed or rolled back. This mechanism can be used, for example, to restore values that were updated during the transaction execution to their original states if the transaction is rolled back.

ODB事务类(ODB::transaction)允许应用程序注册一个回调,该回调将在事务完成后(即提交或回滚)被调用。 例如,如果事务回滚,可以使用该机制将事务执行期间更新的值恢复到原始状态。

The callback management interface of the transaction class is shown below.

事务类的回调管理接口如下所示。  

namespace odb

{

  class transaction

  {

    ...

  public:

    static const unsigned short event_commit = 0x01;

    static const unsigned short event_rollback = 0x02;

    static const unsigned short event_all = event_commit | event_rollback;

    typedef void (*callback_type) (

      unsigned short event, void* key, unsigned long long data);

    void

    callback_register (callback_type callback,

                       void* key,

                       unsigned short event = event_all,

                       unsigned long long data = 0,

                       transaction** state = 0);

    void

    callback_unregister (void* key);

    void

    callback_update (void* key,

                     unsigned short event,

                     unsigned long long data = 0,

                     transaction** state = 0);

  }

}

The callback_register() function registers a post-commit/rollback callback. The callback argument is the function that should be called. The key argument is used by the transaction to identify this callback. It is also normally used to pass an address of the data object on which the callback function will work. The event argument is the bitwise-or of the events that should trigger the callback.

callback_register() 函数的作用是:注册一个提交后/回滚回调函数。callback 参数是应该被调用的函数。 事务使用key参数来标识这个回调。 它通常还用于传递回调函数将在其上工作的数据对象的地址。 event参数是按位的——或者应该触发回调的事件的。 

The optional data argument can be used to store any POD user data that doesn't exceed 8 bytes in size and doesn't require alignment greater than unsigned long long. For example, we could store an old value of a flag or a counter that needs to be restored in case of a roll back.

可选的data参数可用于存储大小不超过8字节的任何POD用户数据,并且不需要比unsigned long long更大的对齐方式。 例如,我们可以存储标志或计数器的旧值,在回滚时需要恢复该值。 

The optional state argument can be used to indicate that the callback has been unregistered because the transaction was finalized. In this case the transaction automatically resets the passed pointer to 0. This is primarily useful if we are interested in only one of the events (commit or rollback).

可选的state参数可用于指示由于事务已结束,回调已被注销。 在这种情况下,事务自动将传递的指针重置为0。 这在我们只对其中一个事件(提交或回滚)感兴趣时非常有用。 

The callback_unregister() function unregisters a previously registered callback. If the number of registered callbacks is large, then this can be a slow operation. Generally, the callback mechanism is optimized for cases where the callbacks stay registered until the transaction is finalized.

callback_unregister() 函数的作用是:注销先前注册的回调函数。 如果已注册回调的数量很大,那么这可能是一个缓慢的操作。 通常,回调机制是针对以下情况进行优化的:回调在事务完成之前一直处于注册状态。

Note also that you don't need to unregister a callback that has been called or auto-reset using the state argument passed to callback_register(). This function does nothing if the key is not found.

还需要注意的是,您不需要注销已经被调用的回调函数,或者使用传递给callback_register()的state参数自动重置回调函数。 如果没有找到键,此函数将不执行任何操作。 

The callback_update() function can be used to update the event, data, and state values of a previously registered callback. Similar to callback_unregister(), this is a potentially slow operation.

callback_update()函数可用于更新先前注册的回调的事件、数据和状态值。 与callback_unregister()类似,这可能是一个缓慢的操作。 

When the callback is called, it is passed the event that triggered it, as well as the key and data values that were passed to the callback_register() function. Note also that the order in which the callbacks are called is unspecified. The rollback event can be triggered by an exception. In this case, if the callback throws, the program will be terminated.

当调用回调函数时,它会被传递触发它的事件,以及传递给callback_register()函数的key 和data 值。 还请注意,回调调用的顺序是未指定的。 回滚事件由异常触发。 在这种情况下,如果回调抛出,程序将被终止。 

The following example shows how we can use transaction callbacks together with database operation callbacks (Section 14.1.7, "callback") to manage the object's "dirty" flag.

 下面的例子展示了如何使用事务回调和数据库操作回调(章节14.1.7,“callback”)来管理对象的“dirty”标志。 

#include <odb/callback.hxx>

#include <odb/transaction.hxx>

#pragma db object callback(update)

class object

{

  ...

  #pragma db transient

  mutable bool dirty_;

  // Non-NULL value indicates that we are registered

  // with this transaction.

  //

  #pragma db transient

  mutable odb::transaction* tran_;

  void

  update (odb::callback_event e, odb::database&) const

  {

    using namespace odb::core;

    if (e == callback_event::post_update)

      return;

    // Mark the object as clean again but register a

    // transaction callback in case the update is rolled

    // back.

    //

    tran_ = &transaction::current ();

    tran_->callback_register (&rollback,

                              const_cast<object*> (this),

                              transaction::event_rollback,

                              0,

                              &tran_);

    dirty_ = false;

  }

  static void

  rollback (unsigned short, void* key, unsigned long long)

  {

    // Restore the dirty flag since the changes have been

    // rolled back.

    //

    object& o (*static_cast<object*> (key));

    o.dirty_ = true;

  }

  ~object ()

  {

    // Unregister the callback if we are going away before

    // the transaction.

    //

    if (tran_ != 0)

      tran_->callback_unregister (this);

  }

};

15.2 Persistent Class Template Instantiations 持久类模板实例化 

Similar to composite value types (Section 7.2, "Composite Value Types"), a persistent object can be defined as an instantiation of a C++ class template, for example:

 类似于复合值类型(7.2节,“复合值类型”),一个持久化对象可以被定义为一个C++类模板的实例化,例如: 

template <typename T>

class person

{

  ...

  T first_;

  T last_;

};

typedef person<std::string> std_person;

#pragma db object(std_person)

#pragma db member(std_person::last_) id

Note that the database support code for such a persistent object is generated when compiling the header containing the db object pragma and not the header containing the template definition or the typedef name. This allows us to use templates defined in other files, for example:

请注意,这种持久化对象的数据库支持代码是在编译包含db object pragma的头文件时生成的,而不是编译包含模板定义或typedef名称的头文件时生成的。 这允许我们使用在其他文件中定义的模板,例如: 

#include <utility> // std::pair

typedef std::pair<unsigned int, std::string> person;

#pragma db object(person)

#pragma db member(person::first) id auto column("id")

#pragma db member(person::second) column("name")

You may also have to explicitly specify the object type in calls to certain database class functions due to the inability do distinguish, at the API level, between smart pointers and persistent objects defined as class template instantiations. For example:

由于无法在API级别区分智能指针和定义为类模板实例化的持久对象,您可能还必须在调用某些数据库类函数时显式地指定对象类型。 例如: 

person p;

db.update (p); // Error.

db.reload (p); // Error.

db.erase (p);  // Error.

db.update<person> (p); // Ok.

db.reload<person> (p); // Ok.

db.erase<person> (p);  // Ok.

It also makes sense to factor persistent data members that do not depend on template arguments into a common, non-template base class. The following more realistic example illustrates this approach:

将不依赖于模板参数的持久数据成员分解为一个通用的、非模板基类也是有意义的。 下面这个更现实的例子说明了这种方法: 

#pragma db object abstract

class base_common

{

  ...

  #pragma db id auto

  unsigned long id;

};

template <typename T>

class base: public base_common

{

  ...

  T value;

};

typedef base<std::string> string_base;

#pragma db object(string_base) abstract

#pragma db object

class derived: public string_base

{

  ...

};

 

15.3 Bulk Database Operations  数据库大容量操作 

Some database systems supported by ODB provide a mechanism, often called bulk or batch statement execution, that allows us to execute the same SQL statement on multiple sets of data at once and with a single database API call. This often results in significantly better performance if we need to execute the same statement for a large number of data sets (thousands to millions).

ODB支持的一些数据库系统提供了一种机制,通常称为批量或批处理语句执行,该机制允许我们一次对多个数据集执行相同的SQL语句,并且只使用一个数据库API调用。 如果我们需要对大量数据集(数千到数百万)执行相同的语句,这通常会带来显著的性能改善。

ODB translates this mechanism to bulk operations which allow us to persist, update, or erase a range of objects in the database. Currently, from all the database systems supported by ODB, only Oracle and Microsoft SQL Server are capable of bulk operations. There is also currently no emulation of the bulk API for other databases nor dynamic multi-database support. As a result, if you are using dynamic multi-database support, you will need to "drop down" to static support in order to access the bulk API. Refer to Chapter 16, "Multi-Database Support" for details.

ODB将这种机制转换为批量操作,允许我们持久化、更新或删除数据库中的一系列对象。 目前,在ODB支持的所有数据库系统中,只有Oracle和Microsoft SQL Server能够进行批量操作。 目前还没有针对其他数据库的批量API模拟,也没有动态多数据库支持。 因此,如果您正在使用动态多数据库支持,您将需要“下拉”到静态支持,以便访问批量API。 详细信息请参见第16章“多数据库支持”。

As we will discuss later in this section, bulk operations have complex failure semantics that is dictated by the underlying database API. As a result, support for bulk persist, update, and erase is limited to persistent classes for which these operations can be performed with a single database statement execution. In particular, bulk operations are not available for polymorphic objects (Section 8.2, "Polymorphism Inheritance") or objects that have containers (inverse containers of object pointers are an exception). Furthermore, for objects that have sections (Chapter 9, "Sections") the bulk update operation will only be available if all the sections are manually-updated. On the other hand, bulk operations are supported for objects that use optimistic concurrency (Chapter 12, "Optimistic Concurrency") or have no object id (Section 14.1.6, "no_id").

正如我们将在本节稍后讨论的,批量操作具有由底层数据库API指定的复杂故障语义。 因此,对批量持久化、更新和擦除的支持仅限于持久化类,这些操作可以通过单个数据库语句执行。 特别是,批量操作对于多态对象(第8.2节,“多态性继承”)或具有容器的对象(对象指针的反向容器是一个异常)是不可用的。 此外,对于有section的对象(第9章,“section”),批量更新操作只有在所有section都被手动更新的情况下才可用。 另一方面,对于使用乐观并发(第12章,“乐观并发”)或没有对象id(第14.1.6节,“no_id”)的对象,支持批量操作。 

To enable the generation of bulk operation support for a persistent class we use the bulk pragma. For example:

为了支持生成持久化类的批量操作,我们使用了bulk pragma。 例如: 

#pragma db object bulk(5000)

class person

{

  ...

  #pragma db id auto

  unsigned long id;

};

The single argument to the bulk pragma is the batch size. The batch size specifies the maximum number of data sets that should be handled with a single underlying statement execution. If the range that we want to perform the bulk operation on contains more objects than the batch size, then ODB will split this operation into multiple underlying statement executions (batches). To illustrate this point with an example, suppose we want to persist 53,000 objects and the batch size is 5,000. ODB will then execute the statement 11 times, the first 10 times with 5,000 data sets each, and the last time with the remaining 3,000 data sets.

批量编译的单个参数是批处理大小。 批处理大小指定单个底层语句执行应该处理的最大数据集数量。 如果我们想要执行批量操作的范围包含的对象超过批处理大小,那么ODB将把该操作分割为多个底层语句执行(批处理)。 为了用一个示例来说明这一点,假设我们希望持久化53,000个对象,并且批处理大小为5,000个。 然后ODB将执行该语句11次,前10次使用5,000个数据集,最后一次使用剩余的3,000个数据集。

The commonly used batch sizes are in the 2,000-5,000 range, though smaller or larger batches could provide better performance, depending on the situation. As a result, it is recommended to experiment with different batch sizes to determine the optimum number for a particular object and its use-cases. Note also that you may achieve better performance by also splitting a large bulk operation into multiple transactions (Section 3.5, "Transactions").

通常使用的批大小在2000 - 5000之间,尽管更小或更大的批可以提供更好的性能,这取决于情况。 因此,建议使用不同的批处理大小进行实验,以确定特定对象及其用例的最佳数量。 还需要注意的是,您可以通过将一个大的批量操作分割成多个事务来获得更好的性能(章节3.5,“事务”)。 

For database systems that do not support bulk operations the bulk pragma is ignored. It is also possible to specify different batch sizes for different database systems by using the database prefix, for example:

对于不支持批量操作的数据库系统,将忽略批量编译。 也可以通过使用数据库前缀为不同的数据库系统指定不同的批处理大小,例如: 

#pragma db object mssql:bulk(3000) oracle:bulk(4000)

class person

{

  ...

};

Note that while specifying the batch size at compile time might seem inflexible, this approach allows ODB to place internal arrays of the fixed batch size on the stack rather than allocating them in the dynamic memory. However, specifying the batch size at runtime may be supported in the future.

注意,虽然在编译时指定批处理大小似乎不太灵活,但这种方法允许ODB将固定批处理大小的内部数组放在堆栈上,而不是将它们分配到动态内存中。 但是,将来可能支持在运行时指定批处理大小。 

Once the bulk support is enabled for a particular object, we can use the following database functions to perform bulk operations:

一旦对特定对象启用了批量支持,我们就可以使用以下数据库函数来执行批量操作: 

template <typename I>

void

persist (I begin, I end, bool continue_failed = true);

template <typename I>

void

update (I begin, I end, bool continue_failed = true);

template <typename I>

void

erase (I obj_begin, I obj_end, bool continue_failed = true);

template <typename T, typename I>

void

erase (I id_begin, I id_end, bool continue_failed = true);

Every bulk API function expects a range of elements, passed in the canonical C++ form as a pair of input iterators. In case of persist(), update(), and the first erase() overload, we pass a range of objects, either as references or as pointers, raw or smart. The following example illustrates the most common scenarios using the persist() call:

每个批量API函数都需要一个元素范围,以规范的C++形式作为一对输入迭代器传递。 对于persist()、update()和第一个erase()重载,我们传递了一个对象范围,可以是引用,也可以是指针,原始的或智能的。 下面的例子演示了使用persist()调用的最常见场景: 

// C array of objects.

//

person a[2] {{"John", "Doe"}, {"Jane", "Doe"}};

db.persist (a, a + sizeof(a) / sizeof(a[0]));

// Vector of objects.

//

std::vector<person> v {{"John", "Doe"}, {"Jane", "Doe"}};

db.persist (v.begin (), v.end ());

// C array of raw pointers to objects.

//

person p1 ("John", "Doe");

person p2 ("Jane", "Doe");

person* pa[2] {&p1, &p2};

db.persist (pa, pa + sizeof(pa) / sizeof(pa[0]));

// Vector of raw pointers to objects.

//

std::vector<person*> pv {&p1, &p2};

db.persist (pv.begin (), pv.end ());

// Vector of smart (shared) pointers to objects.

//

std::vector<std::shared_ptr<person>> sv {

  std::make_shared<person> ("John", "Doe"),

  std::make_shared<person> ("Jane", "Doe")};

db.persist (sv.begin (), sv.end ());

he ability to perform a bulk operation on a range of raw pointers to objects can be especially useful when the application stores objects in a way that does not easily conform to the pair of iterators interface. In such cases we can create a temporary container of shallow pointers to objects and use that to perform the bulk operation, for example:

当应用程序存储对象的方式不容易符合一对迭代器接口时,对对象的原始指针范围执行批量操作的能力特别有用。 在这种情况下,我们可以创建一个由对象的浅指针组成的临时容器,并使用它来执行bulk操作,例如:

struct person_entry

{

  person obj;

  // Some additional data.

  ...

};

typedef std::vector<person_entry> people;

void

persist (odb::database& db, people& p)

{

  std::vector<person*> tmp;

  tmp.reserve (p.size ());

  std::for_each (p.begin (),

                 p.end (),

                 [&tmp] (person_entry& pe)

                 {

                   tmp.push_back (&pe.obj);

                 });

  db.persist (tmp.begin (), tmp.end ());

}

The second overload of the bulk erase() function allows us to pass a range of object ids rather than objects themselves. As with the corresponding non-bulk version, we have to specify the object type explicitly, for example:

bulk erase()函数的第二个重载允许我们传递范围内的对象id,而不是对象本身。 与对应的非批量版本一样,必须显式指定对象类型,例如: 

std::vector<unsigned long> ids {1, 2};

db.erase<person> (ids.begin (), ids.end ());

Conceptually, a bulk operation is equivalent to performing the corresponding non-bulk version in a loop, except when it comes to the failure semantics. Both databases that currently are capable of bulk operations (Oracle and SQL Server) do not stop when a data set in a batch fails (for example, because of a unique constraint violation). Instead, they continue executing subsequent data sets until every element in the batch has been attempted. The continue_failed argument in the bulk functions listed above specifies whether ODB should extend this behavior and continue with subsequent batches if the one it has tried to execute has failed elements. The default behavior is to continue.

从概念上讲,批量操作相当于在循环中执行相应的非批量版本,除非涉及到失败语义。 当前能够进行批量操作的两个数据库(Oracle和SQL Server)不会在批处理中的数据集失败时停止(例如,由于唯一的约束违反)。 相反,它们继续执行后续的数据集,直到尝试了批处理中的每个元素。 上面列出的批量函数中的continue_failed参数指定ODB是否应该扩展此行为,并在它尝试执行的批中有失败元素时继续后续的批。 默认的行为是继续。

The consequence of this failure semantics is that we may have multiple elements in the range failed for different reasons. For example, if we tried to persist a number of objects, some of them might have failed because they are already persistent while others — because of a unique constraint violation. As a result, ODB uses the special odb::multiple_exceptions class to report failures in the bulk API functions. This exception is thrown if one or more elements in the range have failed and it contains the error information in the form of other ODB exception for each failed position. The multiple_exceptions class has the following interface:

这种失败语义的结果是,我们可能有多个元素由于不同的原因而失败。 例如,如果我们尝试持久化多个对象,其中一些可能会失败,因为它们已经持久化了,而另一些可能会失败——因为有一个唯一的约束冲突。 因此,ODB使用特殊的ODB::multiple_exceptions类来报告批量API函数中的失败。 如果范围中的一个或多个元素失败,则会抛出此异常,并且它包含每个失败位置的其他ODB异常形式的错误信息。 multiple_exceptions类有以下接口: 

struct multiple_exceptions: odb::exception

{

  // Element type.

  //

  struct value_type

  {

    std::size_t

    position () const;

    const odb::exception&

    exception () const;

    bool

    maybe () const;

  };

  // Iteration.

  //

  typedef std::set<value_type> set_type;

  typedef set_type::const_iterator iterator;

  typedef set_type::const_iterator const_iterator;

  iterator

  begin () const;

  iterator

  end () const;

  // Lookup.

  //

  const value_type*

  operator[] (std::size_t) const;

  // Severity, failed and attempted counts.

  //

  std::size_t

  attempted () const;

  std::size_t

  failed () const;

  bool

  fatal () const;

  void

  fatal (bool);

  // Direct data access.

  //

  const set_type&

  set () const;

  // odb::exception interface.

  //

  virtual const char*

  what () const throw ();

};

The multiple_exceptions class has a map-like interface with the key being the position in the range and the value being the exception plus the maybe flag (discussed below). As a result, we can either iterate over the failed positions or we can check whether a specific position in the range has failed. The following example shows what a catch-handler for this exception might look like:

multiple_exceptions类有一个类似map的接口,键是范围中的位置,值是异常加上maybe标志(下面讨论)。 因此,我们既可以迭代失败的位置,也可以检查范围中的特定位置是否失败。 下面的例子显示了这个异常的catch-handler是什么样子的: 

std::vector<person> objs {{"John", "Doe"}, {"Jane", "Doe"}};

try

{

  db.persist (objs.begin (), objs.end ());

}

catch (const odb::multiple_exceptions& me)

{

  for (const auto& v: me)

  {

    size_t p (v.position ());

    try

    {

      throw v.exception ();

    }

    catch (const odb::object_already_persistent&)

    {

      cerr << p << ": duplicate id: " << objs[p].id () << endl;

    }

    catch (const odb::exception& e)

    {

      cerr << p << ": " << e.what () << endl;

    }

  }

}

If, however, all we want is to show the diagnostics to the user, then the string returned by the what() function will contain the error information for each failed position. Here is what it might look like (using Oracle as an example):

 但是,如果我们只想向用户显示诊断结果,那么what()函数返回的字符串将包含每个失败位置的错误信息。 下面是它的样子(以Oracle为例): 

multiple exceptions, 4 elements attempted, 2 failed:

[0] object already persistent

[3] 1: ORA-00001: unique constraint (ODB_TEST.person_last_i) violated

Both databases that currently are capable of bulk operations return a total count of affected rows rather than individual counts for each data set. This limitation prevents ODB from being able to always determine which elements in the batch haven't affected any rows and, for the update and erase operations, translate this to the object_not_persistent exceptions. As a result, if some elements in the batch haven't affected any rows and ODB is unable to determine exactly which ones, it will mark all the elements in this batch as "maybe not persistent". That is, it will insert the object_not_persistent exception and set the maybe flag for every position in the batch. The diagnostics string returned by what() will also reflect this situation, for example (assuming batch size of 3):

目前能够进行批量操作的两个数据库都返回受影响的行总数,而不是每个数据集的单个计数。 这个限制使ODB无法始终确定批处理中的哪些元素没有影响任何行,并且对于更新和擦除操作,将其转换为object_not_persistent异常。 因此,如果批处理中的某些元素没有影响任何行,而ODB无法准确确定哪些行,那么它将把该批处理中的所有元素标记为“可能不是持久性的”。 也就是说,它将插入object_not_persistent异常,并为批处理中的每个位置设置maybe标志。 what()返回的诊断字符串也会反映这种情况,例如(假设批处理大小为3):

multiple exceptions, 4 elements attempted, 4 failed:

[0-2] (some) object not persistent

[3] object not persistent

The way to handle and recover from such "maybe failures" will have to be application-specific. For example, for some applications the fact that some objects no longer exist in the database when performing bulk erase might be an ignorable error. If, however, the application needs to determine exactly which elements in the batch have failed, then a load() call will be required for each element in the batch (or a query using a view to avoid loading all the data members; Chapter 10, "Views"). This is also something to keep in mind when selecting the batch size since for larger sizes it will be more expensive (more loads to perform) to handle such "maybe failures". If the failures are not uncommon, as is the case, for example, when using optimistic concurrency, then it may make sense to use a smaller batch.

处理和从这种“可能失败”中恢复的方法必须是特定于应用程序的。 例如,对于某些应用程序,在执行批量擦除时,数据库中不再存在某些对象,这可能是一个可以忽略的错误。 但是,如果应用程序需要确定批处理中的哪些元素失败了,则需要对批处理中的每个元素调用load()(或者使用视图进行查询,以避免加载所有的数据成员第十章,“意见”)。 这也是在选择批处理大小时要记住的事情,因为对于更大的批处理大小,它将更昂贵(执行更多的负载)来处理这样的“可能失败”。 如果失败并不少见,例如,在使用乐观并发时,那么使用较小的批处理可能是有意义的。

The lookup operator (operator[]) returns NULL if the element at this position has no exception. Note also that the returned value is value_type* and not odb::exception* in order to provide access to the maybe flag discussed above.

如果该位置的元素没有异常,则查找操作符(operator[])返回NULL。 还要注意,返回值是value_type*,而不是odb::exception*,以便提供对上面讨论的maybe标志的访问。

The multiple_exceptions class also provides access to the number of positions attempted (the attempted() accessor) and failed (the failed() accessor). Note that the failed count includes the "maybe failed" positions.

multiple_exceptions类还提供了对尝试位置(尝试的()访问器)和失败的位置(失败的()访问器)的访问。 请注意,失败计数包括“可能失败”的位置。 

The multiple_exceptions exception can also be fatal. If the fatal() accessor returns true, then (some of) the exceptions were fatal. In this case, even for positions that did not fail, no attempts were made to complete the operation and the transaction must be aborted.

multiple_exceptions异常也可能是致命的。 如果fatal()访问器返回true,则(一些)异常是致命的。 在这种情况下,即使对于没有失败的头寸,也不会尝试完成操作,事务必须中止。

If fatal() returns false, then the operation on the elements that don't have an exception has succeeded. The application can ignore the errors or try to correct the errors and re-attempt the operation on the elements that did fail. In either case, the transaction can be committed.

如果fatal()返回false,则对没有异常的元素的操作已经成功。 应用程序可以忽略错误,或者尝试更正错误,并重新尝试对失败的元素进行操作。 在这两种情况下,事务都可以被提交。

An example of a fatal exception would be the situation where the execution of the underlying statement failed summarily, without attempting any data sets, for instance, because of an error in the statement itself.

致命异常的一个例子是,底层语句的执行突然失败,没有尝试任何数据集,例如,因为语句本身的错误。

The fatal() modifier allows you to "upgrade" an exception to fatal, for example, for specific database error codes.

fatal()修饰符允许你将异常“升级”到fatal,例如,针对特定的数据库错误代码。 

15.1 事务回调

15.2 持久类模板实例化

15.3 批量数据库操作