NJDX Tutorial

By Priti Narang

Introduction to NJDX OR Mapper

Software Tree's NJDX is a powerful, flexible, and easy-to-use object-relational mapping (OR-Mapping) product that simplifies and accelerates the development of .NET applications by providing intuitive, object-oriented access to relational data. Adhering to some well thought-out KISS (Keep It Simple and Straightforward) principles, NJDX supports complex object modeling, provides integration with popular databases and legacy data, and employs a highly optimized mapping engine. NJDX avoids code generation approach by employing a metadata driven programming model. NJDX has been tightly integrated with Visual Studio .NET and can be used with any CLR-based language, including C#, VB.NET, and J#.

Tutorial Objective

This is an introductory tutorial for new users of NJDX OR Mapper. NJDX makes a programmer's job easy by providing simple, intuitive and flexible APIs that take care of eliminating all the complexity of writing low-level ADO.NET/SQL code involved in integrating with relational data sources. NJDX helps you concentrate on business logic without worrying about the plumbing code. On our way to using NJDX, we’ll learn some new terminology that will help us understand NJDX better.
We will learn the basic steps that need to be followed to make our application up and running with NJDX. We will explore how classes are mapped to database tables and how an application can interact with a database without having to explicitly issue SQL commands.

The Development Process

There are three simple steps to use NJDX:

Define business objects (.NET classes).
Step 1: Define business objects (.NET classes) of the domain model
NJDX does not impose any restriction in terms of how a class should be defined. Instances of any POCO (Plain Old CLR Object) can be persisted using NJDX. Here is an example of a class definition (ABC.SimpleCompany) used in the above domain model:
                      
  • using System;
  •  
  • using System.Collections;
  •  
  • namespace ABC
  •  
  • {
  •  
  • [Serializable]
  •  
  • public class SimpleCompany--------------------------------(1)
  •  
  • {
  • private String companyId_;--------------------------(2)
  • private String companyName_;
  • private String city_;
  • private String state_;
  • private String country_;
  • private ArrayList depts_;
  • public SimpleCompany()------------------------------(3)
  • {
  • }
  •  
  • public SimpleCompany(String companyId,
  • String companyName,
  • String city,
  • String state,
  • String country)
  • {
  • companyId_ = companyId;
  • companyName_ =companyName;
  • city_ = city;
  • state_ = state;
  • country_ = country;
  • depts_ = null;
  • }
  • public String CompanyId-----------------------------(4)
  • {
  • get { return companyId_; }
  • set { companyId_ = value; }
  • }
  • public String CompanyName
  • {
  • get { return companyName_; }
  • set { companyName_ = value; }
  • }
  • public String City
  • {
  • get { return city_; }
  • set { city_ = value; }
  • }
  • public String State
  • {
  • get { return state_; }
  • set { state_ = value; }
  • }
  • public String Country
  • {
  • get { return country_; }
  • set { country_ = value; }
  • }
  • public ArrayList Depts
  • {
  • get { return depts_; }
  • set { depts_ = value; }
  • }
  • }
  • }
  1. A persistent class should be declared as a public class. NJDX does not require the persistent class to be inherited from any base class, nor does it require a persistence class to implement certain interfaces.
  2. Fields can be defined as either private or public.
  3. All persistent classes must have a default constructor with no arguments.
  4. For a private field, the public property should be defined.
Here is an example of another domain model class (ABC.SimpleDept):
        
  • using System;
  • namespace ABC
  • {
  • [Serializable]
  • public class SimpleDept
  • {
  • private int deptId_;
  • private String companyId_;
  • private String deptName_;
  • public SimpleDept()
  • {
  • }
  • public SimpleDept(int deptId, String companyId, String deptName)
  • {
  • deptId_ = deptId;
  • companyId_ = companyId;
  • deptName_ = deptName;
  • }
  • public int DeptId
  • {
  • get { return deptId_; }
  • set { deptId_ = value; }
  • }
  • public String CompanyId
  • {
  • get { return companyId_; }
  • set { companyId_ = value; }
  • }
  • public String DeptName
  • {
  • get { return deptName_; }
  • set { deptName_ = value; }
  • }
  • }
  • }
Define/refine object-relational mapping.
Step 2: Define/refine object-relational mapping
Object-Relational Mapping File (ORMFile)
NJDX needs to know where to load from and where to store objects of the persistent domain model classes. This is where the NJDX mapping file (ORMFile) comes into play. The mapping file contains an object-relational mapping (OR-Mapping) specification, which defines the mapping information for all the persistent classes belonging to an application domain. The specification includes, among other things, table names, primary key attributes and object relationships. NJDX provides an innovative and declarative way of specifying human-readable and user-friendly object-relational mapping information based on a simple grammar. The mapping file can be generated easily using a text editor, NJDXStudio (add-in to Visual Studio), a modeling tool, or even programmatically. A mapping specification can correspond to only one database. While a mapping specification cannot span multiple databases, multiple mapping specifications can be defined for the same database. Also, an application can use multiple mapping specifications, where each specification may correspond to a different database.

Here is an example of how we can define an OR-Mapping specification for the given domain model in a mapping file (e.g., Simple.jdx). Please note that NJDX, through it’s reverse-engineering facility, can automatically create a default mapping file and C# class definitions from an existing database schema.
                      
  • CLASS ABC.SimpleDept TABLE Simple_Dept--------------------------------------------------------(1)
  • PRIMARY_KEY deptId_-------------------------------------------------------------------------(2)
  • SQLMAP FOR companyId_ SQLTYPE VARCHAR(20)---------------------------------------------------(3)
  • ;
  • COLLECTION_CLASS CollectionDept COLLECTION_TYPE NETCOLLECTION ELEMENT_CLASS ABC.SimpleDept----(4)
  • PRIMARY_KEY companyId_----------------------------------------------------------------------(5)
  • ORDERBY deptId_
  • ;
  • CLASS ABC.SimpleCompany TABLE Simple_Company
  • PRIMARY_KEY companyId_
  • SQLMAP FOR companyId_ SQLTYPE VARCHAR(20)
  • RELATIONSHIP depts_ REFERENCES CollectionDept BYVALUE WITH companyId_-----------------------(6)
  • QUERY_NAME companyLocationQuery PREDICATE 'city_=? AND state_=?'
  • ;
  • CLASS ABC.SimpleAddr TABLE Simple_Address
  • PRIMARY_KEY addrId_
  • SQLMAP FOR addrId_ SQLTYPE VARCHAR(20)
  • SQLMAP FOR addr2_ NULLABLE
  • SQLMAP FOR country_ NULLABLE
  • ;
  • CLASS ABC.SimpleEmp TABLE Simple_Employee
  • PRIMARY_KEY empId_
  • REFERENCE_KEY ssnKey ssn_
  • SQLMAP FOR empId_ SQLTYPE VARCHAR(20) COLUMN_NAME emp_id
  • RELATIONSHIP dept_ REFERENCES ABC.SimpleDept WITH deptId_
  • RELATIONSHIP address_ REFERENCES !ABC.SimpleAddr BYVALUE WITH empId_
  • ;
Notice that the NJDX mapping file has a simple grammar and is easy to create, modify, and comprehend. There are no XML complexities.
  1. A CLASS specification encapsulates all the Object-Relational Mapping information for one class. The class name may include a namespace (e.g., ABC.SimpleEmp).
  2. A PRIMARY_KEY specification identifies the attribute (property) names whose combined values uniquely identify a particular object.
  3. A SQLMAP specification allows one to refine the mapping of a class attribute to the SQL column in one of the following ways - using a column name different from the attribute name, using an SQL datatype different from the default SQL data type for the attribute type, and allowing the column to be nullable.
  4. COLLECTION_CLASS specification encapsulates all the Object-Relational Mapping information about a collection class. A collection is actually a pseudo-class; there may not be an actual class by that name in the program.
  5. A PRIMARY_KEY specification for a collection class specifies names of those attributes whose values are the same for all the objects in a collection.
  6. A RELATIONSHIP specification defines the mapping for a complex attribute referencing an object or a collection of objects. It keeps the application developer from worrying about explicitly initializing the "primary or foreign key" attribute values. BYVALUE keyword is used to indicate that, by default, the related objects should also be inserted, updated, or deleted with the containing object (CASCADE semantics).
Develop applications using NJDX APIs.
Step 3: Develop applications using NJDX APIs
In this step we’ll see how to develop applications using NJDX APIs. We will do some basic Insert, Update, Delete and Query operations using those APIs. The basic structure of the application using NJDX looks like this:
                 
  • using com.softwaretree.jx;----
  • ____________________________________________________(1)
  • using com.softwaretree.jdx;---
  • public class Example
  • {
  • JXResource jxResource = null;----------------------------------------------(2)
  • JXSession jxSession = null; -----------------------------------------------(3)
  • JDXS jdxHandle = null;-----------------------------------------------------(4)
  • try
  • {
  • jxResource = new JXResource(jdxURL);----------------------------------(5)
  • jxSession = jxResource.get_JXSessionHandle();
  • jdxHandle = jxResource.get_JDXHandle();
  •  
  • jxSession.tx_begin();
  • {
  • // do query/insert/delete…
  • }
  • jxSession.tx_commit();
  • jxResource.close();
  • }
  • catch
  • {
  • jxResource.close();
  • }
  • }
  1. Namespaces and Reference Assembly: To use NJDX APIs (belonging to the namespaces com.softwaretree.jx and com.softwaretree.jdx), we need to include NJDXCore.dll assembly as a reference library to the project. NJDXCore.dll can be found in the %NJDX_HOME%/bin directory.
  2. JXResource class provides the facilities to work with a mapping unit. A JXResource contains handles for JXSession and JDXS objects.
  3. JXSession interface defines transactional methods (like tx_begin and tx_commit).
  4. JDXS interface defines the methods that can be used by the application programmer for persistence of .NET objects and other relational services.
  5. jdxURL is used to specify locations for the object classes, the database tables, and the mapping specification that we’ll be using in the application. The easiest way is to provide these details to the JXResource constructor, which takes a jdxURL string as an argument. Here's how the ‘jdxURL’ used in the above example may look like:
              
  • jdxURL=JDX:'Provider=sqloledb;DataSource=localhost;SelectMethod=cursor;InitialCatalog=pubs'; USER=sa;PASSWORD=sa;
  • JDX_DBTYPE=MSSQL2000;JDX_ORMFILE=Simple.jdx;DEBUG_LEVEL=5;DM_ASSEMBLY='./bin/Simple.dll';
JDX_DBTYPE specifies what kind of backend database is being used. Possible values are MSSQL2000, ORACLE, IBMDB2, MYSQL, MSACCESS, SYBASE, INTERBASE, INGRES, and GENERIC.

DEBUG_LEVEL controls the diagnostic messages emitted by NJDX. Lower the level, more the amount of diagnostic output. Max value is 5 min value is 0. At DEBUG_LEVEL of 3 or lower, all the SQL statements are emitted.

Domain Model Assembly (DM_ASSEMBLY) is a unit of compiled domain model classes that are persisted using NJDX. That is, the mapping unit classes whose mappings are defined in the mapping specification (e.g., Simple.jdx file) are compiled and assembled in the domain model assembly (e.g., Simple.dll).
Before moving into the details of these steps let’s take a look at the object (domain) model that we’ll be using in this tutorial.
jdxURL specification is typically provided in and read from a configuration file for ease of use.

Some examples of NJDX APIs

Method: insert

Syntax: public void insert(Object object, long insertFlags, ArrayList insertDetails)

object - the object to be inserted in the database
insertFlags - specifies insert behavior (deep, shallow)
insertDetails - controls the insert operation in different ways.
                  
  • ...
  •  
  • SimpleCompany myCompany = new SimpleCompany("C1", "My Corporation", "San Jose", "CA", "USA");
  •  
  • ArrayList depts = new ArrayList();
  • SimpleDept dept1 = new SimpleDept(101, "C1", "Engineering");
  • depts.Add(dept1);
  •  
  • SimpleDept dept2 = new SimpleDept(102, "C1", "Information Technology");
  • depts.Add(dept2);
  •  
  • myCompany.depts_ = depts;
  •  
  • jdxHandle.insert(myCompany, JDXS_Fields.FLAG_DEEP, null);
The above code creates a SimpleCompany object (myCompany) and stores it in the database using the insert method. is will insert the myCompany object into Simple_Company table. Since FLAG_DEEP is specified, the associated SimpleDept objects will also be inserted in the database.

Method: query

Syntax:public ArrayList query( String className, String predicate, long maxObjects, long queryFlags, ArrayList queryDetails)

className - name of the class whose objects need to be retrieved.
predicate - search condition for the objects.
maxObjects - number of objects to be returned (JDXS.ALL or -1 => all qualified objects)
queryFlags - specifies query behavior (deep, shallow, streaming etc.); multiple flags may be combined with a + operation
queryDetails - directed operation options, which may be used to control the query in different ways.
                  
  • ...
  •  
  • ArrayList queryResults1, queryResults2;
  •  
  • queryResults1 = jdxHandle.query("ABC.SimpleCompany", "state_ = 'CA'", 3, JDXS_Fields.FLAG_SHALLOW, null);--------------------------(1)
  •  
  • queryResults2 = jdxHandle.query("ABC.SimpleCompany", "'Admin' IN jdxObject.depts_.deptName_", -1, JDXS_Fields.FLAG_DEEP, null);----(2)
  •  
  • ...
  1. This query retrieves at most 3 SimpleCompany objects in the state of CA (California). Since the query is done with a SHALLOW flag, none of the associated SimpleDept objects are retrieved.
  2. This query retrieves only those SimpleCompany objects which have an associated 'Engineering' department. Since the query is done with a DEEP flag, all the associated SimpleDept objects in addition to the 'Engineering' department are also retrieved

Method: update

Syntax: public void update(Object object, long updateFlags, ArrayList updateDetails)

object - the object to be updated in the database
updateFlags - specifies update behavior (deep, shallow, positioned)
updateDetails - controls updates in different ways.
                  
  • ...
  •  
  • myCompany.city = "San Francisco";
  • jdxHandle.update(myCompany, JDXS_Fields.FLAG_SHALLOW, null);
  •  
  • ...
The above code updates the myCompany object into the Simple_Company table. Since FLAG_SHALLOW is specified, the associated SimpleDept objects would not be updated.

Method: delete

Syntax: public void delete(Object object, long deleteFlags, ArrayList deleteDetails)

object - the object to be deleted from the database
deleteFlags - specifies delete behavior (deep, shallow, positioned)
deleteDetails - directed operation options, which may be used to control delete in different ways.
                  
  • ...
  •  
  • jdxHandle.delete(myCompany, JDXS_Fields.FLAG_DEEP, null);
  •  
  • ...
The above code deletes the record corresponding to the myCompany object in the Simple_Company table. Since FLAG_DEEP is specified, all the (BYVALUE) related SimpleDept objects will also be deleted.

Advanced Topics

NJDX is a versatile tool that provides many other advanced features. Some of them, which you might want to explore next, are:

API methods, other than the basic operations that we have covered:

  • Named Query - NJDX provides the facility to define a named query with optional parameter markers. The named query can be defined once and used (executed) multiple times. Named queries can provide greater convenience, more flexibility, and better performance in many situations. The query will be executed by NJDX using a prepared statement for faster performance. The named query is automatically defined and executed for all the subclasses.
  • InsertMany – This method is similar to "insert operation" but you may insert many independent objects with one call.
  • Update2 – This allows bulk updates (in the database) of selected attributes of all objects of the given class, satisfying the given search condition (predicate) with the new attribute values.
  • Delete2 - This allows bulk deletes (from the database) of all objects of the given class, satisfying the given search condition (predicate).

Other Interesting Features:

  • Object caching - Provides super-fast access to business objects from high-performance memory cache.
  • Stored procedure integration - Simple mapping specification for stored procedures. Flexible APIs supporting in, in/out, and out parameters. Automatic object-instantiations using stored procedure data sets.
  • Class-hierarchies - Flexible storage options and polymorphic queries for objects belonging to a class hierarchy.
  • Aggregate operations - Returns the aggregate value (COUNT, MIN, MAX, AVG, SUM) for the specified attribute of the qualifying objects. Enables easy data analysis.
  • Path-expressions - Enables simple object-oriented specification of powerful query predicates involving referenced objects (e.g. “'Engineering' IN jdxObject.depts_.deptName” while querying over the SimpleCompany class). NJDX generates all the required SQL join statements to accomplish the task.
  • Optimistic locking for concurrency control - Provides a faster and more scalable locking alternative to database locking. Especially useful for long-running transactions with minimal conflicting update operations.
  • Forward-engineering of relational schema from object models.
  • Reverse engineering of C# classes from existing schemas.
  • JXResourcePool - Provides multiple JXResource components in an extensible pool and methods for thread-safe sharing of these components.
  • NJDXStudio is an add-in of NJDX OR-Mapper related tools for Visual Studio. NJDXStudio provides a seamless integration of these OR-Mapping tools using a handy pulldown menu, a simple toolbar, and helpful wizards, allowing developers to easily perform such tasks as defining object-relational mapping, creating database schema, reverse-engineering C# classes from an existing database schema, and verifying OR-Mapping specification against live data.
  • NJDXDemo is a utility program that provides a graphical front end for user input and invokes NJDX method calls and displays the results. We can use NJDX to easily verify an Object-Relational Mapping specification against existing data. NJDXDemo can also help in formulating ad-hoc query predicates, which can then be incorporated in the application programs.
To learn more about NJDX feature details, you can refer to a comprehensive user manual that ships with the software.

Conclusion

NJDX OR-Mapper makes the .NET application development task significantly simpler by presenting a more intuitive object-oriented view of the relational data. NJDX does it without any code generation and without imposing any new query language. This tutorial is just a quick introduction to NJDX and there is much more to NJDX than the mapping specification and calls like insert, query, update, and delete . NJDX provides many advanced features that let you fine tune it for the persistence needs of your application. Many helpful tools and utilities like forward-engineering and reverse-engineering streamline the development process. You can rely on NJDX not only for simple applications but also for more complex and high-performance enterprise class applications. I won’t say we have reached the end - it’s just the beginning of the wonderful world of NJDX!


About the author

Priti Narang is a freelance software engineer based in the San Francisco Bay Area.