Skip to main content
Previous sectionNext section

Using the C++ Binding

This chapter provides concrete examples of code that uses the Caché C++ binding. The following subjects are discussed:

Many of the examples presented here are modified versions of the sample programs. The argument processing and error trapping (try/catch) statements have been removed to simplify the code. See Sample Programs for details about loading and running the complete sample programs.

C++ Binding Basics

A Caché C++ binding application can be quite simple. Here is a complete sample program:

#include "Sample_Person.h"
#include "Sample_Address.h"

int main()
{
  //  Connect to the Cache' database
  d_connection conn = tcp_conn::connect("localhost[1972]:Samples",
                                        "_system", "SYS");
  Database db(conn);

  // Create and use a Cache' object
  d_ref<Sample_Person> person = Sample_Person::create_new(&db);
  person->setName("Doe, Joe A");
  person->setSSN("123-45-6789");

  person->save();
  // Print the result
  std::cout << "w p.Name\n"
            << person->getName() << '\n';
  return 0;
}
Copy code to clipboard

This code imports the Sample header files, and then performs the following actions:

  • Connects to the Samples namespace in the Caché database:

    • Defines the information needed to connect to the Caché database.

    • Creates a d_connection object (conn).

    • Uses the d_connection object to create a Database object (db).

  • Creates and uses a Caché object:

    • Uses the Database object to create an instance of the Caché Sample.Person class.

    • Sets the Name property of the Sample.Person object.

    • Gets and prints the Name property.

The following sections discuss these basic actions in more detail.

Connecting to the Caché Database

To establish a physical connection to the Caché database, create an instance of the d_connection class. d_connection is a proxy class that acts as a smart pointer to a Conn_t (connection) class instance. It automatically calls Conn_t::disconnect() when the last reference of the Conn_t object that it refers to goes out of scope. This means that the user never has to call Conn_t::disconnect() directly and that a Database object will always have a valid connection that will not be accidentally disconnected. Conn_t provides a common interface for all these connections.

Before initializing a Database object with a d_connection object, the Conn_t object that the d_connection object refers to has to be connected and not be in use by some other Database instance. In order to test whether a d_connection object satisfies these requirements, you can use the is_connected() and is_busy() functions. For example, if you want to test a d_connection object called conn:

   if (!conn->is_connected())
   // code that makes conn point to an active connection
Copy code to clipboard

Because a d_connection object that doesn't point to an active connection is useless, its constructor is made private and the only way to create a d_connection object is to call Conn_t::connect() that returns a d_connection object that points to an active connection. If a connection could not be established, the d_connection object refers to an inactive connection.

The TCP/IP connection class is tcp_conn. Its static connect() method takes the following arguments:

  • connection string — in format “host[port]:namespace”

  • username

  • password

  • timeout

  • error — optional address of a Db_err that will contain the error information if the connect fails.

For example:

   Db_err conn_err;
   d_connection conn = tcp_conn::connect("localhost[1972]:Samples",
         "_SYSTEM", "SYS", 0, &conn_err);
   if (conn_err) {
      // error handling
      std::cerr << conn_err << '\n';
      return -1;
   }
   try {
      // establish the logical connection to Cache'
      Database db(conn);
      // code to use db here
   }
   catch(const Db_err& err) {
      // handle an error from the C++ binding library
      std::cout << err << std::endl;
   }
Copy code to clipboard

A Sample C++ Binding Application

This section contains a simple C++ application that demonstrates the use of the Caché C++ Binding.

The sample program connects to the Caché SAMPLES database, opens and modifies an instance of a Sample.Person object saved within the database, and saves it back to the database.

   #include "../Sample_Person.h"
   #include "../Sample_Address.h"
   
   typedef d_ref<Sample_Person> d_Sample_Person;
   
   int main()
   {
      // establish the physical connection to Cache'
      Db_err conn_err;
      d_connection conn = tcp_conn::connect("localhost[1972]:Samples",
                                            "_SYSTEM", "SYS", 0, &conn_err);
      if (conn_err)
      {
         std::cerr << conn_err << '\n';
         return -1;
      }
   
      try {
         // establish the logical connection to Cache'
         Database db(conn);
   
         std::wstring id;
         std::cout << "Enter ID of Person object to be opened:\n";
         std::wcin >> id;
   
         // open a Sample.Person object using %OpenId
         d_Sample_Person person = Sample_Person::openid(&db, id.c_str());
   
         // Fetch some properties of this object
         std::cout << "Name " << person->getName() << '\n'
                  ~<< "City " << person->getHome()->getCity() << '\n'
                  ~<< '\n';
   
         // Modify some properties
         person->getHome()->setCity("Ulan Bator");
   
         // Save the object to the database
         person->save();
   
         // Report the new residence of this person
         std::cout << "New City: " << person->getHome()->getCity() << '\n';
   
         return 0;
      }
      catch(const Db_err& err) {
         std::cerr << err << '\n';
         return -1;
      }
   
      // all objects are closed automatically
   }
Copy code to clipboard

Using Proxy Objects

Object-valued proxy types are represented using the d_ref< > template class (or the lc_d_ref< > class for Light C++ Binding classes). The template takes the corresponding C++ classname as its parameter. For example, a reference to a Company object would be represented as d_ref<Company>.

An instance of d_ref<T> is a smart pointer to a proxy object of the referenced type T. This means that you can:

  • Call methods of the proxy object using the "->" (pointer) operator.

  • Copy one d_ref< > to another. The two d_ref< > instances will point to the same proxy object.

  • Pass a d_ref< > as an argument to a proxy method that may change the d_ref< > to point to another proxy object.

Note:

Two variables that represent the same server object may still point to two different proxy objects.

While the library tries to use only one proxy object for each open server object, it may have to use a different proxy object for the same server object if you open it using a different proxy class. You can use "==" (equality test) and "!=" (the inequality test) to test whether a d_ref<P> and a d_ref<Q> point to the same server object

Even though d_ref<T> acts as a pointer to T, it is not a real pointer, so testing for null or making a d_ref<T> point to null should be done via method calls is_null() and make_null(). For example,

   d_ref<Sample_Person> p1 = Sample_Person::openid(&db, L"1");
   if (p1.is_null())
      std::cerr << "the object is null";
   p1.make_null();
Copy code to clipboard

These methods are used for data type classes as well.

Casting Proxy Objects

It is possible to cast a d_ref<P> to a d_ref<Q> if P is a subclass of Q and the type checking will work at compile time. For example,

   d_ref<Sample_Employee> e1 = Sample_Employee::openid(&db, L"1");
   d_ref<Sample_Person> p1 = e1; // ok
   d_ref<Sample_Employee> e2 = p1; // gives a compile time error
Copy code to clipboard

It is also possible to cast d_ref<Q> to d_ref<P> if you know that Q is really a subclass of P, but, similarly to the interface related to null, it should be done via a function call conv_to(), not dynamic_cast(). The reason is that the "isa" relationship is really between P and Q. conv_to takes the d_ref< > that will contain the result as an argument passed by reference and if conversion is impossible sets it to null. For example:

   d_ref<Sample_Employee> e1 = Sample_Employee::openid(&db, L"1");
   d_ref<Sample_Person> p2 = e1;
   d_ref<Sample_Employee> e2; p2.conv_to(e2);
Copy code to clipboard

Resource Management

A d_ref< > automatically takes care of all system resources associated with the proxy object that it points to. For example:

   d_ref<Sample_Person> p1 = Sample_Person::openid(&db, L"1"); 
   p1->setDOB(1970,2,1); 
   d_ref<Sample_Person> p2 = p1->getSpouse(); 
   change_to_spouse(p2); // p2 points to the same server object as p1 
Copy code to clipboard

In the first line, openid(), a static method of Sample_Person, creates an instance of the d_ref<Sample_Person>. In the second line, the instance is used to modify the date of birth of the Person object. In the third line, p2 is set to point to the person's spouse, and in the fourth line, p2 is changed to point to the same person as p1. All the resources taken by p1 and p2 are released automatically when p1 and p2 go out of scope.

Using Collections

The proxies for collections are designed to fit into the framework of the C++ standard library. Proxies for %ListOfObjects and %ListOfDataTypes provide an interface which is almost identical to the interface of std::vector. Similarly, proxies for %ArrayOfObjects and %ArrayOfDataTypes provide an interface which is almost identical to the interface of std::map.

Object Collections

Proxies for collections of objects of type T contain objects of type d_obj_coln_type<T> that can be manipulated as d_ref<T>. They are different from d_ref<T> in that they ensure that all changes with the objects that they point to also take place in collections on the server. In order to change the value of an object for a particular collection and a given key, it is enough to assign the object a different d_ref<T>. These objects also amortize the cost of opening objects in collections by delaying opening of objects on the server until they are needed on the client.

Primitive Data Type Collections

Proxies for collections of data type T contain objects of type d_prim_coln_type<T> that can be manipulated as T. They are similar to the objects contained in collections of objects, but their initialization is not delayed because the overhead is insignificant.

Interface

A %ListOfObjects that holds elements of type T is generated as d_obj_vector<T> that holds elements of type d_obj_coln_type<T> that can be manipulated as d_ref<T>. An %ArrayOfObjects that holds elements of type T is generated as d_obj_map<T> that also holds elements of type d_obj_coln_type<T>.

A %ListOfDataTypes that holds elements of type T is generated as d_prim_vector<T> that holds elements of type d_prim_coln_type<T> that can be manipulated as T. An %ArrayOfDataTypes that holds elements of type T is generated as d_prim_map<T> that also holds elements of type d_prim_coln_type<T>.

Similar to other objects, collections have to be manipulated via d_ref<T>, which means that they can be instantiated by calling the static methods create_new(), and openref(), the methods used to initialize proxies for serial objects.

d_obj_coln_type<T> and d_prim_coln_type<T> can be constructed from T, which means that any function that takes d_obj_coln_type<T> or d_prim_coln_type<T> can be also called with T if the argument is constant.

Examples

The following are simple examples of collections in use.

Constructors

If a class CPP.Coln has a property, Lst, which is a %ListOfObjects that holds one or more instances of Sample.Person, then that property can be accessed in the C++ binding by

   d_ref< d_obj_vector<Sample_Person> > list = obj->getLst();
Copy code to clipboard

where obj is an object of type d_ref<CPP_Coln>.

Element Access

The third Sample.Person in the collection pointed to by list can be accessed by

   (*list)[2]
Copy code to clipboard

or

   *(list->begin()+2)
Copy code to clipboard

The person's name can be accessed by

   (*list)[2]->getName()
Copy code to clipboard

or

   (*(list->begin()+2))->getName()
Copy code to clipboard

"->" is used instead of "." because list is a d_ref<T>, so it acts like a pointer to the actual object.

Methods

p(of type d_ref<Sample.Person>) can be inserted into the collection pointed to by list by

   list->push_back(p);
Copy code to clipboard

the second Sample.Person in the collection pointed to by list can be erased by

   list->erase(list->begin()+1);
Copy code to clipboard
Algorithms

All persons of the collection pointed to by list can be printed by

   class Print_person : public std::unary_function<d_ref<Sample_Person>, int> { 
   private:
      std::ostream& out; 
   public:
      explicit Print_person(std::ostream& o)
         : out(o)
         {};
      result_type operator()(const argument_type& p) const;
         { out << p->getName() << std::endl; return 0; };
   };
   void print_people(d_ref<CPP_Coln>& obj) 
   {
      d_ref< d_obj_vector<Sample_Person> > list = obj->getLst();
      std::for_each(list->begin(), list->end(), Print_person(std::cout)); 
   };
Copy code to clipboard

Using Collection Elements in Methods

Most of the time, it is possible to forget that the actual type of a collection proxy is d_prim_coln_type<T> or d_obj_coln_type<T> (instead of T or d_ref<T>). However, these types (T or d_ref<T>) cannot be used as non-constant arguments to functions, although they can be used as constant arguments. Even if it's possible to get around the compilation error that should result from this incorrect usage, the seemingly changed value will not change in the collection. The proper way to change an element of a collection this way is to use a temporary and then assign the changed value to the element of the proxy object. For example:

   d_ref<Sample_Person> p = (*list)[2]; 
   change_to_someone_else(p); 
   (*list)[2] = p; 
Copy code to clipboard

But the following works fine if calc_some_value() does not change its argument:

   int val = calc_some_value((*list)[2]);
Copy code to clipboard

Data in Collection Proxies

In order to fit into the C++ standard library framework, the proxies for collections have to contain data. This means that two different proxies of the same collection may change the data on the server and their representation of the collection, but they may not know about each other, so they may lose synchronization. This problem does not exist if a collection is accessed via proxy objects of the same type (which is the intended usage) but if the types are different, loss of synchronization is possible.

Using Streams

Caché allows you to create properties that hold large sequences of characters, either in character or binary format; these sequences are known as streams. Character streams are long sequences of text, such as the contents of a free-form text field in a data entry screen; binary streams are usually image or sound files, and are akin to BLOBs (binary large objects) in other database systems. When you are writing to or reading from a stream, Caché monitors your position within the stream, so that you can move backward or forward.

Here is a simple program that creates and manipulates a Caché stream object:

   #include <database.h>
   #include <streams.h>
   
   int main(){
   // establish the physical connection to Cache'
      Db_err conn_err;
      d_connection conn = tcp_conn::connect("localhost[1972]:Samples",
         "_SYSTEM", "SYS", 0, &conn_err);
   
   // establish the logical connection to Cache'
   // database and create a low level stream object.
      db(conn);
      d_ref<d_char_stream> stream = d_char_stream::create_new(&db);
   
   // create an IOStreams extension stream object, put
   // "Hello, World!" in the stream, and rewind the stream
      d_iostream io(stream);
      io << "Hello, World!";
      io.rewind();
   
   // read each word and copy it to standard output
      std::string s;
      while (io.good()) {
         io >> s;
         std::cout << s << ' ';
      }
      std::cout << '\n';
      return 0;
   } 
Copy code to clipboard

Using Relationships

As in Caché, relationships are treated as properties. For example, the relationship between Sample.Employee and Sample.Company results in the following generated code:

  class Sample_Employee : public Sample_Person {
  // code
    virtual d_ref<Sample_Company> getCompany() const;
    virtual void setCompany(const d_ref<Sample_Company>&);
  // code
  };
  
  class Sample_Company : public Persistent_t {
  // code
    virtual d_ref< d_relationship<Sample_Employee> > getEmployees() const;
    virtual void setEmployees(const d_ref< d_relationship<Sample_Employee> >&);
  // code
  };
Copy code to clipboard

The d_relationship<T> class template is a standard container that supports iterators begin() and end(), and reverse iterators rbegin() and rend(). Here is a simple program that uses this relationship to access a list of employees:

   #include "Sample_Company.h"
  #include "Sample_Employee.h"
  
  #include <algorithm>
  
  class Print_person : public std::unary_function<d_ref<Sample_Person>, int> {
    private:
      std::ostream& out;
    public:
      explicit Print_person(std::ostream& o)
        : out(o)
        {}
      result_type operator()(argument_type p) const
        { out << p->getName() << std::endl; return 0; }
  };
  
  int main()
  {
  // establish the connection to Cache'
    Db_err conn_err;
    d_connection conn = tcp_conn::connect(
      "localhost[1972]:Samples", "_SYSTEM", "SYS", 0, &conn_err);
    Database db(conn);
  
    d_ref<Sample_Company> obj = Sample_Company::openid(&db, L"1");
    d_ref< d_relationship<Sample_Employee> > r = obj->getEmployees();
  
  // print the names of all employees in the order they are
  // stored in the relationship
    std::for_each(r->begin(), r->end(), Print_person(std::cout));
    std::cout << std::endl;
  
  // print the names of all employees in the reverse order
    std::for_each(r->rbegin(), r->rend(), Print_person(std::cout));
  
    return 0;
  } 
Copy code to clipboard

Using Queries

A Caché query is treated as type d_query, which is designed to fit into the framework of ODBC but provides a higher level of abstraction by hiding direct ODBC calls behind a simple and complete interface of a dynamic query. It has methods for preparing an SQL statement, binding parameters, executing the query, and traversing the result set.

The basic procedure for using a Caché query is as follows:

  • Prepare

    The method for preparing a query is:

       void prepare(const wchar_t* sql_query, int len);
    Copy code to clipboard

    where sql_query is the query to execute.

  • Set Parameters

    Assign values for any parameters.

       template<typename D_TYPE> void set_par(int index, const D_TYPE& val);
    Copy code to clipboard

    This function sets parameter index to val. The function works for any C++ binding data type. This function can be called several times for the same parameter. The previous value for the parameter will be lost. The new value need not be of the same type.

  • Execute

    This function executes the query. Do not call it until all parameters are bound.

       void execute(); 
    Copy code to clipboard
  • Fetch

    Determine if there is data available for retrieval.

       bool fetch(); 
    Copy code to clipboard

    This function tries to get the next row from the result set. It returns true if it succeeds and false if it fails. This function does not fetch any data. It only checks if there is more data to be fetched.

  • Retrieve Data

    If the query successfully executes, it returns a result set with one row for each record. The data in each row can be accessed by iterating the row from left to right by calling

       void get_data(T* val);
    Copy code to clipboard

    where T can be any data type of C++ binding. For d_string, you may specify how you want the data to be converted:

       void get_data(d_string* val, str_conv_t conv = NO_CONV);
    Copy code to clipboard

    The default is not to convert the data (the “NO_CONV” value). Using “CONV_TO_MB” converts the data to multibyte; using “CONV_TO_UNI” converts the data to Unicode.

    After each call, the implicit iterator moves to the next column (so you cannot access the data in the same column twice by calling get_data() twice). This eliminates the need for the implementation to store all the data on the client. Otherwise, using queries could result in large memory overhead. Applications that need random access to the data should read all the data in a row first.

    You can skip one or several columns by calling:

       void skip(int num_cols = 1)
    Copy code to clipboard

    You can get the index of the column that will be processed next by calling:

       int get_cur_idx();
    Copy code to clipboard

Here is a simple function that queries Sample.Person:

   void example(Database& db)
   {
      d_query query(&db);
   
      d_string name;
      d_int id;
      d_date dob;
   
      const wchar_t* sql_query = L"select ID, Name, DOB from Sample.Person
         where ID > ? and FavoriteColors = ? ";
      int size = wcslen(sql_query);
   
      query.prepare(sql_query, size);
   
      query.set_par(1, 1);
      query.set_par(2, "Blue", 4);
   
      query.execute();
   
      std::wcout << L"results from " << std::wstring(sql_query) << std::endl;
      while (query.fetch())
      {
         query.get_data(&id);
         query.get_data(&name);
         query.get_data(&dob);
         std::cout << std::setw(4) << id
                  ~<< std::setw(30) << std::string(name)
                  ~<< std::setw(20) << dob << std::endl;
      }
      std::cout << std::endl;
   }
Copy code to clipboard

Using Transactions

There are two options for performing transactions.

  • Database class methods — Perform standard nested transactions.

  • Transaction class methods — No nesting, but guarantees an automatic rollback if an exception is encountered.

Using Database Class Methods

Nested transactions can be performed using following methods of the Database class (also inherited by the LC_Database class):

  • tstart() — Starts a new level of nested transaction.

  • tcommit() — Marks the current level of the transaction as committed. Committing the outermost level causes the entire transaction to be committed.

  • trollback() — Rolls back all levels of the transaction.

  • tlevel() — Returns the current transaction level.

For example:

   for (i = 0; i < numPersons; i++) {
      db->tstart()
   // perform the transaction
      {...}
      if (goodtransaction)
         db->tcommit();
      else
         db->trollback();
   }
Copy code to clipboard

The tstart() and tcommit() methods are also called implicitly whenever a proxy object's save(), insert(), update(), or delete_object() member functions are called. This ensures a transaction scope for temporary locks, and for rollback in case of error.

Using Transaction Class Methods

The Transaction class provides a guaranteed automatic rollback in case of exceptions. When a Transaction object goes out of scope, the transaction is rolled back if neither commit() nor rollback() has been called. This class does not allow nested transactions.

The Transaction methods are:

  • Transaction() — The constructor starts the transaction (unlike a Database object, which requires a call to tstart()).

  • Transaction::commit() — Commits the transaction. Calling commit() more than once for the same Transaction object does nothing (unlike Database::tcommit(), which can be called repeatedly to commit multiple levels of a nested transaction).

  • Transaction::rollback() — Rolls back the transaction. Called automatically if the Transaction object goes out of scope before the transaction is committed or rolled back.

For example:

   for (i = 0; i < numPersons; i++) {
      Transaction tran(db);
   // perform the transaction
      {...}
   // transaction will rolled back if an exception 
   // occurs before this point
      if (goodtransaction)
         tran->commit();
      else
         tran->rollback();
   }
Copy code to clipboard

As shown above, the Transaction object must be instantiated with:

   Transaction tran(db);
Copy code to clipboard

rather than:

   Transaction tran = new Transaction(db);
Copy code to clipboard

If the object is allocated from the heap using new, it will not automatically be destroyed when it goes out of scope, and therefore the transaction will not be rolled back.

Using Transactions with the Light C++ Binding

In Light C++ Binding applications, if an exception is thrown within the projection class member functions save(), delete_object(), insert(), or update(), automatic rollback occurs. Exceptions thrown in other contexts do not cause transactions to be automatically rolled back, unless an instance of the Transaction class has been declared as an automatic variable in a scope within which the exception is thrown, and the exception is not caught within that scope.