Using C++ with Caché
Using the C++ Binding
[Back] [Next]
   
Server:docs1
Instance:LATEST
User:UnknownUser
 
-
Go to:
Search:    

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;
}
This code imports the Sample header files, and then performs the following actions:
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
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:
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;
   }
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
   }
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:
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();
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
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);
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 
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();
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]
or
   *(list->begin()+2)
The person's name can be accessed by
   (*list)[2]->getName()
or
   (*(list->begin()+2))->getName()
"->" 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);
the second Sample.Person in the collection pointed to by list can be erased by
   list->erase(list->begin()+1);
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)); 
   };
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; 
But the following works fine if calc_some_value() does not change its argument:
   int val = calc_some_value((*list)[2]);
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;
   } 
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
  };
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;
  } 
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:
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;
   }
Using Transactions
There are two options for performing transactions.
Using Database Class Methods
Nested transactions can be performed using following methods of the Database class (also inherited by the LC_Database class):
For example:
   for (i = 0; i < numPersons; i++) {
      db->tstart()
   // perform the transaction
      {...}
      if (goodtransaction)
         db->tcommit();
      else
         db->trollback();
   }
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:
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();
   }
As shown above, the Transaction object must be instantiated with:
   Transaction tran(db);
rather than:
   Transaction tran = new Transaction(db);
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.