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

The Light C++ Binding (LCB) is most useful in applications where high performance is the primary concern, and class design is relatively simple. It is a special purpose, limited subset of the C++ binding, intended primarily for applications that must load data into a persistent database at very high speed. For example, some applications capture raw real-time data at such a high rate that it must typically be stored it in an in-memory database before it can be processed and transferred to persistent storage. LCB can offer a similar level of performance while also offering failover, which is not possible with an in-memory database.

For basic object manipulation (creating objects, opening objects by Id, updating, and deleting), LCB is ten to twenty times faster than the standard C++ binding.
Constraints on LCB Applications
The most significant tradeoff for this added speed is a limitation in the complexity of the objects to be stored. The primary constraints on LCB applications are as follows:
See Installing the Light C++ Binding for LCB installation requirements.
Light C++ Binding Architecture
LCB gains much of its improved performance because it provides a much faster way for the C++ application to communicate with the Caché Object Server. LCB does not use the standard binding's client/server architecture, with Caché running as a separate server process. Instead, Caché is loaded as a DLL or shared object, allowing it to execute as part of the application process. The following diagram illustrates this structure:
Light C++ Binding Architecture
Both LCB and the standard C++ binding use the Caché C++ Generator and the Caché C++ Binding Library, but there are major differences at runtime:
LCB Classes in the Caché C++ Library
The Caché C++ library implements the Light C++ Binding with a special set of classes, most of which are LCB versions of classes used by the standard binding. The following classes provide the functions that you will need for an LCB application:
Connections and Multithreading
An LCB connection uses the following classes:
The following code fragment demonstrates how these classes are used. Compare the calls in this code to the example in Connecting to the Caché Database. Only the class names have changed.
   #  include "lc_connection.h"
   #  include "lc_database.h"
      Db_err conn_err; 

      d_connection conn =  lc_conn::connect(
         conn_str, user, pwd, timeout, &conn_err); 
      LC_Database db(conn);
Important:
For all LCB applications, the GLOBALS_HOME environment variable must be set to the root directory of the Caché instance (see Additional LCB Requirements). All connection attempts will fail if this environment variable is not set.
Multithreading
LCB is thread-safe, and uses the Callin API to provide parallel multithreading on multiprocessor machines. Performance is similar to that of multithreaded Callin applications. Unlike Callin applications, LCB applications do not require detailed knowledge of class implementations, since the C++ code can be regenerated whenever class definitions change.
A separate database connection (including both d_connection and LC_Database objects) must be used in each thread. If member functions of LC_Database, or of projection objects connected to an LC_Database instance, are called in a different thread than the thread in which the LC_Database object was created, the following exception is thrown: "Database connection may not be shared by multiple threads"
For examples of multithreaded LCB code, see the mttest.cpp and qtest.cpp sample programs located in <cachesys>\Dev\cpp\samples (see Default Caché Installation Directory in the Caché Installation Guide for the location of <cachesys> on your system).
Connections and Multiple Threads
Projection objects can only be connected to one database connection at a time, and can only be used in the thread in which that database connection was created. To use a projection object in more than one thread, use the projection object methods disconnect() and connect(). The is_connected() method can be used to determine the connection state.
Attaching and Detaching LCB Objects
Since the C++ objects contain the only in-memory copy of the data, the C++ application can continue to work with them even if there is no connection between the Caché Object Server and the persistent database. This is especially important for multithreaded applications that want to share an object between two or more threads.
An object is attached when:
An object is detached when:
Transactions and Multithreading
Each thread / database connection has its own transaction context:
Using Objects in LCB
LCB projected objects are different from standard C++ binding objects in a number of ways. It is important to understand these differences in the following situations:
Using Persistent Object References as Properties
When a Caché class uses a persistent class as a property, LCB projects the property as a reference to a persistent object. Each persistent reference property consists of an lc_d_ref that points to the referenced object, and a string representing the id of the object.
Property accessors get and set lc_d_ref<LC_xxx>& where LC_xxx is the client type of the property (for example, LC_User_Person is the client type for Caché class User.Person). Logically, the accessors work as follows:
For example, assume a Caché class Sample.Employee that contains a property Office of persistent class Sample.Office. Each class has a user-assigned idkey (Office on the office's city, and Employee on the employee's name). The following example opens the Office object for the New York office, and the Employee object for John Kent. If Kent is currently assigned to the Boston office, he is reassigned to the New York office:
   lc_d_ref<LC_Sample_Office> o = LC_Sample_Office::openid(db, "New York"); 
   lc_d_ref<LC_Sample_Employee> e = LC_Sample_Employee::openid(db, "Kent, John");
   if (e->getOffice()->getCity() = "Boston")  {
      e->setOffice(o);
      e->save();
   }
In generated code for classes that contain persistent reference properties, the save() function, and getter and setter functions for persistent reference properties, are generated in the .cpp file rather than inline in the .h file, as is normally the case. This prevents the .h file of a projected class from indirectly including itself due to circular references.
Getter functions for persistent reference properties, unlike other getter functions, are not declared as const, since they must modify the referencing object when they cause it to swizzle.
Using Classes that Inherit from Other Persistent Classes
The Light C++ Binding supports retrieval and update of persistent objects whose classes inherit from other persistent classes. Batch insert is not supported for classes that inherit.
The LCB projection class for a Caché class inherits from the projection class of that Caché class's superclass. With multiple inheritance, this applies only to first class in the list of superclasses. Other superclasses are transparent to LCB, which simply sees their properties as belonging directly to the subclass.
Opening a Subclass Object from its Superclass
An object of a class that inherits from another class can be opened by the openid method of the inherited superclass, provided that the C++ code for the subclass has been generated and linked into the application.
For example, assume a class LC_Sample_Employee that inherits from class LC_Sample_Person. If variable sub_id contains the id of an LC_Sample_Employee object, the following statement can be used to open it:
   lc_d_ref<LC_Sample_Person> newemployee = LC_Sample_Person::openid(db, sub_id); 
LCB will detect the actual class of the object to be opened, and will transparently open newemployee as an object of subclass LC_Sample_Employee.
Since LCB performs its storage operations in client-side code, it needs to be aware of the actual class of the object. If the generated code for a subclass is not linked into the application, LCB cannot open the object as that class. Instead, it will open the object as the nearest ancestor of the subclass for which linked code is available. In that case, an exception is thrown if the application attempts to update or delete the object, because LCB cannot ensure that all index entries will be properly updated or deleted.
Because LCB transparently opens an object as the most specific-possible subclass, if the same object is opened once from the superclass and once from the subclass, both lc_d_refs will reference the same subclass object in memory.
Note:
Unlike LCB, the standard C++ binding does not have the ability to transparently open an object as a more specific subclass than the class whose openid method was called. However, since the standard binding performs all of the actual storage operations on the Caché Server (which knows the actual class of the object), it is still possible to update or delete objects opened from a superclass that are actually of a subclass.
Using Embedded Serial Object Properties
LCB objects can have embedded serial objects as properties. An LCB serial property has a getter function that returns a pointer to the type of the property. For example, assume that Caché class Sample.Person contains a property Home of serial class Sample.Address. The getter function for the projected serial property would be LC_Sample_Address *getHome(). The following example retrieves the old street from a person's home address, and then assigns a new street value:
   lc_d_ref<LC_Sample_Person> p = LC_Sample_Person::openid(db, 1); 
   d_string oldStreet; 
   d_string newStreet("Broadway"); 
   oldStreet = p->getHome()->getStreet(); 
   p->getHome()->setStreet(newStreet); 
   p->save(); 
LCB serial classes (projected from Caché classes based on %SerialObject) inherit from LC_Serial_t. The generated code for an LCB serial class consists only of a header file (unlike persistent LCB classes, which have code in both .h and .cpp files). For example, a header file named LC_Sample_Address.h would contain the only projection code generated for Caché class Sample.Address.
Using List and Array Properties
The only collection types that can be used with LCB are lists or arrays of datatype. The following Caché datatype classes are supported: %Integer, %Float, %Decimal, %Double, %String, %Date, %Time, %TimeStamp, and %Currency (see the Reference for Simple Datatype Classes for a discussion of these datatypes).
Long strings must be enabled in order to store lists or arrays larger than 32K. To enable support for long strings system-wide, open the Management Portal and select [System Administration] > [Configuration] > [System Configuration] > [Memory and Startup], then select the Enable Long Strings check box. The long string setting can also be overridden temporarily using $ZUTIL(69,69).
Lists of Datatypes
LCB projects list properties as std::vector<d_xxx>, where d_xxx is a valid LCB datatype class. For example, a list of %String would be projected as std::vector<d_string>.
Arrays of Datatypes
LCB projects array properties as std::map<d_string,d_xxx>, where d_xxx is the property's array element datatype. For example, an array of %String would be projected as std::map<d_string,d_string>.
Important:
When defining an array property in a Caché class, the property parameter STORAGEDEFAULT must be set to list. For example, a Caché class might define a %String array as follows:
   Property MyArray As array Of %String(STORAGEDEFAULT = "list");
The default parameter value, array, is not supported in LCB.
Standard LCB Projection Class Methods
The C++ Generator provides LCB projection classes with a set of methods similar to those generated for standard proxy classes (see Standard Proxy Class Methods). The methods listed in this section are added to all projection classes.
BuildIndices()
Invokes the Caché %BuildIndices class method (see %Library.Persistent) to completely rebuild all indices of the class. It can enhance performance when used after save() or delete_object() (called with defer_indices set to true).
   static InterSystems::d_status BuildIndices(
      InterSystems::LC_Database * db)
connect()
Connects (or reconnects) a projection object to a database connection. An exception will be thrown if the object is not disconnected.
   void connect(
      InterSystems::LC_Database * db)
create_new()
Creates a new projection object. The projection object is not saved to the database until save() or insert() is called. Once save() is called, the object is attached. If save() is called again, the object is updated. To cause save() to create a different new object, you must first call create_new() again, or call detach().
   static lc_d_ref<LCBclass> create_new(
      LC_Database* db,
      const_str_t init_val = 0,    // const_str_t is a typedef of const wchar_t*
      Db_err* err = 0)
The create_new() method inherited from LC_Persistent_t is overridden by a version that returns a reference to the specific class (lc_d_ref<LCBclass> where LCBclass is the name of the projection class).
delete_object()
Deletes an open object from the database. This method allows you to delete an open object from the database without destroying the projection object.
   d_status delete_object(
      bool defer_indices = false,
      int timeout = -1,
      Db_err* err = 0)
After delete_object() is called, the projection object still contains property data values, but is no longer attached. If the object was locked, the lock is released.
Calling save() right after delete_object() will create a new database object with the same values, except for any properties explicitly set to different values
detach()
Detaches a projection object from an object in database. This is a no-op if the object is not attached). If a retained lock is held on the object, it is released.
   void detach()
Property values are retained in the projection object. This permits reusing the projection object to create multiple new database objects, avoiding the overhead of calling create_new() and copying properties that have same values in multiple objects.
direct_save()
A very-high-performance alternate interface for creating new objects while avoiding the overhead of projection object instantiation and data conversions. It can only be used to insert new objects, not to update existing objects. direct_save() can be used with a Unicode database, but it only supports ASCII characters in strings.
   static d_status direct_save(
      LC_Database* db,
      const char *<prop1>,
      ..., 
      const char *<propN>)
The parameters <prop1>...<propN> are properties of the class.
This method does not create index entries (although you can use BuildIndices() after saving).
disconnect()
Disconnects a projection object from a database connection. An exception will be thrown if the object is not detached, or if the object's current database connection was created in a different thread.
   void disconnect()
id()
Gets an id from an attached object. The *buf parameter can be either a multibyte string or a Unicode string.
   int id(
      wchar_t *buf,
      size_t bufsiz)
   int id(
      char *buf,
      size_t bufsiz)
insert()
Inserts a new object into the database.
   d_status insert(
      bool defer_indices = false, 
      int timeout = -1, 
      Db_err* err = 0)
If called for an attached object, insert() detaches the object, and attempts to insert a new object with the same property values. For a user-assigned idkey, this causes a duplicate idkey exception.
is_attached()
Returns true if the projection object is attached.
   bool is_attached()
is_connected()
Returns true if the projection object currently has a connection to the database.
   bool is_connected()
openid()
Opens an existing object, specified by id. Projection object data members are set to current values from the database object, and the projection object is attached to the database object.
   static lc_d_ref<LCBclass> openid(
      LC_Database* db,
      const_str_t ident,         // const_str_t is a typedef of const wchar_t*
      int concurrency = -1,
      int timeout = -1,
      Db_err* err = 0)
   static lc_d_ref<LCBclass> openid(
      LC_Database* db,
      const char * ident,
      int concurrency = -1,
      int timeout = -1,
      Db_err* err = 0)
The ident parameter can be either a multibyte string or a Unicode string. The openid() method inherited from LC_Persistent_t is overridden by a version that returns a reference to the specific class (lc_d_ref<LCBclass> where LCBclass is the name of the projection class).
release_shared_lock()
Explicitly releases a shared lock when the projection object has been opened with concurrency mode LC_CONCURRENCY_SHARED_RETAINED (see LCB and Concurrency).
void release_shared_lock()
save()
Saves the projection object to the database. It must be explicitly called to save a newly-created object to the database, or to save changes to the database.
   d_status save(
      bool defer_indices = false,
      int timeout = -1,
      Db_err* err = 0)
If the projection object is attached, save() updates the object. If the object is detached, save() creates a new object. In transactions, save() brackets the update with implicit calls to tstart() and tcommit() (see Using Transactions).
set_from_err_list()
Sets the projection object's property values from the error list entry returned by a batch insert (see Using LCB Batch Insert).
   void set_from_err_list(
      const std::pair<d_status,d_binary> & list_entry)
update()
Updates an existing database object.
   d_status update(
      bool defer_indices = false, 
      int timeout = -1, 
      Db_err* err = 0)
The object must already be attached. If not, the following exception is thrown: "Object must be opened or inserted before being updated".
Using Queries in LCB Applications
Queries in LCB applications use the same API calls as the regular binding (see Using Queries), and provide similar performance. To run a query in an LCB application, pass an instance of LC_Database as a database argument to the d_query constructor. For example:
   LC_Database *db;
   d_query q(db);
LCB queries do have some limitations compared to the regular binding:
Using LCB Batch Insert
The LC_Batch class provides methods for batch insertion using the Light C++ Binding. The << operator is used to serialize objects and add them to a batch. When a batch is saved with the flush() method, there is a separate implicit transaction for each object, and the transaction is rolled back if there is any error. The get_errors() method returns a list of failed transactions.
Performance is only slightly better than with save(), but a single projection object can be serialized repeatedly with different property values, which may significantly reduce the processing overhead compared to calling create_new() for each insert.
Transaction Handling
If the _do_tx parameter of the LC_Batch() constructor is set to true, there is a separate implicit transaction for each object when a batch is saved. The implicit transaction is committed if the object is saved successfully, or rolled back if there is any error. If the entire batch should be either committed or rolled back, the call to flush() should be bracketed with calls to LC_Database methods tstart() and tcommit() (see Using Transactions). This will cause the entire batch insert to be rolled back if an error is encountered.
Optimization
LCB and Concurrency
LCB supports the standard Caché concurrency model (see Object Concurrency in Using Caché Objects). Use the following constants to specify concurrency level:
Specify concurrency level when opening an object. For example:
   d_ref<User_Person> person = 
      User_Person::openid(db, id, LC_CONCURRENCY_EXCLUSIVE);
Note:
You cannot specify a non-default concurrency level when creating a new object (although you can subsequently call openid() to set the desired concurrency level). LCB concurrency always defaults to LC_CONCURRENCY_ATOMIC.
Update Semantics
Calling save() or update() sets all properties of a database object from the projection object, whether or not the C++ application has modified them.
An object is protected from modification by other applications if it was opened with concurrency level SHARED_RETAINED or EXCLUSIVE.
If the object was opened with a lower concurrency level, save() may overwrite properties set by someone else's intervening update, or re-create the object after an intervening delete. This will not corrupt the object or indices, because appropriate locks are taken. You can explicitly call openid() again with a higher concurrency level, to lock an object that wasn't previously locked. This always reloads the current property values from the database.
When updating an object that was opened with a concurrency level lower than SHARED-RETAINED, if index updating is enabled and an object's indexed properties have been modified, the object is locked and the old property values are reloaded from the database. This is necessary so that the old index entries can be deleted.
Optimization and Troubleshooting
For best performance:
Workarounds for SUSE 12 Linux Build Problems
Light C++ Binding applications on SUSE 12 Linux systems require one of the following two simple workarounds to build without error, due to a change in ld (the linker) from previous versions of SUSE Linux:
Either option resolves the issue, or both options can be used together. The workarounds are made necessary because the linker now requires application makefiles to explicitly specify dependent libraries of other libraries with which they link, even if those other libraries specified the dependent libraries when they were linked. In this case, libcachet.so depends on libpthread.so, so starting with SUSE 12 Linux, both libraries must be explicitly specified.
Detecting “object not found” Errors
If openid() is called with the optional Db_err* parameter and no object with the specified id exists, the Db_err code is set to -3, and msg is set to "object not found". The d_ref to which the result of openid() is assigned is set to null, which can be detected by calling its is_null() member function.
It is the caller's responsibility to either test whether the Db_err code is non-zero, or to test whether the d_ref is null, before dereferencing the d_ref. Dereferencing a null d_ref causes an exception to be thrown, with code -2 and msg "Can not dereference a null d_ref<> value". For example, assume the following code fragment is executed for id "2", and no object with id value 2 exists:
   Db_err openerr; 
      person = User_Person::openid(db, id, concurrency, timeout, &openerr); 
   if (openerr) 
      std::cerr << openerr << ",\n source = " << openerr.get_src() << '\n'; 
   else 
      printf("Object was found\n"); 
   if (person.is_null()) 
      std::wcout << L"Person with id " << id << L" doesn't exist\n"; 
   // Go ahead and dereference the d_ref whether or not it is null 
   d_string name = person->get_name(); 
Since the object was not found, the output is:
  Error: code = -3, msg = object not found, 
   source = LC_Database::lc_openid_obj 
  Person with id 2 doesn't exist 
  Error: code = -2, msg = Can not dereference a null d_ref<> value, 
   source = abs_d_ref::operator->() 
Calling the LC_Database and d_connection Destructors
It is important to call the LC_Database and d_connection destructors, in order to cleanly disconnect the application from Caché, causing the license and other resources to be released. The lcbdemo sample application shows an example of this.
If d_connection or LC_Database instances are declared as local variables, their destructors will automatically be called when they go out of scope. But if they are allocated via new and assigned to pointers, they must be explicitly destroyed using C++ "delete".
Using lc_conn::connect