DatabaseA CE Database Primer

A CE Database Primer

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

In the most basic sense, there are really only two things you can do with a computer: store data and manipulate data. Even executable code really amounts to a very specialized kind of data, stored in a canonical database. Given this distillation of computing reality, it’s not surprising that a sleek, spare architecture like CE would build database management into the system. It’s just so sensible.

If you are porting a data management application to CE from Win32, in all likelihood you’ve given the issues around database APIs some fairly serious thought. And well you should. You’ll probably also have considered finding a way to avoid using the built-in functions and instead using files or “rolling your own” more familiar type of database API. If there is one bit of porting advice I would emphatically offer, it’s to make use of the CE database API. While it is certainly very different in character from contemporary relational database tools, it has some really overwhelming strengths and advantages.

  • It’s simple. Learn six APIs and you are in business—you can do all the significant database operations with an absolute minimum learning curve.
  • It’s flexible. In terms of storage, it accommodates any kind of data. In addition, the database APIs are highly versatile. Use a variety of flags and argument values to implement a rich set of behaviors.
  • It’s storage space efficient. The libraries for database functionality are stored in ROM. Therefore, your code incurs a miniscule amount of overhead.
  • It’s fast. Data access is as good as it’s going to get under CE. Anything you can design independently will either be slower or considerably more memory hungry.

Getting Started

If you take the leap of faith that brings you to the conclusion that the CE database API is the way to go, you probably have some fairly large-scale code surgery in store. If you’ve been circumspect about segregating data access code in libraries or sets of source files, the work of porting begins with these. If your circumstances are less ideal, you’ll need to locate data management operations dispersed throughout your code.

Porting Tip: Here’s how to find data management calls dispersed in the body of code you are porting.

  • If you are using a third-party library, remove it from the link specification, and build the application. DBM calls will show up as unresolved external references.
  • If you are using a homegrown DBM that relies on files, remove the applicable header file from your application’s #includes and build the application. File access functions will come up as undefined names.

Integrating Database Functionality, CE Style

The beauty of CE’s database API is that, although it is spare, it is very flexible. You only need to know a handful of APIs, and the key to using them effectively is in understanding how to modify their behavior by using various flags and argument values.

We are going to take a slow, careful tour of database basics in this chapter, by means of two example programs. The first one, the BirthdayReminder example, will demonstrate how to create, open, and add records to a database. The second example, BetterBirthdays, will expand on these topics, by showing how to open a database with a sort order active, and detect and interpret error information. In addition, BetterBirthdays shows how to position and search in a database, how to update existing records, and how to iterate records based on a sort order. On with the show!

Overview of The Steps

The CE database API is fairly simple to use, but you need to take sort of a canonical view. You have to do database operations in a specific order, and many operations depend on the manner in which prior operations were handled. Here is a basic chronology:

  • Create a database, and save its CE object identifier ( CEOID )
  • Open the database, by name or CEOID, and save its handle
  • Position in the database by seeking to a specific location
  • Perform data access operations
  • Close the database

We’ll use the BirthdayReminder example to examine each of these actions in more depth.

Creating a Database

Most of the business of managing the Birthdays database is handled in the BirthdayDlgProc() function. We create the database in response to the WM_INITDIALOG message, with a call to the CeCreateDatabase() function.

case WM_INITDIALOG:   //Init the dialog controls      InitDlgCtrls( hDlg );   //Create data store   if( ceoidDB = CeCreateDatabase(TEXT("Birthdays"),       0x2468, 0, NULL))      { return TRUE; }

The parameters to CeCreateDatabase(), in the order shown, are the database name as a Unicode string, a DWORD value that identifies a general, application specific database type, the number of active sort orders for this database, and a pointer to the array of SORTORDERSPEC structures for the database.

The way in which you initially create the database has great influence on the operations you’ll be able to perform subsequently. Let’s look at each of the parameters in a bit more depth.

  • The database name is a Unicode string. It may be no more than 32 characters in length, including the required terminal null. Longer names are used, but truncated. Because you’ll most likely use the name for subsequent open operations, you want to make certain you stay within this limit.
  • The database type is actually an arbitrary number. You choose this number and assign it to a related group of databases. For example, if you have several databases of part numbers, you could assign them a common “part number” database type. This database type identifier is used to enumerate specific, related databases. We’ll examine methods for enumerating databases in later examples. For now, it’s enough to be aware that if you specify a database type in the create call, it should be unique. Currently, the native CE database type constants are less than 100 decimal. If you pick a large number in this range for your database type, you are most likely safe.
  • The number of active sort orders specifies how many properties (fields) of your database are sortable keys. The maximum number of active sort orders is 4. To search a field by value, it must have an active sort order. If you don’t need to search records by property values, set this field to 0.
  • The address of the array of SORTORDERSPEC structures is set to NULL if the number of active sort orders is 0. In this example, we aren’t sorting records, and set it to NULL. In the BetterBirthday example, we’ll use this field to set up a sort order, so we’ll explore the related issues more closely then.

A couple of points remain before we move on to opening the database. Notice that CeCreateDatabase() returns a value of type CEOID. A CEOID is a unique object identifier that specifies an object in the Windows CE Object Store. Unlike a handle, the CEOID for the database is persistent. It can be used to recover information about the database even if the database isn’t open. Unlike file functions and the like, CE database functions often don’t return values that directly encode status information. To find out whether database calls are successful, you’ll often need to use GetLastError(), as we did following the call to CeCreateDatabase().

//either the disk is full or the db existsrc = GetLastError();//this case is okif( rc == ERROR_DUP_NAME )   { break; }//uh-oh...if( rc == ERROR_DISK_FULL  ){   iCaption = IDS_CANT_CREATE_DB;   iTitle = IDS_NO_DATABASE;   { goto FAIL; }}

Notice that in this case we test for two conditions that might cause failure to create the database. If the database already exists, GetLastError() will report ERROR_DUP_NAME. This isn’t a problem for us, as it won’t prevent us from opening the database by name. If we get a return of ERROR_DISK_FULL, we notify the user, clean up, and bail out. If we just wanted to unconditionally test for success or failure, we could do so like this:

//sample code fragmentif( rc != ERROR_SUCCESS )   {      //rock on...   }

Strangely enough, a return code of ERROR_SUCCESS signifies that everything is fine. If we get this far, the next step is to open the database we just created, with a call to CeOpenDatabase():

globalCEOID = 0;globalHDB = CeOpenDatabase (&globalCEOID, TEXT("Birthdays"),                            0, CEDB_AUTOINCREMENT, hDlg);if( globalHDB == INVALID_HANDLE_VALUE )   {goto FAIL; }

The parameters to CeOpenDatabase(), in the order shown, are the address of a variable of CEOID type that will contain the database’s CEOID on successful return; the name of the database as a Unicode string; a null placeholder for the property ID of a primary key sort order field; the flag CEDB_AUTOINCREMENT, which means that the current record location is incremented on each read operation, and the handle to the window that will receive notifications if another application modifies the database after we open it.

The fashion in which you open the database has a great deal of influence on what data access operations you are able to perform later. Also, the meanings of the parameters to this function depend on one another in subtle ways. Let’s explore them further:

First, notice that, in this case, we set the globalCEOID parameter equal zero before calling the function. This indicates that we want the CEOID returned in this value. If we had set globalCEOID equal to a valid CEOID for the database, we could open it without using (or even knowing) its literal name.

In the example code, we use the second parameter, a Unicode literal, to open the database by its literal name. If we had chosen to open it using a CEOID value, we’d set the database name parameter to NULL.

The next parameter, the property ID of a primary sort key, is set to NULL here. The most significant effect of this is that with no active sort key, we won’t be able to locate a record by subjecting one of its properties (fields) to a specific search criterion. (For example, we can’t look for a record with a field greater than, less than, or equal to a reference value). However, even without a sort order active, you can iteratively read records, write records, and position (seek) in the database by absolute location (For example, you can set the “current record” pointer at the third record from the beginning of the database.) or position using a record’s CEOID.

The open mode flag parameter is set to CEDB_AUTOINCREMENT in this case. This flag causes the “current record” pointer to move ahead one record after each read operation. If you set this parameter equal to zero, the record pointer only moves when you explicitly set it to a new location by seeking.

The final parameter, in this case the handle to the BirthdayDlg, identifies the window that is to be notified if the database is modified by another application. Our example includes no code to handle such a notification, and for application-specific databases, it probably isn’t necessary to worry much about this. However, if you are using a native CE database (the Contacts database, for example) you’ll need to be ready to handle such notifications.

A few final observations on opening CE databases are in order. First, notice that there is no explicit provision for open “permissions.” Once the database is opened, you may read, write, and delete at will. Second, how you open the database makes a lot of difference in terms of what kind of searching and positioning operations you are subsequently able to perform. For these reasons, you’ll probably find it both more convenient and more robust to open the database in the way that is most conducive to your access needs for a given circumstance, and then close and reopen for different types of operations. In the example above, we opened the database in the way that makes it easiest to sequentially read records.

Porting Tip: Open the database in the way that most closely supports immediate access needs; then close and reopen as access requirements change.

Looking Ahead

In the next installment, we’ll look into the mechanics of CE database access.

About the Author

Nancy Nicolaisen is a software engineer who has designed and implemented highly modular Windows CE products that include features such as full remote diagnostics, CE-side data compression, dynamically constructed user interface, automatic screen size detection, and entry time data validation.

In addition to writing for Developer.com, she has written several books, including Making Win 32 Applications Mobile.

# # #

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories