NexusDB has an internal event infrastructure that you can hook into by means of Monitors and Extenders. Monitors/Extenders allow to change the default behavior of database events that are fired on particular changes or conditions that occur within NexusDB's core engine. These function very much like triggers but due to its integration into the core of the database engine are actually much more powerful. Monitors and extenders always work together.
Some background information
Monitors get notified on creation of server internal objects such as sessions, databases, cursors, ... At this time the monitor can instantiate extenders and attach them to these objects.
Extenders can be attached to a variety of server internal objects. Once created they get notifications of events occurring within these objects (e.g. the posting of a record in a Cursor object) and can stop, change or extend the default behavior (e.g. disallowing post for certain users).
Monitors and Extenders can be used for many purposes: logging, custom security systems, referential integrity, ...
The event mechanism within NexusDB is extensive and complete; Monitors and Extenders empower you to exploit this mechanism.
An example - Keeping track of last update time of a record
An often needed requirement is to keep track when a record was created or last updated. This sounds easy to implement on the client side on first sight. Just use the OnBeforePost event of the TDataset and change the date. Well, bad luck if you're using a C/S solution that is running clients in different time zones for example. You will end up in a big mess.
Luckily this can be implement very easily in NexusDB. Here's how.
|•||Add a Created and LastUpdated field with type TDateTime to the table|
|•||Create a Monitor/Extender combo|
|•||Implement the methods for the Monitor class|
|•||Implement the methods for the Extender class|
|•||Add an instance of the Monitor to the Server, set the properties and recompile the server|
Add a Created and LastUpdated field with type TDateTime to the table
Now that's pretty simple and straightforward, isn't it? If you haven't already, please download and unzip the Demo Database into a directory of your choice. Then start the NexusDB Enterprise Manager (nxEnterpriseManager.exe). In the Servers window double click the Internal Server entry. The databases you've added in earlier sessions will appear now. Select the database pointing to the Demo Database directory or if that doesn't exist, add a new Database by clicking the right mouse button and selecting New Database Alias. Open the database by double clicking and press the right mouse button on the CD Table. In the menu select the Redefine item. In the dialog window that appears please add the two fields as shown here:
Please note the selection of the CurrentDateTime descriptors in the Default Descriptor column. This will for all new records set both fields to the Server time of the record creation. In other words that means that we don't have to bother with setting the Created date manually for newly added records.
Hint: Default Value Descriptors
In NexusDB every field is set to a certain default value when a new record is created and is in the default setting the constant NULL. When defining the structure of a table the developer can assign a different value as Default Value to each field or can assign a different Default Value descriptor. Readily available descriptors are Const and CurrentDateTime. The developer can implement his own descriptors and register them into the NexusDB server if required.
Please note that the default values will be applied to newly added fields on restructure of the table. They are not applied to already existing fields, even if they are NULL.
When you have added the new fields press the Restructure button and repeat the same for the Tracks table. The two tables now have these new fields which are initialized to the restructure time. We have now prepared the data structure and can commence to step 2.
Create a Monitor/Extender combo
As mentioned above a monitors and extenders always work together. So we will now create a monitor that will get notified every time a new NexusDB server object is created. For this we will create a new package so we can register and install it into the Delphi component palette.
Start Delphi, and select New->Other from the File menu. In the New Items dialog select Package from the New tab and press ok. This will create new Delphi package project. Press CTRL-S and save the new project to your liking. Now press the Add button to create a new component. Fill out the New Component tab of the popping up dialog like this:
Press the OK button and Delphi will create new unit including the component registration code for you. Press CTRL-S to save the unit to nxLastUpdatedMonitor.pas. If you compile and install the package now Delphi will add a new component to the NexusDB palette.
We've successfully created and installed our own monitor class, so it's time now to add the code for our own extender.
Implement the methods for the Monitor class
Since every extenders has to be descended from nxBaseEngineExtender, we define our own class descendet from it. For this we add the following code to the interface of nxLastUpdatedMonitor:
TnxLastUpdatedExtender = class(TnxBaseEngineExtender)
As mentioned above, the monitor will later be attached to a ServerEngine and get notified by this engine about every creation of a server object (which are all descendants of TnxExtendableServerObject). This notification is done by calling the ExtendableObjectCreated method of the monitor, which is defined as
procedure ExtendableObjectCreated(aExtendableObject : TnxExtendableServerObject); override;
The purpose of a monitor is to attach a certain extender instance to the object created. To do this we simply override above method and create an instance of our TnxLastUpdatedExtender class.
// check if the Object created was a cursor
if aExtendableObject is TnxAbstractCursor then
As you can see we first check, if the object created at the server is a TnxAbstractCursor descendant. As we want to extend inserting and updating of records, we only want to attach our extender to cursors, and not to e.g. sessions.
Hint: Make sure to attach your server to the needed server objects only!
An extender is typically only interested in a small subset of all available notification events. Attaching extenders to objects that never will trigger the needed notifications is unnecessary and will influence the performance of the whole server (if used excessively). As a consequence always think about what you want to achieve and which objects you want to attach to. The most important ones are
We now have a system in place, that will monitor the creation of server objects and if a cursor is created we attach an instance of our extender to it. So all that is left is the actual implementation of the notification handler.
Implement the methods for the Extender class
Let's look at the methods of TnxBaseEngineExtender and we find
function Notify(aAction : TnxEngineAction; aBefore : Boolean;
const aArgs : array of const) : TnxResult; override;
This function is called for every functional call inside the server objects. Since our extender is attached to a cursor this can be eaRecordGet, eaRecordInsert, eaRecordModify or eaRecordDelete. For a complete list of notifications and the arguments, please look here. Back to our notification handler - what do we have to do:
|•||first we check for the correct notification id|
|•||then we will check if we are dealing with a table|
|•||we look for the LastUpdate field and if the field is of the correct type (DateTime)|
|•||then we need to convert the current date & time to the internal NexusDB storage format|
|•||and at last replace the field value in new record buffer.|
The source code for above steps looks like this:
function TnxLastUpdatedExtender.Notify(aAction : TnxEngineAction;
aBefore : Boolean; const aArgs : array of const) : TnxResult;
lFieldIndex : Integer;
lLen : Integer;
lBuffer : PnxByteArray;
lRecordBuffer : PnxByteArray;
lCursor : TnxAbstractCursor;
lDateTime : Variant;
Result := DBIERR_NONE;
// if the action is a eaRecordModify we will now set the field LastUpdate
// (if available!) to be the current datatime. this field will then
// always hold the LastChange date.
if ((aAction in [eaRecordModify]) and aBefore) then begin
// since we already know that the object we are attached to is a cursor
// we can savely cast the object to a TnxAbstractCursor
lCursor := TnxAbstractCursor(beeExtendableObject);
// first we check if this is a table. after all it could be a specialized
// cursor that has nothing to do with our purpose
// if yes we also need to check if this is the right table
// if no we've nothing to do
if not (lCursor is TnxServerCursor) then
// next we check if there IS a field LastUpdated
// if not we've nothing to do
lFieldIndex := lCursor.Dictionary.GetFieldFromName('LastUpdated');
if lFieldIndex < 0 then
// check for the correct fieldtype!
// if wrong we've nothing to do
if lCursor.Dictionary.FieldDescriptor[lFieldIndex].fdType <> nxtDateTime then
// now we need to convert the value we want to set to the internal
// storage value, as we are working on low level here.
// for convenience we use the VariantToNative from the nxSQLProxies unit
lDateTime := now;
// get the field length
lLen := lCursor.Dictionary.FieldDescriptor[lFieldIndex].fdLength;
// get a buffer for the internal data
VariantToNative(nxtDateTime, lDateTime, lBuffer, lLen);
// get the record buffer from the passed parameters
// for eaRecordModify the aArgs is a pointer to a byte array to the
// new record buffer
lRecordBuffer := PnxByteArray(aArgs.VPChar);
// finally set the field
// first get the field
// pass in the passed parameter for the record buffer
// pass in the value of the field
// free the buffer
I admit it looks long and complex at first sight. Actually it is not, it's just a bit different from what we are used to, but essentially it's the same as doing a FindField followed by a Field.AsDateTime:=Now.
The majority of the code is to check the environment of the notification and if we can apply the new value. The main reason for this being a bit more complicated than the way we're used to is, that we need to use the low level routines of the data dictionary to access fields.
As all fields in NexusDB are stored in a native format we also need to convert the TDateTime value accordingly. As mentioned in the comment we use the VariantToNative function defined in nxSQLProxies. For NexusDB V1 this will add a dependency to the SQL package to our package (for V2 these will be moved to a different place). If you don't want this you need to copy them into our package sources.
To compile the above you need to add nxllTypes, nxllBDE, nxsdTypes, nxsqlProxies, nxsrServerEngine and nxllMemoryManager to the uses clause of our unit. That's it, the Monitor/Extender combo is implemented. Let's use it!
Add an instance of the Monitor to the Server
The rest is trivial. Open the nxServer project in Delphi. Load nxdmServer.pas and drag a TnxLastUpdateMonitor on the data module form.
Set the ServerEngine property of the monitor to the server engine, ActiveRuntime to True (if you want it to auto start by default) and make sure that the Enabled property is True.
Recompile the server and run. You should now see nxLastUpdatedMonitor1 entry in the Database Settings tree view. We now have a server that automatically updates LastUpdated fields (of type DateTime) in all tables.
To test it, just open a table with this field in Enterprise Manager and modify it. Congratulations, you've built your first Monitor/Extender for NexusDB.
Now that you know how to do it you can use that knowledge to extend and/or change the functionality of NexusDB core to your liking.