Introduction to the Query Object Framework
QOF puts the emphasis on the application data - the data is designed first, the backends handle whatever data is generated and the only real programming task is to design the interface.
QOF tries to provide a generic interface to generic data without requiring the original program. It does this without preventing the original program from importing the data at a later date by always maintaining the data at the level of an object. Objects can be translated between programs but the original remains and can be updated by data in the translated objects.
QOF supports data-centric applications by removing the burden of having to write the code to communicate with multiple backends, handle user arbitrary queries and exchange data between applications.
Objects are defined to match the data source, Entities are created when real data is loaded, one entity per record. Entities are grouped in Collections and Collections are grouped in Books. A single book (QofBook) can contain any number of unique collections (QofCollection). A collection always contains only entities of the one type. All entities in a book must be in the matching collection. (These rules are implemented within ?QofObject, ?QofClass and ?QofBook - objects do not have to implement them directly.)
Data-Centric programming
When starting a new QOF application, design starts with the data. Decide which parameters you need to store in each of your data objects (QofObject), write the functions to get and, if appropriate, set data in those parameters (QofClass) and then present those parameters and objects in whatever frontend you care to create. Any application can mix and match any number of QOfObjects (subject to the needs of the objects themselves) in any order.
QOF fits naturally into a data-centric model because it can provide a mechanism for adding backends, such as the QOF SQlite backend, to an existing program simply by installing the backend package and using an "access_method": sqlite:/home/user/mysqlite.db. The program itself does not necessarily need to even be changed - a user can install the QOF SQLite backend and immediately use that backend with pilot-qof. Applications can also devise their own backend and still exchange data seamlessly with all backends supported by QOF.
As QOF has no pre-determined front-end, it can free the application from a specific database or database format and by doing so, it automatically adds more backends. In effect, it gives you the best of both. You get an engine that can work with multiple front ends and multiple backends, with the same data being mapped or translated between them all.
Starting with the C code
QofObject setup
QofObject defines the basic parameters of the object - name, description, functions to create,
iterate, compare and handle dirty/clean flags. In most cases, only the create_me
and my_printable_func functions needs any object-specific code (and the printable
pointer can be set to NULL anyway).
static ?QofObject bus_obj = {
.interface_version = QOF_OBJECT_VERSION,
.e_type = YOUR_MODULE_NAME,
.type_label = YOUR_MODULE_DESC,
.create = (gpointer)create_me,
.book_begin = NULL,
.book_end = NULL,
.is_dirty = qof_collection_is_dirty,
.mark_clean = qof_collection_mark_clean,
.foreach = qof_collection_foreach,
.printable = my_printable_func,
.version_cmp = (gint (*)(gpointer, gpointer)) qof_instance_version_cmp,
};
(printable can be a way of printing basic, complete or debug information about the object, QOF does not use it directly.)
Available parameters (QofParam):
- String QOF_TYPE_STRING
- 64-bit Time QOF_TYPE_TIME - no risk of overrun in 2038 on any architecture, unlike time_t or Timespec.
- 128-bit Maths QOF_TYPE_NUMERIC
- 128bit Globally Unique Identifiers QOF_TYPE_GUID
- 32-bit integers QOF_TYPE_GINT32
- 64-bit integers QOF_TYPE_GINT64
- Floating point numbers QOF_TYPE_DOUBLE
- boolean QOF_TYPE_BOOLEAN
- character QOF_TYPE_CHAR
- Key:Value pairs QOF_TYPE_KVP
- Collections (not covered in this starter guide).
Get functions (QofAccessFunc)
All parameters need a "Get" function that simply retrieves the data and returns it in a ?QofParam. Parameters that do not need to be set (e.g. calculated values like account balances or summaries) can be complete with just a "Get". In QOF, "get" functions use the ?QofAccessFunc prototype:
typedef gpointer(* ?QofAccessFunc)(gpointer object, const ?QofParam *param)
Set functions (QofSetterFunc)
Parameters that actually need to be stored in a backend (QofBackend) (and restored from that backend when loading a data source) must use a "set" function that QOF can use - a ?QofSetterFunc, with the prototype:
typedef void(* ?QofSetterFunc)(gpointer, gpointer);
and implemented as:
void setter_func (object_type *self, param_type *param);
Defining Parameters
Parameters are defined in a static ?QofParam struct:
static ?QofParam params[] = {
{OBJ_AMOUNT, QOF_TYPE_NUMERIC, (QofAccessFunc) dyn_getAmount, (QofSetterFunc) dyn_setAmount, NULL},
{OBJ_KVP, QOF_TYPE_KVP, (QofAccessFunc) qof_instance_get_slots, NULL, NULL},
{QOF_PARAM_BOOK, QOF_ID_BOOK, (QofAccessFunc) qof_instance_get_book, NULL, NULL},
{QOF_PARAM_GUID, QOF_TYPE_GUID, (QofAccessFunc) qof_instance_get_guid, NULL, NULL},
{NULL, NULL, NULL, NULL, NULL},
};
Parameter names are often best created with #define:
#define YOUR_MODULE_NAME "object_test" #define YOUR_MODULE_DESC "Test Object" #define OBJ_AMOUNT "anamount" #define OBJ_KVP "kvp"
Important - parameter names have a syntax requirement
- No spaces
- No hyphens or dashes - underscores only
- All other characters must be alphanumeric
- No book can contain more than one object with the same e_type.
As a regular expression, any string used for an e_type must match:
/^([a-z_0-9])+$/
This is because the e_type is used in various SQL operations. Parameter descriptions can contain any valid gchar and do not even need to be unique.
Your first ?QofObject
This is a sample object from the QOF test suite (test-book-merge.c), without the get and set functions.
#define TEST_MODULE_NAME "book_merge_test"
#define TEST_MODULE_DESC "Test Book Merge"
#define OBJ_NAME "somename"
#define OBJ_AMOUNT "anamount"
#define OBJ_DATE "nottoday"
#define OBJ_GUID "unique"
#define OBJ_DISCOUNT "hefty"
#define OBJ_VERSION "early"
#define OBJ_MINOR "tiny"
#define OBJ_ACTIVE "ofcourse"
#define OBJ_FLAG "tiny_flag"
/* simple object structure */
typedef struct obj_s
{
?QofInstance inst;
gchar *Name;
gchar flag;
?QofNumeric Amount;
const GUID *obj_guid;
?QofTime *date;
gdouble discount; /* cheap pun, I know. */
gboolean active;
gint32 version;
gint64 minor;
} myobj;
static myobj *
obj_create (QofBook * book)
{
myobj *g;
g_return_val_if_fail (book, NULL);
g = g_new (myobj, 1);
qof_instance_init (&g->inst, TEST_MODULE_NAME, book);
obj_setGUID (g, qof_instance_get_guid (&g->inst));
g->discount = 0;
g->active = TRUE;
g->version = 1;
g->minor = 1;
g->flag = 'n';
qof_event_gen (&g->inst.entity, QOF_EVENT_CREATE, NULL);
return g;
}
static ?QofObject obj_object_def = {
.interface_version = QOF_OBJECT_VERSION,
.e_type = TEST_MODULE_NAME,
.type_label = TEST_MODULE_DESC,
.create = (gpointer) obj_create,
.book_begin = NULL,
.book_end = NULL,
.is_dirty = NULL,
.mark_clean = NULL,
.foreach = qof_collection_foreach,
.printable = NULL,
.version_cmp = (gint (*)(gpointer, gpointer)) qof_instance_version_cmp,
};
gboolean
myobjRegister (void)
{
static ?QofParam params[] = {
{OBJ_NAME, QOF_TYPE_STRING, (QofAccessFunc) obj_getName, (QofSetterFunc) obj_setName, NULL},
{OBJ_AMOUNT, QOF_TYPE_NUMERIC, (QofAccessFunc) obj_getAmount, (QofSetterFunc) obj_setAmount, NULL},
{OBJ_GUID, QOF_TYPE_GUID, (QofAccessFunc) obj_getGUID, (QofSetterFunc) obj_setGUID, NULL},
{OBJ_DATE, QOF_TYPE_TIME, (QofAccessFunc) obj_getDate, (QofSetterFunc) obj_setDate, NULL},
{OBJ_DISCOUNT, QOF_TYPE_DOUBLE, (QofAccessFunc) obj_getDiscount, (QofSetterFunc) obj_setDiscount, NULL},
{OBJ_ACTIVE, QOF_TYPE_BOOLEAN, (QofAccessFunc) obj_getActive, (QofSetterFunc) obj_setActive, NULL},
{OBJ_VERSION, QOF_TYPE_INT32, (QofAccessFunc) obj_getVersion, (QofSetterFunc) obj_setVersion, NULL},
{OBJ_MINOR, QOF_TYPE_INT64, (QofAccessFunc) obj_getMinor, (QofSetterFunc) obj_setMinor, NULL},
{OBJ_FLAG, QOF_TYPE_CHAR, (QofAccessFunc) obj_getFlag, (QofSetterFunc) obj_setFlag, NULL},
{QOF_PARAM_BOOK, QOF_ID_BOOK, (QofAccessFunc) qof_instance_get_book, NULL, NULL},
{QOF_PARAM_GUID, QOF_TYPE_GUID, (QofAccessFunc) qof_instance_get_guid, NULL, NULL},
{NULL, NULL, NULL, NULL, NULL},
};
qof_class_register (TEST_MODULE_NAME, NULL, params);
return qof_object_register (&obj_object_def);
}
Getting and setting data
The sample object above is very simple - it uses QOF prototypes to store the data in the core struct. Other objects may use customised structs or structs that act as wrappers around real data structs in other libraries or components (e.g. pilot-qof gets and sets data directly into pilot-link structs).
The get and set routines for this simple object are themselves simple (if repetitive):
static void
obj_setActive (myobj * g, gboolean h)
{
if (!g)
return;
g->active = h;
}
static gboolean
obj_getActive (myobj * g)
{
if (!g)
return FALSE;
return g->active;
}
static void
obj_setAmount (myobj * g, ?QofNumeric h)
{
if (!g)
return;
g->Amount = h;
}
static ?QofNumeric
obj_getAmount (myobj * g)
{
if (!g)
return qof_numeric_zero ();
return g->Amount;
}
static void
obj_setName (myobj * g, char *h)
{
if (!g || !h)
return;
g->Name = strdup (h);
}
static gchar *
obj_getName (myobj * g)
{
if (!g)
return NULL;
return g->Name;
}
Note that all these functions are declared as static - the only public function should be the myobjRegister function. This makes it simple to package objects as a GModule where the init routine for the module simply calls myobjRegister.
If you choose to package the objects as a shared library, the header file for this object is trivial:
#ifndef MY_OBJECT_H #define MY_OBJECT_H gboolean myobjRegister (void); #endif
Say goodbye to API breakage! (at least for your objects).
Neil Williams <linux@codehelp.co.uk> Mar 2008.
