From 8e69f40ab32dc8604b68f360ae30fa961ba036ee Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 4 Feb 2015 17:23:54 +0200 Subject: Implement object loading views See section 10.2 in the manual for details. --- doc/manual.xhtml | 432 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 412 insertions(+), 20 deletions(-) (limited to 'doc') diff --git a/doc/manual.xhtml b/doc/manual.xhtml index 6183d2b..f91a4af 100644 --- a/doc/manual.xhtml +++ b/doc/manual.xhtml @@ -469,11 +469,12 @@ for consistency. 10Views - - - - - + + + + + +
10.1Object Views
10.2Table Views
10.3Mixed Views
10.4View Query Conditions
10.5Native Views
10.6Other View Features and Limitations
10.2Object Loading Views
10.3Table Views
10.4Mixed Views
10.5View Query Conditions
10.6Native Views
10.7Other View Features and Limitations
@@ -9962,7 +9963,7 @@ struct employee_employer below. The optional join-condition part provides the criteria which should be used to associate this object with any of the previously associated objects or, as we will see in - Section 10.3, "Mixed Views", tables. Note that + Section 10.4, "Mixed Views", tables. Note that while the first associated object can have an alias, it cannot have a join condition.

@@ -10311,7 +10312,398 @@ t.commit (); query expression, and you need to qualify the column with the table, then you will need to use the table alias instead.

-

10.2 Table Views

+

10.2 Object Loading Views

+ +

A special variant of object views is object loading views. Object + loading views allow us to load one or more complete objects + instead of, or in addition to, a subset of data member. While we + can often achieve the same end result by calling + database::load(), using a view has several advantages.

+ +

If we need to load multiple objects, then using a view allows us + to do this with a single SELECT statement execution + instead of one for each object that would be necessary in case of + load(). A view can also be useful for loading only + a single object if the query criterion that we would like to use + involves other, potentially unrelated, objects. We will examine + concrete examples of these and other scenarios in the rest of this + section.

+ +

To load a complete object as part of a view we use a data member of + the pointer to object type, just like for object relationships + (Chapter 6, "Relationships"). As an example, here + is how we can load both the employee and + employer objects from the previous section with a single + statement:

+ +
+#pragma db view object(employee) object(employer)
+struct employee_employer
+{
+  shared_ptr<employee> ee;
+  shared_ptr<employer> er;
+};
+  
+ +

We use an object loading view just like any other view. In the + result of a query, as we would expect, the pointer data members + point to the loaded objects. For example:

+ +
+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 ();
+  
+ +

As another example, consider a query that loads the employer + objects using some condition based on its employees. For instance, we + want to find all the employers that employ people over 65 years old. + We can use this object loading view to implement such a query:

+ +
+#pragma db view object(employer) object(employee)
+struct employer_view
+{
+  shared_ptr<employer> er;
+};
+  
+ +

And this is how we can use this view to find all the employers that + employ seniors:

+ +
+typedef odb::query<employee_employer> query;
+
+db.query<employer_view> ((query::employee::age > 65) + distinct))
+  
+ +

We can even use object loading views to load completely unrelated + (from the ODB object relationships point of view) objects. For example, + the following view will load all the employers that are named the + same as a country (notice the inner join type):

+ +
+#pragma db view object(employer) \
+  object(country inner: employer::name == country::name)
+struct employer_named_country
+{
+  shared_ptr<employer> e;
+  shared_ptr<country> c;
+};
+  
+ +

An object loading view can contain ordinary data members + in addition to object pointers. For example, if we are only + interested in the country code in the above view, then we + can reimplement it like this:

+ +
+#pragma db view object(employer) \
+  object(country inner: employer::name == country::name)
+struct employer_named_country
+{
+  shared_ptr<employer> e;
+  std::string code;
+};
+  
+ +

Object loading views also have a few rules and restrictions. + Firstly, the pointed-to object in the data member must be associated + with the view. Furthermore, if the associated object has an alias, + then the data member name must be the same as the alias (more + precisely, the public name derived from the data member must + match the alias; which means we can use normal data member + decorations such as trailing underscores, etc., see the previous + section for more information on public names). The following view + illustrates the use of aliases as data member names:

+ +
+#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_;
+};
+  
+ +

Finally, the object pointers must be direct data members of + the view. Using, for example, a composite value that contains + pointers as a view data member is not supported. Note also + that depending on the join type you are using, some of the + resulting pointers might be NULL.

+ +

Up until now we have consistently used shared_ptr + as an object pointer in our views. Can we use other pointers, + such as unique_ptr or raw pointers? To answer + this question we first need to discuss what happens with + object pointers that may be inside objects that a view + loads. As a concrete example, let us revisit the + employee_employer view from the beginning of + this section:

+ +
+#pragma db view object(employee) object(employer)
+struct employee_employer
+{
+  shared_ptr<employee> ee;
+  shared_ptr<employer> er;
+};
+  
+ +

This view loads two objects: employee and + employer. The employee object, + however, also contains a pointer to employer + (see the employed_by_ data member). In fact, + this is the same object that the view loads since employer + is associated with the view using this same relationship (ODB + automatically uses it since it is the only one). The correct + result of loading such a view is then clear: both er and + er->employed_by_ must point to (or share) the + same instance.

+ +

Just like object loading via the database class + functions, views achieve this correct behavior of only loading + a single instance of the same object with the help of session's + object cache (Chapter 11, "Session"). In fact, + object loading views enforce this by throwing the + session_required exception if there is no current + session and the view loads an object that is also indirectly + loaded by one of the other objects. The ODB compiler will also + issue diagnostics if such an object has session support + disabled (Section 14.1.10, + "session").

+ +

With this understanding we can now provide the correct implementation + of our transaction that uses the employee_employer view:

+ +
+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 ();
+  
+ +

It might seem logical, then, to always load all the objects from + all the eager relationships with the view. After all, this will + lead to them all being loaded with a single statement. While + this is theoretically true, the reality is slightly more nuanced. + If there is a high probability of the object already have been + loaded and sitting in the cache, then not loading the object + as part of the view (and therefore not fetching all its data + from the database) might result in better performance.

+ +

Now we can also answer the question about which pointers we can + use in object loading views. From the above discussion it should + be clear that if an object that we are loading is also part of a + relationship inside another object that we are loading, then we + should use some form of a shared ownership pointer. If, however, + there are no relationships involved, as is the case, for example, + in our employer_named_country and + employee_country views above, then we can use a + unique ownership pointer such as unique_ptr.

+ +

Note also that your choice of a pointer type can be limited by the + "official" object pointer type assigned to the object + (Section 3.3, "Object and View Pointers"). + For example, if the object pointer type is shared_ptr, + you will not be able to use unique_ptr to load + such an object into a view since initializing unique_ptr + from shared_ptr would be a mistake.

+ +

Unless you want to perform your own object cleanup, raw object + pointers in views are not particularly useful. They do have one + special semantics, however: If a raw pointer is used as a view + member, then, before creating a new instance, the implementation + will check if the member is NULL. If it is not, then + it is assumed to point to an existing instance and the implementation + will load the data into it instead of creating a new one. The + primary use of this special functionality is to implement by-value + loading with the ability to detect NULL values.

+ +

To illustrate this functionality, consider the following view that + load the employee's residence country by value:

+ +
+#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;
+};
+  
+ +

Here we are using a virtual data member + (Section 14.4.13, "virtual") to + add an object pointer member to the view. Its accessor expression + returns the pointer to the res member so that + the implementation can load the data into it. The modifier + expression checks the passed pointer to initialize the + NULL value indicator. Here, the two possible + values that can be passed to the modifier expression are + the address of the res member that we returned + earlier from the accessor and NULL (strictly + speaking, there is a third possibility: the address of an + object that was found in the session cache).

+ +

If we are not interested in the NULL indicator, + then the above view can simplified to this:

+ +
+#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;
+};
+  
+ +

That is, we specify an empty modifier expression which leads to + the value being ignored.

+ +

As another example of by-value loading, consider a view that allows + us to load objects into existing instances that have been allocated + outside the view:

+ +
+#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;
+};
+  
+ +

And here is how we can use this view:

+ +
+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 ();
+  
+ +

As a final example of the by-value loading, consider the following + view which implements a slightly more advanced logic: if the object + is already in the session cache, then it sets the pointer data member + in the view (er_p) to that. Otherwise, it loads the data + into the by-value instance (er). We can also check + whether the pointer data member points to the instance to distinguish + between the two outcomes. And we can check it for nullptr + to detect NULL values.

+ +
+#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) {}
+};
+  
+ +

We can use object loading views with polymorphic objects + (Section 8.2, "Polymorphism Inheritance"). Note, + however, that when loading a derived object via the base pointer + in a view, a separate statement will be executed to load the + dynamic part of the object. There is no support for by-value + loading for polymorphic objects.

+ +

We can also use object loading views with objects without id + (Section 14.1.6, "no_id"). + Note, however, that for such objects, NULL values + are not automatically detected (since there is no primary key, + which is otherwise guaranteed to be not NULL, there + might not be a column on which to base this detection). The + workaround for this limitation is to load an otherwise not + NULL column next to the object which will serve + as an indicator. For example:

+ +
+#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 Table Views

A table view is similar to an object view except that it is based on one or more database tables instead of persistent @@ -10400,7 +10792,7 @@ struct employee_max_vacation column names, and query expressions. The optional join-condition part provides the criteria which should be used to associate this table with any of the previously associated tables or, as we will see in - Section 10.3, "Mixed Views", objects. Note that + Section 10.4, "Mixed Views", objects. Note that while the first associated table can have an alias, it cannot have a join condition.

@@ -10468,7 +10860,7 @@ t.commit (); -

10.3 Mixed Views

+

10.4 Mixed Views

A mixed view has both associated objects and tables. As a first example of a mixed view, let us improve employee_vacation @@ -10531,7 +10923,7 @@ struct employee_prev_employer }; -

10.4 View Query Conditions

+

10.5 View Query Conditions

Object, table, and mixed views can also specify an optional query condition that should be used whenever the database is queried for @@ -10642,7 +11034,7 @@ struct employer_age }; -

10.5 Native Views

+

10.6 Native Views

The last kind of view supported by ODB is a native view. Native views are a low-level mechanism for capturing results of native @@ -10651,7 +11043,7 @@ struct employer_age db query pragma to specify the native SQL query, which should normally include the select-list and, if applicable, the from-list. For example, here is how we can re-implement the - employee_vacation table view from Section 10.2 above + employee_vacation table view from Section 10.3 above as a native view:

@@ -10777,7 +11169,7 @@ result n (db.query<sequence_value> (
       for each database system in Part II, "Database
       Systems".

-

10.6 Other View Features and Limitations

+

10.7 Other View Features and Limitations

Views cannot be derived from other views. However, you can derive a view from a transient C++ class. View data members cannot be @@ -14608,7 +15000,7 @@ class employer

The table specifier specifies a database table that should be associated with the view. For more information - on table associations refer to Section 10.2, "Table + on table associations refer to Section 10.3, "Table Views".

14.2.3 query

@@ -14617,9 +15009,9 @@ class employer for an object or table view or a native SQL query for a native view. An empty query specifier indicates that a native SQL query is provided at runtime. For more information - on query conditions refer to Section 10.4, "View + on query conditions refer to Section 10.5, "View Query Conditions". For more information on native SQL queries, - refer to Section 10.5, "Native Views".

+ refer to Section 10.6, "Native Views".

14.2.4 pointer

@@ -16183,7 +16575,7 @@ class person object data member, the potentially qualified column name, or the column expression for the data member of a view class. For more information, refer to Section 10.1, "Object Views" and - Section 10.2, "Table Views".

+ Section 10.3, "Table Views".

14.4.11 transient

@@ -20134,7 +20526,7 @@ class object

17.7 MySQL Stored Procedures

-

ODB native views (Section 10.5, "Native Views") +

ODB native views (Section 10.6, "Native Views") can be used to call MySQL stored procedures. For example, assuming we are using the person class from Chapter 2, "Hello World Example" (and the corresponding person @@ -23399,7 +23791,7 @@ for (result::iterator i (r.begin ()); i != r.end (); ++i) t.commit ();

-

Finally, if a native view (Section 10.5, "Native +

Finally, if a native view (Section 10.6, "Native Views") contains one or more long data members, then such members should come last both in the select-list of the native SQL query and the list of data members in the C++ class.

@@ -24147,7 +24539,7 @@ class object

21.7 SQL Server Stored Procedures

-

ODB native views (Section 10.5, "Native Views") +

ODB native views (Section 10.6, "Native Views") can be used to call SQL Server stored procedures. For example, assuming we are using the person class from Chapter 2, "Hello World Example" (and the corresponding person -- cgit v1.1