Foreword

Welcome to the second edition of the Microsoft® Jet Database Engine Programmer’s Guide! We are proud to bring to you, the database developer looking to get the most out of the world’s most powerful file-sharing database engine, a new edition of this popular guide. This guide will assist the database developer and user alike to discover and use many of the features and capabilities that are beyond the scope of the standard how-to documentation supplied with the many products that use the Jet database engine. Based on the success of and the feedback from the first edition, we are very happy to see our goal of helping developers create faster, smaller, and more robust database applications being fulfilled.

The Microsoft Jet Database Engine Programmer’s Guide is designed to assist you in getting the most out of the Jet database engine. As Michael Mee noted in the first edition, we frequently hear about applications not taking full advantage of the wealth of advanced, as well as basic, features Microsoft Jet has to offer. We here on the Microsoft Jet team are very proud of the robustness of our product, and take great joy in seeing our work used to its fullest potential!

This guide is also for the experienced Microsoft Jet user looking for technical information new to Microsoft Jet version 3.5. The theme for Microsoft Jet 3.5 was, essentially, to improve performance and embrace the Internet. On the performance front, many improvements were made that are enabled “right out of the box.” In addition, we also opened up several areas of the engine for your individual tuning. (We discuss some unsupported tuning features in this guide as well!) We talk in-depth about performance in this guide, including the exciting new ODBCDirect functionality in Data Access Objects (DAO) version 3.5.

For just about every product here at Microsoft, embracing the Internet has become paramount. Microsoft Jet 3.5 introduces the capability to access (import, link, or export) HTML tables over the Internet. Users can now also use the Internet to access external data on HTTP or FTP servers through Microsoft Jet’s installable ISAM (IISAM) drivers. Applications that use replication can now perform exchanges over the Internet too! These revisions will help you use these as well as other exciting new features and improvements in Microsoft Jet and DAO 3.5 to their fullest potential.

You also get:

  • Internal information previously known only by the software engineers who develop Microsoft Jet.

  • Tips from the testers who have each written more code using Microsoft Jet than most development teams write in a lifetime.

  • Expanded information that was left out of product documentation because it was “too technical.”

  • Insights from the people who actually designed, developed, and tested Microsoft Jet and DAO.

  • Common pitfalls, traps, and useful techniques from the talented product support engineers who take your phone calls.

  • Tables and appendixes that assemble technical errata into useful formats.

This guide is the culmination of the efforts of a broad range of technical contributors. Information comes from Microsoft Jet, DAO, and Microsoft Access engineers, product support personnel, technical writers and editors, and last, but not least, very experienced Microsoft Jet users, who bring real-world experience to the material. Many thanks to the original authors of the Microsoft Jet Database Engine Programmer’s Guide, Dan Haught and Jim Ferguson, for helping provide the excellent foundation this new edition is based upon.

Thank You!

Lee Woods
Microsoft Jet Product Unit Manager


Acknowledgments

As we completed work on Microsoft Jet version 3.5 we, the Microsoft Jet Database Engine Team, knew that “the Jet book” deserved, nay needed, to be updated. Comprehensive information needed to be made available to developers about new features, improvements, and the latest technologies.

Many of those who contributed so much to writing the first edition of “the Jet book” had moved on to other challenges at Microsoft (or had retired), thus the experience in producing the book went with them. Though there was no lack of expertise on the current Microsoft Jet, Data Access Objects (DAO), and Microsoft Access teams, the members of these teams were busy wrapping up Microsoft Access 97 and Microsoft Jet 3.5, and they were already at work on the next versions. Bandwidth was at a premium. Nonetheless, everyone pitched in and made the time to publish this, the second edition of the Microsoft Jet Database Engine Programmer’s Guide. For this effort and dedication we want to extend our heartfelt thanks and appreciation.

First, we want to thank the Office, Microsoft Access, C++, and Database & Transaction Processing (DTP) user education teams for providing the writers, editors, production support, and project leadership necessary for completing this project. These were the folks who worked day to day on this project and made sure that everything that needed to be done was done. Our thanks to: David Shank and Mark Roberts who shouldered the lion’s share of the writing, with strong support from Robert Flake and Jim van de Erve; to Amy Blair and Dana Schmeller who edited everyone’s contributions, ensuring that what was written was consistent and accurate; to Emily Hylton, Kristine Haugseth, Maggie Sparkman, Debra Harrigan, Donna Johnston, and Jeff Gilbert who assisted Amy and Dana; to Bart McKeirnan and Kristin Lynn Bergsma for production support and art; to Robin Lyle for telling everyone when they needed to be done (scheduling) and making sure that nothing fell in between the cracks; to Tamra Myers for all of the code samples and sample databases, and for testing it all; to Sam Dawson for the index (just try to find something without it); to John Gronquist and Gregory Frederick for their help in CD-ROM production and testing, and all-around technical support; and to Byron Krystad for advice and moral support. Thanks to Beth Carter for helping to get the project rolling early on. And an extra and special thanks to Dana for taking on the role of project lead.

And of course there was honorary user education team member Alyssa Henry who, in addition to her day job as an ActiveX™ Data Objects (ADO) program manager, rewrote Chapter 11, “Programming with DAO in C++ and Java™.” Thank you, Alyssa.

We also want to thank Casey Doyle and Maureen Zimmerman of Microsoft Press®. Casey and Maureen guided us through the challenges of getting the book published and helped us sort out complicated contractual and production issues.

And thanks to the original authors of the Microsoft Jet Database Engine Programmer’s Guide, Dan Haught, Jim Ferguson, Peter Bateman, Mark Bukovec, Joel Gilman, Jim Gordon, and Jim van de Erve for giving us a solid base to build upon.

There were also a large number of reviewers who provided the technical expertise that the writers assimilated and cast on to the pages of the book. Without the contributions of these folks who shared their accumulated knowledge, this book would have been nearly impossible to write. They include: Mark Buettemeier, Richard Dickinson, Gregory Ellison, Frank Ai-Jun Fei, Michael Kaplan, Emily Kruglick, Ryan LaBrie, Jim Lewallen, Gretchen Loihle, Jim Lucey, Michael Mee, Kevin Mineweaser, Rick Nasci, Tad Orman, Mark Prentice, Cameron Price, Elena Sellers, Monte Slichter, James Sturms, Tanka Sunuwar, Rudy Torrico, Peter Tucker, Susan Vinson, Don Willits, and Charmain Wilson. Thank you very much for your contributions and your help.

We must single out and thank Joel Gilman not only for his contributions as an original author and a reviewer, but especially for his laugh which was (and is) like cool water in a parched and dusty land.

And last, but certainly not least, there are those who, though no longer a part of the Microsoft Jet team, nonetheless deserve acknowledgement for the parts they have played in making Microsoft Jet 3.5 a success, and for their continued friendship: Matthew Bellew, Eric Christensen, Dennis Comfort, Theresa Daly, Nigel Ellis, Phillip Garding, Brad Hammond, Kimberly Harms, Steve Hecht, Charumathy Narayanan, Matt Senft, and Gordon Smith.

Sincerely,

The Microsoft Jet Database Engine Team (version 3.5)

Trudy Anthony-Hoppe | Curtis Deems | Claudio Mauad | Ram Singh
Kam Foo Aw | Bob Delevan | Sherri Newman | Rich Swinford
Raj Batra | Debra Dove | Muon Ngu | Dean Talley
Neil Black | Amha Georgis | Jennifer Perret | Kelly Tipton
Kevin Boske | Monte Guise | Tony Poll | Lee Woods
David Brocchetto | Brianne Hoppe | Todd Ragland | Beth Woodman (Chapman)
Joel Check | Mark Johnson | Hyma Ramakrishnan | Song Xue
Kevin Collins | Janet Josephson | Bob Sarver |
Conor Cunningham | Toshio Kameda | Beth Scott |
Vince Curley | Elena Kharitidi | Dave Sell |


Preface

Since its introduction in 1992, the Microsoft Jet database engine has been in a unique position. Because Microsoft Jet is not a stand-alone product — you cannot buy it at your local software retailer — most developers who use it have learned about its functionality in a second-hand fashion from the documentation included in Microsoft Access, Microsoft Office, Microsoft Visual Basic®, Microsoft Visual C++®, or Microsoft Visual J++®.

The Microsoft Jet Database Engine Programmer’s Guide is the definitive reference for all users of Microsoft Jet, from developers programming in applications such as Microsoft Access and Microsoft Excel, to those working in a dedicated programming environment such as Visual Basic, Visual C++, or Visual J++. This guide offers comprehensive coverage of all aspects of Microsoft Jet, including the structure of the engine, the services it provides, and complete examples showing how to use its functionality in your development projects.

Who This Guide Is For

How to Use This Guide


Who This Guide Is For

Because many applications use Microsoft Jet, this guide is directed toward a large audience of developers and programmers. If you are leafing through this guide, chances are you already have at least a rough idea of what Microsoft Jet is and where it is used. As of this writing, the following applications use various versions of Microsoft Jet.

Application | Microsoft Jet version
Microsoft Access 1.0 | 1.0
Microsoft Access 1.1 | 1.1
Microsoft Access 2.0 | 2.0 and 2.5
Microsoft Access 95 | 3.0
Microsoft Access 97 | 3.5
Visual Basic 3.0 | 1.1 (or 2.0 using the Visual Basic compatibility layer)
Visual Basic 4.0 (16-bit) | 2.5
Visual Basic 4.0 (32-bit) | 3.0
Visual Basic 5.0 | 3.5
Microsoft Excel 95 | 3.0
Microsoft Excel 97, Microsoft PowerPoint® 97, and Microsoft Word 97 | 3.5
Microsoft Project 4.1 and Microsoft Project 95 | 3.0
Microsoft Foundation Classes (MFC) versions 4.x | 3.0
MFC version 5.0 | 3.5
Microsoft Internet Information Server 3.0 | 3.5

Although this guide will be most helpful if you are primarily developing database applications in Microsoft Access, Visual Basic, or Visual C++, it also provides information that you can apply when developing applications that require database access from Microsoft Excel 97 and other applications that support Visual Basic for Applications (VBA) version 5.0.


What This Guide Covers

This guide covers Microsoft Jet version 3.5. All examples throughout this guide are compatible with this version. Special notes are placed throughout that describe differences between version 3.5 and earlier versions.


What You Should Already Know

This guide is written to enhance your existing database knowledge with information specific to Microsoft Jet. It’s assumed you’re comfortable with programming in a procedural language, are familiar with the concepts of relational databases and queries, and know something of Structured Query Language (SQL).

Most of the code examples are written in VBA 5.0, which is supported by Visual Basic 5.0, all Microsoft Office 97 Professional Edition applications except Microsoft Outlook™, and other VBA 5.0 host applications, such as Visio Technical 4.5. Although most code examples are given in VBA 5.0, they can be easily ported to other languages, such as C++. Chapter 11, “Programming with DAO in C++ and Java,” provides Data Access Objects (DAO) code examples for Visual C++ and Visual J++.


How to Use This Guide

The Microsoft Jet Database Engine Programmer’s Guide contains all the information necessary to use Microsoft Jet in your development efforts. Whether you use Microsoft Access, Visual Basic, or any other environment that hosts Microsoft Jet, you will learn the details of this complex and powerful system.

The guide is divided into thirteen chapters, with five appendixes. Although much of the topic matter is not linear in nature, careful attention has been paid to the order of the chapters. Information presented in later chapters builds on material in previous chapters. Feel free to skip to specific chapters, but you should read this introduction, and Chapter 1, “An Overview of Microsoft Jet,” as a prerequisite for later chapters.

Each chapter follows the same basic structure. An introduction summarizes the topic matter to be covered and includes a brief outline of the chapter contents. The main body of the chapter is then presented. Each chapter, where applicable, ends with an optimizations section that describes how to improve performance in the areas covered in the chapter.

The following section outlines each of the chapters.

Chapter 1 An Overview of Microsoft Jet

This chapter introduces you to Microsoft Jet with a brief history of its development. It then covers the structure of Microsoft Jet, the components used, the services provided, and the layout of the database file.

Chapter 2 Introducing Data Access Objects

Data Access Objects (DAO) provide a powerful framework for calling Microsoft Jet functions from your programming code. Coverage includes the DAO object hierarchy, a description of each of the object types, a primer on using DAO in your applications, an overview of features new to DAO 3.0 and 3.5, a discussion of compatibility issues, and some tips on optimizing DAO code.

Chapter 3 Data Definition and Integrity

Microsoft Jet provides powerful facilities for defining data and maintaining data integrity. It provides two interfaces for data definition, DAO and SQL, and offers a rich array of data integrity tools. This chapter shows you how to create tables, fields, queries, and indexes with both DAO and SQL. You also learn how to set up validation rules and referential integrity constraints.

Chapter 4 Queries

Microsoft Jet has one of the most powerful query processors available in a desktop database. This chapter is the complete reference to the query services that Microsoft Jet provides. An overview of the query engine explains the query processor and the operations it performs. You learn about the different types of queries, how to create and save queries, and how to optimize query performance.

Chapter 5 Working with Records and Fields

Manipulating data is the most fundamental operation in a database. For Microsoft Jet, the DAO layer provides programmable data manipulation with the Recordset and Field objects. This chapter provides a comprehensive look at the properties and methods of the Recordset object, along with several of the data-manipulation properties and methods of the Field object. You also learn how to use transactions to maintain the integrity of your data.

Chapter 6 Creating Multiuser Applications

Microsoft Jet provides services for database applications shared among multiple users. This chapter explores strategies for controlling multiuser access to data by locking records and handling resource contention. It discusses how an application can handle locking conflicts in a multiuser environment. It also compares how the engine works in file-server and client/server systems, and provides a brief introduction on how Microsoft Jet databases can be shared on the World Wide Web.

Chapter 7 Database Replication

Introduced with Microsoft Jet 3.0, database replication provides powerful distributed computing capabilities. By using database replication, programmers and database administrators can reproduce a database so that each user can simultaneously work on his or her own copy, or replica, of the database. Once created, the replicas can be located on different computers, in different offices, or even in different countries, and updates to the design and data in replicas can be synchronized with one another. This chapter describes the major uses for — and components of — database replication, how to program replication into your applications, and important issues affecting the synchronization of data updates and design changes. It also discusses two new replication features added in Microsoft Jet 3.5: creating and maintaining partial replicas (replicas that contain a subset of the data contained in a master database), and synchronizing replicas located on Internet or intranet servers.

Chapter 8 Accessing External Data

One great strength of Microsoft Jet is its ability to seamlessly connect to data in foreign formats. This chapter shows you how to use Microsoft Jet to connect to other desktop databases such as FoxPro®, dBASE, and Paradox; to spreadsheets such as Microsoft Excel and Lotus 1-2-3; to character-delimited and fixed-length text files; to Microsoft Exchange and Outlook folders and address books; and to tables and lists contained in HTML files. You will learn how to link to each external data source Microsoft Jet can connect to and how to directly open data sources. How to create tables in foreign formats from Microsoft Access by using the TransferDatabase method is also discussed. Performance guidelines are also provided.

Chapter 9 Developing Client/Server Applications

Client/server computing has become the accepted way of upsizing database applications. This chapter introduces you to the concepts of client/server development using Microsoft Jet. The relationship between Microsoft Jet and the Open Database Connectivity (ODBC) standard is discussed, and examples are given for connecting to Microsoft SQL Server™ as a back-end database by using both Microsoft Jet and the new ODBCDirect technology introduced in Microsoft DAO 3.5. This chapter also discusses techniques for creating client/server applications and offers tips for optimizing their performance.

Chapter 10 Managing Security

Microsoft Jet provides a sophisticated security environment that controls access to object definitions and data. This chapter discusses the Microsoft Jet security model and how to implement it successfully. A discussion of workgroups and permissions explains the model, and complete code examples for programmatically creating and maintaining security settings augment the text. This chapter also discusses how to password-protect a database in order to control opening a database, and how to secure Visual Basic code in a Microsoft Access database by saving the database as an .mde file.

Chapter 11 Programming with DAO in C++ and Java

This chapter describes how to programmatically access Microsoft Jet by using the DAO C++ classes, also known as dbDAO. The dbDAO classes expose the same Data Access Objects and functionality as Visual Basic, and use similar syntax. By handling such tasks as managing object-instance lifetime, these classes make programming in C++ as convenient as programming in Visual Basic. This chapter also discusses how to programmatically access Microsoft Jet databases with Java by using the DAO Automation interfaces in Visual J++.

Chapter 12 ODBC Desktop Database Drivers

ODBC drivers are used by ODBC-enabled applications to access data created in any of a variety of database management systems (DBMSs). The Microsoft ODBC Desktop Database Drivers use the Microsoft Jet database engine to access data in six desktop DBMSs. This chapter discusses the architecture of the drivers (and their use of the Microsoft Jet engine), Microsoft Jet features exposed by the drivers, implementation details, and performance considerations.

Chapter 13 Optimizing Performance

This chapter covers database repair and compacting, along with an in-depth technical discussion of how Microsoft Jet 3.5 has been optimized for performance. It also includes a discussion of how to tune Microsoft Jet engine performance by using settings in the Windows® registry or by using the SetOption method. Information about unsupported tuning functions rounds out the chapter.

Appendix A Specifications

This appendix lists the specifications of Microsoft Jet data types, showing table and field types, query types, and data limits.

Appendix B SQL Reference

Using Backus-Naur Form (BNF), this appendix provides a syntactical description of the Microsoft Jet implementation of SQL.

Appendix C Registry Settings

This appendix describes each of the settings that Microsoft Jet 3.5 installs in the Windows registry.

Appendix D Error Reference

This appendix lists the error number, error message, and class of each Microsoft Jet error.

Appendix E Microsoft Jet Components

This appendix list the file names and descriptions of the components installed for Microsoft Jet 3.5.


Typographic Conventions

This guide provides comprehensive examples for many of the topics discussed. The following typographic conventions are used:

Example of convention | Description
Sub, If, ChDir, Print, Time$,
RecordsAffected, Recordset 		Words in bold with initial letter capitalized indicate language-specific keywords with special meaning to VBA. Objects, methods, statements, functions, and properties appear in bold with the initial letter capitalized. Concatenated names may contain other capital letters.
expr, path | In text, italic letters are used for defined terms, usually the first time they occur in the guide. Italics are also used occasionally for emphasis. In syntax, italics indicate placeholders for information you supply.
[expressionlist] | In syntax, items in square brackets are optional.
{While|Until} | In syntax, braces and a vertical bar indicate a choice between two or more items. You must choose one of the items unless all of the items are enclosed in square brackets.
Dim rstCust As Recordset
A monospace font indicates code.
Set rstCust = dbCurrent._
OpenRecordset("Customers"_
dbOpenDynaset)
If you are using Access Basic, Visual Basic 3.0, or VBA 3.0, the line-continuation character ( _ ) indicates that code continued from one line to the next in the guide should be typed as one line in the editor by omitting the line-continuation character and the preceding space.

If you are using Visual Basic 4.0 or VBA 4.0 and later, you can continue a line of code in the editor by using the line-continuation character.
ALT+F1, ENTER | Small capital letters are used for the names of keys and key sequences, such as ENTER and CTRL+R. A plus sign (+) between key names indicates a combination of keys.


Code Examples and Programming Style

This guide contains numerous code examples that you can use to experiment with the concepts covered. Unless otherwise noted, the code is compatible with VBA 5.0 as it is implemented in Visual Basic 5.0, Microsoft Access 97, Microsoft Excel 97, Microsoft PowerPoint 97, Microsoft Word 97, and other applications that host VBA 5.0.

This guide uses the following conventions in the sample code:

  • Keywords appear with an initial letter capitalized; concatenated words may contain other capital letters. Constants and variables appear in mixed case:
    If rstCustomerList.RecordCount = 0 Then strText = "Empty"
Public Const dbOpenRecordset = 2
  • In full procedures, all variables are declared locally or are listed with an initialization procedure that declares public variables. All code has been tested with the VBA Option Explicit statement to ensure that there are no undeclared variables.

  • An apostrophe (') introduces a comment:
    ' This is a comment.
Dim intNew As Integer  ' This is also a comment.
  • Control-flow blocks and statements in Sub and Function procedures are indented:
    Sub MyCode ()
    Dim intX As Integer, intP As Integer, intQ As Integer

    If intX > 0 Then
        intP = intQ
    End If
End Sub
  • Naming conventions are used to make it easier to identify objects and variables in the code examples. The following tables list some of the conventions.

Naming Conventions for Database Objects and Variables

Object | Prefix | Example
Container | con | conForms
Database | dbs | dbsCustomers
Document | doc | docReport
Dynaset | dyn | dynOrders
Error | err | errCurrent
Field | fld | fldLastName
Group | gru | gruUsers
Index | idx | idxPrimaryKey
Parameter | prm | prmBeginDate
Property | prp | prpValue
QueryDef | qdf | qdfShowCustomers
Recordset | rst | rstCustomers
Relation | rel | relOrdersItems
Snapshot | snp | snpReadOnly
Table | tbl | tblOrders
TableDef | tdf | tdfCustomers
User | usr | usrMary
Workspace | wrk | wrkPrimary

Naming Conventions for Visual Basic Variables

Data type | Prefix | Example
Byte | byt | bytAttributes
Currency | cur | curDollars
Date | dte | dteToday
Double | dbl | dblPi
Boolean | f | fInitialized
Integer | int | intCount
Long | lng | lngAttributes
Single | sng | sngDays
String | str | strLastName
Type (user-defined) | typ | typPartRecord
Variant | var | varTemp

See Also For more information about naming conventions, see “The Leszynski Naming Conventions for Microsoft Jet” Help file in the Papers folder on the companion CD-ROM.


Using the Companion CD-ROM

Inside the back cover of this guide you can find the companion CD-ROM for the Microsoft Jet Database Engine Programmer’s Guide. The CD-ROM contains online versions of all the code examples in this guide, nine white papers giving additional information about the Microsoft Jet database engine, several important utilities, and the Microsoft Access 97 Upsizing Tools.


Using the Code Examples

This guide contains over 200 code examples that show how to correctly use the Microsoft Jet database engine. To help you learn about these features and incorporate them into your applications, the companion CD-ROM contains an electronic version of each code example used in the printed guide.

The code examples are stored in the JetBook\Samples subfolder on the companion CD-ROM. All of the code examples for Microsoft Access 97 are stored as modules in a Microsoft Access database named JetSamples.mdb. All of the code examples for Visual Basic 5.0 and other applications that support VBA 5.0 are stored as modules (.bas files) that are referenced in a Visual Basic project named JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder. Additionally, some of the code examples for Chapter 8, “Accessing External Data,” use sample data files in other formats, such as the Paradox sample data file, Author.db. These sample data files are located in subfolders in the JetBook\Samples folder and must be present in order for these code examples to work properly.

All of the code examples are designed to be executed from your hard disk. Use the Windows Explorer to copy the complete directory structure to the root directory of your C drive. Because files stored on a CD-ROM must have their read-only file attribute set, you may need to clear the read-only file attribute after copying the files. To make this process easier, you can run the function named RemoveReadOnlyAttr from the FileAttributes module in JetSamples.mdb, or from FileAttributes.bas. For more information about specific file names or additional requirements for the code examples in each chapter, see the introduction in that chapter.

To use the Visual Basic code modules referenced in JetSamples.vbp in Visual Basic 5.0, you can open JetSamples.vbp directly. To use these modules from a VBA 5.0 host application other than Microsoft Access 97, such as Microsoft Word 97, you must import the module(s) into the Visual Basic Editor. Additionally, you must set a reference to the Microsoft DAO 3.5 object library (see below).

Important Don’t copy and paste code examples from JetSamples.mdb into Visual Basic 5.0 or other VBA 5.0 host applications. Many code examples in JetSamples.mdb include code that uses Microsoft Access-specific Visual Basic keywords and techniques that will fail if run from other applications. The modules referenced in JetSamples.vbp are designed to run in Visual Basic 5.0 and other VBA 5.0 host applications.

Û To import a .bas file into the Visual Basic Editor

  1. Start the VBA 5.0 host application and open the document you want to work with.

  2. Open the Visual Basic Editor. To do this in Microsoft Excel, Microsoft PowerPoint, or Microsoft Word, point to Macro on the Tools menu, and then click Visual Basic Editor. A different procedure may be required in other VBA 5.0 host applications. Check your product documentation for details.

  3. On the File menu, click Import File.

  4. In the Import File dialog box, navigate to the folder where you installed the code samples, select the module (.bas) you want to work with, and then click Open.

  5. To view the code in the module, expand the Modules folder in the Project Explorer, and then double-click the module you imported.

Û To set a reference to the Microsoft DAO 3.5 Object Library

  1. Start the VBA 5.0 host application, open the document you want to work with, and then open the Visual Basic Editor.

  2. On the Tools menu, click References.

  3. In the Available References box, select Microsoft DAO 3.5 Object Library, and then click OK.

Note The reference for the Microsoft DAO 3.5 object library is available only if you selected the Data Access Objects For Visual Basic option under the Data Access option when you ran the Setup program for Microsoft Access 97, Microsoft Excel 97, Microsoft Office 97, or Microsoft Office 97 Professional Edition. If the object library is not available, re-run the Setup program and select this option. The Data Access Objects For Visual Basic option is not available in the Setup program for the stand-alone versions of Microsoft PowerPoint 97 or Microsoft Word 97.


White Papers

Contractors and Microsoft staff have prepared a number of white papers that describe specific aspects of Microsoft Jet and the applications that use it. The following white papers pertaining to the Microsoft Jet database engine are included on the companion CD-ROM in the Papers folder (the file names are indicated in parentheses):

  • Collins, Kevin. “Understanding Microsoft Jet Locking.” (JetLock.doc)

  • Collins, Kevin. “Microsoft Jet 3.5 Performance Overview and Optimization Techniques.” (V35Perf.doc)

  • Collins, Kevin. “Microsoft Jet 3.0 Performance Overview.” (V3Perf.doc)

  • Gilman, Joel. “Setting Connection String Parameters in DAO.” (DAOConnectString.doc)

  • Kruglick, Emily and Tucker, Peter. “Data Access Objects (DAO) 3.5 and ODBCDirect.” (ODBCDir.doc)

  • Leszynski, Stan. “The Leszynski Naming Conventions for Microsoft Jet.” (LNC95Jet.hlp)

  • Poll, Tony. “Database Replication in Microsoft Jet.” (Replication Manager Whitepaper.doc)

  • Roberts, Mark. “Accessing Microsoft Exchange and Microsoft Outlook Data Using Visual Basic.” (Accessing Exchange And Outlook Data.doc)

  • Scott, Beth. “Accessing External Data with Microsoft Access.” (Accessing External Data With Microsoft Access.doc)

Important The information contained in these white papers represents the current view of Microsoft Corporation on the issues discussed as of the date of publication. Because Microsoft must respond to changing market conditions, the statements in these papers should not be interpreted as commitments on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication.


Utilities

The companion CD-ROM contains several helpful utilities for understanding database locking. The utilities in the Utilities folder on the companion CD-ROM are explained in the white paper, “Understanding Microsoft Jet Locking.”


The Microsoft Access 97 Upsizing Tools

The Upsizing folder on the companion CD-ROM contains the Setup program for the Microsoft Access 97 Upsizing Tools. You can use the Microsoft Access 97 Upsizing Tools to re-create the structure of a Microsoft Access 97 database in Microsoft SQL Server. For more information about the Upsizing Tools, see “Converting a Microsoft Access Application Using the Upsizing Wizard” in Chapter 9, “Developing Client/Server Applications.”


Wizards

Several wizards are included on the companion CD-ROM. Two wizards for linking and importing data from Microsoft Exchange and Microsoft Outlook are included in the ExchWizard folder. To install these wizards, along with the Microsoft Exchange ISAM driver, run the Wzmapi80.exe file in the folder.

Two wizards for use with replication are also included. The Partial Replica Wizard, which makes it easy to create partial replicas, is located in the PrtlReplWiz folder. The Replica to Regular Database Wizard, located in the ReplWizard folder, makes it easy to convert a Design Master to its original, nonreplicated state.

See Also For more information about using these wizards, see the Microsoft Access Developer Forum Web site at http://www.microsoft.com/accessdev/.


Using the Companion DAO Hierarchy Maps

Inside the back cover of this guide you can also find two maps (printed back to back) showing the hierarchy of DAO objects, properties, and methods. These maps are valuable learning tools and present the complete family of objects for each of the two kinds of Workspace objects supported by Microsoft Jet 3.5. You may find that posting the maps on a wall near your workstation provides a faster reference tool than using Help to look up the properties and methods available for a particular object.


Chapter 1 An Overview of Microsoft Jet

In November 1992, Microsoft released its first desktop database product for Microsoft Windows, Microsoft Access version 1.0. This product was singular in its ability to provide database functionality to a broad spectrum of Windows users — from complete beginners to seasoned developers — and quickly became the top-selling database for Windows. Powerful form and report engines accompanied by wizards helped Microsoft Access redefine the balance between ease of use and power. But hidden behind this new user interface was a piece of software every bit as powerful: the Microsoft Jet database engine.

A database engine is the component of an application that provides the link between an application and its data. Microsoft Jet is a powerful, application-independent, advanced database engine that can be used with a variety of programs from Microsoft Word and Microsoft Excel to Microsoft Visual C++.

Database programs such as Microsoft Access consist of a number of important components, one of which is the database engine. The application’s user interface provides a means by which a user can interact with the database application. The database engine provides a means by which the application can interact with and manipulate data. Typically, users view and manipulate data through the application’s user interface. However, the Data Access Objects (DAO) of Microsoft Jet provide a means by which a programmer can manipulate the capabilities of the database engine directly. DAO is also an ActiveX component that supports Automation (formerly known as OLE Automation), and other applications that support Automation can access the Jet database engine programmatically by using DAO.

The Microsoft Jet database engine version 3.5 handles database processing for Microsoft Office 97 applications, Microsoft Visual Basic version 5.0, and Microsoft Visual C++ version 5.0. Microsoft Jet also exposes its services through Open Database Connectivity (ODBC) by using version 3.5 of the Microsoft Jet ODBC driver. Microsoft Jet exposes a rich and expanded set of features through DAO, which now includes a new client/server connection mode called ODBCDirect. In addition to data manipulation, Microsoft Jet services include support for data definition, multiuser services, advanced workgroup security, engine-level referential integrity, database replication and replication over the Internet or an intranet, query processing and updatable recordsets, query optimization (by using the Rushmore™ technology), transaction processing, and remote data access. The Jet database engine can read and write data stored in other database formats as well as data stored in spreadsheets, text files, HTML tables and lists, ODBC databases, and Microsoft Exchange folders and address books.

A Brief History of Microsoft Jet

Services Provided by the Microsoft Jet Database Engine


A Brief History of Microsoft Jet

Microsoft Jet is a mature product that has grown into a powerful relational database engine. The development of Microsoft Jet, which began in 1988, has been an evolutionary process that continues to this day. What follows is a brief history of that evolution. The release of Microsoft Jet 3.5 marks the fifth update to the Jet database engine. Following is a brief review of the prior versions and their more significant enhancements.


Microsoft Jet Versions 1.x

The Jet database engine first appeared in 1992 as a component of Microsoft Access 1.0. Version 1.0 of Microsoft Jet was a relational database engine providing not only standard database management system (DBMS) functionality such as data definition, data manipulation, querying, security, and maintenance, but also technology that had not yet been seen in desktop databases. These features included updatable views, query-on-query capabilities, and seamless access to heterogeneous data. The engine provided a few methods for accessing table and query structures, but didn’t have a cohesive model for programmatic control over object structures and data manipulation.

In May 1993, Microsoft released Microsoft Jet version 1.1 for Microsoft Access version 1.1 and Microsoft Visual Basic version 3.0. Microsoft Jet 1.1 provided greater connectivity between Microsoft Jet and ODBC databases. It included a new Oracle ODBC driver, introduced a connection manager to provide services for sharing and caching connections, and included general improvements in the ability to work with any ODBC driver. This version also saw the addition of the MSysConf table so that database administrators could prevent the saving of password and user ID information in an attached ODBC table. Additionally, the maximum size of a database file was increased from 128 MB to approximately 1.1 GB. Microsoft also added support for linking (attaching) to Microsoft FoxPro tables, and added some new collating sequences for international users. Microsoft Jet 1.1 also included version 1.1 of the DAO interface, adding the data definition language (DDL) capabilities that were missing from DAO version 1.0.


Microsoft Jet Versions 2.x

Introduced in April 1994 with Microsoft Access version 2.0, Microsoft Jet version 2.0 offered major enhancements over previous versions. Key areas of improvement included enforced referential integrity and data validation at the engine level, optimized query performance (by using Rushmore technology), increased conformance with ANSI-standard Structured Query Language (SQL) syntax rules, support for UNION, sub-SELECT, and data-definition queries, and a full programming interface to Microsoft Jet by using the DAO version 2.0 object model. Microsoft Jet 2.0 also introduced support for cascading updates and deletions, remote transaction management, SQL pass-through queries, and new MSysConf settings to control how often and how many data fetches are made against ODBC data sources. Additionally, support was added for remote index joins and new initialization file settings for debugging and tuning data operations.

In Microsoft Jet 2.0, the DAO object model was greatly enhanced to provide collections, objects, properties, and methods that gave developers almost complete access to the engine’s underlying services, including support for data definition and manipulation, workspace and transaction management, and programmatic access to security.

Microsoft Jet version 2.5 was introduced in October 1994 in the Microsoft Access 2.0 Service Pack (16-bit) and in the ODBC Desktop Database Drivers Pack version 2.0 (16- and 32-bit). This was the first 32-bit version of the Jet database engine. Many minor features were added to support ODBC desktop database drivers. In addition, the Text and Microsoft Excel data formats were included as true formats rather than formats just for importing and exporting. Finally, Microsoft Jet included a new expression service provided by Visual Basic for Applications.


Microsoft Jet Version 3.0

Introduced in the fall of 1995, Microsoft Jet 3.0 offered new features and improved performance in an exclusive 32-bit implementation for use with the Microsoft Windows 95 and Microsoft Windows NT® operating systems. This version saw changes to the database format and the way the engine handles data, with the goal of making most operations substantially faster. Microsoft Jet 3.0 also introduced database replication services that developers could use to create database replicas that could be used in different locations. The Jet database engine performed all the services necessary to create and synchronize replicas to keep data current and to resolve problems associated with record conflicts. In addition, version 3.0 further expanded the DAO object model by adding additional objects, collections, properties, and methods.


Microsoft Jet Version 3.5

Version 3.5 was released in the fall of 1996 with Microsoft Office 97. A sampling of the new features in version 3.5 is summarized in the following table.

Feature | Description
Reduction in database growth | When using Microsoft Jet 3.0, some users experienced unusual database growth, especially when manipulating modules in Microsoft Access. In Microsoft Jet 3.5, database growth has been substantially reduced as a result of changes to the way Microsoft Jet recycles long value pages.
Programmatic changes to Windows registry settings at run time | DAO now exposes the SetOption method of the DBEngine object. SetOption method features don’t permanently change values, but allow the developer to change specified values at run time to “tune” the application for optimal performance. It’s no longer necessary to change registry settings on each machine or point to shadow registries.
Improved interoperability with Novell NetWare | Microsoft Jet 3.5 resolves a previous problem with NetWare’s 10,000 lock limit by introducing a new registry setting called MaxLocksPerFile. By default, this setting splits transactions and partially commits them when 9500 locks are placed, thus allowing large rowset operations to complete.
Programmatic refreshing of data | To improve performance, Microsoft Jet 3.0 would check to see if data had changed before refreshing its internal cache. This provided a significant performance improvement for a shared database and brought performance close to that of a database opened exclusively. The down side of this was that users would wait by default five seconds before seeing other user’s changes. To remedy this situation in Microsoft Jet 3.5, DAO exposes a new option off the Idle method of the DBEngine object called dbRefreshCache that causes Microsoft Jet to refresh its cache. This allows developers to refresh the cache on demand, while still getting the best performance benefits.
Programmatic purging of write commands to disk | Microsoft Jet issues write commands to the operating system and occasionally, when code explicitly says to commit a transaction, the data will not have actually been written to disk. DAO now exposes a new option for the CommitTrans method, called dbForceOSFlush, which forces the operating system to immediately write the Microsoft Jet transaction to disk.
Performance improvements | Implicit transactions were removed for all SQL data manipulation language (DML) statements to provide significant performance improvements for DML statements that affect many rows, by reducing the amount of I/O in the Microsoft Jet temporary database.
New caching mechanism | Microsoft Jet is now able to use the maximum amount of cache space without causing concurrency issues. This results in substantial performance improvements when Microsoft Jet is manipulating a large number of records.
Improved locking mechanism | Microsoft Jet 3.5 uses fewer locks when reading long value (LV) pages (pages that store fields with the OLE Object or Memo data types). The use of fewer locks can significantly improve concurrency in situations where users are simultaneously reading and updating data in Memo fields.
Improved query optimizer | Microsoft Jet now optimizes more predicates for faster performance.
Remote data performance improvements | The new MaxRecords property allows the user to specify a maximum number of records to retrieve from an ODBC server, regardless of the size of the result set. The new FailOnError property (used for update and delete queries against a single table on an ODBC data source) sends the entire query to the server for processing when the property is set to No. This means that the query succeeds or fails in its entirety rather than allowing partial failures, which is the default behavior when the property is set to Yes.
Direct access to ODBC data sources without using Microsoft Jet | DAO now supports access to remote data by using ODBCDirect. For the first time, you can bypass the Jet database engine and use DAO to access remote data directly. ODBCDirect establishes a connection directly to an ODBC data source without loading the Jet database engine in memory, and this is useful in situations where specific features of ODBC are required.
Partial replica support | Replicas containing only a subset of the data contained in a full replica are now supported.
Replication over the Internet or an intranet | Synchronization to and from replicas located on Internet or intranet servers is now supported through FTP and HTTP.
Access to data in HTML tables | Importing, exporting, and making read-only links to HTML tables embedded in HTML pages are now supported.
Access to data in Microsoft Exchange folders | Importing, exporting, and making append-only links to Microsoft Exchange message and address book folders are now supported.


Services Provided by the Microsoft Jet Database Engine

Microsoft Jet provides a variety of database management services, including data definition, data manipulation, data integrity, and security. There are seven basic services that a DBMS should provide. This section introduces these services and shows how Microsoft Jet accomplishes each one. The seven basic functions of a DBMS are:

  • Data definition and integrity: create and modify structures for holding data, such as tables and fields, and ensure that rules to prevent data corruption from invalid entries or operations are applied to data operations.

  • Data storage: store data in the file system.

  • Data manipulation: add new data, modify or delete existing data.

  • Data retrieval: retrieve data from the system.

  • Security: control users’ access to database objects and data, thereby protecting the objects and data against unauthorized use.

  • Data sharing: share data in a multiuser environment.

  • Database maintenance: compact, repair, and convert data and database objects.


Data Definition and Integrity

Probably the first operations you perform with a DBMS are the creation and definition of objects that hold your data, objects that define relationships between the data, and objects that allow you to restrict access to the data. With Microsoft Jet, you can create and modify the following types of objects.

Databases

All Microsoft Jet objects are contained in a single file known as a database file. This file contains both object definitions and data.

Tables

Tables can contain data in a native Microsoft Jet format, or they can represent links to data stored in a variety of other data formats such as Microsoft Exchange, Microsoft Excel, FoxPro, or ODBC data sources such as Microsoft SQL Server.

Fields

A variety of field data types are available, including Text, Number, Currency, and Memo. Tables can contain up to 255 fields. Microsoft Jet supports both variable-length and fixed-length fields. For example, text fields store only the amount of text in the field; extra space isn’t wasted in the database file to pad out the field to its maximum size.

Indexes

Microsoft Jet allows both primary and secondary indexes that can be based on one or more fields. You can create indexes by using both DAO and SQL.

Relationships

In a Microsoft Jet database, relationships define how data in multiple tables is related. With Microsoft Jet, you can use Relation objects to define relationships between fields and enforce referential integrity with cascading updates and deletes.

Queries

You can create and save a variety of query types by using Microsoft Jet, including queries for data retrieval and data manipulation.

Users and Groups

To implement security, Microsoft Jet uses a form of workgroup security that defines users and the groups to which they belong. User and Group objects represent specific users and groups who are authorized to use Microsoft Jet.

Application-Specific Objects

Applications that use Microsoft Jet can store their own objects in the database. These objects are maintained through a Containers collection. For example, Microsoft Access uses Container objects to store its forms, reports, macros, and modules.

See Also For more information on data definition, see Chapter 3, “Data Definition and Integrity.”


Data Storage

Microsoft Jet uses a form of data storage known as the Indexed Sequential Access Method (ISAM). The Microsoft Jet ISAM implementation has the following characteristics:

  • Records can be ordered by the use of an index on any field or combination of fields.

  • Variable-length records are the standard method of storage.

  • Data is stored and managed in 2 KB pages, similar to those in Microsoft SQL Server.

In Microsoft Jet, data stored in the database is structured as a set of pages, each containing one or more records. A record can’t span more than a single page; Memo and Long Binary (OLE Object) fields are stored in pages separate from the rest of the record. Each record occupies only as much space as needed to store its data, so each record within the same table may not be the same length.


Data Manipulation and Retrieval

Microsoft Jet provides a powerful query engine that supports data retrieval and manipulation by using SQL. You can also use DAO to programmatically access data in tables.

By using the data-manipulation services of Microsoft Jet, you can add, modify, and delete records in tables. These changes can be accomplished through action queries or directly through DAO by using Recordset objects. Microsoft Jet supports updatable queries, heterogeneous joins, and queries on queries.


Security

Microsoft Jet provides a sophisticated security model with which you can define your application’s users and groups, and set specific permissions for each object in the database for these users and groups. With Microsoft Jet, you can create workgroups to define users, groups, and passwords in a secure environment. This function is supplied in a stand-alone executable called Workgroup Administrator (WrkgAdm.exe), which is included with Microsoft Access.

See Also For more information on Microsoft Jet security, see Chapter 10, “Managing Security.”


Data Sharing

Microsoft Jet provides data-sharing services that enable multiple users to access and change a shared database. Microsoft Jet locks all data in the 2 KB pages, even if the user is only retrieving or manipulating some of the records on those pages. When a database resides on a file server, Microsoft Jet uses the underlying network operating system’s basic locking techniques to lock pages when a table record is being modified.

Microsoft Jet supports pessimistic and optimistic record locking. Pessimistic locking locks the page that contains the record being edited as soon as the editing begins. The page is unlocked when editing is completed. No two users can edit data on the same locked page at the same time. Optimistic locking locks the page that contains the record being edited only when the editing is committed. In this scenario, two users can edit the same record at the same time, but a trappable run-time error occurs when a user attempts to save changes after the other user has saved changes to the record.

To coordinate locking in a multiuser environment, Microsoft Jet uses a locking information file (by using the same file name as the database but with an .ldb extension) that is stored in the same directory as the database itself. Each user who opens the database generates an entry in this file. This list of current users is used to determine which records in a database are locked and who has them locked. This system is designed to prevent possible file-contention errors and to avoid data corruption. The .ldb file is created when you open a database for shared access and is deleted after the last user closes the database.


Maintenance Functions

All databases require periodic maintenance. Microsoft Jet provides compaction, repair, and conversion functionality to make the day-to-day task of administering your databases manageable.

As a database file is used, it can become fragmented as objects and records are created and deleted. Periodic compaction reduces the amount of wasted space in the file and can enhance performance. Microsoft Jet provides a CompactDatabase method that compacts the database file and reclaims wasted space.

If Microsoft Jet is terminated when writing data (as the result of workstation failure or power outage, for example), the data can become corrupted. Microsoft Jet provides repair functionality that scans the database file for inconsistencies and attempts to repair damaged objects.

See Also For more information on maintenance functions, see Chapter 13, “Optimizing Performance.”


Application Independence

Although the Jet database engine was introduced with Microsoft Access 1.0, it quickly was adopted as the database engine for other applications and is now used by Visual Basic, Visual C++, Microsoft Visual J++, and all of the Office 97 applications.

Although Microsoft Jet doesn’t recognize any application-specific objects, applications that use Microsoft Jet can dynamically add application-specific information to the database engine through application-defined properties.


Compatibility Among Versions

With each new version of Microsoft Jet, enhancements in functionality and changes to the structure of the database file cause problems with backward compatibility. Wherever possible, efforts have been made to ensure an easy migration path between versions. However, incompatibilities do exist. The following table illustrates how database files and objects can be used with different versions of Microsoft Jet.

 | Microsoft Jet version
 |  | 1.0 | 1.1 | 2.0 | 2.5 | 3.0 | 3.5
.Mdb (Microsoft Access) version | 1.0 | Y | Y | Y | Y | Y | Y
 | 1.1 | N | Y | Y | Y | Y | Y
 | 2.0 | N | N | Y | Y | Y | Y
 | 7.0 | N | N | N | N | Y | Y
 | 8.0 | N | N | N | N | N | Y

A “Y” indicates that the database can be used without conversion; “N” indicates that the database can’t be used or converted.

By using DAO, you can open databases created with the version of Microsoft Jet that is running the DAO code, and those created with any previous version of Microsoft Jet. For example, if you’re running DAO code by using Microsoft Jet 3.0, you can open version 1.0, 1.1, 2.0, 2.5, and 3.0 databases, but not 3.5 databases.


Microsoft Jet Components

To provide database management services, Microsoft Jet uses a variety of components, each designed to perform a specific group of functions. These components, implemented as dynamic-link libraries (DLLs), constitute the Jet database engine as a whole. These DLLs work together to provide all the services supplied by the Jet database engine. The Microsoft Jet DLLs can be grouped into the following component groups.

DLL group | Description
Microsoft Jet DLLs | Responsible for storing data in the database file and for providing query and security services.
DAO DLLs | Responsible for providing the programming interface to the Jet database engine.
External ISAM DLLs | Responsible for providing access to external (non-native) data (for example, Microsoft Excel, Microsoft Exchange, HTML, FoxPro, Borland dBASE and Paradox, and Lotus 1-2-3).
Replication DLLs | Responsible for creating and synchronizing full or partial database replicas at multiple locations.
Visual Basic for Applications DLLs | Visual Basic for Applications isn’t a component of Microsoft Jet, but provides the expression service to evaluate functions in SQL statements.
ODBC DLL | In addition to the components discussed above, Microsoft Jet uses the ODBC Driver Manager DLL to provide access to ODBC data sources.

The following diagram represents an overview of how the various Microsoft Jet components fit together.

Figure 1.1 The structure of Microsoft Jet

See Also For information on the specific files that make up each component, see Appendix E, Microsoft Jet Components.


Structure of the Microsoft Jet Database File

Unlike other DBMSs, Microsoft Jet stores all database objects in a single file. This file has an .mdb extension and is often referred to as the .mdb file, though it can have any extension. The benefits of a single database file include: easy object management (because all objects can be moved as a single unit), long object names (up to 64 characters), and control over where the individual database objects are located.

Given that the database file contains a variety of objects, its format is quite complex. This section points out some of the characteristics of this file.


Pages

All the information stored in a database file is organized into 2 KB pages. There are a variety of page types used to store data.

Database Header Page

The first page in every database is the database header page. It stores 256 commit bytes that track user operations in a multiuser environment. There is no set order for subsequent pages.

Index Pages

These pages store indexes. Microsoft Jet 3.0 and 3.5 use an entirely new layout format for compressing index data that results in a substantial reduction in storage size and a significant reduction in the time needed to create indexes that are highly duplicated. When the database is compacted, Microsoft Jet 3.0 and 3.5 rewrite a table’s records in primary key order.

Long Value (LV) Pages

LV pages store two types of data. The first is Memo data that has flowed over from data pages. The second type includes all other binary data types, such as Long Binary (OLE Object) fields. In Microsoft Jet 2.0, these pages are all stored in one hidden table. This results in contention issues when multiple users are accessing Microsoft Jet. In Microsoft Jet 3.0 and 3.5, long values of a particular field are stored with other long values of the same field. This helps reduce multiuser contention when using long values from different fields. Additionally, LV pages now have two distinct types: Fixed-length LV pages are used internally for special database scenarios; variable-length LV pages are used for external data.

System Table Pages

These pages contain the system tables MSysACES, MSysObjects, MSysQueries, and MSysRelationships. These system tables are discussed in the following section.

See Also For more information on pages, see Chapter 6, “Creating Multiuser Applications,” and Chapter 13, “Optimizing Performance.”


System Tables

In system tables, Microsoft Jet stores information about each of the objects. These tables are much like the tables you create in a database — with a few exceptions. First, they are identified internally as system objects so that you can differentiate your regular tables from system tables. Secondly, some system tables are inherently read-only. Microsoft Jet prevents you from updating the contents of some system tables to avoid breaches in security. The following table describes each of the system tables.

Warning The existence and structure of system tables are subject to change in every revision of Microsoft Jet. When their format changes or they disappear, applications that use system tables may no longer work. System tables are documented here solely for the purpose of giving you a better understanding of how Microsoft Jet works internally.

System table | Description
MSysACES | Stores each object’s permission information for each object for each user and group. As discussed in Chapter 10, “Managing Security,” Microsoft Jet implements security by using a workgroup model. In this model, users are assigned access to specific objects through permission settings.
MSysObjects | Stores additional information about objects in the database, including tables and queries. The ID field in this table is used to identify objects within other system tables. This table extends the application-independent nature of Microsoft Jet by including binary fields that allow the host application (such as Microsoft Access) to store information about application-specific objects such as forms, reports, and modules.
MSysQueries | Stores information about the queries stored in the database. This table includes information about the type of query, the tables and fields it uses, and its criteria, sorting, and parameters.
MSysRelationships | Stores information about each of the relationships in the database, including information about the tables and fields used in the relationship.

Microsoft Access Users Microsoft Access defines its own additional system tables to store application-specific information. These include MSysCmdbars, MSysIMEXColumns, MSysIMEXSpecs, MSysModules, and MSysModules2. When you create a Microsoft Jet database by using Microsoft Access, these system tables are automatically created.


Engine Configuration Information

Versions of Microsoft Jet prior to 3.0 store configuration information in an initialization file that follows the Windows .ini file standard. (The one exception to this is version 2.5, which stores the location of the installable ISAM DLL in the registry.) Microsoft Jet 3.0 and 3.5 use the Windows registry exclusively. The registry is a structured file used by the Windows 95 and Windows NT operating systems to store information about the system’s configuration, including software components such as Microsoft Jet.

See Also For more information on the Windows registry, see Appendix C, Registry Settings.


Chapter 2 Introducing Data Access Objects

Data Access Objects (DAO) is the programming interface for the Microsoft Jet database engine. You can use the objects and collections in the DAO object hierarchy to manipulate your data and the structure of your database. DAO is a shared component of Microsoft Office. Any application that supports Automation can use DAO to work with objects in a database. This chapter introduces you to DAO and discusses how to use Microsoft Visual Basic for Applications code.

Hello DAO

DAO Objects

DAO Basics

Using DAO

Features New to DAO 3.0 and 3.5

Compatibility Issues

Optimizing DAO

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


Hello DAO

“Hello World” is the first code example in the now-classic book, The C Programming Language, by Brian Kernighan and Dennis Ritchie. The following examples show you how to perform a simple task with DAO using Hello World. But instead of printing a string variable to the screen, our version searches for a value in a table and retrieves a message for display.

The NorthwindTables database includes a table with one text field, Greeting, that contains different greetings. The following table shows the data that the Greeting field contains.

Greeting
Bonjour
Buenos dias
Hello World!
Hi
Howdy

The following code shows one way to retrieve the greeting. In this example, strDbPath is the path to the NorthwindTables database, and strGreeting is the string for which you are searching:

Dim dbs As Database
Dim rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Greetings", dbOpenTable)

With rst
    .Index = "GreetingIndex"
    .Seek "=", strGreeting
    If Not .NoMatch Then
        MsgBox !Greeting
    Else
        MsgBox "Could not find " & strGreeting & "."
    End If
End With
Set rst = Nothing
Set dbs = Nothing

The following explains what’s happening when this code runs:

  1. First, two object variables named dbs and rst are declared. These are used to point to the actual Database and Recordset objects used in the example.

  2. The Set statement and the OpenDatabase method return a reference to the NorthwindTables database, represented by the variable strDbPath, and assign the variable dbs to the database. If you install the sample databases to the default folder, then the value of strDbPath is "C:\JetBook\Samples\NorthwindTables.mdb". If you open NorthwindTables.mdb in Microsoft Access and paste this code into a module, you can set the Database object variable by using the CurrentDb function.

  3. The Set statement is used again, this time to assign the rst object variable to the table named Greetings. This is done by using the OpenRecordset method of the Database object. This method tells DAO to look in the NorthwindTables database (NorthwindTables.mdb) for the Greetings table.

  4. Next, the code tells DAO which index to use when searching the table. This is done by setting the Index property of the Recordset object to the value "GreetingIndex". This index exists on the Greeting field in the Greetings table.

  5. Then the Recordset object’s Seek method is called to find the record where the value of the indexed field is equal to value of strGreeting. You can experiment with searching for other values in the table by using the Seek method to look for other values in the table.

  6. Next, the NoMatch property of the Recordset object is checked to see if a match was found. If a match was found, the value of the Greeting field is displayed. If not, an error message is displayed.

  7. Finally, the Set statement is used at the end of the procedure to set the value of the Database and Recordset object variables to Nothing, freeing up any memory that they may have used while they existed.

The end result is a program that displays one of the greetings (such as “Hello World!”) stored in the Greetings table of the NorthwindTables database. The key concepts in this example are:

  • DAO is a hierarchy of objects. The Greetings TableDef object is in the TableDefs collection of the Database object, and the GreetingIndex Index object is in the Indexes collection of the Greetings TableDef object.

  • Methods initiate actions on objects. This example uses methods to open a database and a table, and to find a record.

  • Properties define characteristics of objects. In this example, the value of the Index property of the Recordset object is set to tell DAO how to order the records that you are going to search.

As you progress through the rest of this chapter, you’ll see how these elements work together to form a cohesive DAO object model that provides flexible access to the objects and data in your database.


DAO Objects

DAO is a hierarchy of objects and collections of objects. As shown in Figure 2.1, each table, field, index, query, and so on is represented by objects organized into collections. Each object has a set of properties that define its characteristics and one or more methods that you use to perform various operations on the object. This section introduces the DAO object hierarchy and provides information about each type of object it contains.

Figure 2.1 Collections, objects, properties, and methods

In the DAO object hierarchy, all objects are contained in a single DBEngine object. Figure 2.2 shows the DAO object hierarchy for Microsoft Jet workspaces.

Note DAO version 3.5 includes a second object hierarchy — the ODBCDirect object hierarchy. You can use the ODBCDirect object hierarchy to work with ODBC data sources without going through the Jet database engine. The objects in the ODBCDirect object hierarchy are not shown in Figure 2.2. For information about ODBCDirect and to see the ODBCDirect object hierarchy, see Chapter 9, “Developing Client/Server Applications.”

Figure 2.2 The DAO object hierarchy for Microsoft Jet workspaces


DBEngine Object

The DBEngine object contains and controls all other collections and objects in the DAO object hierarchy. The DBEngine object doesn’t have an associated collection.

Note Prior to version 3.0 of DAO, you could run up to 10 applications that simultaneously used the DBEngine object. You can now run as many processes (executions of Microsoft Jet, other applications, and so on) as your system resources allow. Additionally, within each process, you can have up to 64 instances of Microsoft Jet. For example, you can create 64 private DBEngine objects within an application.

The DBEngine object has several properties and methods and contains two collections: the Workspaces collection and the Errors collection. You use the DBEngine object when referring to a database. When using DAO from within Microsoft Access, you can use the CurrentDb function as a shortcut to refer to the DBEngine object for the current database.


Error Objects

As you perform operations by using DAO, errors may occur. Each error is stored as an Error object in the Errors collection. You can use information contained in the Error object to determine what caused the error, and to display meaningful error messages to your users.

It’s important to note that as each DAO error occurs, the Errors collection is cleared of previous errors, and the new Error object is placed in the Errors collection. There can be several related Error objects in the collection caused by a single operation.

See Also For a list of errors returned by Microsoft Jet, see Appendix D, “Error Reference.”


Workspace Objects

There are two types of Workspace objects: Microsoft Jet Workspace objects and ODBCDirect Workspace objects. This section discusses Microsoft Jet Workspace objects. For information about ODBCDirect Workspace objects, see Chapter 9, “Developing Client/Server Applications.”

A Workspace object exists for each active session of the Jet database engine. A session delineates a sequence of operations performed by Microsoft Jet. A session begins when a user logs on and ends when the user logs off. All operations during a session are subject to permissions determined by the logon user name and password. The Workspaces collection contains all Workspace objects defined by the currently running instance of DAO.

The ability to create multiple workspaces in code is useful when you have to log on to Microsoft Jet as another user. Also, because each Workspace object maintains its transactions independent of other workspaces, managing multiple workspaces can be useful when you have to manage multiple sets of independent transactions.

When you start DAO, what happens with workspaces depends on whether or not you have security established. If you don’t have security established, DAO automatically creates the default Workspace object. The settings of the Name and UserName properties of the default Workspace object are #Default Workspace# and Admin, respectively.

If you want to use security, you can set the SystemDB property to the current location of the workgroup information file (System.mdw), set the DefaultUserName property, and set the DefaultPassword property. When you use the OpenDatabase method without specifying a Workspace object, a default Workspace object is created by using these defaults.


Database Objects

The Databases collection contains all Database objects currently open within a Workspace object. In DAO, a Database object represents a currently open database. This can be a native Microsoft Jet database or an external database. You can have multiple databases open at one time, even databases of different types.

In Visual Basic, after you open a database and return a reference to it by using the OpenDatabase method, you can refer to the database according to its position in the Databases collection. If you open only one database, then you can refer to that database in any of the following ways:

DBEngine.Workspaces(0).Databases(0)
DBEngine.Workspaces(0)(0)
DBEngine(0)(0)

If you’re working in Microsoft Access 97, you can use either the CurrentDb function or the DBEngine(0)(0) syntax to return a reference to the database that is currently open in the Microsoft Access user interface. The CurrentDb function differs from the DBEngine(0)(0) syntax in that it creates another instance of the current database and returns a reference to that instance, and it refreshes all the collections in the current database. On the other hand, the DBEngine(0)(0) syntax doesn’t refresh any collections, so it may perform faster.

If you switch from using the CurrentDb function to the DBEngine(0)(0) syntax, make sure that your code doesn’t depend on any collections being refreshed. If it does, you can refresh these individual collections separately.

Note If you specify a value for the connect argument of the OpenDatabase method, you must also specify a value for the preceding read-only argument.

Microsoft Access and Visual Basic Users In Microsoft Access versions 1.x and 2.0, several databases are automatically opened when you open your database through the Microsoft Access user interface. These include the various library databases that Microsoft Access uses to perform its own functions. The other databases that Microsoft Access opens are used internally by Microsoft Access and are not part of the Databases collection. In Microsoft Access 97, library databases are identified by references to specific type libraries, but are not actually loaded until needed.

Microsoft Access also creates a default Workspace object when you open a database. The user account that’s currently logged on to the database is the account used to create the default workspace.

In Visual Basic, no databases are open in a workspace until you explicitly open them by using the OpenDatabase method — unless you use the Data control, which automatically opens the appropriate database for you when the form containing the Data control is loaded.


Container Objects

The DBEngine object is application independent. This means that the engine isn’t specifically tied to any one host application. However, Microsoft Jet does provide a generic collection and object type so that an application can create and store its own objects in a database. This generic object is known as a container. The Containers collection holds all Container objects for a Database object.

As an example, Microsoft Access relies on Microsoft Jet to store its application-specific objects such as forms, reports, macros, and modules. Microsoft Jet keeps track of these foreign objects through the Containers collection. It’s the Container object that enables Microsoft Jet to store application-specific objects without violating its application independence. You can use the Container object to set security to control access to all database objects of a particular type.


Document Objects

The Document object represents a specific object in the Documents collection. As noted in the preceding section, Microsoft Access uses the Containers collection to organize its own objects. The Document object stores a specific instance of an application-specific object. For example, when Microsoft Access creates a database, it creates a variety of Container objects, one each to store forms, reports, macros, and modules. As the Microsoft Access user creates forms, the forms are stored as individual Document objects in the forms Container object that is added to the database by Microsoft Access. You can set security to control access to a document.


QueryDef Objects

The QueryDef object represents a query in the database and maintains information about the query’s properties, including its SQL representation. The QueryDefs collection contains all QueryDef objects for a database.


Parameter Objects

With Microsoft Jet queries, you can define formal parameters. Formal parameters represent unknown values that must be supplied by the user running the query or the program executing the query. A query’s formal parameters are represented by Parameter objects in the Parameters collection of a QueryDef object. This collection is particularly useful when you execute a parameter query in code and have to supply values for the query parameters at run-time.

See Also For more information about formal and implicit parameters, see Chapter 4, “Queries.”


Recordset Objects

You use Recordset objects to manipulate data in a database at the record level. There are five types of Recordset objects: table-type Recordset objects, dynaset-type Recordset objects, snapshot-type Recordset objects, forward-only-type Recordset objects, and dynamic-type Recordset objects. For more information about the types of Recordset objects, see “Manipulating Data” later in this chapter.

The Recordset object is somewhat different from other DAO objects discussed so far in that it exists only while your code or application is running. A Recordset object represents, in memory, a temporary set of records from one or more tables. Recordset objects are one of the most powerful objects in DAO because you can use them to programmatically access not only native Microsoft Jet tables, but any tables from ISAM data sources such as Microsoft FoxPro, or ODBC data sources such as Microsoft SQL Server. In addition, Recordset objects can be based on queries that join multiple tables from heterogeneous data sources.

The Recordsets collection contains all Recordset objects open in the current Database object. Note that a Recordset object is an object that you explicitly open through code. For example, in Microsoft Access, when you are viewing a table through the user interface, this table isn’t a Recordset object.

See Also For more information about Recordset objects, see Chapter 5, “Working with Records and Fields.”


Relation Objects

You can use the Relation object to create new relationships and examine existing relationships in your database. A Relation object represents a relationship between fields in two or more tables. The Relations collection contains all Relation objects for a given Database object.


TableDef Objects

The TableDef object represents a table in a database. This includes tables in the current database, as well as linked tables (known as attached tables in previous versions of Microsoft Jet). It’s important to note that TableDef objects don’t represent data stored in the table — they represent the structure of the table. The TableDefs collection contains all TableDef objects for a database.


Index Objects

Index objects specify the order of records accessed from database tables and whether or not duplicate records are accepted, providing efficient access to data. For external databases, Index objects describe the indexes established for external tables. The Indexes collection contains all the stored Index objects for a TableDef object.

Microsoft Jet uses indexes when it joins tables and creates Recordset objects. Indexes determine the order in which table-type Recordset objects return records, but they don’t determine the order in which Microsoft Jet stores records in the base table or the order in which any other type of Recordset object returns records.


Field Objects

Field objects define a specific field (sometimes referred to as a column). TableDef, QueryDef, Index, Relation, and Recordset objects all contain Fields collections. The following table explains the Field object’s representation for each type of object that can contain a Fields collection.

Object | Description of Fields collection
TableDef | Defines the fields in a table, but doesn’t contain data.
QueryDef | Defines the fields in a query, but doesn’t contain data.
Index | Defines the characteristics of the fields that make up an index in the context of the information the index must maintain about the field.
Relation | Defines the characteristics of the fields that make up a relationship in the context of the information the relationship must maintain about the field.
Recordset | Defines the fields that exist in the tables or queries that the recordset is based on. Contains data in the field’s Value property.


Group Objects

Microsoft Jet provides security services you can use to protect objects in the database from unwanted access. The security model relies on users and groups defined in a workgroup information file. The Groups collection is used by Microsoft Jet in its security model to control access to objects. The Groups collection contains all Group objects. A Group object defines a group of users as it exists in the Workspace object’s workgroup information file. Each Group object also has a Users collection that contains a User object for each of the users in the group.


User Objects

The Users collection contains all User objects for a Workspace object. A User object defines a user account as it exists in the Workspace object’s workgroup information file. Each User object also has a Groups collection that contains a Group object for each group to which the user belongs.

See Also For more information about workgroup information files and security, see Chapter 10, “Managing Security.”


Property Objects

Every DAO object has a Properties collection, which contains all the properties associated with the object. A Property object contains information about the characteristics of a given property. You can read property values to get information about an object’s characteristics or write to a property to define an object’s characteristics. You can also create your own properties and add them to an object’s Properties collection.


DAO Basics

Now that you’re familiar with DAO objects and collections, it’s time to move on to the basics of using DAO. This section shows you how to work with DAO objects and collections, how to refer to objects, and how to represent objects by using object variables.


DAO Objects and Collections

As you’ve seen from the discussion of the DAO object hierarchy, the concept of objects and collections is central to an understanding of DAO. Most object types have collections that contain each of that type’s members. For example, the TableDefs collection represents a set of all tables in the database, and it contains all individual TableDef objects.


Guidelines for Naming DAO Objects

When you name your DAO objects, you must adhere to the following guidelines. Names of DAO objects:

  • Can be up to 64 characters long.

  • Can include any combination of letters, numbers, spaces, and special characters except a period (.), an exclamation point (!), an accent grave (`), and brackets ([ ]).

  • Can’t begin with leading spaces.

  • Can’t include control characters (ASCII values 0 through 31).

Although you can include spaces, they can produce naming conflicts in Visual Basic in some circumstances.

When you name an object, it’s a good idea to make sure the name doesn’t duplicate the name of a property or other element used by DAO; otherwise, your database can produce unexpected results.


Referring to Objects

When you refer to objects in code, specify the hierarchical path that points to the object to which you want to refer. In general, start with the DBEngine object and work your way through the object hierarchy, as shown in the following syntax:

DBEngine.ParentCollection.ChildCollection("ObjectName")

For example, in the following code, the dbs object variable represents the DBEngine object. The code sets the value of a Field object variable to the CompanyName field (a member of the Fields collection) in the Customers table (a member of the TableDefs collection):

Set fld = dbs.TableDefs("Customers").Fields("CompanyName")

See Also For more information about using object variables to refer to objects, see “Using Object Variables” later in this chapter.

There are three ways to refer to an object in a collection:

  • Collection("name")

  • Collection(expression) where expression is a string variable containing the name of the object

  • Collection(index) where index is the object’s position within the collection

The first way to refer to an object is to use the Collection("name") syntax. In the following example, strDbPath is the path to the NorthwindTables database, which contains the Customers table:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
' Return reference to Customers table.
Set tdf = dbs.TableDefs("Customers")

The second way to refer to an object is to use the Collection(expression) syntax. You can use this technique to refer to an object by using a variable. In this example, strDbPath is the path to the database, and strTableName is the name of a table in that database:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
' Return reference to table specified by value of variable.
Set tdf = dbs.TableDefs(strTableName)

The value of strDbPath in the preceding example may be "C:\JetBook\Samples\NorthwindTables.mdb", and the value of strTableName may be "Customers".

By using the Collection(index) syntax, you can refer to an object according to its position in a collection. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
' Return reference to first TableDef in TableDefs collection.
Set tdf = dbs.TableDefs(0)

Indexes on DAO collections are always zero-based. This means that the first element in the collection has an index number of zero. This is important to note if you’re used to working with one-based indexes, where the first element has an index number of one.

It’s important to note that an object’s position within a collection isn’t fixed. It can change as objects are added to and removed from the collection. You shouldn’t refer to objects according to their position within a collection except when referring to all members within a loop.

Using the ! and . (Dot) Operator Syntax

In DAO syntax, you separate parts of your object references by using either the ! operator or the . (dot) operator. When to use the ! operator and when to use the . (dot) operator depends on the context of its use. In general:

  • Use the . (dot) operator when what follows is an item created by Microsoft Jet.

  • Use the ! operator when what follows is a user-defined item.

For example, to refer to the Name property of a TableDef object (a property created by Microsoft Jet), use the . (dot) operator, as show in the following line of code:

dbs.TableDefs("Customers").Name

However, to refer to a field in a TableDef object (an item that you create), use the ! operator, as follows:

dbs.TableDefs("Customers")!CustomerID


Using Default Collections to Write Shorter Code

Almost all of the DAO objects have default collections. With these default collections, you can use a shorter reference to objects in code. By using the full syntax, you can return a reference to a TableDef object and a Field object, as shown in the following code. In this example, strDbPath is the path to the database, strTableName is the name of the table, and strFieldName is the name of the field:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs(strTableName)
Set fld = tdf.Fields(strFieldName)

Because the TableDefs collection is the default collection of the Database object, and the Fields collection is the default collection of the TableDef object, you can shorten this code to:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs(strTableName)
Set fld = tdf(strFieldName)

Note Although using default collections produces shorter code that may be more efficient, it also reduces the readability of the code. It’s always advisable to use comments in your code to provide information about what your code is doing.

The following table lists the default collections available in DAO.

Object | Default collection
DBEngine | Workspaces
Workspace | Databases
Database | TableDefs
Container | Documents
QueryDef | Parameters
Recordset | Fields
Relation | Fields
TableDef | Fields
Index | Fields
Group | Users
User | Groups


Using Object Variables

The preceding sections have shown how to return a reference to an object in order to work with that object in Visual Basic. It’s possible to use an object reference throughout your code each time you need to refer to a particular object. However, your code runs more quickly if you declare an object variable to represent the object instead. An object variable is a variable that represents an object in Visual Basic. Object variables differ from regular variables in that they have no intrinsic value. They point to an object, not a value. In other words, an object variable points to an object’s representation in memory. All object variables that are assigned to an object point to the same object.

There are several advantages to using object variables. By using object variables to refer to objects in your code, you can:

  • Avoid typing and maintaining long lines of code that refer to objects. By using object variables, you can set a reference to an object once, and then continually refer to the object variable instead of making repeated explicit references to an object through the DAO object hierarchy.

  • Write code that is easier to read and maintain.

  • Improve the performance of your application. When you use object variables, DAO doesn’t need to repeatedly parse and resolve recurrent long object references. Because DAO has to resolve the reference to the object only once, your application should run marginally faster.

Object variables are a special type of variable supported by Visual Basic. Each object type in DAO has a corresponding object variable type. For example, the Workspace object has a corresponding workspace object variable, the Database object has a corresponding database object variable, and so on.

To use an object variable, first declare the variable with the Dim statement and then use the Set statement to assign an object to it. The syntax is:

Dim ObjectVariable As ObjectType

Set ObjectVariable=SomeDataAccessObject

For example, the following code, which prints the names of all the fields in a table, uses object variables to refer to Database, TableDef, and Field objects. In this example, strDbPath is the path to the database and strTableName is the name of the table:

Dim dbs As Database
Dim tdf As TableDef
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs(strTableName)
For Each fld In tdf.Fields
    Debug.Print fld.Name
Next fld


Closing Object Variables

Four of the DAO objects are considered temporary objects. That is, they exist only in memory and are never permanently saved as part of the database file. These are the Workspace, Database, Connection, and Recordset objects. QueryDef objects can be either permanent or temporary.

Note QueryDef objects are not typically temporary objects. As mentioned earlier, a QueryDef object represents a saved query in the database. However, unlike other permanent objects, if a QueryDef object is created by using DAO, the QueryDef object can be used without first appending it to the QueryDefs collection.

Another slight exception is the Database object. Although it’s a temporary object, it does refer to the database file on disk, which is permanent.

Because these objects exist in memory, you should close them explicitly when your code no longer needs them. To close an object explicitly, use the Close method and then set the object variable to Nothing.

Closing Workspace Objects

Prior to Microsoft Jet version 2.0, you couldn’t close a Workspace object that contained any open Database objects. In Microsoft Jet 3.0 and 3.5, if you close a Workspace object, Microsoft Jet closes all the Workspace object’s Database objects and rolls back any pending transactions.

Microsoft Access Users In Microsoft Access, a default workspace, Workspaces(0), is automatically created when you open a database through the user interface. Microsoft Access ignores attempts to close this default Workspace.

Closing Database Objects

If you open a database with the OpenDatabase method, you should close it explicitly when you are done using it. To close a database explicitly, first use the Close method to close the database, then set the Database object variable to Nothing.

In DAO 3.0 and 3.5, if you explicitly close a Database object that has an open Recordset object with pending edits or uncommitted transactions, the edits are canceled, the transactions are rolled back, and the Recordset object is closed. If you implicitly close the same Database object (by exiting a procedure that has the Database object as a local object variable), the Recordset object stays open.

Note This behavior is different from the behavior of DAO version 2.x. In DAO 2.x, if you explicitly close a Database object that has an open Recordset object, an error occurs. If you implicitly close the same Database object, the Recordset object is closed as well. DAO 2.x had Dynaset objects (replaced by dynaset-type Recordset objects in DAO 3.0). If you explicitly or implicitly close a Database object that has an open Dynaset object, the Dynaset object stays open.

Closing Recordset Objects

You should always close a Recordset object after you finish using it. Close the Recordset object by using its Close method. Then set the Recordset object variable equal to Nothing.


Determining the Boundaries of Collections

To work effectively with collections, it’s important to understand how collections are ordered and how to determine their boundaries. To determine the boundaries of a collection, use the Count property of the collection. The value of the Count property reflects the number of objects in the collection. For example, if a collection’s Count property returns 5, then there are five objects in that collection.

However, remember that DAO collections are zero-based. This means that members of DAO collections are indexed starting with 0, with the first object in the collection having an index number of 0. Therefore, when your code compares the value of the Count property to the current index position of the collection, it must subtract one. For example, the following code illustrates how to use the Count property to determine the number of TableDef objects in a database and to print the name of each table. In this code, strDbPath is the path to the database:

Dim dbs As Database
Dim intX As Integer

Set dbs = OpenDatabase(strDbPath)
For intX = 0 To dbs.TableDefs.Count - 1
    Debug.Print dbs.TableDefs(intX).Name
Next intX

You can see how this example subtracts one from the Count property to make sure the comparison works.

You can simplify your code by using the For Each…Next statement instead of a For...Next statement. The following code accomplishes the same task, but without requiring you to handle collection boundaries. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
For Each tdf In dbs.TableDefs
    Debug.Print tdf.Name
Next tdf

The first example above shows how you can refer to an object by using its location within a collection. When you add or delete objects from a collection, the Count property changes and no longer provides a reliable means of specifying an object’s location in a collection. Therefore, you should not use the Count property to determine the boundaries of a collection with code that loops through and deletes objects.


DAO Properties

All DAO objects have properties that define their characteristics. You can retrieve the values of properties to see the characteristics of an object, and you can change the values of properties to change the characteristics of an object.

Properties are represented by Property objects. Property objects themselves contain properties, as described in the following table.

Property | Data type | Description
Name | Text | Name of the property
Value | Variant | Value of the property
Type | Integer | Data type of the property
Inherited | Boolean | Identifies the property as inherited from another object. For example, a recordset Field property inherited from a field in a TableDef object.


Types of Properties

Microsoft Jet identifies two types of properties. Built-in properties are those automatically created and maintained by DAO. For example, when you create a TableDef object, DAO automatically creates a set of properties for the table, including the Name property and the Connect property.

User-defined properties are those properties you create and add to an object. For example, you may want each field in your table to store a description that explains what the field is for. By adding a user-defined property called Description to the field object’s Properties collection, you can store and retrieve this information.

Replication Properties

When you create a replicated database, Microsoft Jet automatically adds new properties to tables, indexes, and application-specific objects in the Containers collection. These properties are used by Microsoft Jet to determine whether or not you can replicate objects.

See Also For more information about replicating databases, see Chapter 7, “Database Replication.”


The Properties Collection

Each DAO object has an associated Properties collection. This collection contains the Property objects that identify each characteristic of the object. Like other collections, you can iterate through the collection and refer to specific elements. You refer to Property objects and Properties collections by using the same syntax as you do for other DAO collections:

  • Properties("PropertyName")

  • Properties(expression) where expression is a string expression or variable that is the name of a property

  • Properties(index) where index is the property’s position within the collection

For example, to refer to the value of the Type property of the CustomerID field in the Customers table, you could use any of the following lines of code, where dbs is a Database object:

dbs.TableDefs("Customers").Fields("CustomerID").Properties("Type").Value
dbs.TableDefs("Customers").Fields("CustomerID").Properties(strType).Value
dbs.TableDefs("Customers").Fields("CustomerID").Properties(3).Value

In the second line of code, a string variable, strType, is set to the value of "Type".

In the last line, the code assumes that the Type property is always going to be in the fourth position in the Properties collection. As mentioned earlier in the “Objects and Collections” section, you should not rely on an object’s position within its collection. The only time you would refer to a property by its index number is when you are iterating through all the Properties in a collection.


Read-Only Properties

When you create a DAO object in code, you set its properties to define its characteristics. You then append the object to its collection to make it a permanent part of the database. However, some properties become read-only after the property’s parent is appended. For example, after you append a new TableDef object and its Field objects to its collection, the Type property becomes read-only. To change these properties, you must delete the object and re-create it.


Setting Bitwise Properties

Some properties store bitwise values. To compare bitwise values you use the bitwise comparison operators: And, Eqv, Not, Or, Xor, and Imp. A bitwise comparison is a bit-by-bit comparison of the individual bits of a value, rather than the value as a whole. To correctly store and retrieve values for these properties, you must use bitwise operators. For example, the Attributes property of a TableDef object defines certain characteristics such as how the table is linked. To check this property for a TableDef object, you may use the following line of code, where tdf represents the TableDef object.

If (tdf.Attributes And dbAttachedTable) = dbAttachedTable Then
    ' Perform some action on TableDef object.
    .
    .
    .
End If

If the result is a nonzero value, then the attribute dbAttachedTable is set for the table. If the result is zero, then that particular attribute isn’t set.

The previous example uses the And operator to retrieve bitwise values. You can use the Or operator to set bitwise values, and the And and the Not operators together to clear them.

Microsoft Access Users When you create an object through DAO by using Microsoft Jet, only the standard built-in properties are created in the new object’s Properties collection. However, when Microsoft Access creates a Microsoft Jet object, it may add several user-defined properties to objects. These properties are a special case of user-defined properties known as application-defined properties. For example, when you create a table in the Microsoft Access user interface, and type a value in the Description field, it automatically adds a new property to the TableDef object to represent the description.

Microsoft Access doesn’t automatically add these application-defined properties to new objects. For example, when you create a new table through the user interface in Microsoft Access, the Description and Format properties are not created unless you type a value in the Description and Format fields. This can cause a problem because when you try to read the value of these properties on a table where they have not been specified, a run-time error occurs. The technique for avoiding this problem, and for writing more robust property-retrieval code in general, is to trap for errors when looking at Property objects. For more information, see “Retrieving Properties” later in this chapter.

You can add properties not created by Microsoft Access through the user interface, by using the user interface, or by creating the property in code and appending it to the appropriate object’s Properties collection. For example, if you want to include a Description property in tables you create from code, you must add it to the Properties collection by using the CreateProperty method of the TableDef object.


DAO Methods

Methods are procedures that act on objects. Most DAO objects have associated methods you can use to interact with the object in useful ways. Methods are similar to procedures that you create in your code, with the important distinction that methods are explicitly tied to an object, whereas your procedures can generally be called independent of a specific object.

Like a procedure, a method can return a value or return nothing. If the method returns a value, use the equal sign (=), as shown in the following syntax:

variable=object.method [arglist]

If the method doesn’t return a value, omit the variable, equal sign (=), and the parentheses surrounding the argument list, as follows:

object.method arglist


Using DAO

So far, this chapter has discussed DAO in general terms, describing its syntax and architecture. In this section, those ideas are put to work.

Note that this section introduces the creation and manipulation of objects. It doesn’t fully cover all of the DAO operations you may want to perform. For complete information, see the chapters listed in the following table.

For information about | See
Using data definition objects (databases, tables, fields, and so on) | Chapter 3, “Data Definition and Integrity”
Recordset objects that represent actual data | Chapter 5, “Working with Records and Fields”
Setting multiuser properties | Chapter 6, “Creating Multiuser Applications”
Using ODBCDirect workspaces | Chapter 9, “Developing Client/Server Applications”
Using groups, users, and permissions to control access to your application and its data | Chapter 10, “Managing Security”


Creating Database Objects

The first operation you are likely to perform using DAO involves data definition: creating objects, assigning characteristics to objects by using properties, and interrogating the structure of existing objects.

Creating a new object is a two-step process. First you create the object and define its characteristics, and then you append the object to its collection. The process of appending an object to a collection makes it a permanent or persistent part of the database. Additionally, if the object you’re creating contains child objects, you first create the parent object, then create and append the child object, and finally, append the parent object to its collection.

Note Unlike other DAO objects, Workspace objects can be created and then appended to the Workspaces collection, but they don’t become a permanent part of the database. When you exit the application, all Workspace objects are not saved. It’s important to remember that a Workspace object is only a “space” used to identify a specific user and transaction sequence. Recordset objects work in the same way.

Also, when you create a new Database object by using the CreateDatabase method, the new database automatically becomes a persistent object on the drive where it’s created. You don’t have to explicitly append it to the Databases collection to make it persistent.

You can also create linked tables, which are tables stored in files outside the current database. Because Microsoft Jet supports a variety of external data sources, you are not limited to linking to only Microsoft Jet databases.

See Also For more information, see Chapter 8, “Accessing External Data.”


Creating Your Own Properties

Earlier, this chapter introduced user-defined properties, which you can use to store and retrieve your own information about an object. DAO is extensible by using these properties. The following function tries to set a property on an object. If the property doesn’t exist in that object’s Properties collection, then the function creates the property and appends it to the collection:

Function SetCustomProperty(obj As Object, strName As String, _
        intType As Integer, varSetting As Variant) As Boolean
    ' This procedure attempts to set a property on an object.
    ' If the property does not exist in the object's Properties collection,
    ' then the procedure creates the object.
    Dim prp As Property
    Const conPropNotFound As Integer = 3270

    On Error GoTo ErrorSetCustomProperty
    ' Explicitly refer to Properties collection.
    obj.Properties(strName) = varSetting
    obj.Properties.Refresh
    SetCustomProperty = True

ExitSetCustomProperty:
    Exit Function

ErrorSetCustomProperty:
    If Err = conPropNotFound Then

        ' Create property, denote type, and set initial value.
        Set prp = obj.CreateProperty(strName, intType, varSetting)
        ' Append Property object to Properties collection.
        obj.Properties.Append prp
        obj.Properties.Refresh
        SetCustomProperty = True
        Resume ExitSetCustomProperty
    Else
        MsgBox Err & ": " & vbCrLf & Err.Description
        SetCustomProperty = False
        Resume ExitSetCustomProperty
    End If
End Function

For example, to create a property called DateLastModified on each table in a database, you may call this function from the following code, which creates the property on each table that isn’t a system object. In this example, strDbPath is the path to the database:

Dim dbs As Database, tdf As TableDef
' Define constant to specify system object.
Const conSystemObject As Long = -2147483648

Set dbs = OpenDatabase(strDbPath)
For Each tdf In dbs.TableDefs
    ' Check to see whether table is system object or hidden.
    If Not (tdf.Attributes And conSystemObject) = conSystemObject Then
        If Not (tdf.Attributes And dbHiddenObject) = dbHiddenObject Then
            ' Set property.
            SetCustomProperty tdf, "DateLastModified", dbDate, Now
        End If
    End If
Next tdf

Of course, simply adding a property doesn’t automatically update and maintain it. In the previous example, the DateLastModified property is added to the TableDef object, but that’s the extent of what the code does. Your application must programmatically update the value of this property when appropriate.


Adding Groups and Users

Microsoft Jet implements security through users, groups, and permissions settings on specific objects. You can use DAO to create users and groups in code, and to assign permissions to objects.

See Also For more information, see Chapter 10, “Managing Security.”


Using the Error Object

The Errors collection contains Error objects that represent the Microsoft Jet errors that have occurred. You can use these objects to determine if errors have occurred, and then to work around them. The following code attempts to open a database that doesn’t exist, and then displays the errors that result:

Dim dbs As Database
Dim errX As Error

' Disable error handling.
On Error Resume Next

' Try to open a nonexistent database.
Set dbs = OpenDatabase("XYZ123.456")

' Look at the errors generated.
For Each errX In DBEngine.Errors
    Debug.Print errX.Description
    Debug.Print errX.Number
    Debug.Print errX.Source
Next errX

The Error object is unlike the error variables and functions in Visual Basic in that more than one error can be generated by a single operation. Also, objects in the Errors collection are appended in a manner different from the other DAO collections. The most detailed errors are placed at the end of the collection, and the most general errors are placed at the beginning.

The set of Error objects in the Errors collection describes one error. The first Error object is the lowest-level error, the second the next-highest level, and so on. For example, if an ODBC error occurs while trying to open a Recordset object, the last Error object contains the DAO error indicating that the object can’t be opened. The first Error object contains the lowest-level ODBC error. Subsequent errors contain the ODBC errors returned by the various layers of ODBC.


Interrogating Existing Objects in Your Database

In addition to creating objects, you can look at existing objects and determine their structures. When you retrieve structures, you iterate (loop) through collections. For example, you can iterate through the TableDefs collection to find all the tables, and then iterate through a specific TableDef object’s Fields collection to find information about the table’s fields.

Refreshing Collections

As objects are appended to and deleted from collections, DAO usually keeps track of the collection. However, there are situations when DAO doesn’t know the current state of a collection. This occurs when:

  • Users are modifying a shared database’s object in a multiuser environment.

  • SQL statements alter structures (for example, “DROP TABLE Customers”).

  • Changes are made through a host program’s user interface. Note that this isn’t the case for applications that use DAO directly, such as Visual Basic and Microsoft Excel.

If you want to make sure that you’re looking at the most up-to-date version of a collection, refresh the collection. This is done by using the Refresh method of the collection. The syntax for the Refresh method is:

Collection.Refresh

Collection is the name of the collection you want to refresh. For example, if your code needs a complete list of TableDef objects in the database, you may want to refresh the collection before using it. You can accomplish this with the following line of code, where dbs is a pointer to the database:

dbs.TableDefs.Refresh

Use the Refresh method only when necessary because it may take time to refill the collection.


Listing the Objects in a Database

To get a list of the objects in a database, you iterate through each Collection object. The following example iterates through each TableDef, QueryDef, Relation, and Container object in a database, and prints the object name to the Debug window. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef, qry As QueryDef
Dim rel As Relation, ctr As Container

Set dbs = OpenDatabase(strDbPath)

Debug.Print "Tables: "
For Each tdf In dbs.TableDefs
    Debug.Print tdf.Name
Next tdf

Debug.Print "Querys: "
For Each qry In dbs.QueryDefs
    Debug.Print qry.Name
Next qry

Debug.Print "Relations: "
For Each rel In dbs.Relations
    Debug.Print rel.Name
Next rel

Debug.Print "Containers: "
For Each ctr In dbs.Containers
    Debug.Print ctr.Name
Next ctr


Retrieving Properties

To identify the properties associated with an object, you write code to iterate through the object’s Properties collection, retrieving the information you want. Some special cases when referencing properties may be problematic:

  • Some properties are not readable. The Password property of the User object is a good example. You can set the value of the property through code, but you can’t retrieve the value. (If you could, it would be an obvious hole in the Microsoft Jet security model.)

  • Some properties are readable at some times, but not at others. For example, you can’t read the Value property of a Field object in a recordset if there is no current record in the recordset.

  • Certain properties that you are explicitly looking for may not exist. For example, if you’re using Microsoft Access, you may have code that retrieves the Description property of a TableDef object. However, unless you explicitly add a description to the table through the Microsoft Access user interface or through DAO code, this user-defined property doesn’t exist, and a run-time error occurs when the code executes.

The following code displays each of the properties of each User object in the current Workspace. After it has tried to retrieve the property value, it checks to see if an error has occurred, and if so, checks the specific error number. If the error is anticipated, as in the case of trying to retrieve a write-only property (error 3251), error text is printed. If the error isn’t anticipated, the code displays a message to the user.

Dim usr As User
Dim prp As Property
Dim varValue As Variant

For Each usr In Workspaces(0).Users
    For Each prp In usr.Properties
        ' Disable error handling.
        On Error Resume Next
        varValue = prp.Value
        ' See if an error occurred.
        Select Case Err
            Case 3251				' Operation not supported error.
                Debug.Print prp.Name & ": <Error " & Err _
                    & ": " & Err.Description & ">"
            Case 0					' No error.
                Debug.Print prp.Name & ": " & varValue
            Case Else				' Unanticipated error.
                MsgBox "Error: " & Err & vbCrLf & Err.Description
        End Select
        Err.Clear
        ' Re-enable error handling.
        On Error GoTo 0
    Next prp
    Debug.Print
Next usr


Getting a Table Structure

The concepts demonstrated in the previous two examples are included in the following example, which prints the properties of a TableDef object, the names of its fields, and the properties of each Field object and their values. The first procedure iterates through the Properties collection for the TableDef object and each Field object, and then calls the GetProperty function for each Property object in each collection. The GetProperty function returns a string containing the property’s name and value. If reading the property’s Value property returns an error, then the error message is included in the string:

Sub GetTableInfo(strDbName As String, strTableName As String)
    Dim dbs As Database
    Dim tdf As TableDef, fld As Field, prp As Property

    ' Open database.
    Set dbs = OpenDatabase(strDbName)
    Set tdf = dbs.TableDefs(strTableName)
    With tdf
        ' Print table properties.
        Debug.Print "Properties for table '" & strTableName & "':"
        For Each prp In tdf.Properties
            Debug.Print GetProperty(prp)
        Next prp
        Debug.Print

        ' Print fields and field properties.
        Debug.Print "Fields and field properties for table '" & _
            strTableName & "':"
        For Each fld In .Fields
            Debug.Print "Field: " & fld.Name
            For Each prp In fld.Properties
                Debug.Print "    " & GetProperty(prp)
            Next prp
            Debug.Print
        Next fld
    End With
End Sub

Function GetProperty(prp As Property) As String
    ' This procedure checks whether a property has a value.
    ' It returns either the value or an appropriate
    ' error string.
    Dim varValue As Variant

    ' Disable error handling.
    On Error Resume Next
    ' Attempt to retrieve the property value.
    varValue = prp.Value
    ' Check for an error.
    If Err Then
        varValue = "<Error: " & Err.Description & ">"
    End If
    ' Re-enable error handling.
    On Error GoTo 0
    ' Return string.
    GetProperty = "Property: " & prp.Name & vbTab & vbTab & "Value: " & varValue
End Function


Creating Temporary Queries with DAO

You can create temporary QueryDef objects that you can use in code. You create a temporary QueryDef object in your code by using a zero-length string ("") in place of the query name. You can then execute the QueryDef object and close it. Temporary QueryDef objects are well-suited to creating and running update, parameterized, and pass-through queries.

The following example shows how you may use a temporary QueryDef object to execute an action query that updates data as defined by an SQL statement passed to the procedure in the strSQL variable. In this example, strDbPath is the path to the database, and strSQL is an SQL statement for an update query:

Dim dbs As Database
Dim qdf As QueryDef

Set dbs = OpenDatabase(strDbPath)
Set qdf = dbs.CreateQueryDef("", strSQL)
qdf.Execute
qdf.Close

For example, you may supply the following values for strDbPath and strSQL:

strDbPath = "C:\JetBook\Samples\NorthwindTables.mdb"
strSQL = "UPDATE Products INNER JOIN Suppliers " & _
    "ON Products.SupplierID = Suppliers.SupplierID " & _
    "SET Products.UnitPrice = [UnitPrice]*1.1 " & _
    "WHERE Suppliers.CompanyName=""Exotic Liquids"";

This update query increases the unit price of all Exotic Liquids products in the Products table by 10 percent. After you run the update query, close the temporary QueryDef object because it’s no longer needed.

Note that if the SQL statement specified a select query rather than an update query, you need to open a Recordset object on the QueryDef object in order to view the result set. For example, suppose you specified a value for strSQL as follows:

strSQL = "SELECT ProductName, UnitPrice FROM Products " & _
    "WHERE UnitPrice >= 40 ORDER BY ProductName;"

Rather than executing the temporary QueryDef object, you can open a Recordset object on it, then close the temporary QueryDef object and manipulate the Recordset object, as follows:

Dim dbs As Database
Dim qdf As QueryDef
Dim rst As Recordset, fld As Field

Set dbs = OpenDatabase(strDbPath)
Set qdf = dbs.CreateQueryDef("", strSQL)
Set rst = qdf.OpenRecordset
qdf.Close
Do Until rst.EOF
    For Each fld In rst.Fields
        Debug.Print fld.Value
    Next fld
    rst.MoveNext
Loop


Modifying Existing Database Objects

You can use DAO to modify the structure of existing objects. However, there are things that you can’t programmatically alter by using DAO. For example, you can’t use DAO to change the Type property of a Field object in a table. Because this would change the data type of the field and require a conversion of existing data in the table, you must delete the Field object and then re-create it, specifying the new data type.

See Also For complete information about modifying database objects, see Chapter 3, “Data Definition and Integrity.”


Manipulating Data

You can use DAO to manipulate data in your database. In DAO, you use the Recordset object to interact with data stored in the database. Recordset objects are extremely powerful because they provide a great deal of flexibility in working with data.

Recordset objects are different from most other DAO objects in that they exist only as temporary objects. Even though the method you use to create Recordset objects is called OpenRecordset, you don’t actually open existing Recordset objects; you create new ones. After you close a Recordset object, it no longer exists.

To create a Recordset, you use the OpenRecordset method based on a Database object. The syntax for the OpenRecordset method is:

Set recordset = object.OpenRecordset(source[, type[, options[, lockedits]]])

In this syntax, recordset is an object variable that represents the Recordset object you want to open. Object is an object variable that represents an existing object from which you want to create the new Recordset object. The source argument is the source of the data, such as a table name, a query name, or an SQL statement that returns records. You can open Recordset objects:

  • Directly on tables in the database.

  • On tables in other databases.

  • On tables linked to external data sources such as ODBC or ISAM.

  • On queries.

  • Based on SQL strings that you dynamically create in code.

  • On other Recordset objects.

The type argument is a numeric constant denoting the type of Recordset to open. There are five types of Recordset objects:

  • Table-type Recordset object. A representation in code of a table in the current database. You can use table-type Recordset objects to retrieve, add, change, or delete records.

  • Dynaset-type Recordset object. The result of a query that can have updatable records. A dynaset is a dynamic set of records that you can use to add, change, or delete records from an underlying database table or tables. A dynaset can contain fields from one or more tables in a database.

  • Snapshot-type Recordset object. A static copy of a set of records that you can use to find data or generate reports. A snapshot can contain fields from one or more tables in a database, but it can’t be updated.

  • Forward-only-type Recordset object. This type of Recordset object is identical to a snapshot-type Recordset object, except that you can only scroll forward through its records. This improves performance in situations where you only need to make a single pass through a result set.

    Note DAO 3.5 introduced the dbOpenForwardOnly constant as a new value for the type argument of the OpenRecordset method. This Recordset object behaves the same as a DAO 3.0 snapshot-type Recordset object opened with the dbForwardOnly option.

  • Dynamic-type Recordset object. This type of Recordset object represents a query result set from one or more base tables in which you can add, change, or delete records from a row-returning query. Further, records that users add, delete, or edit in the base tables also appear in your Recordset object. This Recordset object is similar to an ODBC dynamic cursor and is available in ODBCDirect workspaces only.

You can set the options argument to a combination of constants that specify characteristics of the new Recordset object. For example, with the options argument, you can specify:

  • That users can’t modify or add records in the table or tables you are accessing.

  • That users can’t view records (table-type Recordset only).

  • That users can append new records, but can’t edit or delete existing records (dynaset-type Recordset only).

  • That inconsistent updates are allowed (dynaset-type Recordset only).

  • That only consistent updates are allowed (dynaset-type Recordset only).

Note When you don’t use the options argument, you must specify 0 for it.

The lockedits argument determines the locking for the new Recordset object. For example, you can:

  • Prevent users from making changes to the Recordset object (default setting for ODBCDirect workspaces).

  • Use pessimistic locking to determine how changes are made to the Recordset object in a multiuser environment. The page containing the record you’re editing is locked as soon as you use the Edit method (default for Microsoft Jet workspaces).

  • Use optimistic locking to determine how changes are made to the Recordset object in a multiuser environment. The page containing the record you’re editing isn’t locked until the Update method executes.


Using Private DBEngine Objects

One advantage that Microsoft Visual C++ and Visual Basic developers have over Microsoft Access developers is that when their program starts, the DBEngine object isn’t yet initialized. This allows the program to set important properties such as the SystemDB property (which sets or returns the path for the current location of the workgroup information file), and the DefaultUser and DefaultPassword properties (which set the user name and password to use for default logon). In Microsoft Access, however, the DBEngine object is already initialized when you start, and none of these properties can be changed. Even in Visual Basic, you may want to open a database that uses a different workgroup information file, without having to start a new application through Automation to do so.

There is a new object called a PrivDBEngine object, which is a new, uninitialized copy of the top-level DBEngine object, with its own objects and collections beneath it. You can use the PrivDBEngine object to open a database that uses a different workgroup information file, to determine whether a database is opened exclusively, and to determine whether you can open a database exclusively.

To create a private DBEngine object, declare an object variable as type PrivDBEngine, and then use the Set statement with the New keyword, as shown in the following code:

Dim dbe As PrivDBEngine

Set dbe = New PrivDBEngine


Using a Different Workgroup Information File

As an example, you may have some data in a secured copy of the Northwind database that uses a different workgroup information file than your user may be using by default. To open this database, you can use code such as the following fragment:

Dim dbe As PrivDBEngine
Dim wrk As Workspace
Dim dbs As Database

Set dbe = New PrivDBEngine
dbe.SystemDB = "C:\MyApp\MyWorkgroupFile.mdw"
dbe.DefaultUser = "SecureUser"
Set wrk = dbe.Workspaces(0)
Set dbs = wrk.OpenDatabase("C:\Samples\Secured Northwind.mdb")

Without the PrivDBEngine object, you would have to start another instance of your application or Microsoft Access in order to access a database that uses another workgroup information file. Creating a PrivDBEngine object is significantly faster.


Determining Whether the Current Database Is Open Exclusively

Determining whether the current database is open exclusively isn’t simply a matter of trying to open the database. If you have the database open exclusively and try to open it again, you won’t receive a run-time error as you may expect. Actually, you can open a database as many times as you want to, exclusively or not, with the same DBEngine object.

To determine whether the current database is open exclusively, you can try to open the database by using the PrivDBEngine object, as shown in the following code:

Function IsDbOpenedExclusively(strDbPath As String) As Boolean
    On Error Resume Next
    Dim dbe As PrivDBEngine
    Dim wrk As Workspace
    Dim dbs As Database
    Const conFileInUse = 3045
    Const conDBOpenedExclusively = 3356

    ' Return reference to private DBEngine object.
    Set dbe = New PrivDBEngine
    ' Return reference to default workspace.
    Set wrk = dbe.Workspaces(0)
    ' Attempt to open database.
    Set dbs = wrk.OpenDatabase(strDbPath)
    ' If reference to database isn't returned, check error.
    If dbs Is Nothing Then
        ' If error indicates database is open exclusively, return True.
        If (Err = conFileInUse Or Err = conDBOpenedExclusively) Then
            IsDbOpenedExclusively = True
        ' If unanticipated error occurs, display message.
        Else
            MsgBox "Error: " & Err & ": " & vbCrLf & Err.Description
        End If
    ' If reference to database is returned, it must not be opened
    ' exclusively by any other user.
    Else
        IsDbOpenedExclusively = False
        dbs.Close
        Set dbs = Nothing
    End If
End Function

If this function generates error 3045 or error 3356, then it returns True because these errors indicate that the database is already opened exclusively.


Determining Whether You Can Open a Database Exclusively

You may also want to determine whether your own code is able to open a database exclusively. This information can be helpful with many operations, such as:

  • Repairing a database.

  • Compacting a database.

  • Saving your Microsoft Access 97 database as an MDE file.

  • Converting a database into a Design Master replica.

  • Converting or enabling a database from a previous version.

In addition, the user that is currently logged on may not have permission to open the database exclusively.

See Also For more information about Design Master replicas, see Chapter 7, “Database Replication.” For more information about user permissions, see Chapter 10, “Managing Security.”

To determine whether you can open a database exclusively, use the PrivDBEngine object in code that is similar to the IsDbOpenedExclusively function shown in the preceding example:

Function CanOpenDbExclusively(strDbPath As String) As Boolean
    On Error Resume Next
    Dim dbe As PrivDBEngine
    Dim wrk As Workspace
    Dim dbs As Database
    Const conFileInUse = 3045
    Const conDBOpenedExclusively = 3356

    Set dbe = New PrivDBEngine
    Set wrk = dbe.Workspaces(0)
    Set dbs = wrk.OpenDatabase(strDbPath, True)
    If dbs Is Nothing Then
        ' If error is unanticipated, display message.
        If (Err = conFileInUse Or Err = conDBOpenedExclusively) Then
            ' If database cannot be opened exclusively, return False.
            CanOpenDbExclusively = False
        Else
            MsgBox "Error: " & Err & ": " & vbCrLf & Err.Description
        End If
    Else
        ' If database can be opened exclusively, return True.
        CanOpenDbExclusively = True
        dbs.Close
    End If
End Function

This function returns True if you can open the database exclusively.


Features New to DAO 3.0 and 3.5

This section outlines the functionality new to DAO in 3.0 and 3.5.

See Also For more information about new features, search the DAO Help index for “DAO, new features” or for the name of the language element you want information about.


Accessing DAO

DAO is a component that supports Automation. Any licensed program that can call components that support Automation can access DAO functionality. Unlike regular dynamic-link libraries (DLLs) that expose calls but no details about them, components that support Automation contain a type library that describes every object, method, property, and so forth, in the DLL. Other components that support Automation, such as Microsoft Access, Visual Basic, Microsoft Excel, Microsoft Word, or Microsoft PowerPoint can read the DAO type library and make the appropriate calls to the DLL.

In DAO 3.5, there is a new client/server connection mode called ODBCDirect. You can use the ODBCDirect technology to establish a direct connection to an ODBC data source without having to load Microsoft Jet in memory. ODBCDirect is useful in situations where the specific features of ODBC are required. For more information about ODBCDirect see, Chapter 9, “Developing Client/Server Applications.”

Microsoft Access, which relies on Microsoft Jet for its database functionality, has calls to Microsoft Jet written into its internal code, and doesn’t call the DAO DLL except when Visual Basic code in a Microsoft Access database calls DAO. In other words, when you display a table through the Microsoft Access user interface, Microsoft Access calls Microsoft Jet directly through hard-coded routines. When you open a table by using Visual Basic code, Microsoft Access hands the request to the DAO component.


DBEngine Object

The IniPath property is set to the path to the Windows registry because .ini files are no longer used by Microsoft Jet. Use the SystemDB property of the Database object to point to the workgroup information file you want to use. The SystemDB property sets or returns the path for the current location of the workgroup information file. DAO 3.5 added the SetOption method to the DBEngine object, which you can use to override registry values during the current instance of DAO.


Workspace Object

You can assign a password to a database. You can specify the password to open a password-protected database in a workspace by using the password argument of the CreateWorkspace method.

In DAO 3.5, the Workspace object now also contains the Connection object, which contains information about a connection to an ODBC data source that uses an ODBCDirect workspace. For more information about ODBCDirect workspaces, see Chapter 9, “Developing Client/Server Applications.”


Database Object

There are several new properties that you use when working with replication and Database objects: DesignMasterID, DefaultPartner, MostRecentSyncPartner, Replicable, and ReplicaID. Note that only ReplicaID and DesignMasterID exist on every database, whether it’s replicable or not, and the Replicable property exists only if the user creates and appends it. If a database is made replicable (by appending the Replicable property), then the additional properties DefaultPartner and LastUpdated are created by Microsoft Jet.

In addition, Microsoft Jet 3.5 supports partial replication. If you want to replicate only part of the data in a database, you can create partial replicas, which contain a subset of the data in a full replica. You use the PopulatePartial method of the Database object to synchronize any changes in a partial replica with the full replica, clear all records in the partial replica, and then repopulate the partial replica based on the current replica filters.

With the Synchronize method, you can synchronize the current database with a replica database. The MakeReplica method creates a new replica based on the current replicable database.

See Also For more information about replication, see Chapter 7, “Database Replication.”

You can use the NewPassword method to assign a new password to the database.

The behavior of the Database object’s Close method has changed in the way it closes any pending Recordset changes. For more information, see “Closing Object Variables” earlier in this chapter.


Container Object

The Container object supports the AllPermissions property, which returns all the permissions that apply to the current UserName property of the Container or Document object, including permissions that are specific to the user as well as the permissions a user inherits from membership in groups. The AllPermissions property differs from the Permissions property in that it returns all permissions for an object, including group permissions.


Document Object

The Document object supports the AllPermissions property. For information about the AllPermissions property, see the preceding section, “Container Object.”

You can add properties to and delete properties from the Document object.


Recordset Object

You can use the GetRows method to retrieve records from a Recordset object. The data that is returned is stored in a Variant array.

The AbsolutePosition property, which you can use to set or return the relative record number of a Recordset object’s current record, is now supported and documented. The PercentPosition property, which you can use to set or return a value indicating the approximate location of the current record in the Recordset object based on a percentage of the records in the Recordset, now supports TableDef objects without indexes.


QueryDef Object

DAO 3.5 supports the new MaxRecords property on QueryDef objects. You can use the MaxRecords property to specify the maximum number of records to return from a query.


Relation Object

DAO 3.5 adds the PartialReplica property to the Relation object. You use the PartialReplica property to set or return a value on a Relation object indicating whether that relationship is considered when populating a partial replica from a full replica.


TableDef Object

For replication, the Replicable, KeepLocal, and ConflictTable properties have been added. For partial replication, the ReplicaFilter property has been added to 3.5 TableDef objects.


Field Object

The FieldSize property of the Field object replaces the FieldSize method used in previous versions. You use the FieldSize property to return the size (in bytes) of a Field object in the Fields collection of a Recordset object.


All Objects

You can specify a 3.5-only DAO type library that removes support for obsolete objects, methods, and properties. For more information, see the next section, “Compatibility Issues.”


Compatibility Issues

As DAO has evolved, new objects, methods, and properties have been introduced that render their prior equivalents obsolete. To enhance performance and decrease memory usage, future versions of Microsoft Jet may eliminate support for these obsolete language elements. In order to ease the conversion of applications that use the older objects, methods, and properties, the DAO350.dll file contains two type libraries: the DAO 2.5 type library and the DAO 3.5 type library. These type libraries define the properties and methods that DAO can use.

The DAO 2.5 type library is backward compatible with previous versions of Microsoft Jet and supports all previous objects, methods, and properties. Use this type library if you want existing DAO code to work with Microsoft Jet 3.0 or 3.5 without conversion.

The DAO 3.5 type library removes support for obsolete objects, methods, and properties. Because they are not supported, use of these constructs while the DAO 3.5 type library is in use causes compile errors in your code. The benefit of this behavior is that it allows you to verify that your programs use the latest DAO features by quickly identifying code that uses outdated methods and properties.

In Microsoft Access 97, Visual Basic 5.0, Microsoft Excel 97, and Microsoft Project 97, the Tools menu contains a References command when you are in a code module. Clicking this command gives you a list of object libraries and databases that are available. Two versions of DAO are now available: the DAO 2.5/3.5 compatibility library and DAO 3.5 object library.

By clicking DAO 3.5 Object Library and then recompiling, you can identify any obsolete DAO code with compile errors that place the cursor at the line of code that caused the error.

When you create a new database, the DAO 3.5 Object Library is automatically selected. This ensures that all new DAO code you create uses the new objects. Note that you can’t use both libraries at once.

See Also For a list of obsolete DAO features and their replacement features, search the DAO Help index for “DAO, obsolete features in.”


Optimizing DAO

When using DAO, you can apply specific techniques to your code to improve its performance. This introductory chapter covers only optimizations that are intrinsic to the operation of DAO as a programming interface. The chapters listed in the following table contain additional information about optimizing performance.

To optimize | See
Accessing local data | Chapter 5, “Working with Records and Fields”
Multiuser environments | Chapter 6, “Creating Multiuser Applications”
Accessing external ISAM data | Chapter 8, “Accessing External Data”
Accessing ODBC data | Chapter 9, “Developing Client/Server Applications”
Performance | Chapter 13, “Optimizing Performance”

Few absolutes exist in the area of performance optimization. While a technique may work well in one piece of code, it may slow down other code. As with any optimization tips, use the following techniques judiciously. Try them in your code, and be sure to verify that they actually improve performance. Additionally, by concentrating on the 10 percent of your code that is executed most frequently, you will achieve better results than spending a lot of time tuning code that is rarely used.

Finally, the techniques shown in this chapter relate to using DAO in a generic sense, or demonstrate good coding practices. Usually, the most important optimizations involve your data, and reading and writing data to the disk. As a general rule, you should concentrate 95 percent of your optimization efforts on disk input/output-related code, and 5 percent on the type of optimizations shown here.


Caching References to Objects and Collections

When you refer to an object or a collection more than once, you can improve performance by caching a reference to that object or collection. There are three ways to do this:

  • By returning a reference to an object or collection and assigning it to an object variable

  • By using the For Each…Next statement when you refer to each object in a collection

  • By using the With statement when you refer to an object and its properties, methods, or collections repeatedly

When you refer to an object or collection more than once in your code, you may want to create an object variable and assign a reference to the object to that object variable. Not only does this make your code faster for lengthy operations, it also makes it easier to read.

The following two examples both return the names of all tables in a database, all fields in each table, and the names and values of all properties on those fields. The first example, which stores references to objects in object variables, is considerably faster than the second. In these examples, strDbPath is the path to a database:

' 1 - Faster, easier to read.
Dim dbs As Database, tdf As TableDef
Dim fld As Field, prp As Property

Set dbs = OpenDatabase(strDbPath)
For Each tdf In dbs.TableDefs
    Debug.Print tdf.Name
    For Each fld In tdf.Fields
        Debug.Print fld.Name
        For Each prp In fld.Properties
            On Error Resume Next
            Debug.Print prp.Name, prp.Value
        Next prp
    Next fld
Next tdf
dbs.Close
Set dbs = Nothing

' 2 - Slower, more cumbersome.
Dim intW As Integer, intX As Integer, intY As Integer

OpenDatabase strDbPath
For intW = 0 To DBEngine(0)(0).TableDefs.Count - 1
    Debug.Print DBEngine(0)(0).TableDefs(intW).Name
    For intX = 0 To DBEngine(0)(0).TableDefs(intW).Fields.Count - 1
        Debug.Print DBEngine(0)(0).TableDefs(intW).Fields(intX).Name
        For intY = 0 To _
            DBEngine(0)(0).TableDefs(intW).Fields(intX).Properties.Count - 1
            On Error Resume Next
            Debug.Print _
                DBEngine(0)(0).TableDefs(intW).Fields(intX).Properties(intY).Name, _
                DBEngine(0)(0).TableDefs(intW).Fields(intX).Properties(intY).Value
        Next
    Next
Next

You can improve performance by using the For Each…Next and With…End With statements. Both of these cache a reference to an object, so that your code doesn’t have to repeatedly retrieve the reference in order to use the object.

The following example uses both of these statements to print all of the values in a recordset. In this example, strDbPath is the path to a database, and strRstSource is the source for a recordset:

Dim dbs As Database, tdf As TableDef
Dim rst As Recordset, fld As Field

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset(strRstSource)

With rst
    Do Until .EOF
        For Each fld In rst.Fields
            Debug.Print fld.Name, fld.Value
        Next fld
        .MoveNext
    Loop
    Debug.Print
End With
rst.Close
Set rst = Nothing
dbs.Close
Set dbs = Nothing

The following example uses a For loop rather than the For Each…Next statement, and it refers to the Recordset object variable explicitly each time it’s used rather than caching it with the With statement. The performance differences are not apparent for small recordsets, but become more significant with larger recordsets. When strDbPath is the path to the NorthwindTables database and strRstSource is the Orders table, the following example runs several seconds more slowly than the preceding one:

Dim dbs As Database, tdf As TableDef
Dim rst As Recordset, fld As Field
Dim intC As Integer

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset(strRstSource)

Do Until rst.EOF
    For intC = 0 To rst.Fields.Count - 1
        Debug.Print rst.Fields(intC).Name, rst.Fields(intC).Value
    Next
    rst.MoveNext
Loop
Debug.Print
rst.Close
Set rst = Nothing
dbs.Close
Set dbs = Nothing

Another important consideration for performance is whether or not your object variables are strictly typed. When you strictly type an object variable, you declare the object variable according to the type of object that it will represent. In the following example, the object variables are not strictly typed according to the object type they are to represent. When a variable isn’t typed, Visual Basic automatically designates it as type Variant. This example is otherwise identical to the preceding example, but runs at nearly half the speed for the Orders table in the NorthwindTables database:

Dim dbs, tdf
Dim rst, fld
Dim intC

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset(strRstSource)

Do Until rst.EOF
    For intC = 0 To rst.Fields.Count - 1
        Debug.Print rst.Fields(intC).Name, _
            rst.Fields(intC).Value
    Next
    rst.MoveNext
Loop
Debug.Print
rst.Close
Set rst = Nothing
dbs.Close
Set dbs = Nothing


Using Field Object Variables to Refer to Fields In Loops

To speed up iterative processes through large numbers of records by using DAO, declare object variables to refer to Field objects. The following example doesn’t use Field object variables to refer to Field objects. Instead, it refers to Field objects in the Fields collection. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Products")

Do Until rst.EOF
    With rst
        .Edit
        .Fields!UnitPrice = rst!UnitPrice * 1.1
        .Update
        .MoveNext
    End With
Loop

This code performs better if you use Field object variables to refer to fields, as shown in the following example. In the following code, a Field object variable refers to the UnitPrice field in the recordset. When you return a reference to the field and assign it to a Field object variable, Visual Basic stores a reference to the field in the variable. The code runs more quickly because Visual Basic doesn’t have to return the reference each time the loop iterates. Again, the difference in performance is more noticeable for large recordsets than for small ones:

Dim dbs As Database, rst As Recordset
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Products")
Set fld = rst!UnitPrice

Do Until rst.EOF
    With rst
        .Edit
        fld = fld * 1.1
        .Update
        .MoveNext
    End With
Loop

Note that this operation is even faster if you use an update query rather than updating a field in the recordset, record by record. The following example uses an update query to accomplish the same task, where strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, strSQL As String

Set dbs = OpenDatabase(strDbPath)
strSQL = "UPDATE Products SET Products.UnitPrice = UnitPrice *1.1;"
dbs.Execute strSQL


Refreshing Collections Only When Necessary

You can use the Refresh method to get an up-to-date view of a collection and its objects. This is particularly useful in multiuser environments. Unfortunately, this method is a time-consuming operation relative to most other DAO operations and should be used only when absolutely necessary.

As noted earlier in this chapter, the Microsoft Access CurrentDb function refreshes all of the collections in the current database. To improve performance, you can use the DBEngine(0)(0) syntax and refresh collections only as needed.

See Also For more information about refreshing collections, see “Refreshing Collections” earlier in this chapter.


Taking Advantage of Default Collections

Most DAO objects have several collections, one of which is the default collection for that object. You can use abbreviated syntax when referring to default collections. For example:

DBEngine.Workspaces(0).Databases(0)

can be shortened to

DBEngine(0)(0)

This shorter syntax is also slightly faster. Note that when you use default collections, your code may be difficult to read, so you may want to add explanatory comments.

See Also For more information about default collections, see “Using Default Collections to Write Shorter Code” earlier in this chapter.


Replacing Obsolete Language Elements

Previous versions of DAO included some language elements that have been replaced in DAO 3.5. If your code still uses the old language elements, consider updating it to use newer syntax.

In order to use old language elements, you must set a reference to the Microsoft DAO 2.5/3.5 Compatibility Library. This library is larger than the Microsoft DAO 3.5 Object Library, so it takes longer to load. Your application will run more quickly if you use only the elements available in DAO 3.5. Additionally, Microsoft DAO often maps older language elements to newer elements internally. If you use the newer elements, you can avoid calling the extra mapping code.

See Also For a list of obsolete DAO features and their replacement features, search the DAO Help index for “DAO, obsolete features in.”


Chapter 3 Data Definition and Integrity

This chapter discusses how to create, delete, and maintain tables, indexes, queries, relationships, and referential integrity rules with the Microsoft Jet database engine.

As discussed in Chapter 2, “Introducing Data Access Objects,” Microsoft Jet tables, queries, and other objects are exposed to programming languages through a hierarchical system of objects and collections of objects referred to as Data Access Objects (DAO). For example, a Database object contains a collection of TableDef objects, which further contain collections of Field objects, which in turn contain collections of Property objects, and so on. The DAO object model maps those objects onto a programming interface you can control by using Microsoft Visual Basic, Visual Basic for Applications, Microsoft Visual C++, or Microsoft Visual J++.

You can create tables and define relationships between tables entirely through the native DAO interface. Using DAO gives you a great deal of flexibility and control over how your database objects are created. With DAO, you write code to create a new object, assign values to that object’s properties to specify a data type, length, validation rule, and so on, and then append that object to a collection within the database’s object hierarchy.

Many people are familiar with the industry-standard Structured Query Language (SQL) and its data definition language (DDL) subset. Microsoft Jet supports an SQL dialect that is compliant with the SQL ANSI-89 standard, with a few enhancements. Many of the features of Microsoft Jet that can be manipulated through DAO can also be controlled with SQL DDL statements. This chapter shows both methods and explains the differences.

Creating a Database

Creating and Modifying Tables

Creating and Modifying Table Indexes

Microsoft Jet QueryDef Objects

Referential Integrity and Relationships

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


Creating a Database

As a developer, you may be familiar with creating a new Microsoft Jet database file either interactively through the Microsoft Access user interface, or through the Visual Data Manager utility (Visdata.exe) that ships with Visual Basic. You can also create a new Microsoft Jet database file programmatically by using DAO.

For example, your application may need to build a new database when it’s first installed, or perhaps your application needs to create a database to archive or export data. The following code fragment creates a new database by using Visual Basic, where strNewDbName is the path and name for the new database:

Dim dbs As Database
Set dbs = CreateDatabase(strNewDbName, dbLangGeneral, dbVersion30)

Note The previous example is taken from the CreateNewDatabase function, which is available on the companion CD-ROM.

This example uses the CreateDatabase method to build a new, empty database. The arguments to CreateDatabase specify the path and name for the new database, the language used for sorting and string comparison, and the version of the new database.

The version argument is specified as dbVersion30 when a Microsoft Jet version 3.5 database is created because it uses the same file and table format as Microsoft Jet version 3.0. However, databases created with the Microsoft Access 97 user interface can’t be opened with Microsoft Access 95. This is because a Microsoft Access 97 database’s Visual Basic for Applications project (the set of all code modules in a database, which includes modules associated with forms and reports, as well as standard and class modules) can’t run in the version of Visual Basic for Applications used in Microsoft Access 95. Additionally, some database objects created with Microsoft Access 97 may use application-defined properties and have features that are not supported in earlier versions of Microsoft Access.

If you use the CreateDatabase method to create a database specified as dbVersion30 and use only DAO methods to create tables, relationships, and queries, both Microsoft Access 95 and 97 treat the database as a native database the first time it’s opened. However, both versions of Microsoft Access add an application-defined property named AccessVersion to the Properties collection of a database the first time it’s opened. Microsoft Access 97 also adds a system table named MSysModules2 to contain the database’s Visual Basic for Applications project, which is treated as an invalid database object by Microsoft Access 95. After the first time a database is opened in either version, it’s no longer treated as a native database in the other version.

For a Microsoft Access version 2.0 database, the AccessVersion property returns “02.00.” For a Microsoft Access 95 database, the AccessVersion property returns a string that always begins with “06” followed by a period and two digits. For a Microsoft Access 97 database, the AccessVersion property always begins with “07” followed by a period and two digits. In both Microsoft Access 95 and 97, the final two digits returned by the AccessVersion property may vary depending on the dynamic-link library (DLL) used to create a database’s Visual Basic for Applications project. For this reason, if you want to determine the version that produced or is using a database, your code should ignore the final two digits. The following function returns the version of Microsoft Access used to create or open a database by examining only the first two digits returned for the AccessVersion property. This function takes a String argument that specifies the path to the database.

Function FindVersion(strDbPath As String) As String
    Dim dbs As Database
    Dim strVersion As String
    Const conPropertyNotFound As Integer = 3270

    On Error GoTo Err_FindVersion

    ' Open the database and return a reference to it.
    Set dbs = OpenDatabase(strDbPath)
    ' Check the value of the AccessVersion property.
    strVersion = dbs.Properties("AccessVersion")
    ' Return the two leftmost digits of the value of the AccessVersion property.
    strVersion = Left(strVersion, 2)

    ' Based on the value of the AccessVersion property, return a string indicating
    ' the version of Microsoft Access used to create or open the database.
    Select Case strVersion
        Case "02"
            FindVersion = "2.0"
        Case "06"
            FindVersion = "7.0"
        Case "07"
            FindVersion = "8.0"
    End Select

Exit_FindVersion:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Function

Err_FindVersion:
    If Err.Number = conPropertyNotFound Then
        MsgBox "This database hasn't previously been opened with Microsoft Access."
    Else
        MsgBox "Error: " & Err & vbCrLf & Err.Description
    End If
    Resume Exit_FindVersion
End Function

If you create a dbVersion30 database by using DAO methods and then add forms and reports by using the CreateForm or CreateReport functions (these functions are available only in code run from Microsoft Access), a Visual Basic for Applications project is added that corresponds to the version of Microsoft Access used to run the code. You can also add modules and insert code in a database by using Visual Basic code in Microsoft Access 97. Doing so also creates a Visual Basic for Applications project that is specific to Microsoft Access 97.


The Database Owner

If you don’t create and reference a new Workspace object when you are using the CreateDatabase method, the default Workspace object is used. The effect of using the default Workspace object depends on which host application is running the Visual Basic code. For example, in Microsoft Access, the default Workspace object is identical to the one used to log on to the application database that is executing the code. The new database is owned by the user account that is used to log on to Microsoft Access before the CreateDatabase method is used. If the Logon dialog box hasn’t been activated in Microsoft Access, the database owner is the default Admin user account.

See Also For information about using Microsoft Jet security, see Chapter 10, “Managing Security.”

In Visual Basic version 5.0, the UserName property of the default Workspace object is set to Admin and the Name property is set to #DefaultWorkspace#. Because the user doesn’t log on to the database independently of the code executed by a Visual Basic application, there is no user Workspace object for Visual Basic to inherit as there is with Microsoft Access. This situation also applies when you are using the CreateDatabase method in Visual Basic for Applications code with Microsoft Word, Microsoft Excel, or Microsoft PowerPoint.

Additionally, for any application that is using DAO, you can set the DefaultUser and DefaultPassword properties of the DBEngine object to specify the default user account and password that will be used when a new Workspace object is created.

Note Microsoft Jet 3.5 creates Microsoft Jet Workspace objects by default. However, if you specify the DefaultType property of the DBEngine object as dbUseODBC, or specify dbUseODBC when using the CreateWorkspace method, an ODBCDirect Workspace object is created. The Name and UserName property issues discussed in this section don’t apply to ODBCDirect Workspace objects. For more information about using ODBCDirect features, see Chapter 9, “Developing Client/Server Applications.”

To create a secured Workspace object, provide a user name other than the default for the user argument of the CreateWorkspace method. You can then create a new database in the Databases collection for that Workspace object. The owner of the new database is the user specified when the workspace is created. Microsoft Jet security requires that the owner of a secured database create the database while logged on to the appropriate secured Workspace object. The owner of the database can be specified only when the database is first created.

The owner of the new database can establish permissions on objects in the new database. To determine the owner of a database, check the value of the Owner property for the MSysModules Document object.

Important If you want to secure a database, don’t create the Workspace object by using “Admin” as the value of the user argument of the CreateWorkspace method. The Admin user is the default user, and if the Admin user is the owner of a database, any user can open the database and alter objects or permissions on objects. To create a secured database, you must always use a nondefault user account.

The following example creates a secured workspace and then creates a database within that workspace. In this example, strWrkName is the name of the new secured workspace, strUserName is the name of the user account under which the workspace is created, strPassword is the user’s password, and strDbName is the name of the new database.

Dim wrkSecured As Workspace
Dim dbs As Database

' Create secured workspace.
Set wrkSecured = _
    DBEngine.CreateWorkspace(strWrkName, strUserName, strPassword)

' Create new secured database.
Set dbs = _
    wrkSecured.CreateDatabase(strDbName, dbLangGeneral, dbVersion30)

If you specify a user account that doesn’t yet exist, Microsoft Jet returns an error when you attempt to create the new workspace. To create a new user account automatically when an unknown user name is passed to the CreateWorkspace method, you can handle the error by including the following code in the error handler. You create the new user account on the default Workspace object. In this example, strGroupName is the name of the group to which the new user is added. The code also adds the new user to the Users group.

Dim wrk As Workspace, usr As User
Dim strPID As String, intC As Integer

' Return reference to default workspace.
Set wrk = DBEngine(0)
strPID = InputBox("Enter an alphanumeric PID between 4 and 20 " _
    & "characters. Be sure to write this PID down in a safe " _
    & "place, along with your user name and password.")

' Create user in Users collection of default workspace.
Set usr = wrk.CreateUser(strUserName, strPID, strPassword)
wrk.Users.Append usr

' Create user in Users collection of specified group.
Set usr = wrk.Groups(strGroupName).CreateUser(strUserName)
wrk.Groups(strGroupName).Users.Append usr

Note The previous examples are taken from the CreateSecuredDatabase function, which is available on the companion CD-ROM.

A new database can be created programmatically with DAO only; there is no SQL DDL method of creating a new database. A database, however, can’t be deleted programmatically through DAO. If you want to delete a database programmatically, use the Visual Basic for Applications Kill statement to remove the file at the file-system level of the operating system.


Microsoft Jet vs. Other Data Source Types

To Microsoft Jet, a “database” has different meanings depending on the source of the data. While a Microsoft Jet database is physically represented as an .mdb file, the database for other data sources is just a logical construction. For example, a dBASE or Microsoft FoxPro database is nothing more than a directory on a hard disk that contains one or more .dbf or .cdx files, or both. You must still “open” the database by using a Database object, but the database is not a physical object.

Because a non-Microsoft Jet database isn’t a physical object, you don’t use the CreateDatabase method to create the files that make up that type of database. For example, you can create new dBASE or FoxPro tables by using the CreateTableDef, CreateField, and CreateIndex methods as described in the “Creating Tables and Indexes in Installable ISAM Databases” section later in this chapter.


Creating and Modifying Tables

In Chapter 2, “Introducing Data Access Objects,” you were introduced to the concept of using DAO to add a new table to a database by appending a Field object to the Fields collection of a TableDef object, and then appending that TableDef object to the TableDefs collection of the database. In the same way, you can add field-level and table-level validation rules, or specify indexes and inter-table relationships entirely through DAO code or SQL DDL statements.

When you use the Microsoft Access user interface to add fields to a table or to specify validation rules and default values in a property sheet, Microsoft Access is using DAO behind the scenes to tell the underlying Microsoft Jet database engine how to build or modify your table.

Anything that a user can do through the Microsoft Access user interface can be done with code. In some cases, properties and capabilities of Microsoft Jet that are available to you as a DAO programmer are not available through Microsoft Access. For example, through DAO you can specify fixed-length text fields for your tables. Microsoft Access allows only variable-length fields.


Creating Tables by Using SQL DDL

Even the most basic TableDef object in a Microsoft Jet database must contain at least one field. It’s not necessary to specify a primary key, validation rules, or any of the other optional properties such as Description or DefaultValue. The table is nothing without its fields.

Using SQL DDL statements, the simplest statement you can use to create a new table in a database is as follows:

CREATE TABLE MyTable (MyField TEXT);

The CREATE TABLE statement creates a table called MyTable with one field called MyField. MyField is built as a variable-length text field with a maximum length of 255 characters. The field is not required, and may contain Null values. It’s not indexed.


Communicating with Microsoft Jet by Using SQL DDL

You can pass SQL DDL statements to Microsoft Jet in three ways.

In Microsoft Access, you can use the RunSQL action or the RunSQL method of the DoCmd object to execute an SQL statement that acts on the current database. For example, the CREATE TABLE statement from the previous example can be executed in Microsoft Access by using the RunSQL method as follows:

DoCmd.RunSQL "CREATE TABLE MyTable (MyField TEXT);"

The DoCmd object and its methods are not available in other Microsoft Jet host languages such as Visual C++ and Visual J++, or when you are using Visual Basic for Applications in Visual Basic 5.0, Microsoft Excel 97, PowerPoint 97, or Word 97. In all of these languages and in Microsoft Access 97, you can execute SQL statements by using the Execute method on a Database object declared in code. In the following example, strDbPath is the path to the database, strTableName is the name of the new table, strFieldName is the name of the new field, and strFieldType is the type of field to create.

Dim dbs As Database
Dim strSQL As String

Set dbs = OpenDatabase(strDbPath)
strSQL = "CREATE TABLE " & strTableName & " (" & strFieldName _
    & " " & strFieldType & ");"

' Execute SQL command.
dbs.Execute strSQL

Note To use any DAO method, such as the Execute method, when you are using Visual Basic for Applications in Microsoft Excel, PowerPoint, or Word, you must first install the Data Access Objects for Visual Basic component by rerunning Setup. Then you can use the References command (Tools menu) in the Visual Basic Editor to set a reference to the Microsoft DAO 3.5 object library.

Another way to execute an SQL statement is to create a permanent DAO QueryDef object that contains the SQL statement, and then use the Execute method to run the QueryDef object in code. The following example creates a QueryDef object that includes a CREATE TABLE statement and then executes that QueryDef object. In this example, strDbPath is the path to the database and strQueryName is the name of the new QueryDef object:

Dim dbs As Database, qdf As QueryDef
Dim strSQL As String

Set dbs = OpenDatabase(strDbPath)
strSQL = "CREATE TABLE MyTable (MyField TEXT);"
Set qdf = dbs.CreateQueryDef(strQueryName)

' Execute query.
qdf.Execute

Note Another method of creating a table by using SQL is to use a make-table query. For a complete discussion of using make-table and other queries, see Chapter 4, “Queries.”

See Also For information about creating QueryDef objects, see the “Microsoft Jet QueryDef Objects” section later in this chapter.

For the remainder of this chapter, SQL DDL examples are given with the text of the SQL statement only. Remember, however, that to communicate your SQL statements to Microsoft Jet, you must execute the DDL statement by using one of the previously described techniques.


Creating Tables by Using DAO

The DAO equivalent for the SQL DDL statement CREATE TABLE MyTable (MyField TEXT); is as follows, where strDbPath is the path to the database:

Dim dbs As Database, tdf As TableDef
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)

' Create new TableDef object.
Set tdf = dbs.CreateTableDef("MyTable")

' Create new Field object.
Set fld = tdf.CreateField("MyField", dbText)
tdf.Fields.Append fld
dbs.TableDefs.Append tdf

The DAO method and the SQL DDL method create identical tables. The method you use depends on what you want to do, and the style of programming you prefer. In most cases, DAO is more powerful and flexible than SQL DDL.

The DAO techniques used to create and modify tables are similar to the way Microsoft Jet uses hierarchical collections in other contexts. For example, you can append a Field object to the Fields collection by using DAO in ways similar to those used to add a user-defined property to a form in a Microsoft Access application. In addition, certain types of Microsoft Access-specific properties, such as the ValidationRule and DefaultValue properties of fields, can be set only through the Microsoft Access user interface or through DAO in code.


Specifying Field Properties

The previous examples show a simple case of building a table with a single field by using default property values. Usually, you would specify many additional properties and override certain default values.

This example, which uses DAO, creates a table with four fields and specifies various nondefault values for the fields (ID, Name, Response, and Class), where strDbPath is the path to the database:

Dim dbs As Database, tdf As TableDef
Dim fldID As Field, fldName As Field
Dim fldResponse As Field, fldClass As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.CreateTableDef("MarketingSurvey")

' Create automatically incrementing field.
Set fldID = tdf.CreateField("ID", dbLong)
fldID.Attributes = dbAutoIncrField
fldID.Required = True

' Create text field.
Set fldName = tdf.CreateField("Name", dbText)
With fldName
    .Required = True
    .Size = 40
    .AllowZeroLength = True
    .DefaultValue = "Unknown"
End With

' Create memo field.
Set fldResponse = tdf.CreateField("Response", dbMemo)

' Create text field with validation rule.
Set fldClass = tdf.CreateField("Class", dbText, 10)
With fldClass
    .Required = True
    .ValidationRule = "In('A','B','X')"
    .ValidationText = "Enter one of A, B, or X."
End With

With tdf
    .Fields.Append fldID
    .Fields.Append fldName
    .Fields.Append fldResponse
    .Fields.Append fldClass
End With
dbs.TableDefs.Append tdf

When you use Microsoft Jet SQL DDL statements, you can specify only the field names and data types. Microsoft Jet SQL doesn’t support ANSI SQL constructs that set validation rules or default values (for example, field-level CHECK or DEFAULT clauses). You can’t use SQL DDL to fill in the ValidationRule and DefaultValue properties of a field, for example. The closest you can come to re-creating the previous table by using only SQL DDL statements is:

CREATE TABLE MarketingSurvey
    (ID SHORT,
    Name TEXT (40),
    Response MEMO,
    Class TEXT (10));


Using the Attributes Property to Specify Additional Field Characteristics

In addition to standard field properties, you can use the Attributes property to specify additional characteristics for fields. For example, in Microsoft Access 97 table Design view, you create a Hyperlink field by setting the value in the Data Type column to Hyperlink. However, there is no DAO Type property setting for Field objects that corresponds to a Hyperlink field. This is because, at the Jet database engine level, a Hyperlink field is actually a Memo field that uses the dbHyperlinkField setting of the Attributes property to identify itself to Microsoft Access. While you can use DAO to create a Hyperlink field and work with its data, its hyperlink functionality is only available when the database is opened with Microsoft Access 97.

You can also use the Attributes property to create a field whose value is automatically incremented to a unique Long value for new records. Create a Field object of type Long, and set the Attributes property for the field to dbAutoIncrField. The resulting field is equivalent to a Microsoft Access AutoNumber field.

The following example creates a table named Hyperlinks that contains an automatically incrementing field and a hyperlink field, where strDbPath is the path to the database:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.CreateTableDef("Hyperlinks")

' Create and append fields in one step.
With tdf
    .Fields.Append .CreateField("ID", dbLong)
    .Fields("ID").Attributes = dbAutoIncrField
    .Fields.Append .CreateField("Hyperlink", dbMemo)
    .Fields("Hyperlink").Attributes = dbHyperlinkField
End With
dbs.TableDefs.Append tdf

See Also For information about using other Attributes property settings with Field objects, search the DAO Help index for “Attributes property.”


Built-in vs. Application-Defined Properties

The ValidationRule property, along with other properties such as Size, Required, and DefaultValue, are known as built-in properties.

In addition to these built-in properties, you can specify a variety of other custom properties, which you define yourself, or application-defined properties, which are defined by an application that’s using Microsoft Jet, such as Microsoft Access. For example, when you build a table by using the Microsoft Access user interface, you may want to set the Caption property or Description property for a field. However, because these properties aren’t part of the built-in properties available in a Field object’s Properties collection, DAO may return an error if you try to set these properties from Visual Basic. In the following example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef, fld As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.CreateTableDef("CustomerSurvey")
Set fld = tdf.CreateField("ID", dbLong)
fld.Attributes = dbAutoIncrField
fld.Description = "Survey Identification Number" '<<<Error occurs here.
tdf.Fields.Append fld

dbs.TableDefs.Append tdf

This code doesn’t execute as written because there is no built-in Description property for a Field object in a Microsoft Jet table. This property is set by Microsoft Access whenever a user enters a field description in a table’s Design view, though it’s not created by default. If you want to use DAO to create a Description property for a field, you must use the CreateProperty method to create the property, and then append the new property to the Properties collection of the field.

The following procedure creates a new table and calls the SetCustomProperty function to set the Description property for a field. The SetCustomProperty function attempts to set the property, and if necessary creates a new Property object and appends it to the Properties collection:

Sub SetFieldDescription()
    Dim dbs As Database, tdf As TableDef, fld As Field
    Dim strValue As String, strDbPath As String

    strDbPath = "C:\JetBook\Samples\NorthwindTables.mdb"

    ' Return reference to current database.
    Set dbs = OpenDatabase(strDbPath)

    ' Create new table.
    Set tdf = dbs.CreateTableDef("Inventory")
    Set fld = tdf.CreateField("ID", dbLong)
    fld.Attributes = dbAutoIncrField
    tdf.Fields.Append fld
    dbs.TableDefs.Append tdf

    strValue = "Survey ID Number"
    If SetCustomProperty(fld, "Description", dbText, strValue) Then
        Debug.Print "Property set successfully."
    Else
        Debug.Print "Property not set successfully."
    End If
    dbs.Close
    Set dbs = Nothing
End Sub

Function SetCustomProperty(obj As Object, strName As String, _
        intType As Integer, varSetting As Variant) As Boolean
    Dim prp As Property
    Const conPropNotFound As Integer = 3270

    On Error GoTo Error_SetCustomProperty

    ' Explicitly refer to Properties collection.
    obj.Properties(strName) = varSetting
    obj.Properties.Refresh
    SetCustomProperty = True

Exit_SetCustomProperty:
    Exit Function

Error_SetCustomProperty:
    If Err = conPropNotFound Then
        ' Create property, denote type, and set initial value.
        Set prp = obj.CreateProperty(strName, intType, varSetting)
        ' Append Property object to Properties collection.
        obj.Properties.Append prp
        obj.Properties.Refresh
        SetCustomProperty = True
    Else
        MsgBox "Error: " & Err & vbCrLf & Err.Description
        SetCustomProperty = False
    End If
    Resume Exit_SetCustomProperty
End Function

See Also For a complete discussion of built-in, user-defined, and application-defined properties, see “DAO Properties” in Chapter 2, “Introducing Data Access Objects.”


Field-Level Validation

A field-level validation rule is enforced directly by Microsoft Jet. It doesn’t matter whether you attempt to update the field through Microsoft Access, a custom Visual Basic program, or an ODBC call from a third-party program such as PowerBuilder.

The validation rule must be an expression that evaluates either to True or False. If the validation rule doesn’t evaluate to True for the data entered in a field, an error occurs and the contents of the ValidationText property of the field are made available to the calling program.

How this message is conveyed to the user depends on the context of the calling program. For example, if the user attempts to enter an invalid value in the Microsoft Access Datasheet view of a table, Microsoft Access beeps, displays the validation text in a message box, and waits for the user to respond. If a Visual Basic or Visual Basic for Applications code routine violates the validation rule, a trappable run-time error occurs.

Because Microsoft Jet is independent of its host applications, the validation rule may not refer to features that are available from a specific application. You can’t use a custom function in the validation rule expression. Nor can you make reference to objects in the user environment, such as to Microsoft Access forms or controls.

It may seem reasonable to have a validation rule expression such as the following:

Between 10 And MyUpperLimit()

However, even if you defined a function called MyUpperLimit within a Microsoft Access module or within your Visual Basic code, the validation rule expression is still not valid.

By default, field validation rules aren’t evaluated until the entire record is updated. However, in many situations you can improve your application’s performance by using DAO code that sets the ValidateOnSet property of a field to True, so that the field’s validation rule is evaluated when the field’s Value property (its data) is set. For more information about using the ValidateOnSet property, search the DAO Help index for “ValidateOnSet property.”


Table-Level Validation

You may want to create a validation rule for a particular field that depends on the values of one or more of the other fields in the table. You can create a table-level validation rule to compare one field to another.

Suppose you want to specify a rule for a Marketing Survey table that requires that all surveys using the name TestAccount have a Class field value of X. You can’t validate the Name field without knowing the value of the Class field, and vice versa. Only when a record is either updated or inserted is all the information available to evaluate the rule.

The validation rule must contain an expression that can be evaluated as either True or False. If the expression evaluates to True, the update is allowed. If the expression evaluates to False, Microsoft Jet generates a trappable run-time error.

The following example uses DAO to add a table-level validation rule, where strDbPath is the path to the NorthwindTables database:

Function AddTableLevelValidationRule() As Boolean
    ' This procedure checks for an existing table-level validation rule and
    ' validation text on the Orders table and prompts the user to create new ones.
    Dim dbs As Database, tdf As TableDef
    Dim strValRule As String, strValText As String
    Dim strDbPath As String

    On Error GoTo Err_AddTableLevelValidationRule

    Set dbs = OpenDatabase("C:\JetBook\Samples\NorthwindTables.mdb")
    Set tdf = dbs.TableDefs("Orders")

    If Len(tdf.ValidationRule) Then
        If MsgBox("The following validation rule already exists: " & vbCrLf _
            & vbCrLf & tdf.ValidationRule & vbCrLf & vbCrLf _
            & "Delete this rule and create a new one?", vbYesNo) = vbYes Then
                tdf.ValidationRule = ""
        Else
            GoTo Exit_AddTableLevelValidationRule
        End If
    End If
    strValRule = "([RequiredDate]>=[OrderDate]) And ([ShippedDate]>=[OrderDate])"
    tdf.ValidationRule = strValRule

    If Len(tdf.ValidationText) Then
        If MsgBox("The following validation text already exists: " & vbCrLf _
            & vbCrLf & tdf.ValidationText & vbCrLf & vbCrLf _
            & "Delete this validation text and create new text?", vbYesNo) = _
                vbYes Then tdf.ValidationText = ""
        Else
            GoTo Exit_AddTableLevelValidationRule
        End If
    End If
    strValText = "Both the Required Date and the Shipped Date must be " & _
        "the same date or later than the Order Date."
    tdf.ValidationText = strValText
    AddTableLevelValidationRule = True

Exit_AddTableLevelValidationRule:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Function

Err_AddTableLevelValidationRule:
    MsgBox "Error: " & Err & vbCrLf & Err.Description
    AddTableLevelValidationRule = False
    Resume Exit_AddTableLevelValidationRule
End Function

Note that you can test the properties as well as set them from DAO code. The previous example checks that no current table-level validation rule and validation text exist before adding the new ones.

Because table-level validation rules can’t be evaluated until all the fields in a new record are populated, the validation rule is not checked until just before the new record is inserted in the table, or just before the existing record is updated.

The validation rule in the previous example uses the built-in IIf (Immediate If) function. While you can’t use your own custom functions within a validation rule, you can make use of a wide variety of built-in functions, including date functions and string-manipulation functions such as InStr, Len, and Mid.


Modifying Existing Tables

When you want to make structural changes to existing tables, you use the same techniques that you used to create the table. In SQL DDL, you use the appropriate ALTER TABLE statement to modify a table, or the DROP TABLE statement to delete it. In DAO, you either append or delete a field from the Fields collection of a TableDef object, or delete the TableDef object from the TableDefs collection of the database.


Making Structural Changes by Using SQL DDL

By using SQL DDL, you can remove a table from the database with the following statement:

DROP TABLE MyTable;

Note that this action is subject to all the security restrictions and referential integrity constraints that are established for the database. If the table is part of one or more relationships that are defined in the database, you can’t delete the table until you have deleted the relationship. For more information, see the “Creating and Deleting Relationships by Using SQL DDL” section later in this chapter.

Similarly, you can add or remove a field (column) from an existing table by using the following statements:

ALTER TABLE MyTable ADD COLUMN NewColumn TEXT (20);
ALTER TABLE MyTable DROP COLUMN NewColumn;

When you use SQL DDL statements, you have no control over the order in which new fields are added to a table. Fields are inserted at the end of the list of fields when an ALTER TABLE ADD COLUMN statement is used. Each additional field created by an ALTER TABLE ADD COLUMN statement that follows the original CREATE TABLE statement is placed after the most recently created existing field.

CREATE TABLE YourTable (A CHAR (10));

yields the field A in the first field slot.

ALTER TABLE YourTable ADD COLUMN G CHAR(10);

yields the field A in the first field slot and G in the second slot.

ALTER TABLE YourTable ADD COLUMN F CHAR(10);

yields the field A in the first field slot, G in the second slot, and F in the third slot.

You can’t alter the data type of a field once it’s created, or change its name. The only way to convert an existing field from one data type to another is to add a new field, run an update query to populate the field with values from the original field, and then drop the original field. The same limitation applies to changes made through DAO; you can change a field’s name through DAO, but not its data type, and you can work around this limitation by using the same method.


Making Structural Changes by Using DAO

Using DAO to make table modifications gives you much more flexibility than using SQL DDL statements.

Deleting a table by using DAO requires you to delete the TableDef object from the database’s TableDefs collection. In the following example, dbs is a Database object and strTableName is the name of the table to delete:

dbs.TableDefs.Delete strTableName

The following code creates a new field and specifies it as the second field in the table (ordinal position of 1, with numbering beginning at 0). In this example, strDbPath is the path to the database, strTableName is the name of the table, strFieldName is the name of the field, and intType is a constant specifying what type of field to create:

Dim dbs As Database
Dim tdf As TableDef
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs(strTableName)
Set fld = tdf.CreateField(strFieldName, intType)
fld.OrdinalPosition = 1
tdf.Fields.Append fld

The equivalent code to delete a field from a table is:

Dim dbs As Database
Dim tdf As TableDef
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs(strTableName)
tdf.Fields.Delete strFieldName


Altering Field and Table Properties by Using DAO

You can modify the following field and table properties after the table is created.

Property | Applies to this object
AllowZeroLength | Field
Attributes | TableDef and Field
DefaultValue | Field
OrdinalPosition | Field
Name | TableDef and Field
ValidateOnSet | Field, only when accessed with the Recordset object
ValidationRule | TableDef and Field (For an example of adding a table-level validation rule to an existing table, see the “Table-Level Validation” section earlier in this chapter.)
ValidationText | TableDef and Field
Value | Field, only when accessed with the Recordset object after using the AddNew or Edit method

Unlike SQL DDL, which gives you no way to change a field’s name, you can use DAO to change the Field object’s Name property. In the following example, strDbPath is the path to the database, strTableName is the name of the table, strFieldName is the existing name for the field, and strNewFieldName is the new name for the field:

Dim dbs As Database
Dim tdf As TableDef
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs(strTableName)
tdf.Fields(strFieldName).Name = strNewFieldName

Properties other than those listed in the table above can’t be changed. For example, the following code fails because you can’t change the Type property of a field once it has been created:

Set tdf = dbs.TableDefs(strTableName)
tdf.Fields(strFieldName).Type = dbInteger '<<< Error occurs here.


Linking Tables from External Sources

In addition to the native Microsoft Jet database format (.mdb), Microsoft Jet allows you to work with external data in a variety of formats. You can open tables from external data sources directly — by opening table-type Recordset objects in code. For information about how to open tables directly, see “Linking, Opening, and Importing External Tables” in Chapter 8, “Accessing External Data.”

The most flexible way to work with external data sources is to link tables to the local database (referred to as “attaching tables” in Microsoft Access versions 2.0 and earlier, and in Visual Basic version 3.0). The linked tables appear in the TableDefs collection of the database, and, if security restrictions defined for the source database don’t prevent you from doing so, you can freely query and update them as if they were local tables.

For example, it’s common in Microsoft Access applications to place all tables in a back-end database located centrally on a network server, and place the remaining application-specific objects, such as forms, reports, and modules, in a separate front-end database. Links are defined in the front-end database to the tables in the back-end database, and then a copy of the front-end database is distributed to each user. Although the front-end database doesn’t contain any tables, it looks and acts as if the linked tables are local.

This section describes how you can use DAO to add the linked tables to the local TableDefs collection of your database.

See Also For more information about working with external data sources, see Chapter 8, “Accessing External Data.”


Linking Tables by Using DAO

To link to an external table by using DAO, you must create and append a TableDef object to a database’s TableDefs collection that specifies the following items:

  • The name you want displayed for the linked table in the local database

  • The name of the table as it exists in its original location

  • The database format and location of the external table, and any additional information required to open the database where the external table resides

The following code opens a specified database and creates a new table. It then opens a second specified database and creates a link to the new table in the first database. Note that this code works only when both databases are Microsoft Jet databases. In this example, strDbPathBase is the path to the database containing the base table, strBaseTableName is the name of the new base table, strFieldName is the name of the field in the base table, intType is a constant specifying what type of field to create, strDbPathLinked is the path to the linked table, and strLinkedTableName is the name of the new linked table:

Dim dbsBase As Database, dbsLinked As Database
Dim tdfBase As TableDef, tdfLinked As TableDef, fld As Field

' Open the first database and create a new TableDef object.
Set dbsBase = OpenDatabase(strDbPathBase)
Set tdfBase = dbsBase.CreateTableDef(strBaseTableName)
Set fld = tdfBase.CreateField(strFieldName, intType)
tdfBase.Fields.Append fld
dbsBase.TableDefs.Append tdfBase
dbsBase.Close
Set dbsBase = Nothing

' Open the second database and create a new TableDef object.
Set dbsLinked = OpenDatabase(strDbPathLinked)
Set tdfLinked = dbsLinked.CreateTableDef(strLinkedTableName)

' Create a link to the new table in first database.
tdfLinked.SourceTableName = strBaseTableName
tdfLinked.Connect = ";DATABASE=" & strDbPathBase
dbsLinked.TableDefs.Append tdfLinked
dbsLinked.Close
dbsLinked = Nothing

Depending on the database format you are linking to, the contents of the Connect property of the new TableDef object may change. The Connect property in this example begins with:

;DATABASE=

Note that nothing precedes the semicolon. This is the default value used when you are linking to tables in Microsoft Jet-format databases. If you are linking to a table in another format, such as FoxPro, you must specify the name of the installable ISAM driver that Microsoft Jet should use. For example:

FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables

The connection to an external ODBC source may require more information:

ODBC;DSN=SuperDb;UID=sa;DATABASE=NorthwindSQL;TABLE=dbo.Products

In the Microsoft Access user interface, you can view the connection information used to link a table by opening the table in Design view, and then viewing the text in the table’s Description property.

See Also For more information about how to specify different database formats by using the Connect property, search the DAO Help index for “Connect property.”


Creating and Modifying Table Indexes

So far in this chapter, we have discussed only creating tables and adding fields to them. For your tables to be useful, however, they must provide indexes for quickly finding and ordering particular records. Without indexes, it’s necessary to search through each record in the table to find any given value.

A special form of index, the primary key index, serves to uniquely identify each record in the table. You can designate one field or set of fields in a table as a primary key. All values in the primary key must be unique and not Null, and there can be only one primary key for a table.

Another special form of index is the unique index. If you designate one field or combination of fields as a unique index, Microsoft Jet allows only one occurrence of that particular value or combination of values to be entered in your table. The unique index also serves to uniquely identify each record in the table.

This section assumes you know which types of indexes you need, and explains how to build and modify them.

See Also For information about how Microsoft Jet uses indexes for efficient data retrieval, see Chapter 4, “Queries.”


Creating and Deleting Indexes by Using SQL DDL

You can designate one or more indexes for a table either at the time the table is built, or after the fact.


Adding a Primary Key Index with SQL DDL

By using SQL DDL statements, you can specify a CONSTRAINT clause to create a primary key index at the time the table is created:

CREATE TABLE Parking_Tickets
    (Ticket_ID INTEGER CONSTRAINT PrimaryKey PRIMARY KEY,
    Violator_Name TEXT (30),
    Violation_Type TEXT (10));

This DDL statement creates a primary key index for the Ticket_ID field and gives it the name “PrimaryKey.” Although the primary key index can be given any name, PrimaryKey is a good choice because that’s the name Microsoft Access assigns by default when you create a primary key for a table in Design view.


Adding a Unique Index with SQL DDL

By using SQL DDL statements, you can specify a CONSTRAINT clause to add a unique index at the time the table is created:

CREATE TABLE Member_List
    (FirstName TEXT (10),
    LastName TEXT (20),
    DateOfBirth DATETIME,
    CONSTRAINT UniqueKey UNIQUE (FirstName, LastName, DateOfBirth));

This DDL statement creates a unique index based on the combination of the FirstName, LastName, and DateOfBirth fields. The combined values of all three fields in the index must be unique, even if two or more records have the same value in just one of the fields.


Adding a Nonunique Index with SQL DDL

You can’t use the CREATE TABLE statement to add a nonunique index. To add a nonunique index to an existing table, you can use the CREATE INDEX statement:

CREATE INDEX Ix_Violation_Type
    ON Parking_Tickets (Violation_Type)
    WITH DISALLOW NULL;

Because the index is not specified as unique, any number of records within the Parking_Tickets table can have the same Violation_Type value. After the Ix_Violation_Type index is created, searches for records involving the Violation_Type field are noticeably faster. The example uses the optional WITH DISALLOW NULL clause, which prohibits Null values in the indexed field. You can also use the WITH IGNORE NULL clause to prevent records with Null values in the indexed field or fields from being included in the index.

An index can consist of combinations of more than one field:

CREATE TABLE Order_Item
    (OrderID INTEGER,
    LineItem INTEGER,
    ProductType TEXT (10),
    ProductCode TEXT (20),
    CONSTRAINT PrimaryKey PRIMARY KEY
    (OrderID, LineItem));
CREATE INDEX Ix_ProdTypeCode
    ON Order_Item (ProductType, ProductCode);


Deleting an Index with SQL DDL

You can delete an index from a table with the DROP INDEX statement:

DROP INDEX Ix_Violation_Type ON Parking_Tickets;

Note that it may or may not be possible to delete an index at any given time, depending on the inter-table relationships you’ve defined. To set up referential integrity between two tables, the fields used to link tables must be indexed. For more information about setting up table relationships and their effect on indexes, see the “Referential Integrity and Relationships” section later in this chapter.


Creating and Deleting Indexes by Using DAO

As discussed in Chapter 2, “Introducing Data Access Objects,” a TableDef object can have one or more Index objects in its Indexes collection. Each Index object is in turn made up of one or more Field objects in its Fields collection.

Make sure you don’t confuse the Fields collection of the TableDef object itself with the Fields collection of any given Index object. All the Field objects in an Index object’s Fields collection must also be Field objects in the Fields collection of the TableDef object, but each Index object has its own Fields collection. These collections exist as a hierarchy of objects and collections.

Unlike some database systems, such as FoxPro, with Microsoft Jet you can’t create an index on an expression, such as a subset of a field or a calculated field.

It’s common to designate one field, or a combination of fields, as the primary key of a table at the time it’s created. The following function creates a primary key based on one or more fields in a table. The third argument to the function is a parameter array in which you can specify one or more field names to be included in the primary key:

Function AddPrimaryKeyToTable(tdf As TableDef, strPKName As String, _
    ParamArray varFieldName() As Variant) As Boolean
    ' This procedure creates a table with a primary key.

    Dim fld As Field
    Dim idxPK As Index
    Dim intC As Integer
    Const conTableExists As Integer = 3010

    On Error GoTo Err_AddPrimaryKeyToTable

    ' Create index.
    Set idxPK = tdf.CreateIndex(strPKName)

    For intC = 0 To UBound(varFieldName())
        ' Create field in index.
        Set fld = idxPK.CreateField(varFieldName(intC))
        idxPK.Fields.Append fld
    Next

    ' Set Primary property of index.
    idxPK.Primary = True

    ' Append index.
    tdf.Indexes.Append idxPK
    AddPrimaryKeyToTable = True

    Exit_AddPrimaryKeyToTable:
        Exit Function

    Err_AddPrimaryKeyToTable:
        MsgBox "Error: " & Err & vbCrLf & Err.Description
        AddPrimaryKeyToTable = False
        Resume Exit_AddPrimaryKeyToTable
End Function

Note that you can use any valid Microsoft Jet name for the primary key. When you add a primary key to a table in the Microsoft Access user interface, the index is automatically named PrimaryKey, but you can change the name.

See Also For information about what constitutes a valid Microsoft Jet name, see “Guidelines for Naming DAO Objects” in Chapter 2, “Introducing Data Access Objects.”

In most cases, the name you give an index is not significant. However, if you are opening table-type Recordset objects in code and want to specify the Index property of the Recordset object, you must know and specify the actual index name.

See Also For information about the various types of Recordset objects and how to use them, see Chapter 5, “Working with Records and Fields.”

You can delete an index from a table by using the Delete method on the Indexes collection of a TableDef object. In the following example, strDbPath is the path to the database, strTableName is the name of the table containing the index, and strIndex is the name of the index:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs(strTableName)
tdf.Indexes.Delete strIndex

Note For performance reasons, you can’t create more than thirty-two indexes on a table in a Microsoft Jet database.


Index Properties

An Index object can have one or more combinations of several property settings that influence its behavior. For example, the Unique property can be set, which ensures that only one record with a particular value (or combination of values, if the index consists of multiple fields) can exist. You can set the Primary property to True to designate the index as the primary key of the table. Because a primary key must uniquely identify each record, setting an index’s Primary property to True automatically sets its Unique property to True.

Because a primary key is by definition a unique index, here’s how the Primary property and the Unique property differ.

The main differences are that the fields that make up the primary key of a table can’t contain Null values, and that only one primary key can be designated per table. These restrictions don’t apply to an index with the Unique property set to True. In addition to setting the Unique property of an index, you can set the IgnoreNulls property to True, which means Null values are not added to the index; consequently, using the Seek method to find a Null value always fails.

The following code adds an SSN# field and then creates a unique index for SSN#. The index forces all non-Null values to be unique, but allows Null values when the value of the SSN# field isn’t known. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim tdf As TableDef, idx As Index
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.TableDefs("Employees")
Set fld = tdf.CreateField("SSN#", dbText, 11)
tdf.Fields.Append fld

' Create index on SSN# field.
Set idx = tdf.CreateIndex("IX_SSN#")
Set fld = idx.CreateField("SSN#")
With idx
    .Fields.Append fld
    .Unique = True
    .IgnoreNulls = True
End With
tdf.Indexes.Append idx


Indexes and ODBC Views

Earlier in this chapter, linking to tables from external data sources such as FoxPro or Microsoft SQL Server to a Microsoft Jet database file was discussed. To link to external tables, you specify the original source of the table in the Connect property of a new TableDef object and then append the TableDef object to the TableDefs collection of the database.

In addition to linking to tables in external databases, you can also link to views in SQL data sources, such as Microsoft SQL Server or Oracle. A view is a named SQL SELECT statement executed on the server that joins one or more tables and selects one or more fields.

Normally, a linked view is not updatable even if it’s based on an updatable table. For a linked table to be updatable, it must contain a primary key or a unique index.

When you link to a view rather than to a table, there is no index available and Microsoft Jet doesn’t have enough information to treat the view as updatable. To make the linked view updatable, you must create a pseudo index for the TableDef object of the linked view. The pseudo index tells Microsoft Jet which of the fields in the linked view comprise the primary key or unique index in the table underlying the view. The index exists within the Microsoft Jet database only. Nothing is created on the server. In previous versions of DAO, a pseudo index was referred to as an index specification.

The following code creates a view named USCustomers in a SQL Server database. In this case, the SQL Server database is the Microsoft Access Northwind database that’s been upsized to SQL Server by using the Microsoft Access 97 Upsizing Tools:

Dim wrk As Workspace, con As Connection
Dim strSQL As String, strConnect As String

' Create ODBCDirect workspace.
Set wrk = DBEngine.CreateWorkspace("ODBCWorkspace", "admin", "", dbUseODBC)

' Initialize connection string.
strConnect = "ODBC;DATABASE=NorthwindSQL;UID=sa;" _
    & "DSN=NorthwindSQLDSN"

' Open connection.
Set con = wrk.OpenConnection("NorthwindConnection", , , strConnect)
strSQL = "CREATE VIEW USCustomers AS " _
    & "(SELECT * FROM Customers WHERE Country = 'USA')"

' Execute SQL statement.
con.Execute strSQL

Notes

  • This code uses objects in the ODBCDirect object model. For more information about ODBCDirect, see Chapter 9, “Developing Client/Server Applications.”

  • The Microsoft Access Upsizing Tools are located in the Upsizing folder on the companion CD-ROM.

The following code links to a view on the server and then creates an index on the view to make it updatable. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDBPath)

' Create linked table based on SQL view.
Set tdf = dbs.CreateTableDef("USCustomersLinked")

' Specify SQL view as source table.
tdf.SourceTableName = "USCustomers"

' Connect to SQL database.
tdf.Connect = "ODBC;DATABASE=NorthwindSQL;UID=sa;" _
    & "DSN=NorthwindSQLDSN"
dbs.TableDefs.Append tdf
dbs.TableDefs.Refresh

' Create primary key on linked table.
dbs.Execute "CREATE INDEX PrimaryKey " & _
    "ON USCustomersLinked (CustomerID) WITH PRIMARY"

The newly linked view, USCustomersLinked, can now be updated as though it were a regular table.

See Also For more information about the updatability of external data sources, see Chapter 8, “Accessing External Data.” For more information about working with Microsoft SQL Server databases, see Chapter 9, “Developing Client/Server Applications.”


Creating Tables and Indexes in Installable ISAM Databases

As noted earlier, for database data sources other than Microsoft Jet, such as FoxPro or dBASE, a “database” is a directory that contains a number of table files; there is no need to create a physical database for these data sources.

You can use DAO to create new tables, fields, and indexes for these external databases. You can’t, however, store QueryDef objects in any database type other than a native Microsoft Jet .mdb file.

See Also For an example that creates an external table, see “Creating an External Table” in Chapter 8, “Accessing External Data.”


Microsoft Jet QueryDef Objects

As its name implies, a QueryDef object is the definition of a query, either saved permanently in a Microsoft Jet database, or created as a temporary object. A QueryDef object doesn’t contain any data or perform an action; it’s merely the set of instructions Microsoft Jet uses to select information or perform an action, such as the deletion or insertion of records in a database table.

Not everything that can be stored in a QueryDef object is what is usually thought of as a “query.” In addition to SQL SELECT statements, which select, join, and order information from one or more tables, a QueryDef object can contain an SQL DELETE, INSERT, or CREATE TABLE statement.

Some QueryDef objects return their results in the form of a set of records. These objects perform similarly to what are known as views in other relational database systems. You can create an SQL statement and save it as a permanent QueryDef object in a database, and then use that QueryDef object as if it were a table. You can update it or join it to another table or QueryDef object.

Other QueryDef objects can perform an action, such as running an SQL DELETE statement, or modify the database structure, such as the CREATE TABLE statements used throughout this chapter. A special form of the QueryDef object, known as a pass-through query, is used to communicate directly with an ODBC database such as SQL Server or Oracle. The contents of a pass-through QueryDef object are not evaluated by Microsoft Jet, but are passed through to the ODBC database, which then interprets the text contained in the SQL property of the QueryDef object.

See Also For a full discussion of how to use the various forms of QueryDef objects and how to optimize their performance, see Chapter 4, “Queries.” For more information about creating and using pass-through queries, see Chapter 9, “Developing Client/Server Applications.”

This section covers how to create and manipulate QueryDef objects in a database.

Note that a QueryDef object can be created only in a native Microsoft Jet database. A QueryDef object can’t be stored in a FoxPro or SQL Server database, for example.

There is no SQL DDL statement that can be used to create permanent QueryDef objects. All QueryDef objects must be created either interactively, through a product such as Microsoft Access, or programmatically by using DAO.


Creating a Permanent QueryDef Object

A QueryDef object is based on an SQL statement. To create a new permanent QueryDef object, you must provide a value for the name argument of the CreateQueryDef method. You can specify the SQL statement in the sqltext argument of the CreateQueryDef method, or in the SQL property of the newly created QueryDef object.

The following code creates a QueryDef object. In this example, dbs is a Database object, strQueryName is the name for the new QueryDef object, and strSQL is the SQL statement on which the QueryDef object is based:

Set qdf = dbs.CreateQueryDef(strQueryName)

' Set SQL property.
qdf.SQL = strSQL

You may initialize the strSQL variable with an SQL statement such as the following, which returns a result set containing total sales for each employee for May 1996:

strSQL = "SELECT Orders.EmployeeID, " & _
    "Sum((UnitPrice * Quantity) - Discount) AS ExtendedPrice " & _
    "FROM Orders INNER JOIN [Order Details] ON " & _
    "Orders.OrderID = [Order Details].OrderID " & _
    "WHERE (((Orders.OrderDate) " & _
    "Between #5/1/96# And #5/31/96#)) " & _
    "GROUP BY Orders.EmployeeID;"

Note that unlike with other collections, such as Properties, Fields, or Indexes, it’s not necessary to explicitly append the QueryDef object to the QueryDefs collection of the database. Simply assigning values for the name argument and the SQL property immediately creates the QueryDef object.

You can immediately use this new QueryDef object in your application. You can create a Recordset object from it, join it to another query or table, base a report on it, and so on.


Setting and Retrieving QueryDef Object Properties

Some properties of a QueryDef object can’t be set in code. For example, the Type property is a read-only property that indicates the type of SQL statement used to create the QueryDef object. The value of the Type property may be one of dbQSelect, dbQCrosstab, dbQMakeTable, and so on, depending on the value of the SQL property of the QueryDef object.

The ReturnsRecords property is always created when you add a new QueryDef object to the database, but it has meaning only in the context of an SQL pass-through query. If you are creating a pass-through QueryDef object, you should specify whether the query performs an action (such as DELETE or INSERT), or whether it returns its result as a set of records. Microsoft Jet uses this setting to determine whether to create a Recordset object to contain the results of the pass-through query when it’s executed. It’s also used to indicate whether the QueryDef object can be used in an SQL SELECT statement to join to another table or QueryDef object.

Chapter 2, “Introducing Data Access Objects,” explained that a QueryDef object can be both a permanent object stored in the database and a temporary object created and executed within code. Certain properties, such as RecordsAffected, have a value only after the QueryDef object is executed. For example, the following code creates a permanent QueryDef object that deletes records from the Orders table, and then executes the QueryDef object. After the QueryDef object executes, the RecordsAffected property contains the number of records that were deleted. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim qdf As QueryDef
Dim lngReturn As Long

Set dbs = OpenDatabase(strDbPath)

' Delete any existing QueryDef object with same name.
On Error Resume Next
dbs.QueryDefs.Delete "DeleteOldOrders"
On Error GoTo 0

Set qdf = dbs.CreateQueryDef("DeleteOldOrders")
qdf.SQL = "DELETE FROM Orders WHERE OrderDate < #9/1/94#;"

' Execute query.
qdf.Execute

' Return number of affected records.
lngReturn = qdf.RecordsAffected


Creating Parameter Queries

The Parameters collection of a QueryDef object identifies the parameters for a parameter query. The following example creates a new parameter query and prompts the user to input values for the parameters, then displays the records that match their criteria. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim qdf As QueryDef, rst As Recordset
Dim strSQL As String, strResults As String

Set dbs = OpenDatabase(strDbPath)

' Define SQL string with PARAMETERS clause.
strSQL = "PARAMETERS [Enter Start of Customer Company Name] Text, " & _
    "[Enter the City] Text; " & _
    "SELECT Customers.* " & _
    "FROM Customers WHERE " & _
    "(((Customers.CompanyName) Like " & _
    "[Enter Start of Customer Company Name] & '*') " & _
    "AND ((Customers.City)=[Enter the City]));"

' Create QueryDef object.
Set qdf = dbs.CreateQueryDef("qryCustSearch")
qdf.SQL = strSQL

' Prompt user for parameter values.
qdf.Parameters("[Enter Start of Customer Company Name]") = _
    InputBox("Enter the first few letters of the customer's company name.")
qdf.Parameters("[Enter the City]") = _
    InputBox("Enter the customer's city.")

' Open Recordset object on QueryDef object.
Set rst = qdf.OpenRecordset
Do Until rst.EOF
    strResults = strResults & rst!CompanyName & vbCrLf
    rst.MoveNext
Loop

' Show customers returned.
If Not rst.BOF And rst.EOF Then
    MsgBox "Customers matching your criteria: " & vbCrLf & strResults
End If

Unlike a TableDef object, in which you have to explicitly append each Field object to the Fields collection, the Fields and Parameters collections for QueryDef objects are automatically derived by Microsoft Jet based on the contents of the SQL property of the QueryDef object.

See Also For information about using permanent QueryDef objects, including how to supply parameter values programmatically, see Chapter 4, “Queries.”


Deleting a QueryDef Object

You can delete a permanent QueryDef object by using the Delete method on a QueryDefs collection. In this example, dbs is a Database object and strQueryName is the name of a query in that database:

dbs.QueryDefs.Delete strQueryName


Temporary QueryDef Objects

It’s not always necessary to create a permanent QueryDef object in order to execute an SQL statement, or to create a Recordset object in code. For example, you can pass an SQL string directly to the Execute method of a Database object in order to delete or insert records in a table. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database

Set dbs = OpenDatabase(strDbPath)
dbs.Execute "DELETE FROM Orders WHERE OrderDate < #9/1/94#;"

Microsoft Jet automatically parses the SQL statement and creates a temporary QueryDef object in the database, executes the temporary QueryDef object, and then deletes the QueryDef object.

The advantage to this method is that you avoid adding many permanent QueryDef objects to the database. The code becomes more readable because you don’t have to refer to an external tool such as Microsoft Access to find out what the QueryDef object is supposed to accomplish.

However, when you use a temporary QueryDef object, you can’t take advantage of certain properties of a permanent QueryDef object, such as the RecordsAffected property. To work around this problem, you can use the RecordsAffected property on the Database object. In addition, there is a slight decrease in performance each time the temporary QueryDef object is created and deleted; it must be parsed and “compiled” into its internal storage format each time this code is executed. A permanent QueryDef object is parsed and compiled once, when it’s first executed, so subsequent executions can begin more quickly. Also, because the temporary QueryDef object is not named, it can’t be used in a SELECT statement to join to another table or QueryDef object.

For multiuser databases, there are advantages to creating a QueryDef object that is not permanently saved in a database. You may want to do this, for example, if you are building up a SELECT query on the fly in code based on user input. This way, you don’t need to create uniquely named QueryDef objects in a multiuser database. To create a temporary QueryDef object, you simply supply a zero-length string ("") for the name argument of the CreateQueryDef method. You can specify the SQL statement in the sqltext argument of the CreateQueryDef method or as the SQL property setting of the newly created QueryDef object.

A common situation in which it’s to your advantage to create a temporary QueryDef object is when you have to execute it repeatedly, but it’s not needed on a permanent basis. The following example creates a temporary parameter QueryDef object that repeatedly builds a Recordset object until the user clicks Cancel. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim qdf As QueryDef
Dim rst As Recordset
Dim strCust As String, strMsg As String

Set dbs = OpenDatabase(strDbPath)

' Create temporary QueryDef object.
Set qdf = dbs.CreateQueryDef("")

' Construct SQL string.
qdf.SQL = "PARAMETERS [Which CustomerID] Text; " & _
    "SELECT Count(*) AS RecordCount, Orders.CustomerID " & _
    "FROM Orders GROUP BY Orders.CustomerID " & _
    "HAVING Orders.CustomerID Like [Which CustomerID];"

' Initialize variable.
strCust = " "

' Prompt user for input.
Do While Len(strCust) > 0
    strCust = InputBox("Enter first letter or letters of customer name.")
    If Len(strCust) > 0 Then
        ' Append wildcard character to input.
        strCust = strCust & "*"
        ' Provide value for paramter.
        qdf![Which CustomerID] = strCust
        ' Open Recordset object on QueryDef object.
        Set rst = qdf.OpenRecordset
        strMsg = strMsg & vbCrLf & "Orders for Customer(s) like " & strCust & ": "
        ' Loop through Recordset object to append values to string.
        Do Until rst.EOF
            strMsg = strMsg & vbCrLf & rst!CustomerID & ": " & rst!RecordCount
            rst.MoveNext
        Loop
        strMsg = strMsg & vbCrLf
    End If
Loop

' Display string if it is not empty.
If Len(strMsg) > 0 Then
    MsgBox strMsg
End If

If the action specified by the QueryDef object is going to be performed only once, there is no need to create a temporary QueryDef object. You can pass the SQL statement directly to the Execute method of a Database object, for example, or use the SQL SELECT statement as the argument to an OpenRecordset method. In the following example, strDbPath is the path to the database and strTableName is the name of the table whose records are to be counted:

Dim dbs As Database
Dim rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("SELECT Count(*) AS RecordCount FROM " _
    & strTableName & ";")


Referential Integrity and Relationships

So far this chapter has discussed individual table objects in isolation. With Microsoft Jet, you can use Relation objects to define how tables relate to one another. By creating relationships between tables, you can ensure that the data in your tables remains logically consistent. You can, for example, make sure that no orders are taken for nonexistent customers. You can prevent someone from deleting an employee record if there are corresponding entries in a Tasks table.

The rules that govern relationships between tables are known as referential integrity constraints. For relationships in which referential integrity is enforced, you can specify whether you want cascading updates and cascading deletes to be automatically performed on related records. If you enable cascading updates, changing a record in the primary table causes the change to be automatically made to all related records in the related table or tables. If you enable cascading deletes, deleting a record in the primary table causes the deletion of all related records in the related table or tables.

In addition to flagging attempts to violate referential integrity as errors, with Microsoft Jet you can specify what action should be taken when an attempt occurs. For example, by using cascading deletes, your application can automatically delete all of the related task’s entries when you delete an employee record. Cascading updates can be used to change foreign key values in related tables when the primary key of a primary table is changed (a foreign key is a field that refers to the primary key field in another table).

After you establish the referential integrity rules governing the relationships between the tables in your database, the burden of enforcing these rules is removed from your application’s code. You don’t have to check explicitly for conditions that violate referential integrity. Microsoft Jet informs you when a violation attempt occurs.

So, whether your database program is updated by a user modifying the tables from within Microsoft Access, or by an ODBC call from a custom third-party application, the logical consistency of the data in your tables is preserved.


Creating and Deleting Relationships by Using SQL DDL

In the section “Creating and Modifying Table Indexes,” one form of the CONSTRAINT clause used in the SQL DDL language was discussed. In those examples, CONSTRAINT clauses are used to create single-field or multifield indexes for a table, either at the time the table is created (with CREATE TABLE) or after the fact (with ALTER TABLE).

Another use of the CONSTRAINT clause is to create a relationship (or reference) from a foreign key of one table to the primary key of another table. With this constraint in place, it’s impossible to enter a value in the foreign key field of a table that doesn’t already exist in the primary key field of the primary table.

In the following example, two indexes are created. The first is the primary key index for TaskID, and the second is a foreign key index for the EmployeeID field:

CREATE TABLE Tasks
    (TaskID INTEGER CONSTRAINT PrimaryKey PRIMARY KEY,
        EmployeeID INTEGER
    CONSTRAINT FK_EmployeeID REFERENCES Employees
        (EmployeeID),
    DeptID TEXT (10),
    TaskCompleted DateTime);

The next example creates a new table called Departments. The DeptID field is the primary key for the Departments table. The DeptID field is the foreign key in the Tasks table used in the example above:

CREATE TABLE Departments
    (DeptID TEXT (10) CONSTRAINT PrimaryKey PRIMARY KEY,
    Description TEXT (50) );

When these statements are executed, Microsoft Jet creates two new indexes, PrimaryKey and FK_EmployeeID, in the Indexes collection of the newly created Tasks TableDef object. In addition to the new foreign key indexes, a new Relation object is created in the database. As discussed in Chapter 2, “Introducing Data Access Objects,” a database contains a Relations collection, which stores information about all the inter-table relationships defined for your database. Each Relation object contains several properties, such as Table and ForeignTable, which identify the tables that participate in the relationship. Each Relation object also contains a Fields collection, which identifies the Field objects that make up the Relation object.

You can create a relationship for an existing table by using the ALTER TABLE statement. The following example creates a new relationship between the Tasks table and the Departments table:

ALTER TABLE Tasks
    ADD CONSTRAINT FK_DeptID
    FOREIGN KEY (DeptID) REFERENCES Departments (DeptID);

Setting a referential integrity foreign key constraint is the only form of relationship that can be established through SQL DDL. To set other Relation object types, such as cascading updates or cascading deletes, you must create the Relation object through DAO.

There is no way to modify an existing relationship. You must delete the relationship and then re-create it.

To delete a relationship by using SQL DDL, use an ALTER TABLE statement with the DROP CONSTRAINT clause. The following SQL DDL statement deletes the relationship created between the Tasks table and the Departments table in the previous example:

ALTER TABLE Tasks
    DROP CONSTRAINT FK_DeptID


Creating and Deleting Relationships by Using DAO

Relation objects can be created by using DAO in much the same way you can create an Index object or a TableDef object. You can append a new Relation object to the Relations collection of the database after first appending one or more Field objects to the Fields collection of the new Relation object. In addition, you can set the appropriate Attributes property for the Relation object to specify that the Relation object perform cascading updates or cascading deletes to maintain referential integrity.

The following example deletes and re-creates the relationship between the Employees table and the Orders table in the NorthwindTables database. In the following example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim rel As Relation
Dim fld As Field

Set dbs = OpenDatabase(strDbPath)

' Delete existing relationship.
On Error Resume Next
For Each rel In dbs.Relations
    If rel.Table = "Employees" And rel.ForeignTable = "Orders" Then
        dbs.Relations.Delete rel.Name
    End If
Next rel
On Error GoTo 0

Set rel = dbs.CreateRelation("EmployeesOrders")
With rel
    ' Set Table property to table on "one" side of relation.
    .Table = "Employees"
    ' Set ForeignTable property to table on "many" side of relation.
    .ForeignTable = "Orders"
End With

Set fld = rel.CreateField("EmployeeID")
' Specify name of foreign key field in foreign table.
fld.ForeignName = "EmployeeID"

rel.Fields.Append fld
dbs.Relations.Append rel

This code first creates a new Relation object by using the CreateRelation method of the Database object. The Table property specifies the “one” table in the one-to-many relationship; the ForeignTable property specifies the “many” table. Setting these two properties ensures that no EmployeeID value can be entered in the Orders table if that value doesn’t already exist in the Employees table.

You can also set the Attributes property for a Relation object when you create the Relation object. For example, the NorthwindTables database includes a Suppliers table and a Products table. A relationship based on the SupplierID field exists between the two tables. To specify that cascading deletes be performed when a record is deleted from the Suppliers table, set the Attributes property to the built-in constant value dbRelationDeleteCascade. This attribute specifies that if a record is deleted from the Suppliers table, all records in the Products table that have a matching SupplierID field are also deleted. In this way, if a supplier goes out of business, you can automatically delete that supplier’s products from the Products table when you delete the supplier from the Suppliers table. To specify cascading updates, set the Attributes property to dbRelationUpdateCascade.

Multiple attributes can be specified for a relationship by adding the various constant values used to set the Attributes property. For example, the following code specifies an optional left join between two tables, where rel is the name of a relationship:

rel.Attributes = dbRelationLeft + dbRelationUnique + _
    dbRelationDontEnforce

Microsoft Access Users Designating a Relation object attribute as “left” or “right” has no effect on referential integrity. It’s used in Microsoft Access to show the type of join that’s created by default when the tables are added to a new query in Design view.

In the SQL DDL example shown earlier in this chapter, creating a new relationship between two tables involves creating an index on the foreign key field. When you create a new Relation object by using DAO, Microsoft Jet automatically creates an index on the foreign key field or fields, even if one already exists. If you create a relationship between two tables with referential integrity enforced, avoid creating an index on the foreign key, as it will be a duplicate index. The foreign key index is necessary for enforcing referential integrity, so don’t delete it. The index that’s created on the foreign key when you create a relationship appears in the Indexes collection of the foreign table.

Microsoft Access Users Because the foreign key index that Microsoft Jet creates is associated with a relationship, it’s not displayed in the Indexes dialog box when you view the table in Design view in Microsoft Access.

It’s possible to create a relationship that’s based on more than one field. To create a Relation object with two fields, create two Field objects in the Fields collection of the new Relation object. Set the ForeignName property of each Field object to the name of the corresponding field in the foreign table, then append both fields to the Fields collection of the Relation object. For example, suppose you have two tables that both contain FieldA and FieldB. You can create a relationship based on both fields if you want to enforce referential integrity for both fields at the same time. You would create both fields in the Fields collection of a Relation object, and set the ForeignName property appropriately.

Note that the primary key in the primary table does not necessarily need to include both FieldA and FieldB. The primary key can be based on both fields, or on just one of the fields.

You can’t modify an existing relationship from DAO. You must delete the relationship and then re-create it. To delete a relationship, use the Delete method on the database’s Relations collection.


Other Relationship Types

So far, only Relation objects created between two permanent tables that exist in the same database have been discussed. With Microsoft Jet, you can create relationships between tables and QueryDef objects, or between tables in a local database and tables in external databases.

It’s not possible to create a cascading update or cascading delete relationship between a table and a QueryDef object. Nor is it possible to enforce referential integrity between a table and a query. Enforced referential integrity is valid only for relationships between tables. Creating a relationship between a table and a query has no effect on the structure of your database.

The primary benefit of creating a relationship between a table and a query or between a local table and a linked table is for documentation purposes and for interactive use with a product such as Microsoft Access. Microsoft Access can make certain assumptions about the relationships between your tables if you have defined these non-enforceable relationships. For example, if you create a relationship between a table and a query, and then add that table and query to a new query in query Design view, Microsoft Access automatically draws in the join line between the table and the query.


Chapter 4 Queries

Microsoft Jet database engine has sophisticated query and optimization capabilities that are unmatched by other desktop database engines in its class. These features include updatable views, heterogeneous joins, and the ability to work seamlessly with a wide variety of industry-standard database formats.

The Microsoft Jet query engine is designed to accept user requests for information or action in the form of Structured Query Language (SQL) statements. Microsoft Jet parses, analyzes, and optimizes these queries, and either returns the resulting information in the form of a Recordset object or performs the requested action.

Although Microsoft Jet borrows many query techniques from client/server relational database management systems (DBMSs) such as Microsoft SQL Server, it remains a file-server database. All queries are processed on individual workstations running copies of a host application, such as Microsoft Access, or a custom application created by using a tool, such as Microsoft Visual Basic. Microsoft Jet doesn’t act as a true database server, such as SQL Server, that process data requests independently of the application requesting data. However, Microsoft Jet can send queries to SQL Server or other ODBC database servers for processing.

To understand how Microsoft Jet parses, optimizes, and processes queries, it’s important to understand the distinction between a client/server DBMS and a file-server system.

Understanding how the Microsoft Jet query engine works will give you a head start in designing your application to take advantage of the unique strengths and features of Microsoft Jet.

Getting Answers to Your Questions

Updating Your Data with Queries

Other Query Types

Microsoft Jet Query Engine Overview

Query Optimization

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications (VBA) are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

This chapter includes SQL statements that you can use in Visual Basic or run from SQL view of the Query window in Microsoft Access. To make it easier to use the SQL statements in this chapter in code, all SQL statements are saved as string constants in the modules for Chapter 4. In addition, each SQL statement is saved as a query in the NorthwindTables database.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.

Note The examples in this chapter show strings within an SQL statement delimited by single quotation marks: 'string'. You can use single quotation marks to delimit a string in Visual Basic or in SQL view of the Query window in Microsoft Access. However, if the string itself contains an apostrophe, an error occurs.

Pairs of double quotation marks (""string"") are optimal delimiters when you’re including an SQL statement in Visual Basic code. You can also use Chr(34) to return a string containing a pair of double quotation marks; for example, the expression Chr(34) & Chr(34) & "string" & Chr(34) & Chr(34) concatenates to ""string"".

If you’re running an SQL statement from SQL view in Microsoft Access, you may want to use a single set of double quotation marks to delimit a string: "string". If you use a pair of double quotation marks, Microsoft Access returns an error.


Getting Answers to Your Questions

Microsoft Jet uses a dialect of SQL. For more information about the Microsoft Jet implementation of SQL, see Appendix B, “SQL Reference.” You form your requests for information by writing SQL queries or by building the query interactively through a tool such as the query design grid in Microsoft Access.

It’s not the intention of this chapter to provide a tutorial on SQL. Instead, the focus is on the unique features of the Microsoft Jet implementation of SQL and how Microsoft Jet turns SQL statements into the answers you want.


Communicating with Microsoft Jet

Your SQL queries can be saved as permanent QueryDef objects in the database or created, analyzed, and executed on an ad-hoc basis.

The saved QueryDef object is a particularly important object in a Microsoft Jet database. It’s a convenient way to store and reuse commonly asked questions. After you analyze your business problem and create a query that answers that question, you can save the “question” as a permanent query. From then on, you can ask the question again by rerunning the query.

Each time you prepare a request for information, either by sending an SQL statement as an argument to the OpenRecordset method of a Database object or by saving a QueryDef object in your database, Microsoft Jet runs through a complex series of analysis and optimization steps. When you create a QueryDef object, Microsoft Jet performs a parsing phase (reading and interpreting your SQL statements) and an optimization phase (turning that SQL statement into a plan for the most efficient way to retrieve your answer). When you create a permanent QueryDef object, these steps are performed once. You can then execute the saved QueryDef object either to retrieve the answer you need, or to perform the action you requested. For a full discussion of how Microsoft Jet interprets and executes your queries, see “Query Optimization” later in this chapter.

See Also For a full discussion of how QueryDef objects are created and stored in a Microsoft Jet database file, see Chapter 3, “Data Definition and Integrity.”

The QueryDef object is also important because it can be used, for the most part, as if it were a table in your database. You can prepare a complex, multiple-table join that brings together data from a variety of sources and then summarizes and analyzes that data. But because the query is saved as a permanent QueryDef object, that complexity can be hidden from your application. You only have to know the name of the QueryDef object in order to use it. Depending on the type of query created, the query can be updatable: Any changes made to the Recordset object built from an updatable QueryDef object are automatically reflected in the underlying tables.

The examples in this chapter present only the text of the SQL statements that illustrate various Microsoft Jet query features under discussion. To execute these queries, you must save them as permanent QueryDef objects, or execute them in VBA code.

See Also For information about how to create Recordset objects based on these SQL statements, see Chapter 5, “Working with Records and Fields.”


Selecting Fields from a Single Table

As with other SQL dialects, Microsoft Jet SQL uses the SELECT statement to retrieve one or more fields from a given table. You may be familiar with the SQL statements generated by the Microsoft Access query design grid. Following is a simple example that selects all fields from the Customers table:

SELECT Customers.*
FROM Customers;

Here is another example generated by Microsoft Access, which selects particular fields rather than all fields:

SELECT
    Products.ProductID,
    Products.ProductName,
    Products.UnitsInStock,
    Products.UnitPrice*Products.UnitsInStock AS InventoryValue
FROM Products;

Note that when Microsoft Access displays this query it automatically qualifies each field reference with its base-table name. Microsoft Jet accepts a wide variety of table and field identifiers, including names with spaces and other punctuation. If an identifier contains spaces or any nonstandard characters, it must be enclosed in square brackets.

Microsoft Jet is flexible in its interpretation of SQL text strings. The previous query would work just as well if it were rewritten in a style slightly more familiar to experienced SQL users:

SELECT
    P.ProductID,
    P.ProductName,
    P.UnitsInStock,
    P.UnitPrice*P.UnitsInStock AS InventoryValue
FROM Products P;

This example uses the alias P to refer to the Products table and qualifies references to fields with that alias name. Although Microsoft Jet allows the syntax used above, queries built with the Microsoft Access query design grid add the AS reserved word in front of the alias name:

SELECT
    P.ProductID,
    P.UnitsInStock
FROM Products AS P;


Restricting, Ordering, and Summarizing

In most cases you want to do more than just list all items in a single table, or even all items in selected fields in that table. You want to narrow your selection to a manageable subset, and you want to see the result in a certain sequence. It’s also helpful to have the query engine summarize and analyze your data.


The WHERE Clause

You use the WHERE clause to restrict the records returned by your query to those matching your criteria specified by the WHERE clause. The WHERE clause evaluates each record in the set of input records and decides whether the expression contained in the WHERE clause evaluates to True. If it does, the record is selected. If not, the record is omitted from the query’s result set.

While it’s beyond the scope of this chapter to give a complete tutorial on SQL, the following representative examples point out some of the unique features of the Microsoft Jet version of SQL.

See Also For more information about SQL syntax, see Appendix B, “SQL Reference.”

Selecting Matching Values
SELECT Products.*
FROM Products
WHERE Products.CategoryID = 2;

For each record in the Products table where the CategoryID field is equal to the literal value 2, the record is selected.

Selecting by Using Partial String Matching
SELECT Products.*
FROM Products
WHERE Products.ProductName Like 'CH*';

Microsoft Jet uses partial match (or “wildcard”) characters with the Like operator that are different from those used in most SQL dialects. The asterisk (*) character matches zero or more characters and is equivalent to the percent (%) character in ANSI SQL. The other Microsoft Jet partial match characters are the question mark (?), which matches any character in a single field, and the number sign (#), which matches any digit in a single field.

Some SQL dialects require you to enclose text literals within single quotation marks. However, Microsoft Jet accepts literals enclosed in either single or double quotation marks.

Selecting Boolean Values
SELECT Products.*
FROM Products
WHERE Products.Discontinued = True;

Microsoft Jet stores Boolean values as either – 1 or 0. The constant value True is equal to – 1; False is equal to 0. You can substitute Yes for True and No for False within the text of the SQL statement. Note, however, that if you refer to Boolean recordset values within your VBA code, you must use the values – 1 for True and 0 for False, because Yes and No aren’t recognized there.

Selecting by Using Date Literals and the Between...And Operator
SELECT Orders.*
FROM Orders
WHERE Orders.OrderDate Between #3/1/96# And #6/30/96#;

The convention Microsoft Jet uses to search for DATETIME values is to enclose literal search values in number signs (#). The literal either can include the date only, as in the following example, or it can be fully qualified with the date and time:

SELECT Orders.*
FROM Orders
WHERE Orders.OrderDate > #5/24/96#;

Note that this date literal must always be expressed in MM/DD/YY order. To avoid the ambiguity of the meaning of stored queries, Microsoft Jet doesn’t follow the international date format settings specified in the user’s Control Panel.

Selecting by Using the IN Clause and a Value List
SELECT Orders.*
FROM Orders
WHERE Orders.ShipCity IN ('London','Madrid','Rome');

The previous code selects a single value in the underlying table from a list of literal values. The IN clause can be combined with NOT:

...WHERE Orders.ShipVia NOT IN (1,3);
Combining More Than One Expression
SELECT Orders.*
FROM Orders
WHERE Orders.OrderDate > #3/1/96# AND Orders.ShipVia = 2;

A WHERE clause can consist of more than one Sub procedure that combines various AND and OR clauses. The truth of the entire WHERE clause is evaluated by using standard rules of Boolean logic. You can use parentheses with the Sub procedure to ensure the order of evaluation. Note, however, that the absence or presence of parentheses doesn’t affect the order in which joins are performed; parentheses affect only the order in which Boolean expressions are evaluated.

Selecting Unique Values
SELECT DISTINCT Employees.Title
FROM Employees;

Although not related to the WHERE clause, you can use the DISTINCT predicate of the SELECT clause to limit output records to specific unique combinations of output fields. In the previous example, one occurrence of each unique Title field value used in the Employees table is retrieved. If the output contains more than one field, only records unique across all selected fields are output, as in the following example:

SELECT DISTINCT
    Employees.Title,
    Employees.TitleOfCourtesy
FROM Employees;
Selecting Unique Records

The DISTINCTROW predicate omits data based on entire duplicate records, not just duplicate fields. For example, you could create a query that joins the Customers and Orders tables on the CustomerID field. The Customers table contains no duplicate CustomerID fields, but the Orders table does because each customer can have many orders. The following SQL statement shows how you can use DISTINCTROW to produce a list of companies that have at least one order but without any details about those orders:

SELECT DISTINCTROW CompanyName
FROM Customers INNER JOIN Orders
ON Customers.CustomerID = Orders.CustomerID
ORDER BY CompanyName;

If you omit DISTINCTROW, this query produces multiple rows for each company that has more than one order. DISTINCTROW has an effect only when you select fields from some, but not all, of the tables used in the query.

Note DISTINCTROW is ignored if your query includes only one table, or if you output fields from all tables.

Microsoft Access Users The DISTINCTROW predicate is the SQL equivalent of setting the Microsoft Access UniqueRecords property in a query to True. In Microsoft Access 97, the default setting for the UniqueRecords property in a query is False, which eliminates DISTINCTROW from the query’s SQL statement; in prior versions of Microsoft Access, the default setting for the UniqueRecords property is True, which adds DISTINCTROW to the query’s SQL statement. Three types of queries are affected by this change: UPDATE queries, DELETE queries, and SELECT queries.

  • UPDATE queries If you run an UPDATE query that you imported from a Microsoft Access 2.0 or 95 database, or if you run an existing UPDATE query in a database that you converted to Microsoft Access 97 from an earlier version, you may receive the following error message: “Operation must use an updatable query.”

  • To resolve this error message, set the query’s UniqueRecords property to False.

  • DELETE queries When a DELETE query contains more than one table (for example, a query that deletes duplicate records from one of the tables), the UniqueRecords property must be set to True for all versions of Microsoft Access. However, because the default value for the UniqueRecords property is False in Microsoft Access 97, you must set the value of this property manually when you create a new DELETE query in Microsoft Access 97.

  • SELECT queries Some SELECT queries that are updatable in Microsoft Access 2.0 or 95 may display the following message in the status bar when you try to update them in Microsoft Access 97: “This recordset is not updatable.”

    To resolve this error, set the query’s UniqueRecords property to False.

Selecting by Using Nested Subqueries
SELECT Orders.*
FROM Orders
WHERE Orders.CustomerID IN
(SELECT CustomerID FROM Customers WHERE City = 'London');

This example matches each CustomerID value in the Orders table against a list that’s created by selecting the CustomerID values of all customers whose city is London. Although this is a standard SQL query, and legal in Microsoft Jet SQL, this particular search may be executed more efficiently by directly joining the Orders table to the Customers table.

Selecting by Using Correlated Subqueries
SELECT
    T1.LastName,
    T1.FirstName,
    T1.Title,
    T1.Salary
FROM Employees AS T1
WHERE T1.Salary >=
    (SELECT Avg(Salary)
    FROM Employees
    WHERE Employees.Title = T1.Title)
ORDER BY T1.Title;

A correlated subquery is evaluated once for each record processed by the main SELECT query. In this example, the Salary field of each input record in the Employees table (here given the alias name T1) is compared to the results of a sub-SELECT query. The sub-SELECT query uses the value in the main Title field as a selection criterion. Because the Title field in the main Employees table can change for each record processed, the sub-SELECT query must be re-executed for each record of the main table that’s processed. For example, if there are three employees with the title of Sales Representative, and the average of their three salaries is equal to $100,000 dollars, the main query will return only employees whose salary is greater than or equal to $100,000.

In addition to the features shown in the previous examples, Microsoft Jet supports a variety of other predicates for subqueries, including EXISTS, ANY, and ALL.


Ordering the Result Set

Microsoft Jet SQL uses the ORDER BY clause to present the selected records in the order you designate:

SELECT Products.*
FROM Products
ORDER BY UnitPrice DESC, ProductName;

In this example, records are retrieved in descending order by unit price. All records that match a given price are put in ascending order based on the values in the ProductName field. Although it’s legal to use the ASC reserved word with an ORDER BY clause, it’s not required. If ASC is omitted, the default ascending order is used.

If an ORDER BY clause is used in combination with a WHERE clause, it follows the WHERE clause:

SELECT Orders.*
FROM Orders
WHERE Orders.OrderDate >= #4/1/96#
ORDER BY Orders.EmployeeID;

The ORDER BY clause can contain an expression as well as a field in the underlying table, as in this example:

SELECT
    Orders.OrderID,
    Orders.RequiredDate,
    IIf(ShipCountry = 'USA',RequiredDate+5,RequiredDate+10)
        AS PastDueDate
FROM Orders
ORDER BY
    IIf(ShipCountry = 'USA',RequiredDate+5,RequiredDate+10);

It’s also possible to specify the sort fields by their field positions rather than by their names, as in the following example:

SELECT
    Employees.EmployeeID,
    Employees.LastName,
    Employees.FirstName
FROM Employees
ORDER BY 3, 1;

This SELECT statement orders the query output by the third and first fields. In this case, the sort fields are the FirstName and EmployeeID fields. This feature is also useful with UNION queries, which are discussed later in this chapter.


Summing and Analyzing

Microsoft Jet supports the standard SQL aggregate functions Count, Sum, Avg, Max, and Min to perform table-level or group-level totaling:

SELECT
    Count(Products.UnitPrice) AS CountOfUnitPrice,
    Sum(Products.UnitPrice) AS SumOfUnitPrice,
    Avg(Products.UnitPrice) AS AvgOfUnitPrice,
    Min(Products.UnitPrice) AS MinOfUnitPrice,
    Max(Products.UnitPrice) AS MaxOfUnitPrice
FROM Products;

The previous query retrieves a single record that collects statistics on all products in the table. It can be combined with a WHERE clause:

SELECT
    Count(Products.UnitPrice) AS CountOfUnitPrice,
    Avg(Products.UnitPrice) AS AvgOfUnitPrice
FROM Products
WHERE Products.SupplierID = 6;

If no records match the selection criteria, the output is still produced. The value of any Count aggregate fields is zero; the value of the other aggregate types is Null.

You use the SQL function Count(fieldname) to tally occurrences of non-Null values in a specified field. A special form of Count is Count(*), which counts the number of records. As discussed in the section “Common Pitfalls” later in this chapter, Count(*) should be used whenever possible, because this expression can sometimes be executed more quickly by using the Rushmore query-optimization technology.

See Also For information about how to optimize queries used with Open Database Connectivity (ODBC) data sources, see Chapter 8, “Accessing External Data,” and Chapter 9, “Developing Client/Server Applications.”

The argument to these aggregate functions can be an expression as well as a field name, as in the following example:

SELECT Max(UnitPrice*Quantity) AS MaxExtPrice
FROM [Order Details];

In addition to these standard aggregate functions, Microsoft Jet supports the SQL aggregate functions First and Last, which retrieve the first and last values of the set of records, and the StDev, StDevP, Var, and VarP statistical and financial functions. Note that because the StDev, StDevP, Var, and VarP functions are Microsoft Jet-specific, they aren’t likely to be supported on any other client/server DBMS, and so will have to be evaluated locally.


Aggregate Values and GROUP BY

Aggregate values can be calculated at levels other than for the entire table. The SQL GROUP BY clause is used to create one output record per each group of unique values, and optionally, to produce summary values for selected fields. GROUP BY used alone creates an output record that specifies the unique values:

SELECT
    Products.CategoryID,
    Products.SupplierID
FROM Products
GROUP BY
    Products.CategoryID,
    Products.SupplierID;

The previous query lists the unique combinations of CategoryID and SupplierID, and produces the same results as:

SELECT DISTINCT
    Products.CategoryID,
    Products.SupplierID
FROM Products;

As in other SQL dialects, any output fields in a GROUP BY query must be either part of an aggregate function or be in the GROUP BY clause.

When an aggregate function is combined with a GROUP BY clause, one record of output is produced for each unique value in the GROUP BY clause. The aggregate totals are accumulated and output for each record, and then reset for the next group, as in this example, which finds the oldest birth date for each group of employees that have the same title:

SELECT
    Employees.Title,
    Min(Employees.BirthDate) AS MinOfBirthDate
FROM Employees
GROUP BY Employees.Title;

You can also sort the output of the GROUP BY query by adding an ORDER BY clause, as in this example, which finds the most recently hired employee in each country:

SELECT
    Employees.Country,
    Max(Employees.HireDate) AS MaxOfHireDate
FROM Employees
GROUP BY Employees.Country
ORDER BY Max(Employees.HireDate) DESC;


Applying Selection After Grouping with HAVING

You use the HAVING clause to apply selection criteria after data aggregation has been performed. Records are selected and summarized, and then only those meeting the conditions specified in the HAVING clause are displayed:

SELECT
    Products.CategoryID,
    Sum(Products.UnitsInStock) AS SumOfUnitsInStock
FROM Products
GROUP BY Products.CategoryID
HAVING Sum(Products.UnitsInStock) > 400;

The HAVING clause determines which groups created by the GROUP BY clause are returned in the query output. Note that although the HAVING clause and the WHERE clause are similar, they aren’t interchangeable. HAVING is used only in conjunction with the GROUP BY clause.


Joining Tables

The power of a relational database system is evident when you combine results from one table with those from another. Microsoft Jet is flexible in the type and format of the SQL statements used to create a relational join between tables. In addition to joins in which the linking criteria is specified in the WHERE clause, Microsoft Jet SQL can use the JOIN clause to specify inner joins, left and right outer joins, and self-joins, as well as non-equi-joins, in which the linking criterion isn’t a match in values between two tables, but a generic conditional expression that evaluates to True. Microsoft Jet also supports the UNION statement, which concatenates the results of two or more SELECT statements.


Joins Specified in the WHERE clause

With Microsoft Jet, you can create relational joins in SQL by specifying the linking condition in the WHERE clause:

SELECT
    Products.ProductID,
    Products.ProductName,
    Categories.CategoryID,
    Categories.CategoryName
FROM Categories, Products
WHERE Products.CategoryID = Categories.CategoryID;

This query uses two fields each from the Products and Categories tables and selects records from each table where the CategoryID field in Products matches the CategoryID field in Categories.


The INNER JOIN

The SQL query produced by the query design grid in Microsoft Access uses the INNER JOIN clause to join two tables. Here is the same query as produced by Microsoft Access:

SELECT
    Products.ProductID,
    Products.ProductName,
    Categories.CategoryID,
    Categories.CategoryName
FROM Categories
INNER JOIN Products
ON Categories.CategoryID = Products.CategoryID;

The INNER JOIN names the linking criterion used to find matches between the two tables. This is the preferred format for specifying joins with Microsoft Jet, for two reasons. First, this format makes the join criteria explicit, rather than inferring it from the condition in the WHERE clause. Second, Microsoft Jet requires the use of this format in order for the results of this join to be updatable.

See Also For a full discussion of query updatability, see Chapter 5, “Working with Records and Fields.”

Many people also prefer this format because it’s self-documenting, unlike the implicit join specified in the WHERE clause. The WHERE clause is then reserved for selection criteria, rather than doing dual-duty as a join specifier:

SELECT
    Products.ProductID,
    Products.ProductName,
    Categories.CategoryID,
    Categories.CategoryName
FROM Categories
INNER JOIN Products
ON Categories.CategoryID = Products.CategoryID
WHERE Products.CategoryID IN (1,3);

A join can specify more than one linking condition. The NorthwindTables sample database contains the saved query PreferredSuppliers, which includes all suppliers that provide a product that has sold more than $10,000 worth of inventory. The following query is based on the PreferredSuppliers query and the Products table:

SELECT
    PreferredSuppliers.ProductID,
    PreferredSuppliers.SupplierID,
    Products.CategoryID,
    Products.ProductName
FROM PreferredSuppliers
INNER JOIN Products
ON (PreferredSuppliers.SupplierID = Products.SupplierID)
AND (PreferredSuppliers.ProductID = Products.ProductID);

Complex join conditions can be specified. The following example joins six tables:

SELECT
    Orders.OrderID,
    Employees.EmployeeID,
    Customers.CompanyName,
    [Order Details].ProductID,
    Products.ProductName,
    Categories.CategoryName
FROM
    Categories INNER JOIN
     (Products INNER JOIN
        (Employees INNER JOIN
         (Customers INNER JOIN
            (Orders INNER JOIN [Order Details]
             ON Orders.OrderID = [Order Details].OrderID)
            ON Customers.CustomerID = Orders.CustomerID)
         ON Employees.EmployeeID = Orders.EmployeeID)
        ON Products.ProductID = [Order Details].ProductID)
     ON Categories.CategoryID = Products.CategoryID;


The Left and Right Outer Joins

The INNER JOIN clause retrieves records only when there is a match in both tables. In some cases, you may want to retrieve the values from one table even when there are no matching values in the other joined table. For example, you want to create a join from the Orders table to the Employees table, but you don’t want to exclude orders from the result set simply because there is no corresponding record in the Employees table:

SELECT
    Orders.*,
    Employees.LastName
FROM Employees
RIGHT OUTER JOIN Orders
ON Employees.EmployeeID = Orders.EmployeeID;

When you specify a left or right outer join, Microsoft Jet retrieves all records from one table even though there are no matches in the other table. In this example, all records from the Orders table are retrieved even when the value of the EmployeeID field is unknown.

Note that the word “OUTER” is optional in Microsoft Jet SQL syntax, because a left or right join implies an outer join.

Whether the join is considered a left or a right join is determined from the order of the tables listed in the join clause. There is no effect on efficiency, or in the actual records returned. The previous query can be rewritten as follows, and the same records would be returned:

SELECT
    Employees.LastName,
    Orders.*
FROM Orders
LEFT JOIN Employees
ON Orders.EmployeeID = Employees.EmployeeID;

Microsoft Jet treats a right join as if it were a left join with the tables reversed.


Outer Joins and Null Values

Microsoft Jet returns the special value Null in all fields for the records in the “outer” table in which no match is found. You can take advantage of this feature by testing for the presence of Null values in the outer table. For example, the NorthwindTables database contains a query named SuppliersInUSA, which returns records for those suppliers located in the United States. Suppose you want to find all the products in the Products table whose suppliers aren’t in the result set for the SuppliersInUSA query. One way is to create a nested sub-SELECT query:

SELECT Products.*
FROM Products
WHERE Products.SupplierID NOT IN
    (SELECT SupplierID FROM SuppliersInUSA;);

This query creates a list of products from suppliers who aren’t located in the United States.

Another way to achieve the same result makes use of the fact that nonmatching records are Null. The following example creates a left join between Products and SuppliersInUSA, and then tests for a Null value in the SupplierID field in SuppliersInUSA. SupplierID is the primary key of the Suppliers table and can’t normally be Null, either in the Suppliers table or in the SuppliersInUSA query. Therefore, the presence of a Null value in the result set of the left join implies that there is no join between Products and SuppliersInUSA, which is the condition that should be detected:

SELECT Products.*
FROM Products
LEFT JOIN SuppliersInUSA
ON Products.SupplierID = SuppliersInUSA.SupplierID
WHERE SuppliersInUSA.SupplierID Is Null;


Self-Joins

It’s possible to create a join between a table and a second instance of the same table. A common example is an Employees table in which the ReportsTo field contains EmployeeID values found in the Employees table itself.

The following example joins the Employees table to itself. The ReportsTo field in the first instance of the Employees table is linked to the EmployeeID field in the second instance. The second instance of the Employees table is given the alias Supervisors:

SELECT
    Employees.EmployeeID,
    Employees.LastName,
    Supervisors.FirstName & ' ' & Supervisors.LastName AS SupervisorName
FROM Employees
INNER JOIN Employees AS Supervisors
ON Employees.ReportsTo = Supervisors.EmployeeID;

In this example, a calculated field called SupervisorName is created. Note the use of the & concatenation operator, which is used to combine the output of the FirstName and LastName fields in the Supervisors table.

Because an inner join was specified, if the value in the ReportsTo field of any given record isn’t also found in the Employees table, the record isn’t selected. If you want to select the record even when the self-join doesn’t find a match, you can specify a left join, as shown in the earlier examples in this chapter.

The following revised query specifies a left join. The calculated SupervisorName field has been revised to anticipate that certain records may have a Null value in the ReportsTo field (perhaps indicating that the employee is a supervisor, and doesn’t report to another employee in the Employees table). The calculated field returns the specified value “No Supervisor” for those records in which the ReportsTo field is Null:

SELECT
    Employees.EmployeeID,
    Employees.LastName,
    IIf(IsNull(Employees.ReportsTo),
        'No Supervisor',
        Supervisors.FirstName & ' ' & Supervisors.LastName)
        AS SupervisorName
FROM Employees
LEFT JOIN Employees AS Supervisors
ON Employees.ReportsTo = Supervisors.EmployeeID;

Note This query uses the IIf (Immediate If) function in the calculated expression, which creates the field SupervisorName. This is one of many built-in functions understood by Microsoft Jet. Other functions you can use include string-manipulation functions and date functions. While these functions can be extremely useful, they should be used with care. Because they aren’t available on all back-end database systems, they must be evaluated locally by the Jet database engine running on the user’s computer. If calculated fields using these functions are part of the selection criteria, a table scan (a serial read) of all records in the result set must be performed so that the condition can be evaluated locally. Rather than adding a calculated field directly to the query itself, consider creating calculated fields in the form or report to display the query results.


Joining Tables and Queries

In addition to joins based on permanent tables, Microsoft Jet supports joins between tables and saved queries, or even between two queries. Queries can be based on other queries, which can further be based on other queries, and so on. Microsoft Jet automatically resolves all references to queries and tables.

Microsoft Jet QueryDef objects can be treated, for the most part, as if they were tables in the database. A QueryDef object performs the function of a view in SQL. QueryDef and TableDef objects occupy the same “name space” within a Microsoft Jet database. This means that you can’t have a TableDef object and a QueryDef object with the same name, for example. Generally, wherever you can use a base table, you can use a query based on that table instead.

Suppose you want to find the percentage of your total sales of certain products that were generated by particular customers. One way to do this is to create the four queries shown in this section. The first query totals orders by customer and product:

SELECT
    Orders.CustomerID,
    [Order Details].ProductID,
    Sum((UnitPrice*Quantity) - (Discount*(UnitPrice*Quantity))) AS ExtPrice
FROM Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
GROUP BY Orders.CustomerID, [Order Details].ProductID;

Save this query as a permanent QueryDef object in the database with the name SelectCustomersOrdersTotals. You can now treat this query as if it were a table. The sample output it produces may be:

Customer | Product | ExtPrice
ALFKI | Aniseed Syrup | $ 60.00
ALFKI | Grandma’s Boysenberry Spread | $399.95
ALFKI | Rössle Sauerkraut | $774.95
ALFKI | Chartreuse verte | $377.75
ALFKI | Spegesild | $ 23.75

Now you have to find the order totals for each ProductID value. Create a permanent QueryDef object called SelectOrdersTotals with the following SQL statement:

SELECT
    [Order Details].ProductID,
    Sum((UnitPrice*Quantity) - Discount) AS ExtPrice
FROM Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
GROUP BY [Order Details].ProductID;

Part of the output produced by this query may be:

Product | ExtPrice
Chai | $14,274.65
Chang | $18,554.70
Aniseed Syrup | $ 3,079.80
Chef Anton’s Cajun Seasoning | $ 9,423.30
Chef Anton’s Gumbo Mix | $ 5,800.40

Now you can create a third QueryDef object called SelectPercentOfTotal, which joins SelectOrdersTotals with SelectCustomersOrdersTotals:

SELECT
    SelectCustomersOrdersTotals.CustomerID,
    SelectCustomersOrdersTotals.ProductID,
    SelectCustomersOrdersTotals.ExtPrice AS CustTotal,
    SelectOrdersTotals.ExtPrice AS ProdTotal,
    Format(CustTotal/ProdTotal, '#.###') AS PercentOfTot
FROM SelectOrdersTotals
INNER JOIN SelectCustomersOrdersTotals
ON SelectOrdersTotals.ProductID = SelectCustomersOrdersTotals.ProductID;

Part of the output from this query may be:

Customer | Product | CustTotal | ProdTotal | PercentOfTot
Alfreds Futterkiste | Aniseed Syrup | $ 60.00 | $ 3,079.80 | .019
Alfreds Futterkiste | Grandma’s Boysenberry Spread | $399.95 | $ 7,344.63 | .054
Alfreds Futterkiste | Rössle Sauerkraut | $774.95 | $26,864.35 | .029
Alfreds Futterkiste | Chartreuse verte | $377.75 | $13,148.80 | .029
Alfreds Futterkiste | Spegesild | $ 23.75 | $ 6,142.28 | .004

Finally, you can join this third QueryDef object back to the Customers and Products tables to pick up the CompanyName and ProductName fields:

SELECT
    SelectPercentOfTotal.*,
    Products.ProductName,
    Customers.CompanyName
FROM (SelectPercentOfTotal
    INNER JOIN Customers
    ON SelectPercentOfTotal.CustomerID = Customers.CustomerID)
INNER JOIN Products
ON SelectPercentOfTotal.ProductID = Products.ProductID;


Non-Equi-Joins

So far, our join conditions have been based on matching values in selected fields in two or more tables. However, with Microsoft Jet you can also create join conditions based on tests other than equality. For example, you can create joins based on a range of acceptable values.

Suppose you have a table called PerformanceGrade that contains the following values. (This table exists in the NorthwindTables sample database.)

PerformanceGradeKey | LowRange | HighRange
0 | $ 0.00 | $ 100,000.00
1 | $100,000.01 | $ 200,000.00
2 | $200,000.01 | $ 300,000.00
3 | $300,000.01 | $ 400,000.00
4 | $400,000.01 | $9,999,999.99

You want to assign a PerformanceGradeKey value to each employee’s sales. Employees with sales between 0 and $100,000 receive a 0, those with sales between $100,000.01 and $200,000 receive a 1, and so on.

The first step is to create a QueryDef object that sums up the sales by employee. Call this QueryDef object SelectEmployeeSales:

SELECT
    Orders.EmployeeID,
    Sum((UnitPrice*Quantity) - Discount) AS ExtPrice
FROM Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
GROUP BY Orders.EmployeeID;

Sample output from this query may be:

Employee | ExtPrice
Davolio, Nancy | $202,126.72
Fuller, Andrew | $177,738.71
Leverling, Janet | $213,035.35
Peacock, Margaret | $250,161.70
Buchanan, Steven | $ 75,559.95
Suyama, Michael | $ 78,188.95
King, Robert | $141,283.04
Callahan, Laura | $133,286.43
Dodsworth, Anne | $ 82,956.70

Now this saved QueryDef object has to be joined to the PerformanceGrade table. The grade assigned is determined by the high and low ranges, and the actual sales of each employee. Here is the SQL statement that produces the desired result:

SELECT
    SelectEmployeeSales.EmployeeID,
    SelectEmployeeSales.ExtPrice,
    PerformanceGrade.PerformanceGradeKey
FROM SelectEmployeeSales, PerformanceGrade
WHERE (SelectEmployeeSales.ExtPrice Between PerformanceGrade.LowRange
    And PerformanceGrade.HighRange)
ORDER BY PerformanceGrade.PerformanceGradeKey;

As you can see from the sample output shown, Microsoft Jet is able to link values from the SelectEmployeeSales query to the PerformanceGrade table even though neither table has common fields or shares common values. The join is based entirely on the condition specified in the WHERE clause.

Employee | ExtPrice | PerformanceGradeKey
Dodsworth, Anne | $ 82,956.70 | 0
Suyama, Michael | $ 78,188.95 | 0
Buchanan, Steven | $ 75,559.95 | 0
Callahan, Laura | $133,286.43 | 1
King, Robert | $141,283.04 | 1
Fuller, Andrew | $177,738.71 | 1
Peacock, Margaret | $250,161.70 | 2
Leverling, Janet | $213,035.35 | 2
Davolio, Nancy | $202,126.72 | 2

Be aware that Microsoft Jet can’t perform its normal optimization techniques on non-equi-join queries.


UNION Queries

You often have to treat the results of one or more queries, or the contents of one or more tables, as if they were in the same table. You may want to create a list of names and addresses that combines entries from both the Customers and the Suppliers tables. Microsoft Jet supports the UNION statement, which combines the results of two or more SELECT queries.

For example, the combined Customer and Supplier list can be created with the following query:

SELECT
    Customers.CompanyName,
    Customers.Address,
    Customers.City,
    Customers.Region,
    Customers.PostalCode
FROM Customers
UNION
SELECT
    Suppliers.CompanyName,
    Suppliers.Address,
    Suppliers.City,
    Suppliers.Region,
    Suppliers.PostalCode
FROM Suppliers;

The individual SELECT queries are combined by the UNION statement. Each SELECT statement can be as complex as necessary, and can include multiple-table joins. Each SELECT statement can also use GROUP BY to sum values. To specify the order in which the results of all the individual SELECT statements are returned, you can add an optional ORDER BY clause after the last SELECT statement.

The number of fields in each SELECT statement must match, but the names of the fields can differ. The data types of the matching fields don’t have to match exactly. For example, an integer field can be united with a floating-point field.

When the output of the entire UNION query is referred to, the field names for the first SELECT statement are used.

Usually, Microsoft Jet hides records that contain entirely duplicated data in a UNION query. If you want to include duplicate records in the output, use the UNION ALL clause instead of UNION.

If you’re joining two tables or queries that have the same structure, you can use a special form of the UNION statement with which you can specify the tables you want to combine, rather than listing the fields you want to select. For example, instead of listing the fields as in the earlier example that combined values from the Customers and Suppliers tables, you could create one query called SelectCustomerNameAndAddress

SELECT
    Customers.CompanyName,
    Customers.Address,
    Customers.City,
    Customers.Region,
    Customers.PostalCode
FROM Customers;

and another called SelectSupplierNameAndAddress:

SELECT
    Suppliers.CompanyName,
    Suppliers.Address,
    Suppliers.City,
    Suppliers.Region,
    Suppliers.PostalCode
FROM Suppliers;

The following UNION query combines the two queries above without explicitly specifying the names of the fields:

TABLE SelectSupplierNameAndAddress
UNION
TABLE SelectCustomerNameAndAddress;

You can’t explicitly include Memo or Long Binary (OLE Object) fields in the output SELECT statement of a UNION query. For example, the following query results in an error:

SELECT Table1.Memo1
FROM Table1
UNION
SELECT Table2.Memo2
FROM Table2;

If it’s sufficient for your purposes to include only the first 255 characters of the Memo field in the output, one alternative is to select an expression based on the Memo field:

SELECT Left(Table1.Memo1,255) AS FirstPartOfMemo
FROM Table1
UNION SELECT Left(Table2.Memo2,255) AS FirstPartOfMemo
FROM Table2;

Note The results of a UNION query are never updatable.


Other Query Features

Microsoft Jet includes several other unique query features that can solve common problems. With crosstab queries, you can perform spreadsheet-like cross-tabulations, and by using the TOP N or TOP N PERCENT predicates, you can filter large amounts of data based on the desired top number or percentage of values. You can also supply variable parameter input at run time by using parameter queries.


Crosstab Queries

A crosstab query summarizes tabular data and places it into a columnar format. In a crosstab query, matching values can be counted, summed, averaged, compared to other values, and so on.

Suppose you want to know the average salary paid for various employee classifications in different countries. One way to do this is to create a standard SELECT query that uses the GROUP BY clause for the Country and Title fields, and averages the values in the Salary field:

SELECT
    Employees.Country,
    Employees.Title,
    Avg(Employees.Salary) AS AvgOfSalary
FROM Employees
GROUP BY Employees.Country, Employees.Title;

This query results in the following table.

Country | Title | AvgOfSalary
UK | Sales Manager | $59,600.00
UK | Sales Representative | $47,500.00
USA | Inside Sales Coordinator | $39,000.00
USA | Sales Representative | $48,960.00
USA | Vice President, Sales | $85,000.00

It’s difficult, however, to make certain kinds of comparisons. The Country field information is repeated for each row, and in order to compare the average salary for various titles across countries, you must match numbers that are physically distant from one another on the page. You may easily overlook the fact that no sales manager is listed for the United States.

A crosstab query solves this problem by organizing the data so that the summarized information is placed in contiguous columns across the page. The following crosstab query produces the same information as the first query, but in a much more readable format:

TRANSFORM Avg(Employees.Salary) AS AvgOfSalary
SELECT Employees.Title
FROM Employees
GROUP BY Employees.Title
PIVOT Employees.Country;

The contents of the Country field become column headings. The Title field (specified in the SELECT clause) becomes the row heading and the values are the average of the Salary field.

Title | UK | USA
Inside Sales Coordinator |  | $39,000.00
Sales Manager | $59,600.00 |
Sales Representative | $47,500.00 | $48,960.00
Vice President, Sales |  | $85,000.00

To support crosstab operations, Microsoft Jet includes these two extensions to the standard SQL language:

  • The TRANSFORM statement. Specifies that the query is a crosstab query. The first statement following the TRANSFORM statement must be an aggregate function, such as Avg, Sum, Min, Max, or Count, which summarizes the selected data.

  • The PIVOT clause. Specifies the field or expression used to create the column headings in the query’s result set.

By default, Microsoft Jet presents the summarized columns in alphabetical order from left to right. Missing values are returned as Null. In the previous example, because no sales managers exist in the United States, there is no salary to average. The value in the USA column for Sales Manager is Null.

Column headings are based on the actual values contained in the field specified by the PIVOT clause. If values are missing, no column is created. For example, if your company has offices in Germany, but no employees were selected whose country is Germany, no column would be created for Germany.

You can force Microsoft Jet to create columns in an order other than alphabetical and force the creation of columns even if there is no data for that column value by using the optional IN clause with the PIVOT clause. The previous crosstab query can be revised as follows:

TRANSFORM Avg(Employees.Salary) AS AvgOfSalary
SELECT Employees.Title
FROM Employees
GROUP BY Employees.Title
PIVOT Employees.Country IN ('UK','Germany','France','USA');

This query results in the following table.

Title | UK | Germany | France | USA
Inside Sales Coordinator |  |  |  | $39,000.00
Sales Manager | $59,600.00 |  |  |
Sales Representative | $47,500.00 |  |  | $48,960.00
Vice President, Sales |  |  |  | $85,000.00

The target of the SELECT statement used to create the column headings can be an expression as well as an actual field name. A common use of this feature is to create groupings that don’t exist in the data. For example, you can group all orders into month groupings in the following manner:

TRANSFORM Count(*)
SELECT Orders.EmployeeID
FROM Orders
GROUP BY Orders.EmployeeID
PIVOT Format(OrderDate,'mmm');

The following table illustrates a portion of the columns and rows returned by this query.

Employee | Apr | Aug | Dec | Feb
Davolio, Nancy | 9 | 8 | 17 | 12
Fuller, Andrew | 19 | 4 | 12 | 5
Leverling, Janet | 13 | 6 | 13 | 14

The column headings in this query are created with the Format function, which turns the value of the OrderDate field into a three-letter month abbreviation. All orders within a given month (even if they were placed in different years) are summarized together.

Because the IN clause wasn’t used with the PIVOT clause, the months are displayed alphabetically. A more complete query, which combines a WHERE clause to limit the selection to orders within a given year and displays the columns in calendar order, is as follows:

TRANSFORM Count(*)
SELECT Orders.EmployeeID
FROM Orders
WHERE Orders.OrderDate Between #1/1/96# And #12/31/96#
GROUP BY Orders.EmployeeID
PIVOT Format(OrderDate,'mmm') IN ('Jan','Feb','Mar','Apr',
    'May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');

The following table shows a portion of the result from this query.

Employee | Jan | Feb | Mar | Apr
Davolio, Nancy | 1 | 2 | 5 | 3
Fuller, Andrew | 3 | 2 | 3 | 3
Leverling, Janet | 9 | 7 | 5 | 3


TOP N and TOP N PERCENT Predicates

Although you can use the WHERE and HAVING clauses to filter the selection of records, sometimes this isn’t sufficient. For example, you may want to select all records where the state is CA, but only see the orders for the top 10 customers. Microsoft Jet provides TOP N and TOP N PERCENT predicates to limit the presentation of records after they’re selected.

TOP N Predicate

You can use the TOP N predicate to specify that your query return only a specific number of records to your program:

SELECT TOP 5
    Employees.EmployeeID,
    Employees.Salary
FROM Employees
ORDER BY Employees.Salary;

The TOP criteria is applied after the records are selected. TOP doesn’t necessarily imply “higher” or “more.” Think of it as the “first n records” of the result set. For example, the previous query actually produces a list of employees with the lowest salary, not the highest, because the records are sorted in ascending order by salary. If you want to find the employees with the highest salary, sort the records in descending order based on salary:

SELECT TOP 5
    Employees.EmployeeID,
    Employees.Salary
FROM Employees
ORDER BY Employees.Salary DESC;

If more than one record ties for the nth value, all of the tied values are displayed. If the fifth, sixth, and seventh highest salaries are the same, the previous query retrieves seven records, not five.

Note that the selection of records is based on the entire output set, even if the query uses the GROUP BY clause to group like values. It’s not possible, for example, to retrieve the “top 5 records within each group,” or the “top 10 salesmen within each region.”

TOP N PERCENT Predicate

TOP N PERCENT works much the same as the TOP N predicate, except the number of records returned isn’t fixed. A selected percentage of the records are retrieved:

SELECT TOP 10 PERCENT
    Products.UnitsInStock,
    Products.ProductID,
    Products.ProductName
FROM Products
ORDER BY Products.UnitsInStock DESC, Products.ProductName DESC;

This query retrieves the 10 percent of products that have the highest value for the UnitsInStock field.

Note If you’re working with a back-end server such as SQL Server, be cautious about using the TOP N PERCENT predicate. Because most servers don’t natively support this predicate, the output of this query must be evaluated on the local machine. It’s possible that a large number of records will need to be returned from the server in order for Microsoft Jet to determine which records to select, which could cause performance to decrease.


Parameter Queries

With Microsoft Jet, you can specify one or more query parameters. The parameters are used to accept input values at run time from the user. This user-supplied data is then merged with a QueryDef object. Parameters are commonly used to supply selection criteria, but they can also be used to specify terms in an expression.

You declare parameters with the PARAMETERS declaration, which specifies the names and data types of the parameters. The following example takes an EmployeeID value as a parameter, which causes the query to return values for only the selected employee, and another parameter whose value will be multiplied by the employee’s current salary to calculate a proposed new salary. The first parameter, EmployeeIDSelect, is used as part of the selection criteria; the second parameter, SalaryIncreaseFactor, is used as part of an expression:

PARAMETERS EmployeeIDSelect Long, SalaryIncreaseFactor Single;
SELECT
    Employees.LastName,
    Employees.FirstName,
    Employees.Salary,
    CCur(Employees.Salary*SalaryIncreaseFactor) AS NewSalary
FROM Employees
WHERE Employees.EmployeeID = EmployeeIDSelect;

How does your program supply the parameter values? If you’re working with Microsoft Access, you may be familiar with the way it prompts the user to supply missing parameter values.

This behavior isn’t built into Microsoft Jet, however. It’s a function performed by Microsoft Access, and then only when the parameter query is executed through the user interface. Microsoft Access prompts the user for parameter values for forms, reports, and datasheets based on parameter queries.

If you want to supply parameter values to an existing QueryDef object that’s being executed programmatically, you have to do so explicitly, as in the following example. Assume that you have saved the parameter query shown previously as a permanent QueryDef object. In this example, strDbPath is the path to the NorthwindTables database, strQueryName is the name of the query under which you have saved the preceding SQL statement, lngEmpID is the Long value you supply for the EmployeeIDSelect parameter, and sngIncFact is the Single value you supply for the SalaryIncreaseFactor parameter.

For example, to increase the salary for the first employee in the Employees table by 10 percent, you would supply a value of 1 for EmployeeIDSelect and a value of 1.1 for SalaryIncreaseFactor:

Dim dbs As Database
Dim qdf As QueryDef
Dim rst As Recordset
Dim strMsg As String

Set dbs = OpenDatabase(strDbPath)
' Return reference to parameter QueryDef object.
Set qdf = dbs.QueryDefs(strQueryName)

' Supply values for parameters.
qdf.Parameters("EmployeeIDSelect") = lngEmpID
qdf.Parameters("SalaryIncreaseFactor") = sngIncFact

' Open recordset.
Set rst = qdf.OpenRecordset()
With rst
    If .RecordCount <> 0 Then
        MsgBox "The proposed new salary is: $" & !NewSalary
        ' Check whether recordset is updatable.
        If .Updatable Then
            ' Prompt to update salary.
            strMsg = "Would you like to update the salary for " _
                & !FirstName & " " & !LastName & " from $" _
                & !Salary & " to $" & !NewSalary & "?"
            ' Update salary field.
            If MsgBox(strMsg, vbYesNo) = vbYes Then
                .Edit
                !Salary = !NewSalary
                .Update
            End If
        End If
    End If
End With

This method assumes that you know the names of the parameters at the time your code is written. One way to avoid the need to explicitly specify parameters is to iterate through all the parameters in the Parameters collection of the QueryDef object and prompt the user for values, thus emulating the behavior inherent in Microsoft Access. In the following example, strDbPath is the path to the database, and strQueryName is the name of the query:

Dim dbs As Database
Dim qdf As QueryDef
Dim rst As Recordset
Dim prm As Parameter

Set dbs = OpenDatabase(strDbPath)
Set qdf = dbs.QueryDefs(strQueryName)

For Each prm In qdf.Parameters
    prm.Value = InputBox("Enter parameter value: " & prm.Name)
Next prm
Set rst = qdf.OpenRecordset()

Although Microsoft Jet includes the PARAMETERS declaration in order for you to name and type your QueryDef object parameters explicitly, it actually treats any unrecognized term as a parameter. If you make a spelling error when specifying a field name, for example, Microsoft Jet treats the misspelled field name as a parameter.

It’s a good idea to get into the habit of specifying the names and data types of your parameters rather than relying on the implicit parameter behavior of Microsoft Jet.

You can also use the Microsoft Jet PARAMETERS declaration to create a temporary QueryDef object that prompts the user to supply parameter values when your code executes. For more information and an example, see “Temporary QueryDef Objects” in Chapter 3, “Data Definition and Integrity.”

Important You should not use a parameter query to search for Null values. A parameter of Null will never find a Null record because no two Null values match. If you want to find records that contain fields with Null values, you can use a non-parameter SELECT query. For example, the following SQL statement returns a list of all customers that don’t have a fax number:

SELECT * FROM Customers WHERE Fax = Null;

See Also For a discussion of the various database object collections, including QueryDefs and Parameters, see Chapter 3, “Data Definition and Integrity.”


Updating Your Data with Queries

You can use the dynamic selection and sorting capabilities of the query engine for more than static searches; after a result set is retrieved, the data can also be updated through a program such as Microsoft Access, or through a custom Visual Basic program.

See Also Chapter 5, “Working with Records and Fields,” discusses the various types of Recordset objects that can be built from Microsoft Jet queries and how those Recordset objects can be manipulated through Visual Basic.


Single-Table Query Updatability

A query based on a single table can select some or all records, and some or all fields, from that table. In the simplest case, the query returns all records and all fields. For example:

SELECT *
FROM Customers;

Every record retrieved, and every field in every record, is accessible and subject to change (except calculated fields based on expressions). Every record can be deleted and new records can be inserted.

If you were to use Microsoft Access to open a datasheet based on this query, you would be able to freely browse forward and backward, making any changes you wanted (subject to security restrictions, validation rules, and referential integrity constraints). Any changes made to the Recordset object created by this QueryDef object would automatically be reflected in the underlying table.

A query can also select and sort records from the original table and remain updatable:

SELECT *
FROM Customers
WHERE Customers.ContactTitle = 'Owner'
ORDER BY Customers.CompanyName;

An updatable single-table query such as this one can be useful in a data-entry situation in which you would like to present the records to the user in a certain order, or hide certain records based on the selection criteria.

One thing to be aware of, however, is that Microsoft Jet doesn’t prevent the user from adding a new record through a QueryDef object that wouldn’t have met the original selection criteria. For example, the previous query selects only records in which the value of the ContactTitle field is “Owner.” The user can add a new record and specify a value other than “Owner” for the ContactTitle field, in which case the user would be adding a record that the query, if subsequently rerun, would no longer select. It’s up to your application to enforce insertion restrictions such as this.


Single-Table Query Updatability Restrictions

Instead of selecting all fields with the asterisk (*) character, a query can select specific fields. For example:

SELECT
    Customers.ContactName,
    Customers.ContactTitle,
    Customers.Address,
    Customers.City,
    Customers.Region,
    Customers.PostalCode
FROM Customers;

This technique can be useful for hiding certain fields from users, while still allowing them access to the information they need. The fields made available through a query such as this one are fully updatable. It may, however, be impossible to add a new record through this query, either because the fields not included in the output are specified as “required” at the table level, or because the primary key or foreign keys can’t be created with default values.

Another restriction that applies to non-native tables is the requirement of a unique index. For example, Microsoft Jet requires that a primary key be defined for Paradox tables and that ODBC tables have a unique index. Queries based on tables without a unique index aren’t updatable. The exceptions to this are Btrieve and xBase data sources, which don’t require a unique index. For SQL Server, a table may be updatable if it has a timestamp field, even if it doesn’t have a unique index.


Multiple-Table Query Updatability

In addition to allowing updates to single-table queries, Microsoft Jet supports updatable multiple-table joins. This feature is useful because it enables you to combine data from the main table with lookup information from other tables (a many-to-one join), or to join a master table with a related detail table (a one-to-many join) and still have an updatable result set.


Many-to-One Joins

The following example joins the Products table (the focus of the query) with the Suppliers table, which provides lookup information, including the supplier’s company name and city:

SELECT
    Products.*,
    Suppliers.CompanyName,
    Suppliers.City
FROM Suppliers
INNER JOIN Products
ON Suppliers.SupplierID = Products.SupplierID;

The user can change data in any field from the Products table, including the SupplierID field, which links the Products table to the Suppliers table. If you change the value of the linking field through a datasheet or a form in a Microsoft Access application, you will also see that the corresponding lookup information from the “one” table is automatically retrieved and redisplayed. This technique is known as row fix-up or AutoLookup.

Although the focus of this many-to-one query is the Products table, it’s also possible (though perhaps not desirable) to change the values in fields retrieved from the lookup table, such as the CompanyName or City field in the Suppliers table. The user may attempt to alter the CompanyName value for one record of the recordset created from this query, under the impression that the change will affect only the current record. However, because the value is actually stored in the Suppliers lookup table, the value is changed for every record in the recordset.


One-to-Many Joins

There is no logical difference between a “many-to-one” join, as described previously, and a “one-to-many” join, except from the point of view of the user. A one-to-many join is sometimes referred to as a master/detail relationship. A single record in the “one” or “master” table is related to one or more records in the “many” or “detail” table. Updatable multiple-table joins are especially useful with these types of one-to-many relationships.

The following query joins the Orders table to the Order Details table in a classic one-to-many relationship:

SELECT
    Orders.*,
    [Order Details].*
FROM Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID;

Fields in the Orders table focus on the order itself: the customer who placed the order, the employee who took the order, the date the order was taken, and so on. Fields derived from the Order Details table specify the actual items that were ordered: the product’s ID and pricing details. Just as with the many-to-one example shown previously, changes to fields from the “one” table on any given record are automatically made for all other records based on the same value in the “one” table. For example, if the user changes the CustomerID field, which is drawn from the “master” Orders table, for any given record, the change is automatically reflected in all other records for this same order.

Updatable multiple-table joins aren’t limited to a single-level hierarchy. For example, the following query links from the Employees table to the Orders table to the Order Details table:

SELECT
    Employees.EmployeeID,
    Employees.LastName,
    Orders.OrderID,
    Orders.OrderDate,
    [Order Details].ProductID
FROM (Employees
    INNER JOIN Orders
    ON Employees.EmployeeID = Orders.EmployeeID)
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID;

Fields from all three of these joined tables can be updated in the resulting recordset.


Inserting Records into a Multiple-Table Query

When you’re inserting a record into a recordset based on a multiple-table join, records can be added to one, several, or all of the tables included in the join. Records from the “many” side of the join can be appended to the “many” table as needed. Records from the “one” side of the join can be added as well, as long as they don’t violate any referential integrity constraints.

The following query joins the Products (“many”) table to the Categories (“one”) table:

SELECT
    Categories.CategoryID,
    Categories.CategoryName,
    Products.ProductID,
    Products.ProductName,
    Products.SupplierID,
    Products.CategoryID
FROM Categories
INNER JOIN Products
ON Categories.CategoryID = Products.CategoryID;

If, while adding a new record to the Products table, the user specifies a CategoryID value that’s already present in the Categories table, a new record is added only to the Products table. Row fix-up occurs; values from the corresponding record in the Categories table are retrieved.

If the primary key from the Categories table is included in the join, as in the previous example, it’s possible to add a new CategoryID value to the recordset and have that new value automatically added to the Categories table.

In general, values for all required fields, and for the field or fields making up the primary key, must be supplied when you’re inserting a new record into a recordset based on a query. The exception to this rule is for AutoNumber fields (also known as Counter fields). You must not supply an explicit value for AutoNumber fields. The value for AutoNumber fields is automatically assigned by Microsoft Jet.


Updatable One-to-Many Outer Joins

As discussed earlier in this chapter, an outer join selects all records from one table in a multiple-table join, while only selecting records with matching values from another table. For records in which there is no match in one table, artificial Null field values are supplied. Microsoft Jet allows you to “fill in the blanks” in these artificial Null records.

Consider the following outer join and a portion of its results:

SELECT
    Customers.CompanyName,
    Customers.CustomerID,
    Orders.CustomerID,
    Orders.OrderID
FROM Customers
LEFT JOIN Orders
ON Customers.CustomerID = Orders.CustomerID;
Customers.CompanyName | Customers. CustomerID | Orders.CustomerID | Orders.OrderID | Orders.OrderDate
Paris spécialités | PARIS |  |  |
Centro comercial Moctezuma | CENTC |  |  |
FISSA Fabrica Inter. Salchichas S.A. | FISSA |  |  |
Wartian Herkku | WARTH | Wartian Herkku | 10270 | 01-Sep-94
Split Rail Beer & Ale | SPLIR | Split Rail Beer & Ale | 10271 | 01-Sep-94
Rattlesnake Canyon Grocery | RATTC | Rattlesnake Canyon Grocery | 10272 | 02-Sep-94

In this example, the Customers table is outer joined with the Orders table to show all customers regardless of whether they have placed an order. The first three customers listed have not placed orders. The “fill in the blank with key propagation” rule states that the user can add an order for this customer by filling in the Orders.OrderDate field (and any other order information except the customer’s ID). The value in Customers.CustomerID is automatically propagated into Orders.CustomerID. In this update scenario, the child key (Orders.CustomerID) is read-only. The act of “filling in the blank” associates the new record in the Orders table with the customer.


Updatability Restrictions for Multiple-Table Queries

To be fully updatable, a query must meet several requirements:

  • You must specify an explicit inner or outer join between tables. Joins created implicitly in the WHERE clause of the SELECT statement aren’t updatable. For example, the following join isn’t updatable:
    SELECT
    Products.ProductID,
    Products.ProductName,
    Categories.CategoryID,
    Categories.CategoryName
FROM Categories, Products
WHERE Products.CategoryID = Categories.CategoryID;
  • Summary (GROUP BY), UNION, DISTINCT, and crosstab queries are never updatable. Queries joined to one or more summary queries aren’t updatable, even if you don’t attempt to modify fields from an otherwise updatable table. However, a query may be updatable if it refers to a summary query in a sub-SELECT statement, as in the following example:
    SELECT Orders.*
FROM Orders
WHERE Orders.Freight >
    (SELECT Avg(Orders.Freight) AS AvgOfFreight
    FROM Orders;);

    In this case, fields from the Orders table are updatable.

  • To be able to insert new records into a table in any query, all primary key fields must be present.

  • While you’re updating a single record in a query, changes to certain fields may render certain other fields nonupdatable until the edit to the record is either saved or canceled. As soon as the user edits data on the “one” side of a query, the join key on the “many” side can no longer be modified. Usually, the “many” side’s join key is updatable. However, because data on the “one” side was modified first, this field is temporarily rendered unmodifiable because row fix-up would discard changes to the “one” side’s data. As soon as the change to the “one” side of the query is saved or canceled, the “many” side’s join key becomes updatable again.

  • A change to a multiple-table query must not create orphaned records. You can change the join key in the “many” table to a value already present in the “one” table, but you can’t specify a nonexistent value, except in the case of outer joins.


Data Manipulation Language SQL Statements

So far, only selecting, sorting, and summarizing data using various forms of the SQL SELECT clause have been covered. Microsoft Jet also uses SQL to modify a database. You can insert new records, delete existing records, and update values in existing records using syntax that’s similar to that used with the SELECT statement.

Data-manipulation queries can either be created as permanent QueryDef objects in the database, or executed directly in Visual Basic code by using the Execute method of a database object variable.

The single-table and multiple-table query updatability rules for result sets created from SQL SELECT statements that are discussed in the previous section also apply to updates done with the data manipulation language (DML) statements discussed in this section.


The UPDATE Query

You can use the UPDATE query to specify a set of records from one or more tables and assign new values to one or more fields in that result set. The new value can be either a literal value assigned to each record to be updated, or an expression that uses existing values as part of the new value.

The following example updates the Salary field for all sales managers:

UPDATE Employees
SET Employees.Salary = 50000
WHERE Employees.Title = 'Sales Manager';

Every employee record that meets the selection criterion is assigned the same value of 50,000.

The next example applies a formula that increases the employees’ salaries by 10 percent for all employees who aren’t sales managers:

UPDATE Employees
SET Employees.Salary = Employees.Salary*1.1
WHERE Employees.Title <> 'Sales Manager';

The Salary field value for each record in the result set produced by applying the WHERE clause is multiplied by the constant value 1.1 and then stored in the same Salary field.

Multiple-Table UPDATE Queries

In addition to simple single-table updates, with Microsoft Jet you can create an UPDATE query that’s the result of a multiple-table join. The following example joins the Orders and Order Details tables. It uses a selection criterion from the Orders table, but updates values in the Order Details table:

UPDATE Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
SET [Order Details].Discount = 0.25
WHERE Orders.CustomerID = 'FRANS';

In this case, the fields in the table on the “many” side of a one-to-many relationship are being updated. Fields on the “one” side of the join can also be updated, with certain limitations:

UPDATE Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
SET [Order Details].Discount = 0.25,
    Orders.OrderDate = #6/16/95#
WHERE Orders.CustomerID = 'FRANS';

You can’t create a multiple-table UPDATE query if one of the tables in the join is a summary query — a query that uses the GROUP BY clause, for example.

See Also For more information about UPDATE queries, see the “Selecting Unique Records” section earlier in this chapter.


The DELETE Query

The DELETE query uses the same join and selection syntax as the SELECT and UPDATE queries, but instead of retrieving records or updating the retrieved records, it deletes the selected records from the table or tables in the query. The simplest form of DELETE query deletes, from a single table, records that match a selection criterion. For example:

DELETE *
FROM Orders
WHERE Orders.OrderDate < #10/1/94#;
Multiple-Table DELETE Queries

A DELETE query can also be based on a multiple-table join. For example, the following sample query joins the Categories table to the Products table, and deletes records in the Products table that match the selection criterion specified for the Categories table:

DELETE Products.*
FROM Categories
INNER JOIN Products
ON Categories.CategoryID = Products.CategoryID
WHERE Categories.CategoryName = 'Beverages';

See Also For more information about DELETE queries, see the “Selecting Unique Records” section earlier in this chapter.


The Append Query (INSERT INTO)

You can append new records to an existing table by using an INSERT INTO query. In the simplest case, literal values are assigned to a single record that’s inserted into the table:

INSERT INTO PerformanceGrade (PerformanceGradeKey, LowRange, HighRange)
VALUES (5, 500000.01, 600000);
INSERT INTO…VALUES

The INSERT INTO query lists the table to which the record will be added, the fields that will be populated with new values, and a corresponding VALUES clause with the literal values that will be assigned to those fields. The number and data types of the fields in the field list and the value list must match.

It’s not necessary to specify every field in the table. Any fields not specified are filled with the value specified by the DefaultValue property for that particular field. However, all required fields must be assigned, including those fields participating in indexes or acting as part of the primary key.

However, an INSERT INTO query can’t violate any referential integrity constraints or ValidationRule property clauses established at either the field level or the table level. If the Required property is set to True for a particular field, for example, and you don’t assign a value for this field, a trappable run-time error is generated when you attempt to run the INSERT INTO query.

Similarly, if a referential integrity constraint has been established that requires a matching value in a foreign key field and that value doesn’t exist in the related table, the INSERT INTO query will fail.

INSERT INTO…SELECT

An alternative form of this statement (and one likely to be created if you generate this query in Microsoft Access) is as follows:

INSERT INTO PerformanceGrade (PerformanceGradeKey, LowRange, HighRange)
SELECT 5 AS Expr1, 500000.01 AS Expr2, 600000 AS Expr3;

Rather than using the VALUES clause to assign field values, this form of the INSERT INTO statement takes its values from another SELECT statement. In this particular case, because the inner SELECT statement doesn’t contain a FROM clause, a single record is inserted into the new table, using the calculated fields Expr1, Expr2, and Expr3 as the source items.

When the source SELECT statement does have a FROM clause, all records retrieved by the SELECT statement are inserted into the first table:

INSERT INTO Employees
    (EmployeeID,
    LastName,
    FirstName,
    Title,
    TitleOfCourtesy,
    BirthDate,
    HireDate,
    Address,
    City,
    Region,
    PostalCode,
    Country,
    HomePhone,
    Extension,
    Photo,
    Notes,
    ReportsTo)
SELECT
    TemporaryEmployees.EmployeeID,
    TemporaryEmployees.LastName,
    TemporaryEmployees.FirstName,
    TemporaryEmployees.Title,
    TemporaryEmployees.TitleOfCourtesy,
    TemporaryEmployees.BirthDate,
    TemporaryEmployees.HireDate,
    TemporaryEmployees.Address,
    TemporaryEmployees.City,
    TemporaryEmployees.Region,
    TemporaryEmployees.PostalCode,
    TemporaryEmployees.Country,
    TemporaryEmployees.HomePhone,
    TemporaryEmployees.Extension,
    TemporaryEmployees.Photo,
    TemporaryEmployees.Notes,
    TemporaryEmployees.ReportsTo
FROM TemporaryEmployees
WHERE TemporaryEmployees.Title = 'New Hire';

Instead of itemizing the fields in the source and target tables, you can use the shorthand form of this statement:

INSERT INTO Employees
SELECT TemporaryEmployees.*
FROM TemporaryEmployees
WHERE TemporaryEmployees.Title = 'New Hire';

This format requires that the field names in the source and the target tables match.


The Make-Table Query (SELECT…INTO)

With the SELECT…INTO statement, you can simultaneously select fields from one or more tables and create a new table with the result of that selection. The following table selects records from the Products table, in which the Discontinued field is set to True, builds a new table called DiscontinuedProducts, and inserts the selected records into the new table:

SELECT Products.*
INTO DiscontinuedProducts
FROM Products
WHERE Products.Discontinued = True;

If the table specified with the INTO clause already exists, a trappable run-time error occurs.

Only the names and the data types of the original fields are created in the new table. None of the values for DefaultValue, ValidationRule, Caption properties, and so on, are transferred. The new table is built with no indexes and no referential integrity constraints.

An alternative to this approach is to create the empty table, either through Data Access Objects (DAO) methods or by creating a SELECT…INTO query that returns no records. Then create any indexes you want using SQL ALTER TABLE statements or by using the DAO methods discussed in Chapter 3, “Data Definition and Integrity.” Finally, populate the new table with an append query by using INSERT INTO…SELECT rather than SELECT…INTO. Note that deferring creation of the indexes until after the table is populated speeds up the insert operation.


Other Query Types

Microsoft Jet supports two other query types. Data definition language (DDL) queries alter the structure of a Microsoft Jet database, and pass-through queries communicate with back-end ODBC database management systems.


DDL Queries

You use DDL queries to modify the structure of a Microsoft Jet database. They can be used to build and delete tables, to add and delete fields and indexes from those tables, and to create referential integrity relationships between two tables.

The following is an example of a DDL QueryDef object that creates a new table with three fields and builds a primary key index on the Auto_ID field:

CREATE TABLE AUTOS
(Auto_ID TEXT (10) CONSTRAINT PrimaryKey PRIMARY KEY,
Make TEXT(20),
Model TEXT(20));

See Also For specific types of and uses for Microsoft Jet DDL queries, see Chapter 3, “Data Definition and Integrity.”


Pass-Through Queries

Microsoft Jet is designed to work transparently, for the most part, with a wide variety of data sources, whether they’re native Microsoft Jet databases, installable ISAM data sources such as Microsoft FoxPro or Paradox, or ODBC data sources such as SQL Server. Microsoft Jet supports the concept of linked tables — tables that physically reside in an external database, but are treated as though they’re local. When you design a SELECT or UPDATE query, you generally don’t have to worry about whether the tables in the query are native Microsoft Jet tables or are in another data format.

Many data sources have no native SQL interface. Microsoft Jet translates your SQL statement into direct data-manipulation functions that are specific to that data source. Other data sources, such as SQL Server, support the use of SQL by definition, although the dialect of SQL can differ radically from one database management system to another. Even when you create an SQL statement and save it in a QueryDef object, Microsoft Jet transforms the “native” Microsoft Jet SQL statement into a format that you may barely recognize.

Occasionally, you may want to get Microsoft Jet out of the way and directly communicate with your back-end SQL database system. You can use a pass-through query to send any SQL statement to your server. Microsoft Jet does no translation or error checking of any kind: It simply passes the text of the query on to the back-end server for processing.

To create a pass-through query, you create a QueryDef object and set its Connect property to a connection string that contains information about an ODBC data source.

Your SQL statement must conform to the rules of the server you’re using. For example, while Microsoft Jet uses the asterisk (*) character with the Like operator, SQL Server uses the ANSI-standard percent (%) character:

SELECT *
FROM Products
WHERE ProductName Like 'M%'

This query acts like a normal SELECT query. It returns records and can be used to create a Recordset object, but it isn’t updatable.

If you save this pass-through query as a permanent QueryDef object, it can even be used to join to other tables or queries. For example, if you save this pass-through query with the name SQLPassThrough, it can be used to build another query, as follows:

SELECT
    SQLPassThrough.ProductID,
    SQLPassThrough.ProductName,
    SQLPassThrough.SupplierID,
    SQLPassThrough.CategoryID,
    [Order Details].OrderID
FROM SQLPassThrough, [Order Details];

In this example, the pass-through query SQLPassThrough is executed on the server, which retrieves all records from the Products table whose ProductName field begins with an “M.” The result of the SQLPassThrough query is joined with the Order Details table, and the final result is created transparently.

Anything that the back-end server can interpret can be successfully used in a pass-through query. This includes DDL statements that create and delete tables, or DML DELETE and INSERT INTO statements that don’t return records but instead act directly on the back-end server database.

The following code creates a QueryDef object for a pass-through query and sets the QueryDef object’s Connect property. In this example, strDbPath is the path to the database, strQueryName is the name for the new query, strSQL is the SQL statement that defines the QueryDef object, and strConnect is the connection string information used to connect to the ODBC data source:

Dim dbs As Database, qdf As QueryDef

Set dbs = OpenDatabase(strDbPath)
' Create new QueryDef object.
Set qdf = dbs.CreateQueryDef(strQueryName, strSQL)
' Setting Connect property indicates that query
' is a pass-through query.
qdf.Connect = strConnect

An SQL pass-through query may return records to the client computer, or it may perform an operation on the server and not return any records to the client. For example, a SELECT query returns records, while an action query performs an operation on the server and does not return any records. The ReturnsRecords property indicates whether the query is expected to return records. By default Microsoft Jet assumes that a pass-through query will return records, and the ReturnsRecords property is set to True. If a pass-through query will not return records but simply performs an operation on the server, you must set the ReturnsRecords property to False. If you try to run an action query whose ReturnsRecords property is set to True, an error occurs.

The contents of a pass-through query don’t have to be an SQL statement. You can execute stored procedures. If a stored procedure returns records, the pass-through query that executes it can be used to create a Recordset object. For example, if this query were saved as a pass-through QueryDef object called SQLPassThroughStoredProc, it could be used in another query:

SELECT SQLPassThroughStoredProc.*
FROM SQLPassThroughStoredProc
WHERE SQLPassThroughStoredProc.Status = 'sleeping';

You can supply parameters to a stored procedure by concatenating the value of the parameter with the call to the stored procedure. The following example creates a simple SQL Server stored procedure with an input parameter and a return value. It then runs the procedure and retrieves the return value. In this example, strSQL is the remote query definition, cnn is a variable that represents an ODBCDirect Connection object, qdf is the QueryDef object, and var is a variant that contains the value of the parameter for the QueryDef object:

' Create stored procedure on the server.
strSQL = "CREATE PROCEDURE UpdateEmps (@invar int) AS RETURN @invar;"
cnn.Execute strSQL

' Create QueryDef object to run stored procedure.
Set qdf = cnn.CreateQueryDef("qry", "{ ? = call UpdateEmps(?) }")

' Handle parameters.
qdf.Parameters(0).Direction = dbParamReturnValue
qdf.Parameters(1) = 10
qdf.Execute

' Get return value.
var = qdf.Parameters(0).Value

See Also For more information about ODBC, see Chapter 12, “ODBC Desktop Database Drivers.”


Microsoft Jet Query Engine Overview

The Microsoft Jet database engine contains a sophisticated query processor that’s responsible for query execution. There is, however, much more to the query engine than just a query processor. The query engine also contains a sophisticated query optimizer that takes any given query as its input and determines the fastest join strategy for executing that query.

A query may be internally optimized to yield a more efficient join strategy, but this is all done without intervention from the user. The goal of the query engine is to take any query, no matter how poorly structured, and execute that query in the most efficient manner.


Query Engine Components

The query engine has eight major components:

  • Parser/Binder The parser/binder parses the SQL statement that defines a query and binds the names referenced in the query to fields in the underlying tables. The parser/binder first checks the query for proper syntax. If there are any syntactical problems with the query, an error is returned. Next, the parser/binder converts the SQL string into the internal query-object definition format in which queries are stored.

  • Folder The folder moves expressions “up” the query tree. The top of the query tree, or root, is the final result set. By moving expressions to the top of the tree, the expressions don’t have to be computed against records that may be discarded.

  • Splitter The splitter splits conglomerate QueryDef objects into their discrete components. The conglomerate query objects are then removed from the tree. This operation is performed on the query tree starting from the bottom and moving in reverse order up to the root.

  • Flattener The flattener takes the output of the splitter and combines joins, ORDER BY clauses, and WHERE clauses as much as possible.

  • Optimizer The optimizer is one of the most complex components of the query engine. It uses statistics to determine the most efficient way to execute a query. The optimizer in the Jet database engine is a cost-based optimizer, which means the optimizer assigns a cost to each task and then chooses the least expensive list of tasks to perform that will generate the desired result set. The longer a task takes to perform, the more costly or expensive it is.

    The algorithms that the optimizer uses depend on the accuracy of the statistics provided by the underlying engine. For example, the statistics that some ODBC drivers return may not be accurate. This can cause the optimizer to choose a less-than-optimal execution plan. However, if the whole query is sent to the ODBC server for processing (the usual case), the optimizer’s execution plan is irrelevant.

    In the native Jet database engine, statistics can become out of date over time. Statistics become out of date if transactions are performed and then rolled back, or if your machine is turned off before the database is closed. The problem in the latter situation occurs because the statistics are cached in memory, and shutting off the machine without closing the database doesn’t allow the statistics to be written out to disk.

    To update the statistics in a Microsoft Jet database, you must compact the database. Compacting the database may also speed up queries because the process writes all the data in tables in contiguous pages, which makes scanning sequential pages much faster than when the database is fragmented.

  • Remote Post-Processor The remote post-processor determines how much of a query can be sent to an ODBC back-end for processing by the server. The goal is to send as much of the query as possible to the server for processing. The remote post-processor walks through the query tree from the bottom up, marking subtrees that can be sent to the server. As subtrees are found, they are marked as “remoteable.” Finally, SQL strings are generated for the remote queries.

  • Post-Processor The post processor takes the query in its current compiled form and moves it to a new, cleaner, smaller data-execution structure.

  • Join Processor The join processor executes the compiled query. It uses the join strategy that was chosen by the optimizer to return the desired result set.


Join Strategies

Microsoft Jet can choose from five join strategies. Each of these join strategies serves a particular purpose and ranges in complexity from a “brute force” table scan to more complex execution plans. The five join strategies are nested iteration, index, lookup, merge, and index-merge.


Nested Iteration Join

The nested iteration join is a brute force method and is used only as a final attempt at performing a join. A nested iteration is performed only when the tables contain few records and probably no useful indexes.

Algorithm for Performing a Nested Iteration Join
  1. Pair each record in the outer table with a record in the inner table.

  2. Check each pair to make sure it meets the join restrictions.

    If a record matches the restriction, it’s kept for the result set; otherwise, it’s discarded.

Following is an example of a nested iteration join:

SELECT
    Categories.CategoryName,
    Products.ProductName,
    Categories.Description,
    Categories.Picture,
    Products.ProductID,
    Products.QuantityPerUnit,
    Products.UnitPrice, Products.Discontinued
FROM Categories
INNER JOIN Products
ON Categories.CategoryID = Products.CategoryID
WHERE Products.Discontinued = False
ORDER BY Categories.CategoryName, Products.ProductName;


Index Join

Microsoft Jet chooses an index join when the inner table has an index on the joined field. The index join tends to be used when the outer table is small or data doesn’t have to be retrieved from the inner table. Index joins are often used for recordsets based on a table.

Algorithm for Performing an Index Join
  1. Retrieve a value from the outer table.

  2. Create a primary key for the inner table.

  3. Search on the inner table to find matches, based on the key created in step 2.

  4. Check the remaining restrictions for each match.

Following is an example of an index join:

SELECT
    Products.ProductID,
    Orders.OrderDate,
    Sum(CLng([Order Details].UnitPrice*Quantity*(1 - Discount)*100))/100
        AS ProductAmount
FROM Orders
    INNER JOIN (Products
        INNER JOIN [Order Details]
        ON Products.ProductID = [Order Details].ProductID)
    ON Orders.OrderID = [Order Details].OrderID
GROUP BY Products.ProductID, Orders.OrderDate;


Lookup Join

A lookup join is similar to an index join. The only difference is that the lookup join reduces the number of fields in the inner relationship and performs a sort as part of the index creation on the inner table before performing the join. A lookup join is often used for static queries and ordered dynaset-type Recordset objects.

Algorithm for Performing a Lookup Join
  1. Create a sorted and indexed temporary table, selecting the necessary fields from the inner table.

  2. Retrieve a value from the outer table.

  3. Create a primary key for the inner table.

  4. Search on the temporary table to find matches, based on the key created in step 3.

  5. Check the remaining restrictions for each match.


Merge Join

Merge joins tend to be performed when the input tables contain a large number of records. Merge joins also tend to be used in cases when ordering the output of a join can save resorting.


Index-Merge Join

An index-merge join can be used when each input is a table in native Microsoft Jet database format. Each input must have an index on its join field, and at least one of the indexes must not allow Null values if there is more than one join field.

Algorithm for Performing an Index-Merge Join
  1. In the current record, move to the next record where the foreign key is greater than or equal to the primary key.

  2. If equal, return the record.

  3. If greater, advance the outer input one record.

  4. Move backward the number of matches for the previous key on the inner input if the outer input reference isn’t unique and matches the current inner input.

Following is an example of an index-merge join:

SELECT
    [Order Details].OrderID,
    Products.ProductName,
    [Order Details].ProductID,
    [Order Details].UnitPrice
FROM Products
INNER JOIN [Order Details]
ON Products.ProductID = [Order Details].ProductID;


Query Optimization

This section discusses the techniques for maximizing database performance by using the Jet database engine. It covers the Microsoft Jet query engine and how it optimizes queries, how to take advantage of Rushmore query optimization, and how to avoid common pitfalls in query design that can lead to performance degradation.

See Also For information about optimizing access to ODBC data sources, see Chapter 8, “Accessing External Data,” and Chapter 9, “Developing Client/Server Applications.”

Because the purpose of most database applications is to retrieve the data stored in the database, QueryDef objects are usually used more than any other object in a database. Queries are useful in Microsoft Jet-based applications because of the powerful query functionality of Microsoft Jet, such as query updatability, hybrid queries (queries based on more than one data source), and stacked queries (queries that use other queries as input).


The Microsoft Jet Query Engine Optimizer

As previously mentioned, the optimizer uses statistics on the tables in the query to determine which join strategy to use. This section looks more closely into the statistics that are used, as well as how the joins themselves affect join strategy selection.


Optimizer Statistics

The optimizer uses two sets of statistics when determining a join strategy on the base tables and on non-base-table inputs such as other QueryDef objects.

For base-table inputs, the optimizer looks at:

  • The number of records in the base table. This tells the optimizer how large the tables are, which affects which join strategy is used.

  • The number of data pages in the base table. The more data pages that need to be read from disk, the more costly the query is.

  • The location of the table. Is the table in a local ISAM format or is it located in an ODBC database? Each distinct ODBC server is given a number. This helps the optimizer and remote post-processor do their work.

  • The indexes on the table. When looking at indexes, the optimizer is concerned with:
    • Selectivity. If a particular index returns only three matching values out of 100,000, it’s considered highly selective. A unique index is the most highly selective index available, because every value is distinct. It’s always best to use highly selective indexes.

    • Number of index pages. As with data pages, the more index pages that must be read from disk, the more costly the query is.

    • Whether Null values are allowed in the index. Null values in an index may rule out the use of an index-merge join.

    • Whether duplicates are allowed in the index. Duplicates in an index affect its selectivity.

For non-base-table inputs, the optimizer looks at:

  • Record counts. As with base tables, the number of records in the input query affects which join strategy is chosen.

  • Input costs. Because the input is another query, the cost of executing that query must be factored into the total cost.

    Note The record count and input costs are estimates.

  • Location of inputs. Each distinct ODBC server is given a number. This helps the remote post-processor do its work.

  • Record ordering. An ordered input may be a candidate for a lookup join.


Choosing Join Combinations

Using the statistics described, the optimizer chooses the best join strategy. Because Microsoft Jet uses a cost-based optimizer, each possible join combination is examined to determine which will yield the least costly query execution.

In choosing a join combination, the optimizer first selects a base-table access strategy. The optimizer then stores the estimated number of records returned and a cost — how expensive it is to read the table. The optimizer then generates all combinations of pairs of tables and determines the cost of each join strategy. Finally, the optimizer adds tables to the joins and continues to calculate statistics until the cheapest strategy for the entire query is found.


Choosing a Base-Table Access Strategy

Three methods are available for accessing a base table:

  • Table scan. This is generally the slowest method because every data page must be read. When a table scan is used, single-table restrictions are checked for each record in the table, but no base-table page is read from disk more than once.

  • Index range. When an index range is used, a table is opened with a particular index on one of the single-table restrictions (or selection criteria). Records in the index are then checked against the remaining restrictions. A base-table page can be read from disk more than once.

  • Rushmore restriction. A Rushmore restriction is used when there are restrictions on multiple-indexed fields. By using multiple indexes to resolve the query, the number of base-table pages that need to be read from disk is minimized. For more information, see the following section.


Rushmore Query Optimization

One of the most powerful ways to execute queries is to take advantage of Rushmore query optimization.


What Is Rushmore?

Simply put, Rushmore query optimization uses indexes efficiently to quickly find a set of records. Rushmore query optimization is used on queries that involve restrictions on multiple-indexed fields.


Operations That Can Use Rushmore

Index Intersection

Index Intersection involves scanning multiple indexes for records matching criteria such as:

field1 = 'expression' AND field2 = 'expression'

As described in the previous section, Rushmore uses the indexes on both fields to find matching records for each part of the criteria. The results from each of the indexes are intersected to find the records that match both criteria.

For example, suppose you have a table that contains demographic information about all the people in New York City. The table contains information regarding each person’s age, height, weight, hair color, and eye color. Suppose you write the following query to find all the people with blonde hair and blue eyes:

SELECT *
FROM People
WHERE hair_color = 'blonde' AND eye_color = 'blue'

Assuming indexes have been created on the hair_color and eye_color fields, this query would be solved using Rushmore Index Intersection. The index on hair_color is used to find all the blonde-haired people, and then the index on eye_color is used to find all the blue-eyed people. The results of each index search are then intersected to find all the people with both characteristics.

Index Union

Index Union involves scanning multiple indexes for records that match criteria such as:

field1 = 'expression' OR field2 = 'expression'

Again, Rushmore uses the indexes on both fields to find the matching records. The resulting records from each of the indexes are then combined to find the records that match either criterion.

For example, again suppose you have a table that contains demographic information about all the people in New York City. Suppose you write a query to find all the people with black hair or green eyes. The criteria would look like:

SELECT *
FROM People
WHERE hair_color = 'black' OR eye_color = 'green'

Using Rushmore Index Union, the index on hair_color is used to find all the black-haired people, and the index on eye_color is used to find all the green-eyed people. The result of each index search is then combined to find all the people with either characteristic.


Common Pitfalls

Many common mistakes can cause unnecessary bottlenecks when you are running queries. Following is a list of these common mistakes and what to do to correct them.

  • Including expressions in query output. Placing expressions, such as an IIf on an output field of a query can cause optimization problems if the query is used as the input to another query. For example, consider the following two queries, where the first query is saved as Q1:
    SELECT IIf(ShipVia = 2, 'United Package', 'Other')
    AS ShipperName
FROM Orders;

SELECT *
FROM Q1
WHERE ShipperName = 'United Package';

    Because Microsoft Jet can’t optimize the IIf expression in the first query, it can’t optimize the second query. Expressions can get buried so far down in a query tree that it’s easy to forget about them. If expressions are necessary in the output, try to place the expression in a control on a form or report.

    The following query replaces the previous two queries and is the optimal way to write this query:

    SELECT *
FROM Orders
WHERE ShipVia = 2;
  • Using GROUP BY on too many fields. When you’re creating a totals query, use the GROUP BY clause on as few fields as necessary to achieve the query’s goal. The more fields in the GROUP BY clause, the longer the query takes to execute. Make sure you don’t include extraneous fields.

  • Using GROUP BY in same query as a join. If possible, place a GROUP BY clause on a table and then join it to another table, rather than joining the two tables and using the GROUP BY clause in the same query as the join. For example, instead of this query
    SELECT
    Orders.CustomerID,
    Count(Orders.OrderID) AS CountOfOrderID
FROM Customers
INNER JOIN Orders
ON Customers.CustomerID = Orders.CustomerID
GROUP BY Orders.CustomerID;

    you may benefit by using two separate queries, as follows, where the first query is saved as SelectWithGroupByQ1:

    SELECT Customers.CustomerID
FROM Customers
GROUP BY Customers.CustomerID;

SELECT
    Orders.CustomerID,
    Count(Orders.OrderID) AS CountOfOrderID
FROM SelectWithGroupByQ1, INNER JOIN Orders
ON Q1.CustomerID = Orders.CustomerID
GROUP BY Orders.CustomerID;
  • Not including indexes on join fields. When you’re joining tables, always try to index the fields on both sides of a join. This can speed up query execution by allowing more sophisticated join strategies such as index and index-merge joins. The use of indexes also provides better statistics. For more information, see the “Join Strategies” section earlier in this chapter.

  • Under-indexing fields. In appropriate circumstances, for example where the database is used solely for decision-support types of applications, you can place an index on all fields that have unique values and are used in a join or as a restriction. By using Rushmore query optimization, Microsoft Jet can take advantage of multiple indexes on a single table. This makes indexing many fields advantageous. However, keep in mind that adding indexes to fields can cause performance to suffer.

  • Using Count(fieldname) instead of Count(*). When you have to determine the number of records, you should use Count(*) rather than Count(fieldname) because there are Rushmore optimizations that allow Count(*) to be executed much more quickly than Count(fieldname).


Chapter 5 Working with Records and Fields

The Microsoft Jet database engine supports a rich set of Data Access Objects (DAO) features for organizing, sorting, searching, updating, adding, and deleting data. The Recordset object alone provides 24 methods and 34 properties that give you a great deal of control over records in a database. You can use the Recordset object’s Fields collection and a number of Field object properties and methods to manipulate data at the field level. This chapter describes in detail how to manipulate records and fields using the Recordset and Field objects.

Using Recordset Objects

Moving Through a Recordset

Finding a Specific Record

Changing Data

Microsoft Jet Transactions

Extracting Data from a Record

Using Field Objects

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications (VBA) are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


Using Recordset Objects

You use Recordset objects to manipulate the data in a database at the record level. There are five types of Recordset objects — table, dynaset, snapshot, forward-only, and dynamic. Table-type Recordset objects are available only in Microsoft Jet workspaces. Dynamic-type Recordset objects are available only in ODBCDirect workspaces. You specify the type of recordset you want to open by using the type argument of the OpenRecordset method.

  • A table-type Recordset object refers to either a local table in the current database, or to a linked table in an external database created with Microsoft Access or Microsoft Jet. When you create a table-type Recordset object, the database engine opens the actual table, and your subsequent data manipulations operate directly on table data. A table-type Recordset object can be opened against only one table; it cannot be opened against a join or union query.

  • One of the biggest advantages of the table-type Recordset object is that it can be indexed by using an index created for the underlying table. This allows much faster sorting and filtering than is possible with the other Recordset object types. To locate specific records, use the Seek method, which is faster than the Find methods.

  • A dynaset-type Recordset object refers to either local or linked tables, or to the result of a query. It’s actually a set of references to records in one or more tables. With a dynaset, you can extract and update data from more than one table, including linked tables from other databases. Heterogeneous joins are a unique feature of dynasets — they enable you to use updatable join queries against tables in different types of databases.

  • One of the main benefits of the dynaset-type Recordset object is that a dynaset and its underlying tables update each other. Changes made to records in the dynaset are written to the underlying table, and changes made by other users to data in the underlying tables while the dynaset is open are reflected in the dynaset. The dynaset is the most flexible and powerful type of Recordset object, although running queries and manipulating data may take longer than they would with a table-type Recordset object.

  • A snapshot-type Recordset object contains a static copy of the data as it exists at the time the snapshot is created. In a Microsoft Jet workspace, a snapshot can’t be updated. In an ODBCDirect workspace, a snapshot may be updatable, depending on the Open Database Connectivity (ODBC) driver. For more information about ODBCDirect, see Chapter 8, “Accessing External Data,” and Chapter 9, “Developing Client/Server Applications.”

    The main advantage of using a snapshot is that it creates less processing overhead than the other types, so it may execute queries and return data faster, especially when you are working with ODBC data sources. Note that for .mdb files, Long Binary (OLE Object) and Memo fields are represented in a snapshot by pointers, rather than the actual data.

  • A forward-only-type Recordset object is identical to a snapshot-type Recordset object but you can only scroll forward through its records. A forward-only-type Recordset object can’t be updated. Using this type of Recordset object improves performance when you need to make only a single pass through the records.

    Figure 5.1 illustrates the difference between how data is stored in a snapshot-type or forward-only-type Recordset object and how it’s stored in a dynaset. The snapshot stores a copy of the entire record (except for Memo and Long Binary fields). The dynaset stores just the primary key for each record, copying the full record only when it’s needed for editing or display purposes.

    Figure 5.1 The difference between records stored in dynaset- and snapshot-type or forward-only-type Recordset objects.

  • A dynamic-type Recordset object is available only in ODBCDirect workspaces. This Recordset object represents a query result set in which you can add, change, or delete records. In addition, changes made to data in the underlying tables by other users are reflected in the Recordset object.

The type of Recordset object you use depends on what you want to do and whether you want to change or simply view the data. For example, if you must sort the data or work with indexes, use a table-type Recordset object. Because they are indexed, table-type Recordset objects also provide the fastest way to locate data. For example, you can use the Seek method to quickly locate records in an indexed table-type Recordset object only. To search for records in other Recordset object types, you must use the FindFirst, FindLast, FindNext, or FindPrevious method. If you want to be able to update a set of records selected by a query, use a dynaset. If the table-type is unavailable in a specific situation and you only need to scan through a set of records, using a snapshot-type or a forward-only-type Recordset object may improve performance.

Using a table-type Recordset object, if that type is available, almost always results in the best performance.

Note In this chapter, the terms table, dynaset, snapshot, forward-only, and dynamic are often used for the sake of simplicity. Keep in mind, however, that these are all types of Recordset objects. For example, the term dynaset refers to a dynaset-type Recordset object.


Creating a Recordset Variable

The OpenRecordset method is the basic method of creating a Recordset object variable. To use it, you first declare a variable of type Recordset object, and then set the variable to the object returned by the OpenRecordset method.

The OpenRecordset method is available from Database, TableDef, QueryDef and existing Recordset objects. For ODBCDirect workspaces, the OpenRecordset method is available from the Connection, Database, and QueryDef objects. The syntax of the OpenRecordset method for Database and Connection objects is:

Set recordset = object.OpenRecordset (source [, type [, options [, lockedits]]])

The syntax of the OpenRecordset method for all other types of objects is:

Set recordset = object.OpenRecordset ([type [, options [, lockedits]]])

The recordset argument is the name of the new Recordset object.

The object argument is the object from which you’re creating the new Recordset object. Microsoft Jet uses the source argument only for Recordset objects created from Database or Connection objects.

The source argument is an existing TableDef or QueryDef object in the database or a valid record-returning SQL query or statement. For TableDef, QueryDef, and Recordset objects, the object itself is automatically used as the source.

The type argument is an intrinsic constant that specifies the kind of Recordset object you want to create. The available type constants are:

  • dbOpenTable

  • dbOpenDynamic

  • dbOpenDynaset

  • dbOpenSnapshot

  • dbOpenForwardOnly

If you open a Recordset object in a Microsoft Jet workspace and you don’t specify a type, OpenRecordset creates a table-type Recordset object, if possible. If you specify a linked table or query, OpenRecordset creates a dynaset-type Recordset object. In an ODBCDirect workspace, the default setting is dbOpenForwardOnly.

With the options argument of the OpenRecordset method, you can specify a number of other features for a Recordset object by using the following constants:

  • dbAppendOnly With this constant, you can only append new records to the Recordset object; you cannot edit or delete existing records. This is useful in applications that collect and archive data (dynaset only).

  • dbSeeChanges If another user changes data in a record on which this Recordset object has invoked the Edit method, but before it has invoked the Update method, a run-time error will occur. This is useful in applications where multiple users have simultaneous read/write access to the same data (dynaset only).

  • dbDenyWrite When used on a dynaset or snapshot, this constant prevents other users from opening a table or dynaset-type Recordset object, although they can still open snapshot- or forward-only-type Recordset objects (Microsoft Jet workspaces only).

  • dbDenyRead Prevents other users from reading data in the table (table only).

  • dbReadOnly Prevents your Recordset object from modifying data. This is useful where the Recordset object will be used to display data in a form that otherwise allows data entry. The dbReadOnly constant in the lockedits argument replaces this constant, which is provided only for backward compatibility (Microsoft Jet workspaces only).

  • dbForwardOnly Creates a forward-only snapshot. Note that Recordset objects created with this option cannot be cloned and only support the Move and MoveNext methods to move directly through the records (snapshot only). This constant is provided for backward compatibility only. To create a forward-only-type Recordset object, you should use the dbOpenForwardOnly constant in the type argument instead of in this argument.

  • dbSQLPassThrough Where the source argument is an SQL statement, use this constant to pass the SQL statement to an ODBC data source for processing. If used with a dynaset, data is not updatable (Microsoft Jet snapshot only).

  • dbConsistent (Default) Only consistent updates are allowed (Microsoft Jet dynaset and snapshot only).

  • dbInconsistent Inconsistent updates are allowed. Opposite of dbConsistent (Microsoft Jet dynaset and snapshot only).

    See Also For more information about inconsistent updates, see “Inconsistent Updates” later in this chapter.

  • dbRunAsync Runs an asynchronous query (ODBCDirect workspaces only).

  • dbExecDirect Runs a query that skips SQLPrepare and directly calls SQLExecDirect. Use this constant only when you’re not opening a Recordset object based on a parameter query (ODBCDirect workspaces only).

Note When you don’t use the options argument, you must specify 0 for it.

With the lockedits argument of the OpenRecordset method, you can specify locking options for a Recordset object by using the following constants:

  • dbReadOnly Prevents users from making changes to the Recordset object. You can use this constant in either the options argument or the lockedits argument, but not both. If it is used in both arguments, an error occurs. This setting is the default in ODBCDirect workspaces.

  • dbPessimistic Uses pessimistic locking to determine how changes are made to a Recordset object in a multiuser environment. With pessimistic locking, the page that contains the record being edited is locked as soon as the Recordset object’s Edit method is invoked. This setting is the default in Microsoft Jet workspaces.

  • dbOptimistic Uses optimistic locking to determine how changes are made to a Recordset object in a multiuser environment. With optimistic locking, the page that contains the record being edited is not locked until the Recordset object’s Update method is invoked.

  • dbOptimisticValue Uses optimistic concurrency based on record values (ODBCDirect workspaces only).

  • dbOptimisticBatch Enables batch optimistic updating (ODBCDirect workspaces only).

See Also For more information about the dbOptimisticValue and dbOptimisticBatch constants, see Chapter 9, “Developing Client/Server Applications.” For more information about ODBCDirect, see Chapter 8, “Accessing External Data,” and Chapter 9, “Developing Client/Server Applications.”


Creating a Recordset Object from a Table

The method you use to create a Recordset object from a table depends on whether the table is local to the current database or is a linked table in another database. The following discussion explains the differences and provides examples for each type of table.

From a Table in a Local Microsoft Jet Database

The following example uses the OpenRecordset method to create a table-type Recordset object from a table, where strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath, dbOpenTable)
Set rst = dbs.OpenRecordset("Customers")

Note that you don’t need to use the dbOpenTable constant to create a table-type Recordset object. If you omit the type constant, Microsoft Jet chooses the highest-functionality Recordset object type available, depending on the object the Recordset object is created from, and the type of data source being used. Because the table-type Recordset object is available when you open a Recordset object from a local table, Microsoft Jet uses it by default.

From a Linked Table in a Different Database Format

The next example creates a dynaset-type Recordset object for a linked Paradox 3.x table. Because the table type is not available when you open a Recordset object from a linked table in a non-Microsoft Jet database, Microsoft Jet selects the next most efficient type, opening a dynaset-type Recordset object. In the following example, strDbPath is the path to the Microsoft Jet database:

Dim dbs As Database
Dim tdf As TableDef, rst As Recordset

' Get current database.
Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.CreateTableDef("PDXAuthor")

' Connect to Paradox table Author in database C:\JetBook\Samples\pdx.
tdf.Connect = "Paradox 3.X;DATABASE=C:\JetBook\Samples\pdx"
tdf.SourceTableName = "Author.db"

' Link table.
dbs.TableDefs.Append tdf

' Create a dynaset-type Recordset object for the table.
Set rst = tdf.OpenRecordset()

' Print each author's first and last name.
With rst
    Do While Not .EOF
        Debug.Print !au_fname & " " & !au_lname
        .MoveNext
    Loop
End With

You can also open a Paradox table as a table-type Recordset object by first opening the Paradox database.

See Also For more information about connecting to data in external databases and improving performance by using the OpenDatabase method, see Chapter 8, “Accessing External Data.”

Using an Index on a Table-Type Recordset Object

You can order records in a table-type Recordset object by setting its Index property. Any Index object in the Indexes collection of the Recordset object’s underlying table can be specified in the Index property.

The following example illustrates how to create a table-type Recordset object based on the Customers table that uses using an existing index called City. Note how setting the Index property re-sorts the records in the Recordset object. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Customers", dbOpenTable)

With rst
    .MoveFirst				' Move to first record.
    MsgBox !CompanyName	' Show first CompanyName field with no index set.
    .Index = "City"			' Select existing City index to reorder records.
    .MoveFirst				' Move to new first record.
    MsgBox !CompanyName	' Show new first CompanyName field.
End With

You can use the Seek method to locate a record in an indexed table-type Recordset object. You must set the Index property before using the Seek method. If you set the Index property to an index that doesn’t exist, a trappable run-time error occurs. If you want to sort records according to an index that doesn’t exist, either create the index first, or create a dynaset- or snapshot-type Recordset object using a query with an ORDER BY clause that returns records in a specified order.


Creating a Recordset Object from a Query

You can also create a Recordset object based on a stored SELECT query. In the following example, Current Product List is an existing SELECT query stored in the NorthwindTables database, and strDbPath is the path to the database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Current Product List")

See Also For more information about SQL statements, see Chapter 4, “Queries,” and Appendix B, “SQL Reference.”

The OpenRecordset method also accepts an SQL string instead of the name of an existing query. The previous example can be rewritten as follows:

Dim dbs As Database, rst As Recordset
Dim strSQL As String

Set dbs = OpenDatabase(strDbPath)
strSQL = "SELECT ProductID, ProductName FROM Products " & _
    "WHERE Discontinued=False ORDER BY ProductName;"
Set rst = dbs.OpenRecordset(strSQL)

The disadvantage of this approach is that the query must be compiled each time it’s executed, whereas the stored query is compiled when it’s saved, which usually results in slightly better performance.

Note When you create a Recordset object by using an SQL string or a stored query, your code doesn’t continue running until the query returns the first record in the Recordset object. You may want to consider displaying a message in the status bar while the query is running.


Default Recordset Object Types

Because Microsoft Jet automatically chooses the default Recordset object type depending on the data source and how the Recordset object is opened, you don’t need to specify a Recordset object type. However, you can force Microsoft Jet to create specific Recordset objects by specifying the type argument of the OpenRecordset method.

The following list describes the available types and the default type, depending on how you open the Recordset object:

  • OpenRecordset method from a Database object:

    Set rst = dbs.OpenRecordset(datasource)

    If datasource is a table local to the database, all Recordset object types except the dynamic type are available, and the default type is table. The dynamic-type Recordset object is available only in ODBCDirect workspaces. If datasource is not a local table, only the dynaset, snapshot, or forward-only Recordset object types are available, and dynaset is the default.

  • OpenRecordset method from a TableDef object:

    Set rst = tdf.OpenRecordset()

    If tdf refers to a table in a Microsoft Jet database or to an external ISAM database opened directly, all Recordset object types except the dynamic type are available and table is the default type. If tdf is in an ODBC database or is a linked table in an external database, only the dynaset, snapshot, and forward-only Recordset object types are available and dynaset is the default.

  • OpenRecordset method from a QueryDef object or an SQL string argument:

    Set rst = qdf.OpenRecordset()

    Only the dynaset, snapshot, and forward-only Recordset object types are available, and the default is dynaset.

  • OpenRecordset method from an existing Recordset object:

    Set rstNew = rstExisting.OpenRecordset()

    Only the dynaset, snapshot, and forward-only Recordset object types are available. The default is the Recordset object type of the source Recordset object. For example, in the preceding example, if rstExisting is a snapshot-type Recordset object, rstNew will also be a snapshot-type Recordset object.

Microsoft Access Users To create a Recordset object based on a Microsoft Access form, you can use the RecordsetClone property of the form. This creates a dynaset-type Recordset object that refers to the same underlying query or data as the form’s RecordSource property. If a form is based on a query, for example, referring to the form’s RecordsetClone property is the equivalent of creating a dynaset with the same query. You can use the RecordsetClone property when you want to apply a method that can’t be used with forms, such as the FindFirst method. The RecordsetClone property provides access to all the methods and properties that you can use with a dynaset-type Recordset object. The syntax for the RecordsetClone property is:

Set recordset = form.RecordsetClone

The form argument is the name of an open Microsoft Access form.

The following example returns a Recordset object variable from the record source of a form. It then searches for the specified record in the recordset, and if the record exists, the form’s Bookmark property is set to the value of the recordset’s Bookmark property. Setting the form’s Bookmark property displays the corresponding record.

In this example, strFormName is the name of a form, strFieldName is the name of a field on a form, and strFieldValue is the value to search for in the recordset.

Sub FindFormRecord(strFormName As String, strFieldName As String, _
    strFieldValue As String)

    Dim rst As Recordset
    Dim frm As Form

    ' Open the specified form.
    DoCmd.OpenForm strFormName
    Set frm = Forms(strFormName)
    ' Open a recordset based on the form's RecordSource property.
    Set rst = frm.RecordsetClone
    With rst
        ' Search for the specified value.
        .FindFirst strFieldName & " Like """ & strFieldValue & "*" & """"
        ' If value is not found, display message.
        If .NoMatch Then
            MsgBox "No record found for " & strFieldName & ": " & strFieldValue
        ' If value is found, set form's Bookmark property to value of recordset's
        ' Bookmark property.
        Else
            frm.Bookmark = .Bookmark
        End If
        .Close
    End With
End Sub

The RecordsetClone property always creates the type of Recordset object being cloned (the type of Recordset object on which the form is based); no other types are available.

Visual Basic Users You can create a Recordset object from a Data control by setting a new Recordset object variable equal to the value of the control’s Recordset object property. For more information, see the Microsoft Visual Basic Programmer’s Guide.


Sorting and Filtering Records

Unless you open a table-type Recordset object and set its Index property, you can’t be sure records will appear in any specific order. Most of the time, however, you want to retrieve records in some specific order. For example, you may want to view invoices arranged by increasing invoice number or retrieve employee records arranged alphabetically by last name.

To sort non-table Recordset object data, use an ORDER BY clause in the SQL query that constructs the Recordset object, whether that clause is contained in a QueryDef object, a stored query in a database, or in an SQL string passed to the OpenRecordset method.

With any type of Recordset object, use an SQL WHERE clause in a query to filter data (to restrict the result set to records that meet some criteria).

The following example opens a dynaset-type Recordset object, and uses an SQL statement to filter and sort a Recordset object by using the WHERE and ORDER BY clauses. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("SELECT FirstName, " & _
    "LastName FROM Employees WHERE Title = ""Sales Representative"" " & _
    "ORDER BY LastName")
With rst
    Do While Not .EOF
        Debug.Print !FirstName & " " & !LastName
        .MoveNext
    Loop
End With

Microsoft Access Users One drawback of executing an SQL query in an OpenRecordset method is that it has to be compiled every time you run it. If this query is to be used frequently, you can improve performance by creating a stored query that contains the same SQL statement, and then opening a Recordset object against the query. In the following example, Sales Representatives is a saved query:

Dim dbs As Database, rst As RecordsetSet

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Sales Representatives")

For even greater flexibility and control at run time, you can use query parameters to determine the sort order and filter criteria. This is discussed in more detail in the “Using Parameter Queries” section later in this chapter.

Re-creating a Query from a Recordset Object

A Recordset object opened from a QueryDef object can also be used as a template to re-create the QueryDef object using the CopyQueryDef method. This is useful in situations where a Recordset object variable created from a QueryDef is passed to a function, and the function must re-create the SQL equivalent of the query and possibly modify it.

Modifying a Query from a Recordset Object

You can use the Requery method on any Recordset object except a table-type Recordset object when you want to re-execute the underlying query after changing a parameter. This is more convenient than opening a new Recordset object, and the query also executes faster.

The following procedure takes a Recordset object that was opened on a QueryDef object and uses the CopyQueryDef method to extract the underlying query’s SQL string, prompts the user to add an additional WHERE clause to the query, and requeries the Recordset object based on the altered query:

Function AddQueryFilter(rst As Recordset) As Recordset
    Dim qdf As QueryDef
    Dim strNewFilter As String, strQuery As String
    Dim varPosition As Variant, intLength As Integer
    Const conOperationNotSupported As Integer = 3251

    On Error GoTo Err_AddQueryFilter
    Set qdf = rst.CopyQueryDef
    strNewFilter = InputBox("Enter new constraint for WHERE clause, " _
        & "e.g., ProductName Like 'M*'")

    ' Create new query string with added restriction.
    strQuery = qdf.SQL
    ' Return string length.
    intLength = Len(strQuery)
    ' Check for WHERE clause in string.
    varPosition = InStr(strQuery, "WHERE")
    If varPosition > 0 Then
        ' Append new restriction to existing WHERE clause.
        strQuery = Left(strQuery, varPosition + 5) & strNewFilter & " AND " _
            & Right(strQuery, intLength - (varPosition + 5))
        qdf.SQL = strQuery
    Else
        ' If no WHERE clause, alert user and exit function.
        ' You could also add code to handle this situation.
        MsgBox "The SQL statement you supplied did not include a WHERE clause."
        GoTo Exit_AddQueryFilter
    End If

    ' Requery recordset.
    rst.Requery qdf
    Set AddQueryFilter = rst

Exit_AddQueryFilter:
    Exit Function

Err_AddQueryFilter:
    If Err = conOperationNotSupported Then
        Dim strMsg As String
        strMsg = "This recordset was not opened on a QueryDef object. " & _
            "You can call this function only with a recordset opened " & _
            "on a QueryDef object."
        MsgBox strMsg, vbOKOnly
    Else
        MsgBox Err & ": " & vbCrLf & Err.Description
    End If
    Set AddQueryFilter = Nothing
    Resume Exit_AddQueryFilter
End Function

Note To execute the Requery method, the Recordset object’s Restartable property must be set to True. The Restartable property is always True when the Recordset object is created from a query other than a crosstab query based on tables in a Microsoft Jet database. SQL pass-through queries are not restartable. Queries against linked tables in another database format may or may not be restartable. To determine whether a Recordset object can re-execute its query, check the Restartable property.

The Sort and Filter Properties

Another approach to sorting and filtering recordsets is to set the Sort and Filter properties on an existing Recordset object, and then open a new Recordset object from the existing one. However, this is usually much slower than just including the sort and filter criteria in the original query or changing the query parameters and re-executing the query with the Requery method. The Sort and Filter properties are useful when you want to allow a user to sort or restrict a result set but the original data source is unavailable for a new query — for example, when a Recordset object variable is passed to a function, and the function must reorder records or restrict the records in the set. With this approach, performance is likely to be slow if more than 100 records are in the Recordset object. Using the CopyQueryDef method described in the previous section is preferable.


Moving Through a Recordset

A Recordset object has a current position, usually at a record. When you refer to the fields in a Recordset object, you obtain values from the record at the current position, which is known as the current record. However, the current position can also be immediately before the first record in a Recordset object or immediately after the last record. In certain circumstances, the current position is undefined. When you open a Recordset object that contains at least one record, the first record is the current record and the Recordset object’s BOF property is False. If the Recordset object contains no records, the Recordset object’s BOF and EOF properties will both be True. For more information about the BOF and EOF properties, see the section “Detecting the Limits of a Recordset.”

You can use the Move methods to loop through the records in a Recordset object:

  • The MoveFirst method moves to the first record.

  • The MoveLast method moves to the last record.

  • The MoveNext method moves to the next record.

  • The MovePrevious method moves to the previous record.

  • The Move rows, start method moves forward or backward the number of records specified by the rows argument, beginning with the record specified by the start argument. The start argument is optional, and if left blank, the Move operation begins at the current record.

The following example changes the job title of all sales representatives in a table called Employees. After opening the table, the code sets the index, which makes it necessary to use the MoveFirst method to locate the first record. For each record satisfying the title condition, the example changes the title and saves the change with the Update method. It uses the MoveNext method to move to the next record. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Employees", dbOpenTable)

With rst
    ' Set current index.
    .Index = "LastName"
    ' Locate first record.
    .MoveFirst
    ' Begin loop.
    Do Until .EOF
        If !Title = "Sales Representative" Then
            ' Enable editing.
            .Edit
            ' Change title.
            !Title = "Account Executive"
            ' Save changes.
            .Update
        End If
    ' Locate next record.
    .MoveNext
    ' End of loop.
    Loop
    ' Close recordset.
    .Close
End With

Visual Basic Users You can use the Data control to browse and display records in a Recordset object and to edit data in records as they are displayed. For more information on the Data control, see the Microsoft Visual Basic Programmer’s Guide.


Detecting the Limits of a Recordset

In a Recordset object, if you try to move too far in a direction, a run-time error occurs. For example, if you try to use the MoveNext method when you’re already at the end of the Recordset object, a trappable error occurs. For this reason, it’s helpful to know the limits of the Recordset object.

The BOF property indicates whether the current position is at the beginning of the Recordset object. If BOF is True, the current position is before the first record in the Recordset object. The BOF property is also True if there are no records in the Recordset object when it’s opened. Similarly, the EOF property is True if the current position is after the last record in the Recordset object or if there are no records in the Recordset object.

The following example shows how to use the BOF and EOF properties to detect the beginning and end of a Recordset object. This code fragment creates a table-type Recordset object based on the Employees table. It moves through the records, first from the beginning of the Recordset object to the end, and then from the end of the Recordset object to the beginning. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Employees", dbOpenTable)

With rst
    ' Move to first record in recordset.
    .MoveFirst
    Do Until .EOF
        ' Print value of data.
        Debug.Print rst!LastName
        ' Move to next record.
        .MoveNext
    Loop
    Debug.Print
    ' Move to last record.
    .MoveLast
    Do Until .BOF
        Debug.Print rst!LastName
        ' Move to previous record.
        .MovePrevious
    Loop
    .Close
End With

Note that there’s no current record immediately following the first loop. The BOF and EOF properties both have the following characteristics:

  • If the Recordset object contains no records when you open it, both BOF and EOF are True.

  • When BOF or EOF are True, they remain True until you move to an existing record, at which time the value of BOF or EOF becomes False.

  • When BOF or EOF is False, and the only record in a Recordset object is deleted, the property remains False until you attempt to move to another record, at which time both BOF and EOF become True.

  • At the moment you create or open a Recordset object that contains at least one record, the first record is the current record, and both BOF and EOF are False.

  • If the first record is the current record and you use the MovePrevious method to move past the first record in the Recordset object, BOF is set to True. If you use the MovePrevious method while BOF is True, a run-time error occurs. When this happens, BOF remains True and there is no current record.

  • Similarly, moving past the last record in the Recordset object changes the value of the EOF property to True. If you use the MoveNext method while EOF is True, a run-time error occurs. When this happens, EOF remains True and there is no current record.

    Figure 5.2 Relationship between the current record position and the BOF and EOF properties.


Counting the Number of Records in a Recordset

You may want to know the number of records in a Recordset object. For example, you may want to create a form that shows how many records are in each of the tables in a database. Or you may want to change the appearance of a form or report based on the number of records it includes.

The RecordCount property for a table-type Recordset object contains the number of records in the table. The RecordCount property for any other type of Recordset object contains the number of records that have been actually been visited. A Recordset object with no records has a RecordCount property value of 0.

The RecordCount property value equals the number of records actually visited. For example, when you first create a non-table-type Recordset object, you have accessed (or “visited”) only the first record. If you check the RecordCount property immediately after creating this Recordset object (assuming it has at least one record), the value is 1. To visit all the records, use the MoveLast method immediately after opening the Recordset object, then use MoveFirst to return to the first record. This is not done automatically, because it may be slow, especially for large result sets. Opening a table-type Recordset object effectively visits all of the records in the underlying table, and RecordCount totals the number of records in the table as soon as the Recordset object is opened. Aborted transactions may make the RecordCount property value out of date in some multiuser situations. Compacting the database restores the table’s record count to the correct value.

The following example creates a dynaset-type Recordset object (because it’s based on an SQL statement) from the Employees table, and then determines the number of records in the Recordset object. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("SELECT LastName, " _
    & "FirstName FROM Employees")
With rst
    .MoveLast
    MsgBox "There are " & .RecordCount & " records in this recordset."
End With

As records in a dynaset-type Recordset object are deleted by your application, the value of the RecordCount property decreases. However, in a multiuser environment, records deleted by other users aren’t reflected in the value of the RecordCount property until the current record position is a deleted record. At that time, the setting of the RecordCount property decreases by 1. Using the Requery method on a Recordset object, followed by the MoveLast method, sets the RecordCount property to the current total number of records in the Recordset object.

A snapshot-type or forward-only-type Recordset object is static and its RecordCount property value doesn’t change when other users add or delete records in the underlying table.


Finding the Current Position in a Recordset

In some situations, you need to determine how far through a Recordset object you have moved the current record position, and perhaps indicate the current record position to a user. For example, you may want to indicate the current position on a dial, meter, or similar type of control. Two properties are available to indicate the current position: the AbsolutePosition property and the PercentPosition property.

The AbsolutePosition property value is the position of the current record from 0 to one less than the total number of records in a non-table-type Recordset object. However, don’t think of this property as a record number; if the current record is undefined, the AbsolutePosition property returns – 1. Further, there is no guarantee that records will appear in the same order every time the Recordset object is accessed.

The PercentPosition property shows the current position expressed as a percentage of the total number of records indicated by the RecordCount property. Because the RecordCount property does not reflect the total number of records in the Recordset object until the Recordset object has been fully populated, the PercentPosition property reflects the current record position only as a percentage of the number of records that have been accessed since the Recordset object was opened. To make sure that the PercentPosition property reflects the current record position relative to the entire Recordset object, use the MoveLast and MoveFirst methods immediately after opening the Recordset object. This fully populates the Recordset object before you attempt to use the PercentPosition property. If you have a large result set, you may not want to use the MoveLast method because it may take a long time.

Caution The PercentPosition property is only an approximation and shouldn’t be used as a critical parameter. This property is best suited for driving an indicator, such as a “percent complete” control, or a similar indicator that marks a user’s progress while moving though a set of records.

The following example opens a Recordset object on a table called Employees and fully populates it. The program then lists the content of each field of each record in the table, numbering the records as they are printed. When half the records have been printed, a message indicates that the user is more than halfway through the table. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Employees", dbOpenDynaset)

With rst
    ' Move to end of recordset.
    .MoveLast
    ' Move back to start of recordset.
    .MoveFirst
    Do While Not .EOF
        Debug.Print "Record No. " & .AbsolutePosition + 1
        Debug.Print !FirstName & " " & !LastName
        Debug.Print "AbsolutePosition = " & .AbsolutePosition
        Debug.Print "PercentPosition = " & .PercentPosition
        If .PercentPosition > 50 Then
            Debug.Print "More than half way!"
        End If
        Debug.Print
        .MoveNext
    Loop
    .Close
End With


Finding a Specific Record

The previous section, “Moving Through a Recordset,” explores ways you can use the Move methods — MoveFirst, MoveLast, MovePrevious, and MoveNext — to loop through a Recordset object. In most cases, however, it’s more efficient to search for a specific record.

For example, you may want to find a particular employee based on an employee number, or you may want to find all the order detail records that belong to a specific order. In these cases, looping through all the employee or order detail records could be time consuming. Instead, you can use the Seek method to locate records with table-type Recordset objects, and the Find methods with dynaset-, snapshot-, forward-only-, and dynamic-type Recordset objects.


Finding a Record in a Table-Type Recordset Object

You use the Seek method to locate a record in a table-type Recordset object. To locate a record in a non-table-type Recordset object, use one of the Find methods described in the next section. When you use the Seek method to locate a record, Microsoft Jet uses the table’s current index, as defined by the Index property.

The syntax for the Seek method is:

recordset.Seek comparison, key1, key2 ... key13

The table argument is the table-type Recordset object you’re searching. The Seek method accepts a number of arguments, the first of which is comparison, a string that determines the kind of comparison being performed. The following table lists the comparison strings you can use with the Seek method.

Comparison string | Description
"=" | Equal to the specified key values
">=" | Greater than or equal to the specified key values
">" | Greater than the specified key values
"<=" | Less than or equal to the specified key values
"<" | Less than the specified key values

The key arguments are a series of one or more values (up to 13) that correspond to the field or fields that make up the current index of the Recordset object. Microsoft Jet compares these values to values in the corresponding fields of the Recordset object’s records.

The following example opens a table-type Recordset object on the Suppliers table, and uses the Seek method to locate the first record whose SupplierID field matches the value of the intID variable. It then changes the value of the CompanyName field to the value of the strNewName variable and saves the change with the Update method. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Suppliers", dbOpenTable)
With rst
    ' Define current index.
    .Index = "PrimaryKey"
    ' Seek record.
    .Seek "=", intID
    ' If SupplierID is found, change company name.
    If Not .NoMatch Then
        .Edit
        !CompanyName = strNewName
        .Update
    Else
        MsgBox "No record found for SupplierID: " & intID
    End If
    .Close
End With

If you use the Seek method on a table-type Recordset object without first setting the current index, a run-time error occurs.

The next example illustrates how you can create a function that uses the Seek method to locate a record by using a multiple-field index. In this example, strDbPath is the path to the NorthwindTables database, lngOrderID is the value of a record’s OrderID field, and lngProductID is the value of a record’s ProductID field:

Function GetPrice(strDbPath As String, lngOrderID As Long, _
        lngProductID As Long) As Variant
    Dim dbs As Database, rst As Recordset

    Set dbs = OpenDatabase(strDbPath)
    ' Open recordset on table.
    Set rst = dbs.OpenRecordset("Order Details", dbOpenTable)
    ' Specify index for seek.
    rst.Index = "PrimaryKey"
    ' Seek values for both fields in index.
    rst.Seek "=", lngOrderID, lngProductID
    ' Check for matching record.
    If rst.NoMatch Then
        GetPrice = Null
        MsgBox "Couldn't find order detail record."
    Else
        GetPrice = rst!UnitPrice
    End If
    rst.Close
    dbs.Close
    Set dbs = Nothing
End Function

In this example, the table’s primary key consists of two fields, OrderID and ProductID. When you call the GetPrice function with a valid (existing) combination of OrderID and ProductID field values, the function returns the unit price from the found record. If the combination of field values you want can’t be found in the table, the function returns a Null value.

If the current index is a multiple-field index, trailing key values can be omitted and are treated as Null values. That is, you can leave off any number of key values from the end of a Seek method’s key argument, but not from the beginning or the middle. However, if you don’t specify all values in the index, you can use only the ">" or "<" comparison operator with the Seek method.


Finding a Record in a Dynaset- or Snapshot-Type Recordset Object

You can use the following methods to locate a record in a non-table-type Recordset object. (To locate a record in a table-type Recordset object, use the Seek method, which is described in the previous section.) Microsoft Jet supports four Find methods:

  • The FindFirst method finds the first record satisfying the specified criteria.

  • The FindLast method finds the last record satisfying the specified criteria.

  • The FindNext method finds the next record satisfying the specified criteria.

  • The FindPrevious method finds the previous record satisfying the specified criteria.

When you use the Find methods, you specify the search criteria, which is typically an expression equating a field name with a specific value. If no record matching the specified criteria is found, the Recordset object’s NoMatch property is set to True.

The following example looks up phone numbers in an Employees table. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset
Dim strName As String, strCriteria As String
Dim strSQL As String, strInput As String
Dim strMsg As String

Set dbs = OpenDatabase(strDbPath)
strSQL = "SELECT FirstName, LastName, HomePhone " _
    & "FROM Employees ORDER BY LastName"
' Open snapshot-type recordset.
Set rst = dbs.OpenRecordset(strSQL, dbOpenSnapshot)

strInput = "Enter the first few letters of the last " _
    & "name of the employee you want to find: "
' Prompt user for input.
strName = InputBox(strInput)
' Check for input.
If Len(strName) = 0 Then Exit Sub
' Append asterisk to user's input.
strCriteria = "LastName Like """ & strName & "*"""

With rst
    ' Find first instance of criteria.
    .FindFirst strCriteria
    Do
        ' Check for match.
        If Not .NoMatch Then
            strMsg = "The number for " & !FirstName & _
                " " & !LastName & " is " & _
                !HomePhone & ". Search again?"
            ' Display result and prompt user to search again.
            If MsgBox(strMsg, vbYesNo) = vbYes Then
                strName = InputBox(strInput)
                If Len(strName) = 0 Then Exit Sub
                strCriteria = "LastName Like """ & strName & "*"""
                .FindFirst strCriteria
            Else
                Exit Sub
            End If
        Else
            strMsg = "There was no match found for " _
                & " an employee last name like '" & _
                strName & "*'"
            MsgBox strMsg, vbOKOnly, "Find Home Phone"
            Exit Sub
        End If
    Loop
    .Close
End With
dbs.Close
Set dbs = Nothing

You can locate the matching records in reverse order by finding the last occurrence with the FindLast method and then using the FindPrevious method instead of the FindNext method.

Microsoft Jet sets the NoMatch property to True whenever a Find method fails and the current record position is undefined. There may be a current record, but you have no way to tell which one. If you want to be able to return to the previous current record following a failed Find method, use a bookmark (as described in the following section).

The NoMatch property is False whenever the operation succeeds. In this case, the current record position is the record found by one of the Find methods.

The following example illustrates how you can use the FindNext method to find all records in the Customers table that contain a Null value in the Fax field. It then prompts the user to enter a fax number for the customer. In this example, strDbPath is the path to the database:

Dim dbs As Database, rst As Recordset
Dim strCriteria As String, varNewFax As Variant
Dim strMsg As String

Set dbs = OpenDatabase(strDbPath)
' Open dynaset-type recordset.
Set rst = dbs.OpenRecordset("Customers", dbOpenDynaset)
' Check for Null value in Fax field.
strCriteria = "Fax Is Null"
With rst
    ' Find first Null value.
    .FindFirst strCriteria
    Do Until .NoMatch
        ' Prompt user to enter fax number.
        varNewFax = InputBox(!CompanyName & " is missing " & _
            "a fax number. Please enter the fax number now.")
        If Len(varNewFax) = 0 Then
            strMsg = "Do you want to find " _
                & "the next missing fax number?"
            If MsgBox(strMsg, vbYesNo, "Continue Search?") = vbNo Then
                Exit Sub
            Else
                varNewFax = Null
            End If
        End If
        ' Edit and update record.
        .Edit
        !Fax = varNewFax
        .Update
        ' Find next Null value.
        .FindNext strCriteria
    Loop
    .Close
End With

If you need to frequently search a large number of records in a dynaset, you may get better performance by creating a temporary indexed table and using the Seek method instead.


Marking Record Positions with Bookmarks

A bookmark is a system-generated Byte array that uniquely identifies each record. The Bookmark property of a Recordset object changes each time you move to a new record. To identify a record, move to that record and then assign the value of the Bookmark property to a variable of type Variant. To return to the record, set the Bookmark property to the value of the variable.

The following example illustrates how you can use a bookmark to save the current record position and then quickly return to that record position if a Find or Seek method fails. Using a bookmark in this way is useful, because if the method fails, the current record position is undefined. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset
Dim varOrigin As Variant

Set dbs = OpenDatabase(strDbPath)
' Open table-type recordset.
Set rst = dbs.OpenRecordset("Customers", dbOpenTable)
With rst
    ' Set recordset index.
    .Index = "CompanyName"
    MsgBox "Current Record: CompanyName = " & !CompanyName
    ' Return bookmark.
    varOrigin = .Bookmark
    ' Perform seek.
    .Seek ">=", "Z"
    ' Check for match.
    If .NoMatch Then
        MsgBox "Can't find a company name starting with 'Z'. ", _
            vbOKOnly, "There is no current record!"
        ' Set bookmark.
        .Bookmark = varOrigin
    End If
    MsgBox "Current Record: CompanyName = " & !CompanyName
    .Close
End With

In this example, the Bookmark property for the current record is saved, and if the Seek method fails, the Bookmark property is reset to its previous value.

The LastModified property of the Recordset object provides a good illustration of how to use a bookmark. The LastModified property is the bookmark of the last record in the Recordset object to be added or modified. To use it, set the Recordset object’s Bookmark property equal to the LastModified property as follows:

rst.Bookmark = rst.LastModified

This moves the current record position to the last record that was added or modified. This is particularly useful when you are adding new records, because by default, the current record position after you add a new record is the record you were on before using the AddNew method. With the LastModified property, you can easily make the newly added record the current record if that’s what your application expects.


Bookmark Scope

When you close a Recordset object, any bookmarks you saved become invalid. You can’t use a bookmark from one Recordset object in another Recordset object, even if both Recordset objects are based on the same underlying table or query. However, you can use a bookmark on the duplicate (clone) of a Recordset object, as shown in the following example. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rstOrig As Recordset
Dim rstClone As Recordset, varPlace As Variant

Set dbs = OpenDatabase(strDbPath)
Set rstOrig = dbs.OpenRecordset("Customers", dbOpenDynaset)
rstOrig.MoveLast
' Save current record location.
varPlace = rstOrig.Bookmark
' Create duplicate recordset.
Set rstClone = rstOrig.Clone
MsgBox "Before setting bookmark: Current Record CompanyName = " _
    & rstClone!CompanyName
' Go to saved record.
rstClone.Bookmark = varPlace
MsgBox "After setting bookmark: Current Record CompanyName = " _
    & rstClone!CompanyName

Microsoft Access Users You can also use the Bookmark property on the Recordset object underlying a form. With this property, your code can mark which record is currently displayed on the form, and then change the record being displayed. For example, on a form containing employee information, you may want a button that a user can click to show the record for an employee’s supervisor. The following example illustrates the event procedure you would use for the button’s Click event:

Private Sub cmdShowSupervisor_Click()
    Dim rst As Recordset
    Dim varOrigin As Variant, strEmployee As String
    Dim strSuper As String

    ' Open Recordset object.
    Set rst = Me.RecordsetClone

    ' Store bookmark for current record.
    varOrigin = Me.Bookmark
    ' Store first and last name of employee.
    strEmployee = Me!FirstName & " " & Me!LastName
    ' Check whether employee reports to anyone.
    If Not IsNull(Me!ReportsTo) Then
        ' Search for first record matching value of ReportsTo field.
        rst.FindFirst "EmployeeID = " & Me!ReportsTo
        ' Check whether record for supervisor exists in table.
        If rst.NoMatch Then
            MsgBox "Couldn't find " & strEmployee & "'s supervisor."
        Else
            ' If supervisor record exists, set form's bookmark.
            ' Current record is now supervisor's record.
            Me.Bookmark = rst.Bookmark
            ' Store supervisor's first and last name.
            strSuper = Me!FirstName & " " & Me!LastName
            ' Display supervisor's name.
            MsgBox strEmployee & "'s supervisor is " & _
                strSuper & "."
            ' Move back to employee's record.
            Me.Bookmark = varOrigin
        End If
    Else
        MsgBox strEmployee & " has no supervisor."
    End If
    rst.Close
End Sub
Why Use Bookmarks Instead of Record Numbers?

If you have used another database or programming environment, you may be accustomed to referring to record numbers. For example, you may have written code that opens a text file and thereafter refers to specific records by their relative position in the file. The first record in the file would be record 1, the second would be record 2, and so on.

In Microsoft Jet databases, your view of records (a Recordset object) is usually a subset of the records in one or more tables. Because the actual number of records in a Recordset object can change at any time, especially in a multiuser environment, there’s no absolute record number you can always use to refer to a particular record. The AbsolutePosition property is not the same as a record number, because this property changes if a lower-numbered record is deleted.

Furthermore, records returned in a Recordset object appear in no particular order, unless the Recordset object was created with a query that includes an ORDER BY clause, or is a table-type Recordset object with an index. For this reason, record numbers are meaningless in a Recordset object.

Instead of record numbers, Microsoft Jet provides bookmarks so that you can uniquely identify a particular record. A given record retains its unique bookmark for the life of the Recordset object.

Visual C++ Users Bookmarks are especially useful in conjunction with the GetRowsEx method in the DAO Software Development Kit (SDK). If you choose dbBindBookmark as one of the fields to be bound, it will retrieve a bookmark as a pseudo-field. This enables you to combine the speed and convenience of the GetRowsEx method with easy updating.

For example, you could have retrieved code to fill a list box that looks something like:

// Structure for DoGetRowsEx
typedef struct
    {
    LONG		lEmpId;
    LPVOID		lpbm;
    TCHAR		*lpstrLastName;
    TCHAR		strFirstName[20];
    } EMP, *LPEMP ;

// Employee table binding
DAORSETBINDING	Bindings[] =
{
//Index Type			Column			Type				Offset							Size
{dbBindIndexINT,	EMP_ID,		dbBindI4,		offsetof(EMP,lEmpId),		sizeof(LONG)},
{dbBindIndexINT,	NULL,		dbBindBookmark,	offsetof(EMP,lpbm),			sizeof(LPVOID)},
{dbBindIndexINT,	EMP_LNAME,	dbBindLPSTRING,	offsetof(EMP,lpstrLastName),sizeof(TCHAR *)},
{dbBindIndexINT,	EMP_FNAME,	dbBindSTRING,	offsetof(EMP,strFirstName),	sizeof(TCHAR) * 20}
};

// Perform C++ GetRowsEx against the Employee table.
void CDlg::DoGetRowsEx()
{
    LPEMP			pEmpRows = new EMP[MAX_EMP_REC];
    CString			strLBRow;
    TCHAR			szId[16];
    LONG			lNumRecords;
    LONG			lCount;
    LONG			cbBuf = ( MAX_EMP_REC * sizeof(TCHAR) * 15 ); // Allow average of 15 chars/name.
    LPVOID			pvBuf = new LPVOID[cbBuf];
    //Perform GetRows on Employee table.
    //This GetRows uses a specific C++ structure.
    lNumRecords = m_rs.GetRowsEx(pEmpRows, sizeof(EMP),
                                    &Bindings[0], sizeof(Bindings) / sizeof(DAORSETBINDING),
                                    pvBuf, cbBuf,
                                    MAX_EMP_REC); //Arbitrarily get MAX_EMP_REC rows.

//Step through the returned rows.
    for (lCount = 0; lCount < lNumRecords; lCount++)
        {
        strLBRow.Empty();
        wsprintf(szId, _T("%d,  "), pEmpRows[lCount].lEmpId);
        strLBRow += szId;
        strLBRow += pEmpRows[lCount].lpstrLastName;
        strLBRow += _T(", ");
        strLBRow += (LPCTSTR) pEmpRows[lCount].strFirstName;
        //Put the bookmark in the global array.
        m_rgbm[lCount] = CdbBookmark((LPSAFEARRAY)pEmpRows[lCount].lpbm );

        // Put a pointer to it in the ItemData for the list box entry.
        m_lstEmps.SetItemData( m_lstEmps.AddString(strLBRow),
                                (DWORD)&m_rgbm[lCount] );
        }
    delete [] pEmpRows;
    delete [] pvBuf;
}

See Also For more information about the GetRowsEx method, see Chapter 11, “Programming with DAO in C++ and Java.” For a complete sample program, see the Visual C++ project file in the \Program Files\DevStudio\DAOSDK\SAMPLES\GETROWS folder of the DAO SDK, which is installed with Microsoft Visual C++.

The prior code example could be combined with selection code that moves the current record as follows:

void CDlg::OnSelchangeListEmployees()
{
    // Cast the dword held in ItemData back into a pointer to a
    // bookmark. The actual bookmarks are being held in m_rgbm
    // and will automatically deallocate the system safe arrays and
    // destruct on CDlg destruction.
    CdbBookmark *pbm = (CdbBookmark *)m_lstEmps.GetItemData(m_lstEmps.GetCurSel());
    m_rs.SetBookmark( *pbm );
    m_strMoreInfo.Format(
        _T("Notes:  %s\n"),
        (LPCTSTR)( m_rs[15].GetValue().bstrVal ) );
    UpdateData( FALSE );
}


Which Recordset Objects Support Bookmarks?

Dynasets based on certain linked tables, such as Paradox tables that have no primary key, do not support bookmarks, nor do forward-only-type or snapshot-type Recordset objects opened with the dbOpenForwardOnly constant.

You can determine whether a given Recordset object supports bookmarks by checking the value of the Bookmarkable property, as in the following example:

If rstLinkedTable.Bookmarkable Then
    MsgBox "The underlying table supports bookmarks."
Else
    MsgBox "The underlying table doesn't support bookmarks."
End If

If you try to use bookmarks on a Recordset object that doesn’t support bookmarks, a run-time error occurs.


Changing Data

After you’ve created a table- or dynaset-type Recordset object, you can change, delete, or add new records. You can’t change, delete, or add records to a forward-only- or snapshot-type Recordset object. This section presents the methods and procedures for changing data in table- and dynaset-type Recordset objects.


Using Parameter Queries

In many situations, you’ll want a user or another program to provide parameters to your stored queries and Recordset objects. Microsoft Jet provides the means to do this. You first create a QueryDef object, specifying which parameters are to be provided by the end user. When you open a Recordset object based on the QueryDef object, the application opens a dialog box that prompts the user to enter a value, such as the criteria for a WHERE clause or the field on which to sort the selected records.

The following example creates a temporary query that returns the name and hire date of each employee hired after a certain date. Before running the query, the program calls the InputBox function to prompt the user for a threshold date. The names are then shown in the Debug window, starting with the most recent hire. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset
Dim qdf As QueryDef
Dim strSQL As String, strInput As String

Set dbs = OpenDatabase(strDbPath)
' Create SQL statement with parameters.
strSQL = "PARAMETERS BeginningDate DateTime; " & _
    "SELECT FirstName, LastName, HireDate FROM Employees " & _
    "WHERE HireDate >= BeginningDate " & _
    "ORDER BY HireDate ASC;"
' Create temporary QueryDef.
Set qdf = dbs.CreateQueryDef("", strSQL)
' Prompt user for input.
strInput = InputBox("Enter the earliest hire date:")
If Len(strInput) = 0 Then Exit Sub
If Not IsDate(strInput) Then Exit Sub
' Set parameter value.
qdf.Parameters("BeginningDate") = strInput
' Open recordset.
Set rst = qdf.OpenRecordset()
With rst
    If .BOF Then
        MsgBox "No employees hired after date: " & strInput
        Exit Sub
    End If
    Do While Not .EOF
        Debug.Print !FirstName & " " & !LastName & " was hired on: " & !HireDate
        .MoveNext
    Loop
    .Close
End With

Most of the database maintenance tasks described in the rest of this chapter can be accomplished by using stored parameter queries.


Making Bulk Changes

Many of the changes you may otherwise perform by looping through all the records in a Recordset object can be done more efficiently with an update or delete query. For example, the following code shows how to update employee records to change the Title field value for all sales representatives to “Account Executive”. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, qdf As QueryDef

Set dbs = OpenDatabase(strDbPath)
' Create temporary QueryDef object.
Set qdf = dbs.CreateQueryDef("")

With qdf
    .SQL = "UPDATE Employees " & _
        "SET Employees.Title = ""Account Executive"" " & _
        "WHERE Employees.Title = ""Sales Representative"";"
    ' Invoke query.
    .Execute dbFailOnError
End With

Of course, the entire SQL string in this example can be replaced with a stored or temporary parameter query, in which case the program would prompt the user for parameter values. The following example shows how the previous example might be rewritten as a parameter query. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, qdf As QueryDef
Dim strSQLUpdate As String, strOld As String, strNew As String

Set dbs = OpenDatabase(strDbPath)
strSQLUpdate = "PARAMETERS [Old Title] Text, [New Title] Text; " & _
    "UPDATE Employees " & _
    "SET Employees.Title = [New Title] " & _
    "WHERE Employees.Title = [Old Title]; "

' Create the QueryDef object.
Set qdf = dbs.CreateQueryDef("", strSQLUpdate)
' Prompt for old title.
strOld = InputBox("Enter old job title")
If Len(strOld) = 0 Then Exit Sub
' Prompt for new title.
strNew = InputBox("Enter new job title")
If Len(strNew) = 0 Then Exit Sub
With qdf
    ' Set parameter values.
    .Parameters("Old Title") = strOld
    .Parameters("New Title") = strNew
    ' Invoke query.
    .Execute
End With


Modifying an Existing Record

Changing an existing record in a Recordset object is a four-step process:

  1. Go to the record you want to change.

  2. Use the Edit method to prepare the current record for editing.

  3. Make the necessary changes to the record.

  4. Use the Update method to save the changes to the current record.

The following example illustrates how to change the job titles for all sales representatives in a table called Employees. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset, strCriteria As String

Set dbs = OpenDatabase(strDbPath)
' Open dynaset-type recordset.
Set rst = dbs.OpenRecordset("Employees", dbOpenDynaset)
strCriteria = "Title = ""Sales Representative"""
With rst
    ' Move to start of recordset.
    .MoveFirst
    ' Find first matching record.
    .FindFirst strCriteria
    Do While Not .NoMatch
        ' Edit record and update.
        .Edit
        !Title = "Account Executive"
        .Update
        ' Find next matching record.
        .FindNext strCriteria
    Loop
End With

If you don’t use the Edit method before you try to change a value in the current record, a run-time error occurs.

Important If you change the current record and then move to another record or close the Recordset object without first using the Update method, your changes are lost without warning. For example, omitting the Update method from the preceding example results in no changes being made to the Employees table.

You can also terminate the Edit method and any pending transactions without saving changes by using the CancelUpdate method. While you can terminate the Edit method just by moving off the current record, this is not practical when the current record is the first or last record in the Recordset object, or is a new record. It’s therefore generally simpler to use the CancelUpdate method.

Inconsistent Updates

Dynaset-type Recordset objects can be based on a multiple-table query, with the query often implementing a one-to-many relationship. For example, suppose you want to create a multiple-table query that combines fields from the Orders and Order Details tables. Generally speaking, you can’t change values in the Orders table because it’s on the “one” side of the relationship. Depending on your application, however, you may want to be able to make changes to the Orders table. To make it possible to freely change the values on the “one” side of a one-to-many relationship, use the dbInconsistent constant of the OpenRecordset method to create an inconsistent dynaset.

Set rst = dbs.OpenRecordset("Sales Totals",, dbInconsistent)

When you update an inconsistent dynaset, you can easily destroy the relational integrity of the data in the dynaset. You must take care to understand how the data is related across the one-to-many relationship and update the values on both sides in a way that preserves data integrity.

The dbInconsistent constant is available only for dynaset-type Recordset objects. It’s ignored for table-, snapshot-, and forward-only-types, but no compile or run-time error is returned if dbInconsistent is used with those types of Recordset objects.

Even with an inconsistent Recordset object, some fields may not be updatable. For example, you can’t change the value of an AutoNumber field, and a Recordset object based on certain linked tables may not be updatable.

See Also For more information about consistent and inconsistent updates, see Chapter 4, “Queries.”


Deleting an Existing Record

You can delete an existing record in a table- or dynaset-type Recordset object by using the Delete method. You can’t delete records from a snapshot- or forward-only-type Recordset object. The following example deletes all orders prior to 10-1-94 from the Orders table in the NorthwindTables database. In this example, strDbPath is the path to the database:

Dim dbs As Database, rst As Recordset
Dim strCriteria As String

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Orders", dbOpenDynaset)
strCriteria = "OrderDate < #10-1-94#"
With rst
    .FindFirst strCriteria
    Do While Not .NoMatch
        .Delete
        .FindNext strCriteria
    Loop
    .Close
End With

When you use the Delete method, Microsoft Jet immediately deletes the current record without any warning or prompting. Deleting a record doesn’t automatically cause the next record to become the current record; to move to the next record, you must use the MoveNext method. Keep in mind, however, that after you’ve moved off the deleted record, you cannot move back to it.

If you try to access a record after deleting it from a table-type Recordset object, you’ll get error 3167, “Record is deleted.” On a dynaset you’ll get error 3021, “No current record.”

If you have a Recordset object clone positioned at the deleted record and try to read its value, you’ll get error 3167 regardless of the type of Recordset object. Trying to use a bookmark to move to a deleted record will also result in error 3167.


Adding a New Record

Adding a new record to a dynaset- or table-type Recordset object is a three-step process:

  1. Use the AddNew method to prepare a new record for editing.

  2. Assign values to each of the record’s fields.

  3. Use the Update method to save the new record.

The following example adds a new record to a table called Shippers. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Shippers")
With rst
    ' Add new record.
    .AddNew
    ' Supply values for fields.
    !CompanyName = "Global Parcel Service"
    !Phone = "(503)555-9786"
    ' Update.
    .Update
    .Close
End With

When you use the AddNew method, Microsoft Jet prepares a new, blank record and makes it the current record. When you use the Update method to save the new record, the record that was current before you used the AddNew method becomes the current record again. The new record’s position in the Recordset object depends on whether you added the record to a dynaset- or a table-type Recordset object.

If you add a record to a dynaset-type Recordset object, the new record appears at the end of the Recordset object, no matter how the Recordset object is sorted. To force the new record to appear in its properly sorted position, you can use the Requery method or re-create the Recordset object.

If you add a record to a table-type Recordset object, the record appears positioned according to the current index, or at the end of the table if there is no primary key and no current index. Because Microsoft Jet version 3.5 allows multiple users to create new records on a table simultaneously, your record may not appear at the end of the Recordset object as it did in previous versions of Microsoft Jet. Be sure to use the LastModified property rather than the MoveLast method to move to the record you just added.

Important If you use the AddNew method to add a new record, and then move to another record or close the Recordset object without first using the Update method, your changes will be lost without warning. For example, omitting the Update method from the preceding example results in no changes being made to the Shippers table.

Caching ODBC Data with a Recordset

You can use a dynaset-type Recordset object to create a local cache for ODBC data. Creating a local cache lets you retrieve records in batches instead of one at a time as each record is requested, and makes much better use of your server connection, improving performance.

The CacheSize and CacheStart properties establish the size and starting offset (expressed as a Bookmark property value) for the cache. For example, you may set the CacheSize property to 100 records. Then, by using the FillCache method, you can retrieve sufficient records to fill the cache.

See Also For more information about caching ODBC data, see Chapter 9, “Developing Client/Server Applications.”


Tracking Recordset Changes

You may need to determine when the design of the underlying table of a table-type Recordset object was created, or the last time it was modified. The DateCreated and LastUpdated properties, respectively, give you this information. Both properties return the date stamp applied to the table by the machine on which the table resided at the time it was stamped.


Microsoft Jet Transactions

Defined as a “logical unit of work,” a transaction is one of the features common to most database management systems. By wrapping multiple database operations into a single unit, transactions offer the developer the ability to enforce data integrity by making sure multiple operations can be treated by the engine as an “all or nothing” proposition, thereby never allowing the database to end up in an inconsistent state.

The most common example of transaction processing involves a bank’s automated teller machine. The processes of dispensing cash and then debiting the user’s account are considered a logical unit of work and are wrapped in a transaction: The cash is not dispensed unless the system is also able to debit the account. By using a transaction, the entire operation either succeeds or fails. This maintains the consistent state of the ATM database.

Transactions can be defined by what are known as the ACID properties. The following attributes of transactions make up the ACID acronym:

  • Atomic denotes that transactions are all-or-nothing operations. Each operation wrapped in a transaction must be successful for all operations to be committed.

  • Consistent denotes that a transaction enables data operations to transform the database from one consistent state to another, even though at any point during the transaction the database may be inconsistent.

  • Isolated denotes that all transactions are “invisible” to other transactions. That is, no transaction can see another transaction’s updates to the database until the transaction is committed.

  • Durable denotes that after a transaction commits, its updates survive — even if there is a subsequent system crash.

See Also For information about transaction usage and behavior with ODBC data sources, see Chapter 9, “Developing Client/Server Applications.”

With file-server databases such as Microsoft Jet, the concept of durability becomes somewhat complicated. There are currently no file-server based database engines that can fully support this criterion of true transactions. For example, a database connected to a file server cannot be expected to fully support the durability rule if the file-server crashes before a transaction has had time to commit its changes. If you require true transaction support with respect to durability, you should investigate the use of a client/server architecture.

Note The behavior of transactions with Microsoft Jet databases differs from the behavior of ODBC data sources such as Microsoft SQL Server.


Using Transactions in Your Applications

Microsoft Jet supports transactions through the BeginTrans, CommitTrans and Rollback methods of the Workspace object. The basic syntax is shown in the following table.

Method | Operation
workspace.BeginTrans | Begins the transaction.
workspace.Rollback | Undoes the transaction.
workspace.CommitTrans | Posts the transaction, writing its updates to the permanent database objects.

The following example changes the job title of all sales associates in the Employees table of the NorthwindTables database. After the BeginTrans method starts a transaction that isolates all of the changes made to the Employees table, the CommitTrans method saves the changes. Note that you can use the Rollback method to undo changes that you saved with the Update method. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim rst As Recordset
Dim wsp As Workspace
Dim strCriteria As String

' Return reference to default workspace.
Set wsp = DBEngine.Workspaces(0)
Set dbs = OpenDatabase(strDbPath)
' Open dynaset-type recordset.
Set rst = dbs.OpenRecordset("Employees", dbOpenDynaset)
strCriteria = "Title = ""Sales Associate"""

' Begin transaction.
wsp.BeginTrans
With rst
    ' Search recordset for records matching criteria.
    .MoveFirst
    .FindFirst strCriteria
    ' Edit matching records.
    Do While Not .NoMatch
        .Edit
        !Title = "Sales Representative"
        .Update
        .FindNext strCriteria
    Loop
End With

' Prompt user to save changes or roll back.
If MsgBox("Save all changes?", vbQuestion + vbYesNo) = vbYes Then
    wsp.CommitTrans
Else
    wsp.Rollback
End If
rst.Close
dbs.Close

Important Because transactions are scoped to the Workspace object, transactions are global to the workspace, not to a specific database or recordset. If you perform operations on more than one database or recordset within a workspace transaction, the Commit and Rollback methods affect all the objects changed within that workspace during the transaction.


Managing Transactions

Microsoft Jet uses sophisticated algorithms to enhance transaction performance, reliability, and usability. This section discusses topics related to how the engine manages transactions.


The Temporary Database

Almost all database-transaction systems store intermediate changes in a temporary log file instead of writing them directly to the database. Microsoft Jet uses a similar mechanism in that it buffers all transaction activity to a temporary database. When the transaction is committed, the contents of the temporary database are merged into the real database. If you issue a Rollback method to cancel the transaction, the engine frees the pages in the temporary database.

Microsoft Jet does not create the temporary database until it has to. It uses whatever cache memory is available to store changes to data. After the cache is exhausted, the engine creates the temporary database and starts to write changes there.

Microsoft Jet creates the temporary database in the directory specified by the TEMP environment variable of the workstation. If the available disk space for the temporary database is exhausted during a transaction, a trappable run-time error occurs. If you attempt to commit the transaction after this error occurs, the engine will commit an indeterminate number of changes, possibly leaving the database in an inconsistent state. To ensure a consistent database state, you usually should roll back the transaction when this error occurs.

Although the temporary database is a Microsoft Jet database, it’s used internally by the engine only. It cannot be opened from other applications. After a transaction is complete, the engine frees the pages in the temporary database.

Note Prior to version 2.0, Microsoft Jet only supported transactions of 4 MB or less. That is, only transactions that involved logging 4 MB or less of data would work. Since version 2.0, transaction size has been limited only by the amount of physical space on the disk drive where the temporary database resides.


Nesting Transactions

In Microsoft Jet workspaces, you can have up to five levels of transactions active at any one time by nesting combinations of the BeginTrans and CommitTrans methods or the Rollback method. If you nest transactions, you must make sure you commit or roll back the current transaction before trying to commit or roll back a transaction at a higher level of nesting.

If you want to have additional levels of nesting, or want to have transactions with overlapping, non-nested scopes, you can open additional Workspace objects and manage other transactions within those new workspaces.


When a Transaction Is Rolled Back by the Engine

If you close a database variable, any uncommitted transactions that were within the scope of that database variable are automatically rolled back. Microsoft Jet never automatically commits any transactions you have started. This behavior is also true of the Workspace object. If you close a Workspace object, any transactions within the scope of the workspace are rolled back. You should be aware of this behavior when you write your code. Never assume that the engine is going to commit your transaction for you.


Transactions on External Data Sources

Transactions are not supported on external non-Microsoft Jet data sources, with the exception of ODBC data. For example, if your database has linked FoxPro or dBASE tables, any transactions on those objects are ignored. This means that the transaction will not fail or generate a run-time error, but it won’t actually do anything, either.

You may recall from Chapter 1, “An Overview of Microsoft Jet,” that Microsoft Jet 2.x databases are opened by Microsoft Jet 3.5 as external ISAM databases. However, unlike the other non-native external data sources, the engine does support transactions on Microsoft Jet 2.x databases.

You can check the value of the Transactions property on Database and Recordset objects to determine whether the object supports transactions. A value of True indicates that the object does support transactions, and a value of False indicates that the object does not support transactions. In an ODBCDirect workspace, the Transactions property is available on Connection and Database objects, and indicates whether the ODBC driver you are using supports transactions.


Transactions and Performance

In versions 1.x and 2.x of Microsoft Jet, it was generally recommended that you use transactions as a performance enhancement. In almost every case, wrapping DAO updates in a transaction is much faster in versions 1.x and 2.x because the transaction forces the engine to buffer changes in the cache rather than writing them directly to disk. However, various performance enhancements have been made in Microsoft Jet 3.0 and 3.5 to remove the need to use transactions for performance reasons.

See Also For more information about Microsoft Jet 3.5 performance enhancements, see Chapter 13, “Optimizing Performance.”

Microsoft Jet 3.0 and 3.5 provide automatic, internal transactions for all DAO add, update, and delete operations. In most situations, this automatic support provides your application with the best possible performance. However, there may be situations where you want to fine-tune the engine’s internal transaction behavior. You can do this by creating and modifying various settings in the Windows registry.

See Also For more information about the registry settings used by Microsoft Jet, see Chapter 13, “Optimizing Performance,” and Appendix D, “Registry Settings.”

The default settings used by Microsoft Jet for controlling transactions are generally the best available for general-purpose application design. However, you may find that tuning these settings yourself may enable the engine to better handle your data.


The Engine’s Two Modes

Microsoft Jet has two modes of operation when it comes to transaction processing: synchronous and asynchronous. Synchronous processing was the only mode available in version 2.x of the engine.

Synchronous processing allows nothing else to occur until the current operation is completed. This is the model that Microsoft Jet 2.x follows.

Asynchronous processing involves the engine queuing up a series of changes to the database. For example, several recordset Update methods could be grouped together. At some point, these updates are written to disk and the engine begins grouping new changes. The updates occur when one the following happens:

  • A specified period of time passes after the first stored change (controlled by either the FlushTransactionTimeout or the ExclusiveAsyncDelay and SharedAsyncDelay registry settings).

  • The internal memory set aside for updates (as defined by the MaxBufferSize registry setting) is exceeded.

To better understand these modes, consider the following DAO code:

Set rst = dbs.OpenRecordset("SELECT * FROM Customers")
While Not rst.EOF
     rst.Edit
      '......
      rst.Update
      rst.MoveNext
Wend

With synchronous processing, Microsoft Jet would not continue the loop until rst.Update completed successfully.

With asynchronous processing, Microsoft Jet would immediately process rst.Update, and while it was using a thread to do the work, would continue on another thread to process rst.MoveNext.

Microsoft Jet places temporary files on disk according to the paths specified by the TEMP environment variable. The behavior is the same for Windows 95 and Windows NT Workstation or Windows NT Server.


Extracting Data from a Record

After you’ve located a particular record or records, you may want to extract data to use in your application instead of modifying the underlying source table.

A single field of a record can be copied to a variable of the appropriate data type. The following example extracts data from three fields in the first record in a Recordset object. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database, rst As Recordset
Dim strFirstName As String, strLastName As String, strTitle As String

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Employees")
With rst
    .MoveFirst
    strFirstName =!FirstName
    strLastName =!LastName
    strTitle =!Title
End With


Copying Entire Records to an Array

To copy one or more entire records, you can create a two-dimensional array and copy records one at a time. You increment the first subscript for each field and the second subscript for each record.

A fast way to do this is with the GetRows method. The GetRows method returns a two-dimensional array. The first subscript (an index into the array) identifies the field, and the second identifies the record number, as follows:

varRecords(intField, intRecord)

The following example uses an SQL statement to retrieve three fields from a table called Employees and place them in a Recordset object. It then uses the GetRows method to retrieve the first three records of the Recordset object, and stores the selected records in a two-dimensional array. Each record is then printed, one field at a time, by using the two array indexes to select specific fields and records.

To clearly illustrate how the array indexes are used, the following example uses a separate statement to identify and print each field of each record. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim rst As Recordset
Dim varRecords As Variant
Dim intNumReturned As Integer, intNumFields As Integer
Dim intField As Integer, intRecord As Integer
Dim strSQL As String

Set dbs = OpenDatabase(strDbPath)
strSQL = "SELECT FirstName, " & _
    "LastName, Title FROM Employees"
' Open snapshot-type recordset.
Set rst = dbs.OpenRecordset(strSQL, dbOpenSnapshot)

' Return array using GetRows.
varRecords = rst.GetRows(3)
' Determine upper bound of second dimension.
intNumReturned = UBound(varRecords, 2)
' Determine upper bound of first dimension.
intNumFields = UBound(varRecords, 1)

' Print each value in array.
For intRecord = 0 To intNumReturned
    For intField = 0 To intNumFields
        Debug.Print varRecords(intField, intRecord)
    Next intField
    Debug.Print
Next intRecord

You can use subsequent calls to the GetRows method if more records are available. Because the array is filled as soon as you call the GetRows method, this approach is much faster than using assignment statements to copy one field at a time.

Note also that you don’t have to dimension the Variant variable as an array, because this is done automatically when the GetRows method returns records. This enables you to use fixed-length array dimensions without knowing how many records or fields will be returned, instead of using variable-length dimensions that take up more memory.

If you’re trying to retrieve all the records by using multiple calls to the GetRows method, use the EOF property to be sure that you’re at the end of the Recordset object. The GetRows method returns fewer than the number requested if it’s at the end of the Recordset object or if it cannot retrieve a record in the range requested. For example, if the fifth record cannot be retrieved in a group of ten records that you’re trying to retrieve, the GetRows method returns four records and leaves the current record position on the record that caused a problem — and does not generate a run-time error. This situation may occur if a record in a dynaset was deleted by another user. If fewer records were returned than the number requested and you’re not at the end-of-file position, you will need to read each field in the current record to determine what error the GetRows method encountered.

Because the GetRows method always returns all the fields in the Recordset object, you may want to create a query that returns just the fields you need. This is especially important for Memo and Long Binary (OLE Object) fields.

Microsoft Excel Users You can import the contents of a Recordset object into a range on a Microsoft Excel worksheet with the CopyFromRecordset method. This method begins to copy at the current record of the result set; when the transfer is completed, the Recordset object pointer is positioned just past the last record, or at the end-of-file position. For more information, see the Microsoft Excel Visual Basic Help file.


Using Field Objects

The default collection of a Recordset object is its Fields collection. This collection includes a single Field object corresponding to each field (or column) in the Recordset object. Each Field object has a set of properties uniquely identifying the field’s name, data type, and so on, as well as the value of the field in the current record.


Referencing Field Objects

A Field object can be identified by its Name property, which corresponds to the field name in the table from which the data in the field was retrieved. Because the Fields collection is the default collection of a Recordset object, you can use either of the following syntax forms to return the field’s current value:

rst!LastName
rst.Fields("LastName")

Within the Fields collection, each Field object can also be identified by its index:

rst.Fields(0)

The index enables you to walk through the collection in a loop, replacing the index with a variable that is incremented with each pass through the loop. Object numbers in the collection are zero-based, so the first Field in the collection is number 0, the second is 1, and so on. The field order is determined by the underlying table. Fields are usually numbered in the order that they are retrieved when the Recordset object is opened. One drawback to using the index to identify a field is that you can’t be certain which field will be referenced, because the underlying table structure may change, fields may be added or deleted, and so on.

The Field object also provides two properties that indicate its position relative to the other fields in the collection — the OrdinalPosition property and the CollectionIndex property. These properties are closely related, yet subtly different. CollectionIndex is a read-only number that indicates the Field object’s absolute position within the collection as determined by the collection population. The OrdinalPosition property, in contrast, is a read-write property that you can set to any positive integer to change the field order when data is displayed in a form or copied to an array or a Microsoft Excel worksheet, and so on. The value of the OrdinalPosition property usually corresponds to the value of the field’s CollectionIndex property.

When you write a loop to walk through the Fields collection, the loop counter always identifies fields in the same sequence, which corresponds to the CollectionIndex property, regardless of the OrdinalPosition property’s value.

Caution The CollectionIndex property is not exposed to DAO users through the Object Browser. Although CollectionIndex is available, its use is discouraged because at the present time Microsoft is not committed to maintaining it in future versions of DAO. Consequently, this feature is not tested to the same reliability level as other DAO features, and assistance on its use is not available through Microsoft Technical Support.

Microsoft Excel Users You can also copy individual fields from a Recordset object to a ListBox control by using the ListBox control’s AddItem method. For more information, see the Microsoft Excel Visual Basic Help file.


Field Data Types

A Field object has a Type property that can be set to one of the following 13 DAO property constants.

Type property constant | Microsoft Jet data type
dbBoolean | Boolean (Yes/No)
dbByte | Byte
dbInteger | Integer
dbLong | Long
dbCurrency | Currency
dbSingle | Single
dbDouble | Double
dbDate | Date/Time
dbText | Text
dbBinary | Binary
dbLongBinary | Long Binary (OLE Object)
dbMemo | Memo
dbGUID | GUID (Number or AutoNumber)

For a Field object in a Recordset object, the Type property is read-only. However, you must be aware of the Field object’s type when you are copying data to or from a field in code; otherwise, a type mismatch error may occur. For example, you cannot copy text data to a field with the Integer data type.

See Also For more information about equivalent DAO data types, see Appendix A, “Specifications.”

The Type property of a Field object in a Recordset object is determined by the underlying table from which the record was retrieved. If you created the table and its fields by using DAO DDL statements, you can easily determine the source table’s data type. However, if you’re retrieving data from tables created with Microsoft Jet SQL DDL statements or through the Microsoft Access user interface, the data type you specified when creating the table may not match one of the 13 DAO data types.

Note The GUID data type is used to store a globally unique identifier, which is a unique string of digits used to identify entities that need a unique reference identification, such as OLE objects and Microsoft SQL Server remote procedure calls. The GUID data type is also used in Microsoft Jet database applications to identify a replica; for example, the Database object’s ReplicaID property.


Large Value Fields

Long Binary (OLE Object) and Memo fields are collectively referred to as “large value” fields because they are typically much larger than fields of other data types. A record containing one or more large value fields usually exceeds the 2 KB size limit of a record. When this happens, each large value field is represented in the record by a pointer, which references one or more separate 2 KB memory pages on which the data is actually stored.

When you query tables containing large value fields, don’t include those fields in the field list unless you absolutely need them, because returning large value fields takes time.

A snapshot- or forward-only-type Recordset object opened against large value fields in an .mdb file does not actually contain the large value field data. Instead, the Recordset object maintains references to the data in the original tables, the same way a dynaset references all data.

Handling Large Value Data

Sometimes you’ll need to read or copy data from a large value field when you don’t have sufficient memory to copy the entire field in a single statement. You instead have to break up the data into smaller units, or “chunks,” that will fit available memory. The FieldSize method tells you how large the field is, measured in bytes. Then you can use the GetChunk method to copy a specific number of bytes to a buffer, and use the AppendChunk method to copy the buffer to the final location. You then continue using the GetChunk and AppendChunk methods until the entire field is copied.


Reading and Writing Data

When you read and write data to a field, you’re actually reading or setting the Value property of a Field object. The Value property is the default property of a Field object, so the following statements are equivalent:

rst!LastName.Value = strName
rst!LastName = strName


Write Permission

Even if a Recordset object is a dynaset or table type object, which are normally updatable, the tables underlying a Recordset object may not permit you to modify data. Check the Updatable property of the Recordset object to determine whether its data can be changed. If the property is True, the Recordset object can be updated.

Individual fields within an updatable Recordset object may not be updatable, so attempting to write to these fields can generate a run-time error. To determine whether a given field is updatable, check the DataUpdatable property of the corresponding Field object in the Fields collection of the Recordset object.

In the following example, a Recordset object variable is passed to a function that first determines whether the Recordset object is updatable. If it is, the function counts how many Field objects are updatable and returns the total. It also prints the Name property of each Field object that is not updatable. If all the fields are updatable, the function returns – 1. If the entire Recordset object is not updatable, the function returns 0. If some of the fields are updatable, the function returns the number of updatable fields.

Function UpdateTest(rst As Recordset) As Integer
    Dim intCount As Integer, intX As Integer
    Dim fld As Field

    ' Initialize counter variable.
    intCount = 0
    ' Check whether recordset is updatable.
    If rst.Updatable Then
        ' Check whether each field is updatable.
        For Each fld In rst.Fields
            If fld.DataUpdatable Then
                ' Increment counter for each updatable field.
                intCount = intCount + 1
            Else
                ' Print nonupdatable fields.
                Debug.Print fld.Name
            End If
        Next fld
        If intCount = rst.Fields.Count Then
            ' If all fields are updatable, return -1.
            UpdateTest = -1
        Else
            ' If some fields are updatable, return number.
            UpdateTest = intCount
        End If
    Else
        ' If no fields are updatable, return 0.
        UpdateTest = 0
    End If
End Function


Constraints

Any single field can impose a number of constraints on data in that field when records are added or updated. These constraints are defined by a handful of properties. The AllowZeroLength property on a Text or Memo field indicates whether the field will accept a zero-length string. The Required property indicates whether some value must be entered in the field, or if it instead can accept a Null value. For a Field object in a Recordset object, these properties are read-only; their state is determined by the underlying table.

See Also For more information about how to set these properties on the Field objects of a TableDef object, or how to set them by using SQL DDL statements, see Chapter 3, “Data Definition and Integrity.”

Field-Level Data Validation

Validation is the process of determining whether data entered in a field’s Value property is within an acceptable range. A Field object in a Recordset object may have the ValidationRule and ValidationText properties set. The ValidationRule property is a criteria expression, similar to the criteria of an SQL WHERE clause, without the WHERE keyword. The ValidationText property is a string that’s displayed in an error message by Microsoft Access, or by the Data control in Visual Basic if you attempt to enter data in the field that is outside the limits of the ValidationRule property. If you’re coding from DAO directly, you can use the ValidationText property for a message you want to display to the user.

See Also For more information about setting table-level validation properties, see Chapter 3, “Data Definition and Integrity.”

Note The ValidationRule and ValidationText properties also exist at the Recordset object level. These are read-only properties, reflecting the table-level validation scheme established on the base table from which the current record is retrieved.

A Field object in a Recordset object also features the ValidateOnSet property. When this property is set to True, validation is checked as soon as the field’s Value property is set. If this property is set to False, validation is checked only when the completed record is updated. The default value is False. For example, if you’re adding data to a record that contains a large Memo or Long Binary (OLE Object) field and has a setting for the ValidationRule property, you should determine whether the new data violates the validation rule before attempting to write the data, so that you don’t waste time attempting to write an invalid record to disk. (Note that you should write the data when the field’s value is set, not when the entire record is written to disk.) To check validation when the field’s value is set, set the ValidateOnSet property to True.


Tracing the Origin of Dynaset Fields

A dynaset-type Recordset object can include records from more than one source table. In addition, within a single record, fields from different tables can be joined to new records. Sometimes it’s useful to know the table from which a field originated. The SourceTable property of a Field object returns the name of the base table from which the field’s current data was retrieved.

Within a query, a field can be renamed for display purposes. For example, in an SQL SELECT query, the AS operator in the select field list can create an alias for one of the returned fields. In a Recordset object based on an SQL query, a field that has been aliased is represented by a Field object whose Name property reflects the alias, not the original field name. To find out the original field name, check the Field object’s SourceField property.


Chapter 6 Creating Multiuser Applications

Microsoft Jet supports the network environment with a variety of services designed to make your life as a developer easier. Microsoft Jet 3.5 boasts improved multiuser performance and increased concurrency (the simultaneous availability of the same sets of data and objects to multiple users).

To implement multiuser applications that run smoothly using Microsoft Jet, you should be familiar with the engine’s multiuser model. Moving an existing single-user application to a multiuser environment is not always a simple port, and you should plan ahead and address the issues discussed in this chapter. A multiuser application should be designed as such from the ground up, with the database engine’s functionality in mind. This chapter guides you through these issues and shows you how to effectively use Microsoft Jet to manage resources and minimize contention for data and objects. (Contention occurs when two or more users are attempting to modify the same data or object.)

See Also For more information about using Microsoft Jet in a client/server configuration, see Chapter 9, “Developing Client/Server Applications.” For more information about performance in Microsoft Jet 3.5, see Chapter 13, “Optimizing Performance.”

File-Server vs. Client/Server

The Microsoft Jet Multiuser Model

Managing Shared Databases

Microsoft Jet Locking Architecture

Handling Conflicts

Other Issues in Multiuser Design

Optimizing Multiuser Applications

Sharing Microsoft Jet Databases on the Web

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


File-Server vs. Client/Server

When discussing Microsoft Jet multiuser database systems, it’s important to note the differences in available technology. A multiuser database system, as implemented on personal computers, generally falls into one of two categories: a file-server system or a client/server system.

In a file-server system, no intelligent process is active at the server level. All intelligence resides on the individual workstation. When your application needs data, it’s the workstation software’s responsibility to determine which files should be read and how to directly access the network drive. Because all data in the database file must be sent from the server to the workstation, network traffic is increased. Figure 6.1 illustrates the file-server system.

Figure 6.1 A typical file-server system

A multiuser database system within a file-server environment is made up of:

  • A database that resides on a network file server running an operating system such as Microsoft Windows NT Server or Novell NetWare.

  • One or more users accessing the database from a workstation’s application software.

In a client/server system, the server is responsible for intelligently processing requests for data. The workstation does not request data at the file level, but sends a high-level request to the server to execute a specific query and return its results. The primary advantage of this technique is that network traffic is reduced because only the result set of the query is returned to the workstation, as shown in Figure 6.2.

Figure 6.2 A typical client/server system

A client/server system is typified by:

  • A back-end database residing on the server controlled and maintained by the server software, such as Microsoft SQL Server.

  • One or more users running a local application that requests data from the server through an interface such as the Microsoft Open Database Connectivity (ODBC) standard.

This chapter covers the use of Microsoft Jet in a file-server configuration. For more information about using Microsoft Jet in a client/server configuration, see Chapter 9, “Developing Client/Server Applications.”


The Microsoft Jet Multiuser Model

In a file-server environment, a multiuser database system using Microsoft Jet consists of the following components:

  • One or more database files residing on the network server

  • A locking information file (.ldb) for each database

  • A workgroup information file residing either on the network server or on each user’s workstation

  • A copy of Microsoft Jet running on the local workstation(s)

Figure 6.3 illustrates how these components are related.

Figure 6.3 The Microsoft Jet multiuser model


The Locking Information File

When a database is opened in shared mode, it’s necessary to address concurrency issues. This is done through a locking information file.

You can delete a locking information file only when no users have its related database file (.mdb) open. If you move a database file to a new location, it’s not necessary to move the .ldb file. For more information about the locking information file, see “Microsoft Jet Locking Architecture” later in this chapter.


The Workgroup Information File

In addition to the locking information file, there is the workgroup information file (also known as the system database and typically named System.mda in Microsoft Access 2.0 or System.mdw in Microsoft Access 95 and 97). This file is a Microsoft Jet database that stores information about the users, groups, and passwords for a secured database, and its location in multiuser environments is important. The workgroup information file can be placed on the local workstation or shared on a network drive. However, if it’s stored locally, you must take the steps necessary to update it when security settings change.

Microsoft Access Users In versions of Microsoft Access prior to Microsoft Access 97, Microsoft Access uses the workgroup information file to store various options that the user can set. It also stores the most recently used (MRU) list of databases opened. In these versions, if you want your application’s users to have their own MRU lists, you should store the workgroup information file on their local drives. In Microsoft Access 97, settings in the user’s Options dialog box (Tools menu, Options command) and MRU information are stored in the Windows Registry in the \HKEY_CURRENT_USER\Software\Microsoft\Office\8.0\Access\Settings key on each user’s computer.

See Also For more information about workgroup information files, see Chapter 10, “Managing Security.”


Workstation Locking

If a database resides on a workstation’s local hard drive, Microsoft Jet requires the availability of some form of operating-system locking. In Microsoft Jet 3.0 and 3.5, this is handled by the network services built into Microsoft Windows 95 and Windows NT Workstation.


Multiuser Settings

To control how Microsoft Jet behaves in a multiuser environment, you can use a variety of Windows Registry settings. You can change these settings using the Registry Editor (Regedit.exe for Windows 95 and Windows NT Workstation 4.0, or Regedt32.exe for Windows NT Workstation 3.51). The settings in the registry affect the additional settings that Microsoft Access provides (described in the following note.)

Microsoft Access Users In Microsoft Access, you can set a number of options that control how Microsoft Access interacts with Microsoft Jet in a multiuser environment. The following settings are available, either on the Advanced tab of the Options dialog box (Tools menu), or programmatically through the SetOption method of the Application object.

Default Open Mode specifies how the database is to be opened if no explicit options are specified. (You can specify explicit options when you open the database directly or use the DAO OpenDatabase method.) If the database is to be used in a multiuser environment, set this option to Shared.

Default Record Locking specifies how records are to be locked when they are edited within the context of a Microsoft Access form. The three settings correspond to locking modes that Microsoft Jet defines. No Locks corresponds to optimistic locking, Edited Record corresponds to pessimistic locking, and All Records corresponds to recordset locking.

Number of Update Retries specifies how many times Microsoft Access retries saving a locked page.

Update Retry Interval specifies the number of milliseconds Microsoft Access waits before trying again to save a locked page.

ODBC Refresh Interval specifies how often (in seconds) Microsoft Access refreshes a form with data changes from ODBC data sources.

Refresh Interval specifies how often (in seconds) Microsoft Access refreshes a form with data changes from native Microsoft Jet data sources. It should be noted that this setting performs the same function as the PageTimeout setting for Microsoft Jet in the registry. In Microsoft Access, both the Refresh Interval and the PageTimeout settings are in effect at the same time.

If you have applications other than Microsoft Access that use Microsoft Jet for database access, you may want to modify engine settings by directly updating the registry. The advantage of this approach is that you’re making settings directly to Microsoft Jet. These settings are then used by all applications that use the engine. For the most flexibility, and to avoid any impact on other applications using Microsoft Jet 3.5, you can temporarily modify Microsoft Jet engine settings in Visual Basic code using the SetOption method of the DBEngine object. You can also use this method with Microsoft Access applications to tune Microsoft Jet registry settings.

See Also For more information about registry settings, see Appendix C, “Registry Settings.” For more information about modifying Microsoft Jet engine settings, see Chapter 13, “Optimizing Performance.”


File-Server Networks Supported

Microsoft Jet supports most file-server and peer-to-peer networks that work on personal computers. The following networks have been tested with Microsoft Jet and are supported:

  • Banyan VINES 5.52

  • DECnet 4.1

  • LANtastic 5.0

  • Novell NetWare 3.x and 4.x

  • OS/2 LAN Manager, 2.1 and 2.2

  • Microsoft Windows 95

  • Microsoft Windows NT Workstation 3.51 and 4.0

Although Microsoft Jet supports these networks equally, it’s important to note that the performance of a multiuser database system running on a peer-to-peer network may suffer depending on how the computer that’s serving as a file-server is being used.

Microsoft Windows 95, Microsoft Windows NT Workstation 3.51 and 4.0, and LANtastic 5.0 can all be used as peer-to-peer networking systems. If the workstation is also being used to run desktop applications, there will be increased levels of disk and network I/O, which will affect performance. Also, if a user runs another application that locks the system, all users sharing the database will lose their connections to the database. This can compromise the integrity of the database. For the best performance and stability on peer-to-peer networks, make sure that a workstation containing a shared database is dedicated solely to that purpose.


Managing Shared Databases

To effectively use Microsoft Jet in multiuser applications, you must understand how to work with the database file. Because this file contains not only the data, but also structural objects such as queries, controlling its use and location is very important. Your application should be designed to balance the need for centralized administration with the performance benefits gained by moving parts of your application to the local level.


Location of Components

Because network file access is typically slower than local disk access, you might want to improve performance by storing some of your application’s components on the local workstation. With most network operating systems, you have a great deal of flexibility for controlling where various components of your multiuser application reside. For this discussion, components are grouped into two categories:

  • Static components are the parts of your application that don’t change often. The dynamic-link libraries (.dll) and executable files (.exe) that make up the host program calling Microsoft Jet — such as Visual Basic, Microsoft Access, and Microsoft Excel — are examples of static components. Components in your application, such as forms, reports, and program code, are also static components.

  • Active components are the files that contain the actual data that your application’s users access in a multiuser environment. These can include Microsoft Jet databases, external databases such as Microsoft FoxPro database files, or ODBC databases stored on database servers.

Because static components don’t often change, they are ideal candidates for workstation storage. In this scenario, you install the Microsoft Jet components and the host application on the local workstation along with your application’s static objects. Active components, such as your application’s data files, are stored on the network server. When you store static components on local workstations, you optimize performance but have to worry about updating objects on local workstations when the application’s design changes.

In situations where you would like greater control over modification of static components, or user workstations don’t have sufficient disk space, static components can be stored on the network. This has the obvious effect of increasing network traffic and reducing application performance, but it also minimizes administrative issues.


Self-Updating Applications

Administrative concerns arise as you move components to the workstation level. For example, when you need to modify a static object in your application, such as a table’s structure or a program’s code, you must have mechanisms in place for updating all affected components on each individual workstation.

This problem can be somewhat minimized by building your application so that it checks for latest versions upon startup. For example, your application’s startup code can check the modification dates of static objects in the local database (using, for example, the DAO LastUpdated property) and compare them to dates in a central database located on the server. Whenever a newer date is found on a server object, the code automatically imports the latest version of the object, keeping the local database up-to-date.

You can also use the replication features in Microsoft Jet 3.5 to keep objects up-to-date by synchronizing them. For example, you can create replicas of the database and store them at the workstation level. In this scenario, engine replication takes care of the details of synchronizing objects.

See Also For more information about replication, see Chapter 7, “Database Replication.”


The Two-Database Approach

When developing multiuser applications with Microsoft Jet, you may find it helpful to split your objects into two databases. This approach offers many advantages. These two databases are known as the front-end database and the back-end database.

The front-end database has three basic characteristics:

  • It contains non-data objects such as saved queries. If you’re a Microsoft Access user, it also contains your forms, reports, macros, and modules.

  • It serves as an area for managing temporary objects. Most multiuser applications perform some type of data access that is temporary in nature: creating tables with transient data, for example.

  • You distribute a copy for each user to run from his or her workstation.

The back-end database contains the tables that hold your application’s data. The tables in this database are accessed by your application through links in the front-end database. Figure 6.4 illustrates the two-database approach.

Figure 6.4 The two-database approach

See Also For more information about linking tables, see Chapter 8, “Accessing External Data.”

The advantages of the two-database approach become obvious in multiuser environments. First, by storing the front-end database on the user’s workstation, there is no contention for temporary objects because your application doesn’t have to worry about creating a temporary table that may conflict with another user. Second, by storing application-specific objects that are typically static in nature at the local level, the amount of network traffic that occurs while your application runs is minimized.


Microsoft Jet Locking Architecture

Locking is one of the more complex issues you face when developing multiuser applications. This section describes the Microsoft Jet locking behavior, providing information about how Microsoft Jet versions 2.x, 3.0, and 3.5 work in multiuser settings.


Layout of the Locking Information File

The locking information file stores computer and user-level security account names and has extended byte range locks placed on it by Microsoft Jet. One locking information file is created for every Microsoft Jet database file that is opened in shared mode. The file always retains the same name as the database that was opened, except with an .ldb file extension. For example, Northwind.mdb has an associated locking information file called Northwind.ldb. If the locking information file doesn’t exist when the database is opened, Microsoft Jet creates it in the same directory as the database.

The physical structure of the locking information file can best be thought of as an array of up to 255 elements. Each element contains 64 bytes and represents a user. The first 32 bytes contain the computer name of the user (such as Kevin’s Computer) and the second 32 bytes contain the user’s user-level security account name (such as Admin). The computer name is the machine name supplied by the operating system (Microsoft Windows 95 or Microsoft Windows NT Workstation).

Microsoft Jet retrieves the computer name by making a request to the operating system. To view or modify the computer name in Windows for Workgroups, Windows 95, or Windows NT Workstation, open the Control Panel and double-click the Network icon. The computer name can be changed on the Identification tab. Windows for Workgroups stores the computer name in the Network section of the System.ini file. Windows 95 and Windows NT Workstation store the computer name in the \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ ComputerName\ComputerName key in the Windows Registry.

The user-level security account name is determined by passing a value to the DAO Workspace object. The default user-level security account name is Admin.

The physical size of the locking information file never exceeds 16320 bytes. This is because the maximum number of concurrent Microsoft Jet users is 255 (255 * 64 = 16320). Each element in the array is used by Microsoft Jet to determine which users are holding locks on data.

To lock data, Microsoft Jet places extended byte range locks outside of the physical boundaries of a file — no data is physically locked. An example of an extended byte range lock is a lock placed at 10 million hexidecimal (h) for a file that has a physical size of only 64 bytes. In this case, a lock is placed at a virtual location that does not exist on the hard disk. Extended byte range locks are used because they’re not limited by the size of the physical file, thus allowing for locking algorithms that would not otherwise be possible. Also, placing locks inside a data file prevents other users from reading that data. For example, in the earlier versions of dBASE, a user could place a lock on a record located in the data file, preventing everyone else from reading that data.

Note Unlike Microsoft Jet 2.x, versions 3.0 and 3.5 automatically delete the locking information file when the last user closes the database. This is done to reduce the proliferation of lock files for replicated databases and to allow for performance gains when determining which users have locks open. However, this automatic deletion cannot occur if the user doesn’t have sufficient operating system or network operating system rights to delete the file, or the database is in a suspect state (data locking issues can’t be resolved).

Note You can use the Msldbusr.dll utility to programmatically retrieve the following information if you’re running 32-bit VBA applications or using Visual C++: the list of all users in a locking information file, the list of all connected users in a locking information file, and the list of users who left the database in a suspect state. For information about how to use this utility and for more information about Microsoft Jet locking, see “Understanding Microsoft Jet Locking” in the Papers folder on the companion CD-ROM. Its file name is Jetlock.doc.


The Database Header Page

The database header page (DBH) is the first page of data in every Microsoft Jet database. A page is defined as 2K (or 2048 bytes) of data. The DBH is primarily used to store commit byte(s) for each of the 255 possible users connected to the database. A commit byte (or pair of bytes in Microsoft Jet 3.0 and 3.5) is a value used by the database engine to determine the state of the database.

Microsoft Jet version 2.x uses 256 bytes to store the commit byte starting at 700h and continuing to the end of the page at 800h. Microsoft Jet 3.0 and 3.5 use 512 bytes that use 2 bytes per user starting at 600h. The first byte (version 2.x) or the first 2 bytes (versions 3.0 and 3.5) are used only when the database is open in exclusive mode, and the remaining 255 bytes (version 2.x) or 510 bytes (versions 3.0 and 3.5) are used when the database is open in shared mode.

Commit bytes in Microsoft Jet 2.0 databases have only two valid values, 00 and FF. The 00 value indicates a neutral state and the FF value indicates that the engine is in the process of physically writing data to the disk. If the database has an FF value and no corresponding user lock, then a user interrupted the process of writing to disk. New users attempting to open the database receive the message Database is corrupt or is not a database file and will be forced to repair the database before opening it.

Note If an application using an earlier version of Microsoft Jet tries to open a database created with a later version, it will also receive the Database is corrupt or is not a database file message. If your application will be working in a mixed-version environment, you may need to account for this possibility.

Commit bytes in Microsoft Jet 2.5 databases have five valid values ranging from 00 to 04. This range of values provides more information about what users were doing if the database was left in a suspect state. As with Microsoft Jet 2.0, the database is in a suspect state if there is a nonzero value without a corresponding user lock. A value of 00 represents a neutral state. A value of 01 indicates that a user accessed a corrupted page in the database. The value 02 indicates that the database is being created. A value of 03 indicates that the database is being repaired. A value of 04 indicates that the user is in the process of committing a transaction to disk.

Commit bytes in Microsoft Jet 3.0 and 3.5 can have many different values; thus their size has been increased to 2 bytes. Values of 00 00 indicate that the user is in the process of committing a transaction to disk, and values of 01 00 indicate that a user has accessed a corrupted page. Thus, if a value of 00 00 is present without a corresponding user lock, or if a value of 01 00 is present, users will not be able to open the database without first repairing it. Additional values in the 600 – 800h range are used internally by Microsoft Jet for performance tracking, mainly to determine if other users have written data to the database file. If Microsoft Jet determines that other users have not written data to disk, it will delay refreshing its internal cache, resulting in fewer disk reads.

See Also To view the commit byte values discussed in this section and to see what users are currently logged onto the database, you can use the Ldbview.exe utility in the \Utilities\Ldbview folder on the companion CD-ROM. For information about how to use this utility, see the “Understanding Microsoft Jet Locking” white paper in the Papers folder on the companion CD-ROM. Its file name is Jetlock.doc.


Microsoft Jet Locks

Microsoft Jet uses two categories of locks: shared locks and exclusive locks. These locks are used internally by Microsoft Jet and should not be confused with the types of locks that you apply to data in your application through Visual Basic code. Each extended byte range lock (with the exception of user locks) represents a page in the .mdb file. However, no data is physically locked in the .mdb file.

A shared lock occupies only 1 byte and never conflicts with another shared lock. Shared locks are typically used to allow more than one person to read data from a file at the same time.

Exclusive locks span between 256 and 512 bytes and, by their nature, always conflict with other shared and exclusive locks. The exclusive lock always locks the first 256 bytes of the extended byte range lock to prevent any shared locks from being set and to determine if any shared locks exist. In addition, extended locks lock enough additional bytes to determine which user is holding the lock. For example, when a user wants to open a table in deny-read mode, Microsoft Jet tries to place an exclusive lock. This exclusive lock would be prevented if another user had a shared lock on the table. If the exclusive lock succeeded, it would prevent other users from obtaining a shared lock.


Types of Locks

Microsoft Jet uses seven types of locks:

  • User locks

  • Write locks

  • Read locks

  • Commit locks (version 2.x only)

  • Table-read locks

  • Table-write locks

  • Table deny-write locks

Using the extended byte range locks discussed earlier, these seven types are organized into six virtual regions of the locking information file in Microsoft Jet version 2.x, and five virtual regions in Microsoft Jet versions 3.0 and 3.5. These regions are used by Microsoft Jet to place extended byte range locks that range from 10000001 to 6FF800FFh. These locks are present only when users have the database open in shared or read-only mode.

User Locks

User locks are used for modifying the commit byte values in the database header page (DBH), for writing the computer and user-level security account names in the correct location in the locking information file, and for retrieving the computer names and user-level security account names of users with conflicting locks. A user lock is obtained when the user opens the database and is persistent for as long as a user has the database open. (Persistence in this context indicates the duration of the lock, or how long the lock is held.) Only one user lock exists for each connected user; however, multiple instances of Microsoft Jet on the same computer create an additional user lock per instance.

User locks are always in the range of 10000001h through 100000FFh and occupy only 1 byte.

Write Locks

Write locks prevent users from changing data while another user is modifying data. A write lock is typically placed on data pages, index pages, or long value pages. Long value pages are a type of data page that contains ANSI SQL data types of CHARACTER VARYING, BIT, or BIT VARYING. These data types are known as Memo or OLE Object fields in Microsoft Access. The persistence of write locks is directly related to the duration of a transaction.

In Microsoft Jet 2.x and 3.0, all SQL data manipulation language (DML) statements have implicit transactions (transactions automatically defined by Microsoft Jet) placed around them. Therefore, an UPDATE statement holds persistent write locks until the entire update is committed. The persistence of write locks is also determined by the type of locking chosen for recordset navigation and form editing (in Microsoft Access), and the presence of explicit transactions.

In Microsoft Jet 3.5, SQL DML statements do not have implicit transactions placed around them.

See Also For more information about how Microsoft Jet 3.5 handles SQL DML statements, see Chapter 13, “Optimizing Performance.”

Write locks are in the range of 20000000h through 2FF80FFh. They always span between 256 and 512 bytes, and therefore are always exclusive locks.

Read Locks (Microsoft Jet 2.x)

You use read locks, a type of shared lock, primarily for the immediate recycling of index pages (when the index page is no longer being used) and for ensuring that index pages in the engine’s cache are up-to-date. This type of lock is placed on long value pages and index pages. It’s used to prevent an index page from being recycled while that page is being referenced in the Microsoft Jet cache and to provide an integral view of the index.

Read locks are placed in the range of 30000000 to 3FF800FFh. They occupy only 1 byte.

Read locks in version 2.x are perhaps the most troublesome locks to deal with because they can cause locking conflicts that are not obvious to the developer or user. The persistence of read locks is determined by:

  • The LockedPageTimeout setting, specified in tenths of a second. This setting determines the amount of time the engine retains locks before releasing them. By increasing the LockedPageTimeout setting, read locks are retained for a longer period of time, which reduces the need for Microsoft Jet to re-read data pages into the cache. The LockedPageTimeout is set in the ISAM section of the corresponding .ini file (for example, Msacc20.ini) or in the Windows Registry (the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\2.x key).

  • The Idle method of the DBEngine object with the dbFreeLocks constant or FreeLocks method. However, these methods don’t always free all read locks when inside a transaction.

Read locks are typically placed when an index or long value page is read and placed in the Microsoft Jet cache. An example would be performing an SQL UPDATE statement on a table that has a primary key.

See Also For more information about Microsoft Jet configuration settings, see Appendix C, “Registry Settings.”

Read Locks (Microsoft Jet 3.0 and 3.5)

The primary purpose of read locks in Microsoft Jet 3.0 is to allow multiple users to read long value pages, simultaneously preventing others from writing to those pages. Microsoft Jet 3.0 and 3.5 have reduced the number of read locks placed on index pages, which directly results in greater concurrency and improved performance. Read locks placed on index pages are now placed only when referential integrity is being enforced. In Microsoft Jet 3.0, a read lock is persistent until the user moves to a new record. Thus, a read lock on a long value page remains until the user leaves that record. An exception to this is when a long value page contains data from more than one record of data. In this case, the engine releases the lock on that page. An example of this situation is when several records of Memo data are placed in one long value page.

Microsoft Jet 3.0 and 3.5 read locks are placed in the same range as write locks: 20000000h through 2FF80FFh. They are differentiated from write locks in that they are shared locks and occupy only 1 byte.

Important Read locks are persistent in Microsoft Jet 3.0 until the user moves to a new record. Microsoft Jet 2.x leaves read locks until they time out or until the user uses the Idle method of the DBEngine object with the dbFreeLocks constant or uses the FreeLocks method. These statements are not supported in versions 3.0 and 3.5 unless the user references the DAO 2.5/3.0 compatibility type library. For more information about the DAO 2.5/3.0 compatibility type library, see Chapter 2, “Introducing Data Access Objects.”

In Microsoft Jet 3.5, using the Idle method of the DBEngine object without arguments allows Microsoft Jet to perform background tasks that may not be up-to-date because of intense data processing, which releases unneeded read locks. Additionally, you can use the Idle method with the dbRefreshCache constant to force any pending writes to the .mdb file and refresh Microsoft Jet’s cache with the most current data from the .mdb file. For more information about the using the Idle method of the DBEngine object in Microsoft Jet 3.5, see Chapter 13, “Optimizing Performance.”

Commit Locks

Commit locks are present only in Microsoft Jet 2.x. They are similar to read locks, except that they are always exclusive locks. These locks are also placed in the 30001A01 to 3FF800FFh range. They are placed when index pages or long value pages are being written to the database file, and they conflict only with read locks. Read locks are typically placed on index pages whenever an index page is placed in the engine’s cache. Because index pages reference many data pages, users can experience locking conflicts even when they know other users are not editing data on the same page.

One of the best ways to prevent these conflicts when using Microsoft Jet 2.x is to judiciously use the Idle method with the dbFreeLocks constant or the FreeLocks method.

Commit locks do not exist in Microsoft Jet 3.0 and 3.5 because the range for read locks has been moved into the same range as write locks, thus eliminating the need for this type of lock.

Table-Read Locks

Table-read locks are used to control placing a table in deny-read mode, which prevents other users from reading data from the table. Unlike the previous types of locks, table-read locks and the rest of the table-type locks are placed only on a special type of page called a table header (TBH) page. There is one TBH page for each table in the database, and each TBH page contains statistics about the table, such as record count, next counter value, field data types, and index types.

Table-read locks are placed in the 40000000 to 4FF800FFh range and can be placed as shared locks or exclusive locks.

When a table is opened, a shared table-read lock is placed. Deny-read mode is set when a Recordset object is opened through DAO using the dbDenyRead constant for the options argument of the OpenRecordset method. Microsoft Access also places a shared table-read lock on a table when the table is opened in Microsoft Access and the user has clicked All Records under Default Record Locking on the Advanced tab of the Options dialog box (Tools menu). The exclusive table-read lock prevents other users from acquiring shared locks when they try to open a table. The table-read locks are persistent until the user closes the table.

Table-Write Locks

Table-write locks are used in conjunction with table deny-write locks and are placed in the 50000000 to 5FF800FFh range. These shared locks are persistent whenever a table is opened in a state that allows writing.

Table Deny-Write Locks

These locks are used in conjunction with table-write locks and are explicitly set when opening a table in deny-write mode. These locks are placed in the 60000000 to 6FF800FFh range and have a persistent shared lock while a table is open in deny-write mode. An exclusive lock is placed, but not held, to determine which other users have the table opened in deny-write mode.


Summary of Lock Behavior

This table summarizes the locking behavior for each type of lock that Microsoft Jet uses.

Lock name | What sets the lock | What is locked | What the lock prevents | Persistence of the lock | Shared or exclusive | Byte range examples
User | User opens a database | N/A | Nothing | Until user closes the database | N/A | 10000001 –10000001
Write | Insert, update, or delete | All available pages | Updates or deletes to data and sometimes inserts to a table | Controlled by locking mode set by user or the duration of a transaction | Exclusive | 2000A601 –2000A701
Read (version 2.x) | Reads on long value or index pages | Long value, index, or table header pages | Updates or deletes to long value or index pages | Controlled by LockedPage- Timeout setting or the Idle method of the DBEngine object with the dbFreeLocks constant | Shared | 30001E01 –30001E01
Read (versions 3.0 and 3.5) | Reads on certain long value or index pages when referential integrity is being enforced | Long value, index, or directory pages | Updates or deletes to long value or index pages | Until a read or a transaction is complete or the user moves to a new record | Shared | 20063801 –20063801
Commit (version 2.x only) | Writes to long value or index pages | Long value or index pages | Prevents reads when data is being written to disk | Held until data is written to disk | Exclusive | 30001A01 –30001B01

Lock name | What sets the lock | What is locked | What the lock prevents | Persistence of the lock | Shared or exclusive | Byte range examples
Table-read | Shared lock is obtained when table is opened. Exclusive lock is obtained when Default Record Locking in Microsoft Access is set to All Records or a recordset is opened through DAO using the dbDenyRead constant for the options argument of the OpenRecordset method. | Table header page | Prevents exclusive read lock from being set | As long as the table is kept open by the user | Both | 4000C801 –4000C801
Table-write | Shared lock is obtained when table is opened. Exclusive lock is obtained when Default Record Locking in Microsoft Access is set to All Records or a recordset is opened through DAO using the dbDenyRead constant for the options argument of the OpenRecordset method. | Table header page | Prevents exclusive write lock from being set; doesn’t prevent table deny-write lock | As long as the table is kept open by the user | Both | 5000C801 –5000C801
Table deny-write | Can be set only through DAO | Table header page | Prevents all writes to the table | As long as the table is kept open by the user | Both | 6000C801 –6000C801


The User Lock Algorithm

All Microsoft Jet multiuser locking operations revolve around the placement of a user lock. As stated previously, user locks are placed in the 10 million hexidecimal (h) range and occupy only 1 byte. When Microsoft Jet opens a database in shared mode, the following activities occur before a user lock is placed.

First, Microsoft Jet checks to see if the database is in a suspect or read-only state. This is done by examining the DBH page to see what bytes have a corresponding nonzero value. If the first byte (version 2.x) or the first 2 bytes (versions 3.0 and 3.5) contain a nonzero value, or the remaining 255 bytes (version 2.x) or 510 bytes (versions 3.0 and 3.5) have a nonzero value and don’t have a corresponding user lock, Microsoft Jet forces the user to repair the database. When opening a database in shared mode, there is never a need to check for a user lock on the first byte because it’s used only when a database is opened exclusively. A corresponding user lock would be a lock that shares the same offset in the 10 million hexidecimal (h) range as the offset from the first byte in the DBH page. Thus, a nonzero commit byte at 701h (version 2.x) or 602 – 603h (versions 3.0 and 3.5) would need to have a user lock at 10000001h for the database to be opened without being considered suspect.

Second, the engine opens the locking information file, or creates one if it doesn’t exist. It tries to place a lock at 10000001h. If Microsoft Jet is successful in obtaining this lock, it writes the computer and user-level security account name to the first 64 bytes of the file. If this lock cannot be acquired, the engine continues moving 1 byte farther in the file until a lock can be obtained. After the user lock is acquired, the engine writes the computer name and user-level security account name at the corresponding location in the locking information file. For example, a user lock at 10000040h would write an entry at 4096 bytes in the physical part of the locking information file.

Some users may experience delays in opening a database that already has many users connected. This is mainly due to network drivers that are not optimized. An example of this is a situation where a user is running a Microsoft Windows NT Workstation 3.5 client connected to a Novell NetWare server. If a user is trying to open the database and 30 other users are currently connected, it can take more than 30 seconds to perform the user lock algorithm. Microsoft Windows NT Workstation 3.51 has a modified NetWare requester driver that decreases the wait to approximately two seconds.


Handling Conflicts

When working in a single-user environment, your application doesn’t encounter conflicting requests for resources. Because only one user is concurrently accessing data, you can reasonably assume that the needed data will always be exclusively available. However, after you move into a multiuser environment, the only reasonable assumption is that conflicts will occur among users accessing data. Microsoft Jet manages these conflicts through the locking services it provides to the application. By locking data, your application ensures that only one user can access it at a given time.


Locking Modes

With Microsoft Jet, you can lock data at three different levels:

  • Exclusive mode prevents all other users from using the entire database. This is the most restrictive mode.

  • Recordset locking locks a Recordset object’s underlying tables with either table-read locks, table-write locks, or both.

  • Page locking locks the 2,048 byte (2K) page where the data that is being edited resides. This is the least restrictive mode.

To determine the level at which to lock objects in your application, you must decide the level of concurrency needed. For example, if you want the objects to be available as often as possible to as many users as possible, a high-concurrency strategy would dictate that you use page locking, which is the least restrictive level. However, if your application requires guaranteed access to most or all of the data in the database, you may opt for exclusive mode. This ensures that your application has the database open exclusively so that it can’t be shared by other users.

The three levels of locking are not mutually exclusive. Many multiuser applications use all three levels at various times. For example, in an order entry system in which you want many order takers to have access to data simultaneously, you can use page locking to lock data in the Orders table. You can use recordset locking at the end of the day to lock a summary table to update it with summary data. Finally, you can use exclusive mode nightly to compact the database.


Using Exclusive Mode

As previously mentioned, exclusive mode is the most restrictive way you can open a database. It prevents all other users from opening the database. This is generally useful for administrative or bulk changes to the database, such as repair or compact operations, or when making structural changes to the database’s schema.

When you access a database in a single-user environment, you typically open it in exclusive mode. This may provide better performance because Microsoft Jet doesn’t have to lock and unlock objects or refresh its cache. When you deploy your application in a multiuser environment, exclusive mode is no longer an option; you must open the database in shared mode.

Û To open a database in exclusive mode

  1. Declare a database variable using the Dim statement.

  2. Use the OpenDatabase method to open the database, specifying a value of True for the options argument.

  3. Close the database object when finished to allow it to be accessed by other users.

The following code checks to see whether a database can be opened exclusively, using the CanOpenDbExclusively function defined in Chapter 2, “Introducing Data Access Objects.” If this function returns true, then the following code opens the database exclusively. In this example, strDbPath is the path to the database:

Function OpenDatabaseExclusive(strDbPath As String) As Database
    Dim dbs As Database

    ' Check whether database can be opened exclusively.
    If CanOpenDbExclusively(strDbPath) Then
        ' Open database exclusively and return reference.
        Set dbs = OpenDatabase(strDbPath, True)
        Set OpenDatabaseExclusive = dbs
    Else
        MsgBox "Cannot open database exclusively." _
            & "You or another user may already have it open."
        ' Return Nothing.
        Set OpenDatabaseExclusive = Nothing
    End If
End Function

You may want to prevent your application’s users from opening a shared database in exclusive mode. This is particularly useful if your users use applications such as Microsoft Access that allow databases to be opened exclusively through the user interface. You can accomplish this if you have established Microsoft Jet user-level security. Once user-level security is established, you can use the Microsoft Access user interface to prevent a user from opening a database in exclusive mode by removing that user’s Open Exclusive permission for the Database object. If you have defined user-level security groups, you can remove the Open Exclusive permission for the Database object for any group that user belongs to. Note that you probably want to allow administrative users the option of opening the database exclusively. You can set this permission using DAO by setting the Permissions property of the Databases Container object for that user or group to the dbSecDBExclusive constant.

See Also For more information about establishing user-level security and setting permissions, see Chapter 10, “Managing Security.”

When you are setting locks in code with any level of locking, it’s important that you handle any errors that occur. In Microsoft Jet, you don’t check to see if a lock can be set before doing it. Instead, you try the operation and check to see if it succeeded. This approach makes it unnecessary for Microsoft Jet to supply a large number of status functions that usually must be invoked before attempting an operation.

The typical approach to locking is a four-step process:

  1. Turn off error handling.

  2. Attempt the operation.

  3. Check to see if an error occurred. If so, handle the error based on the error number.

  4. Turn on error handling.

This approach works well because you don’t have to anticipate every possible error before attempting to set a lock; you handle the error only if it occurs. When writing multiuser code, you should handle the error by displaying an informational message and giving the user an opportunity to retry the operation.

The OpenDatabaseExclusive procedure shown earlier in this section uses this technique.


Using Read-Only Mode

Read-only mode is a modified form of shared mode. When you open a database in read-only mode, you cannot change objects in the database — data and structural information included. This mode, however, does not prevent other users from changing data and should not be confused with opening the file in read-only mode from an operating system level. To open a database in read-only mode, set the read-only argument of the OpenDatabase method to True. You could modify the previous example to open the database in exclusive, read-only mode by changing the arguments of the OpenDatabase method as follows. In these examples, strDbPath is the path to the database:

Set dbs = OpenDatabase(strDbPath, True, True)

To open a database in shared, read-only mode, use the following code:

Set dbs = OpenDatabase(strDbPath, False, True)

Note Opening a database in read-only mode does not prevent shared table-type locks or read locks; therefore, opening a database in read-only mode does not prevent locking conflicts.


Using Recordset Locking

Recordset locking enables you to control how the Recordset object’s underlying tables are locked. You can specify read-only or write-only access, or both. Note that recordset locking applies only to table- and dynaset-type Recordset objects. Recordset locking doesn’t apply to snapshot-type or forward-only-type Recordset objects, because these are inherently read-only objects. However, shared table-read and shared table-write locks are placed by Microsoft Jet during recordset locking.


Opening the Database in Shared Mode

If you want to implement recordset locking, you must open your database in shared mode. This is the typical mode for opening databases in multiuser environments. When a database is opened in shared mode, multiple users can simultaneously access the database and Microsoft Jet handles conflicts between users.

Û To open a database in shared mode

  1. Declare a database variable using the Dim statement.

  2. Use the OpenDatabase method to open the database, specifying a value of False for the options argument.

The following code checks to see whether a database is open exclusively, using the IsDbOpenedExclusively function defined in Chapter 2, “Introducing Data Access Objects.” If this function returns false, then the following code opens the database in shared mode. In this example, strDbPath is the path to the database:

Function OpenDatabaseShared(strDbPath As String) As Database
    Dim dbs As Database

    ' Check whether database is open exclusively.
    If IsDbOpenedExclusively(strDbPath) Then
        MsgBox "Cannot open database in shared mode. " & _
            "You or another user may have it open exclusively."
        ' Return Nothing.
        Set OpenDatabaseShared = Nothing
    Else
        ' Open database and return reference.
        Set dbs = OpenDatabase(strDbPath, False)
        Set OpenDatabaseShared = dbs
    End If
End Function


Opening the Recordset

After you open the database in shared mode, you can implement recordset locking by specifying various constants in the options argument of the OpenRecordset method. You can combine options to more completely describe the type of locking you want to use.

Û To open a Recordset with locking enabled

  1. Open the Recordset object’s database in shared mode.

  2. Determine the locking mode to use.

  3. Open the Recordset using the OpenRecordset method and set the options argument to the constant or combination of constants that specify the locking mode you want.

  4. Close the Recordset object when you’re finished performing operations on the data to release any recordset-level locks.

For example, the following code opens the Orders table exclusively by combining the dbDenyWrite and the dbDenyRead constants in the options argument.

Function OpenTableExclusive(strDbPath As String, _
        strRstSource As String) As Recordset
    Dim dbs As Database
    Dim rst As Recordset

    ' Open the database in shared mode by calling the
    ' OpenDatabaseShared function defined in preceding section.
    Set dbs = OpenDatabaseShared(strDbPath)

    ' Open table exclusively.
    If Not dbs Is Nothing Then
        Set rst = dbs.OpenRecordset(strRstSource, _
            dbOpenTable, dbDenyRead + dbDenyWrite)
        If Not rst Is Nothing Then
            Set OpenTableExclusive = rst
        Else
            Set OpenTableExclusive = Nothing
        End If
    Else
        Set OpenTableExclusive = Nothing
    End If
End Function

Note If you open a Recordset object without specifying any value for the options argument, Microsoft Jet uses page locking by default. The Recordset is opened in shared mode; it does not lock other users out of its data, and locks only the data being edited in the current page.

See Also For more information about opening a recordset, see “Creating a Recordset Variable” in Chapter 5, “Working with Records and Fields” and search the DAO Help index for “OpenRecordset method.”


Checking for Errors when Opening Recordsets

As with opening databases exclusively, setting locks on recordsets can cause errors if the lock fails. Using the four-step process described earlier, you can handle recordset locking problems with a generic procedure that takes into account the most common multiuser errors.

The most common error in locking recordsets occurs when another user has the recordset open in a way that prevents you from obtaining the lock you want. This is identified as error 3262. The following table explains this error.

Error number and text | Cause and suggested response
3262 Couldn’t lock table <tablename>; currently in use by user <username> on machine <machinename>. | Occurs when you attempt to use the OpenRecordset method to open an object that cannot be locked. This is usually caused by another user who has the same table or tables locked in a way that prevents your lock. Wait a short interval and retry the operation.


Using Page Locking

This level of locking causes only the page containing the record currently being edited to be locked. Microsoft Jet accesses and locks data in pages of 2048 bytes. When you use page locking, other users can read data from the locked page, but cannot change it. Page locking is the default locking level for Recordset objects. In the parts of your application that lock data at the page level, you must determine which locking mode you’re going to use. The locking mode determines how Microsoft Jet actually locks the data. Two modes of page locking are available: pessimistic locking and optimistic locking.


Pessimistic Locking

Using pessimistic locking, the engine locks the page that contains the record being edited as soon as the Edit method is issued, and doesn’t release the lock until the changes to the record are explicitly committed or canceled. Pessimistic locking is the default locking mode for recordsets.

Note Explicit transactions change the behavior regarding when a lock is released. For more information, see the Note at the end of the next section, “Optimistic Locking.”

The primary advantage of pessimistic locking is that after you have obtained a lock, you know that you will not encounter any locking conflicts as long as the record is locked. Additionally, pessimistic locking is the only way to guarantee that your application reads the most current data, because one user can’t change a record after another has begun editing it.

The disadvantage of pessimistic locking is that the entire page containing the record is locked for the duration of the procedure that has edited and locked the record. If you’re giving users access to data through a user interface, there is also the problem of a user starting an edit on a record, locking it, and leaving the computer for a period of time. This causes not only the record the user is editing to be locked, but possibly other records that reside within the locked page.

Û To use pessimistic locking in your code

  1. Open a table- or dynaset-type Recordset on the data you want to edit.

  2. Enable pessimistic locking by setting the LockEdits property of the Recordset to True.

  3. Move to the record you’re interested in.

  4. Try to use the Edit method to see if the record can be locked. If not, prompt the user to retry or cancel.

  5. Once the record is locked, make changes to the record.

  6. Save the changes to the record by using the Update method. After your changes are saved, the lock is released.

The following code gives an example of how to implement pessimistic locking. In this example, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim rst As Recordset
Dim blnEdit As Boolean

' Open the database in shared mode.
Set dbs = OpenDatabase(strDbPath, False)

' Open the Orders table in shared mode for pessimistic locking.
Set rst = dbs.OpenRecordset("Orders", dbOpenDynaset, dbSeeChanges, dbPessimistic)

With rst
    ' Setting LockEdits to True tells Jet to use
    ' pessimistic locking.
    .LockEdits = True
    ' Search for record.
    .FindFirst "[OrderID]=10565"

    ' Try to edit the record. This causes a lock attempt.
    blnEdit = True
    Do While blnEdit = True
        On Error Resume Next
        .Edit
        ' If lock fails because another user is editing
        ' the record, allow the user to retry.
        If Err.Number <> 0 Then
            If MsgBox("Cannot edit: " & Err.Description & " Try again?", _
                vbYesNo + vbQuestion) <> vbYes Then
                blnEdit = False
            End If
        Else
            Exit Do
        End If
    Loop

    ' If lock attempt was successful, make changes and commit.
    If blnEdit Then
        ![ShipAddress] = "New Address 1"
        .Update
    End If
End With

rst.Close
dbs.Close

See Also For more information about working with recordsets, see Chapter 5, “Working with Records and Fields.” For more information about setting pessimistic or optimistic locking, search the DAO Help index for “LockEdits property.”


Optimistic Locking

With optimistic locking, the engine locks the page only when you try to commit the record changes using the Update method. Because the lock is applied only when your application attempts to commit changes, the time the lock is in place is minimized; this is optimistic locking’s main advantage.

The disadvantage of optimistic locking is that when a user begins to edit a record, you can’t be sure that the update will succeed. An update that relies on optimistic locking will fail if another user has updated a record while the first user is still editing it.

Û To use optimistic locking in your code

  1. Open a table- or dynaset-type Recordset object on the data you want to edit.

  2. Enable optimistic locking by setting the LockEdits property of the Recordset object to False.

  3. Move to the record you’re interested in.

  4. Use the Edit method to allow edits to the record (the record is not yet locked).

  5. Make changes to the record.

  6. Commit the record changes using the Update method (this attempts to lock the record).

  7. Check to see if the Update method succeeded. If it didn’t, try again.

The following code gives an example of how to implement optimistic locking. In this example, strDbPath is the path to the NorthwindTables database:

Sub LockOptimistic()
    Dim dbs As Database
    Dim rst As Recordset
    Dim blnEdit As Boolean

    ' Open the database in shared mode.
    Set dbs = OpenDatabase(strDbPath, False)

    ' Open the Orders table in shared mode.
    Set rst = dbs.OpenRecordset("Orders", dbOpenDynaset, _
        dbSeeChanges, dbOptimistic)

    ' Set the multiuser options for the recordset.
    With rst
        ' Setting LockEdits to False tells Jet to use
        ' optimistic locking.
        .LockEdits = False
        ' Find record to edit.
        .FindFirst "[OrderID]=10565"
        .Edit
        ![ShipAddress] = "New Address 2"

        ' Check for errors when trying to commit the record.
        Do While True
            On Error Resume Next
            ' Attempt update.
            .Update
            ' If error occurs, prompt user to try again.
            If Err.Number <> 0 Then
                If MsgBox("Cannot commit: " & Err.Description & " Try again?", _
                        vbYesNo + vbQuestion) <> vbYes Then
                    ' Cancel the edit if user does not wish to try again.
                    .CancelUpdate
                    ' Set flag to False to prevent another iteration.
                    blnEdit = False
                End If
            Else
                ' If no error, exit loop.
                Exit Do
            End If
        Loop
    End With

    rst.Close
    dbs.Close
End Sub

Important Optimistic locking turns into pessimistic locking when transactions are used. Because a transaction holds a write lock until the transaction is committed, a pessimistic locking mode occurs even though the LockEdits property may have been set to False. Also, it’s possible for the Update method to fail in optimistic locking. Just because a write lock has not been placed by an Edit method, it does not mean that a write lock has not been placed by another user using the Update method. In other words, one user could have a recordset open with pessimistic locking and cause another user who has the recordset open with optimistic locking and who is trying to update the same data to fail.


Checking for Errors with Page Locking

When using page locks, your code must check to see if the attempted lock succeeded before proceeding. As with the previous examples, use the four-step process of turning off error handling, attempting the operation that will initiate a lock, checking for errors, and finally, turning on error handling.

The following table describes the most common page-locking multiuser errors your code will encounter.

Error number and text | Cause and suggested response
3186 Couldn’t save; currently locked by user <name> on machine <name>. | This error occurs when a user attempts to update a page that contains a read lock placed by another user. To handle this error, wait for a short interval, and then try to save the record again. Optionally, you can inform users of the problem and allow them to indicate whether or not they want to retry the operation.
3197 The Microsoft Jet database engine stopped the process because you and another user are attempting to change the same data at the same time. | This error occurs when you use the Edit method or the Update method and another user has changed the current record since you opened the Recordset or last read data from the record. If this error occurs when you use the Edit method, you may want to refresh the user’s view of the data with the current data and then attempt the Edit method a second time. If this error occurs when you use the Update method, you’re using optimistic locking and the record has changed since you used the Edit method. Inform the user that someone else has changed the data. You may want to display the current data and give the user the choice of whether to overwrite the other user’s changes or cancel edits.
3260 Couldn’t update; currently locked by user <name> on machine <name>. | This error occurs when you use the Edit method and the page containing the current record is locked. This error also occurs when you use the AddNew method or the Update method to save a record on a locked page. This situation can occur when a user is trying to save a new record or when you’re using optimistic locking and another user locks the page. To handle this error, wait for a short interval, and then try to save the record again. Optionally, you can inform users of the problem and allow them to indicate whether or not they want to retry the operation.

See Also For more information about Microsoft Jet error numbers, see Appendix D, “Error Reference.”

In the previous pessimistic locking example, the code checked to see if an error occurred and reacted regardless of the type of error. This example can be made more versatile by actually checking the error that occurs and responding to it. The following function modifies the pessimistic locking example to include better error handling:

Sub EditARecord()
    Const conMultiUserEdit As Integer = 3197
    Const conRecordLocked As Integer = 3260
    Dim dbs As Database, rst As Recordset
    Dim intLockCount As Integer, intRndCount As Integer
    Dim intChoice As Integer, intX As Integer
    Dim strDbPath As String

    On Error GoTo 0
    strDbPath = "C:\JetBook\Samples\NorthwindTables.mdb"
    ' Open the database in shared mode.
    Set dbs = OpenDatabase(strDbPath, False)

    ' Open the Orders table in shared mode.
    Set rst = dbs.OpenRecordset("Orders", dbOpenDynaset, dbSeeChanges, dbPessimistic)

    With rst
        ' Setting LockEdits to True tells Jet to use
        ' pessimistic locking.
        .LockEdits = True
        .FindFirst "[OrderID]=10565"

        ' Try to edit the row. This will cause a lock attempt.
        On Error GoTo Err_EditARecord
        ' If record is locked, edit will fail.
        .Edit

        ' Make changes and commit.
        ![ShipAddress] = "New Address 3"
        .Update
    End With

Exit_EditARecord:
    On Error Resume Next
    rst.Close
    dbs.Close
    Exit Sub

Err_EditARecord:
    Select Case Err.Number
        ' Data has changed since last read.
        Case conMultiUserEdit
            Resume
        Case conRecordLocked
            ' Row is locked.
            intLockCount = intLockCount + 1
            ' Tried to get the lock twice already.
            If intLockCount > 2 Then
                ' Allow user to choose Cancel/Retry.
                intChoice = MsgBox(Err.Description & " Retry?", _
                    vbYesNo + vbQuestion)
                If intChoice = vbYes Then
                    intLockCount = 1
                Else
                    Resume FailedEdit
                End If
            End If

            ' Yield to Windows.
            DoEvents
            ' Delay a short random interval,
            ' longer each time the lock fails.
            intRndCount = intLockCount ^ 2 * Int(Rnd * 3000 + 1000)
            For intX = 1 To intRndCount: Next intX
            ' Try the edit again.
            Resume
        Case Else
           ' Unanticipated error.
            MsgBox ("Error " & Err.Number & ": " & Err.Description)
            Resume FailedEdit
    End Select

FailedEdit: ' Begin contingency procedure if edit fails.
        MsgBox "This row could not be edited. Please try again later."
        GoTo Exit_EditARecord
End Sub

This code works by moving the program’s execution to the Err_EditARecord section when an error occurs. The code in the Err_EditARecord section checks the specific error message and tries to correct the problem. It also allows the user to retry or cancel the operation. Note that it specifies a random interval for retrying the operation. This is an important technique for making sure two users vying for the same record don’t end up in a deadlock situation where code is trying to lock the record at the same time. By introducing a random element into the timing loop, you can minimize the chances of a deadlock.


Testing a Record for Locking Status

You may want to test a record to check if it’s locked without actually locking its page or pages. In Microsoft Jet 3.0 and 3.5, Recordset objects have an EditMode property. You can use this property in conjunction with the LockEdits property to determine if the current record is locked.

The following procedure tests whether the current record in a given Recordset is on a locked page:

Function IsRecordLocked(rst As Recordset) As Boolean
    IsRecordLocked = False

    If rst.LockEdits Then
        If rst.EditMode = dbEditInProgress Then
            IsRecordLocked = True
        End If
    End If
End Function

If you’re using Microsoft Jet 2.0, the EditMode property is not available. In this situation, you can accomplish the same test by setting the LockEdits property of the Recordset to True, using the Edit method to attempt a lock, and then trapping error message 3260 (or other relevant error messages) if they occur.

The following procedure tests whether the current record in a given Recordset is on a locked page without using the EditMode property:

Function IsRecordLocked2(rst As Recordset) As Boolean
    IsRecordLocked2 = False

    On Error GoTo Err_IsRecordLocked2
    With rst
        .LockEdits = True
        ' Attempt the lock.
        .Edit
    End With
    ' Return to the calling routine to edit the record.
Exit Function

Err_IsRecordLocked2:
    IsRecordLocked2 = True
    Exit Function
End Function


The Page-Locking Challenge

One of the most common questions raised by developers who are starting their first Microsoft Jet multiuser application is: How do I lock a record? The short answer is: You don’t. You have seen that Microsoft Jet reads, writes, and locks data a page at a time, not a record at a time. Depending on the size of the record, the page may contain more than one record. The problem arises that by locking a record, you’re locking all the records on that page. This may cause a problem in high-concurrency applications where unhindered access to specific records is important. You can see how this works in Figure 6.5.

Figure 6.5 Page locking

You can use various strategies to work around this design:

  • Create record sizes that are larger than half a page

  • Use optimistic locking wherever possible

  • Move your application to a client/server environment

  • Implement your own custom locking scheme


Creating Record Sizes That Are Larger Than Half a Page

You can force Microsoft Jet to lock individual records by creating record sizes that are larger than half a page — that is, larger than 1,024 bytes. This works because Microsoft Jet does not begin storing a new record on a partially filled page unless it can fit the entire record on the page. This strategy wastes a lot of disk space and increases network traffic. However, if you decide to use pessimistic locking and absolutely must have record locking, you may want to consider this technique. In order to use this technique, you need to be able to estimate the size of records.

You can use the following table, which gives the size of fields with various data types, to calculate the size of your fields. Then add the sizes of all the fields in a record to estimate the size of the record.

Microsoft Access data type | Microsoft Jet data type | Size
Number with FieldSize property set to byte | Byte | 1 byte
Number with FieldSize property set to Integer | Integer | 2 bytes
Number with FieldSize property set to Long Integer | Long Integer | 4 bytes
Number with FieldSize property set to Single | Single | 4 bytes
Number with FieldSize property set to Double | Double | 8 bytes
Currency | Currency | 8 bytes
AutoNumber with FieldSize property set to Long Integer | Long Integer | 4 bytes
AutoNumber with FieldSize property set to ReplicationID | GUID | 16 bytes
Yes/No | Boolean | 1 bit
Date/Time | DateTime | 8 bytes
Text | Text | Variable
Memo | Long Text | 14 bytes
OLE Object | Long Binary | 14 bytes
Hyperlink | Long Text | 14 bytes

When you estimate the size of records, you also need to account for overhead, which includes the following:

  • Seven bytes per record for record overhead

  • One byte variable-length field overhead for each Text, Memo, Hyperlink, and Long Binary (OLE Object) field

  • One additional byte for every 256 bytes of the total space occupied by all Text, Memo, Hyperlink, and Long Binary (OLE Object) fields

  • One byte fixed-length field overhead for each Yes/No, byte, Integer, Long Integer, AutoNumber, Single, Double, Currency, and Date/Time field

Note These numbers are for estimating the record size rather than calculating the exact size.

The contents of the Memo, OLE Object, and Hyperlink fields are stored elsewhere in the .mdb file, so you need to count only the overhead for their address pointers, as described in the preceding list.

Text fields present the greatest problem for estimating record size because they are variable-length fields. Microsoft Jet uses one byte per actual stored character up to the maximum length specified by the FieldSize property. Zero-length strings ("") use 1 byte; Null values use 0 bytes.

The recommended way to avoid the variable length of Text fields is to use the SQL data definition language (DDL) CHAR data type, which is a fixed-length data type. With this method, you don’t have to pad the field with data as you do with the methods described in the following paragraphs. The CHAR data type, however, is not available through the Microsoft Access user interface and can only be used with SQL DDL commands such as ALTER TABLE and CREATE TABLE.

See Also For more information about creating tables using SQL DDL statements, see Chapter 3, “Data Definition and Integrity.”

A less ideal strategy is to add one or more dummy Text fields to the table with default values that are 255 characters long until the record size exceeds 1,024 bytes. For example, if you estimated the record size to be at least 130 bytes, you would calculate the needed dummy fields as follows:

  • Bytes you need to pad = (1,025 – 130) = 895 bytes.

  • Each whole dummy text field = (255 + 2 bytes overhead) = 257 bytes.

  • You need three completely filled dummy fields (257 * 3 = 771) of 255 xs plus one partially filled dummy field (895 – 771 – 1 overhead byte) of 123 xs.

Do not place these dummy fields on your forms. Whenever a new record is created, Microsoft Access automatically creates a record with the four x-filled dummy fields, which forces it into record-locking mode.

Using either of these techniques may cause performance degradations because the size of the database will increase due to the fact that every record will occupy 2K of disk space.

Using the Microsoft Access Format property is not recommended to force padding when a record is being added because only Microsoft Access recognizes the Format property associated with the table. Other applications that use Microsoft Jet do not recognize the Format property and would not properly pad the record.


Using Optimistic Locking Wherever Possible

Another strategy to work around the fact that page locking locks an entire page of data instead of just the record or records being edited is to use optimistic locking wherever possible. Although optimistic locking does not eliminate all page locking problems, it does minimize the amount of time that a record is locked and therefore minimizes the possibility that an unwanted record will also be locked.

See Also For more information about optimistic locking, see “Optimistic Locking” earlier in this chapter.


Client/Server Environments and Custom Locking Schemes

If neither creating record sizes that are larger than half a page nor using optimistic locking wherever possible is an acceptable approach for your application, there are other options you may want to consider. You could move your application to a client/server environment using a server that supports record locking or you could implement a custom locking scheme.

See Also For more information about client/server applications, see Chapter 9, “Developing Client/Server Applications.” For more information about custom locking schemes, see the following section, “Implementing a Custom Locking Scheme.”


Implementing a Custom Locking Scheme

You might encounter situations in your multiuser application where the behavior of page locking is not appropriate, and where the use of optimistic locking is not a suitable workaround. In this case, you may want to consider your own custom locking scheme. Implemented through a set of routines that you write, custom locking completely bypasses Microsoft Jet locking. Your code controls locking by identifying when a record should be locked and unlocked.

The most common approach to this technique is to have a semaphore, or lock table, that identifies when a record is locked. The lock table typically identifies records by storing the key value of the record, the lock status (locked or unlocked), and the name of the user who has the record locked.

Implementing a custom locking scheme requires a great deal of design, implementation, and testing time. In many cases, a custom locking scheme cannot duplicate the functionality that is built into Microsoft Jet. For example, even if you implement single-record locking, it would be very difficult to handle data in a recordset that is based on more than one table, because you would have to determine all the tables that contain records that have to be locked. Custom locking schemes are most suitable for situations where the data to be locked is stored in a few tables and where the application is not based on a data model with complex joins and relationships.


Other Issues in Multiuser Design

Up to this point, locking has been covered as the main multiuser issue. It is, however, not the only issue. When designing an application for multiuser access, you should keep in mind several additional factors. This section covers these factors.


Refreshing DAO Collections

In a single-user system, Data Access Objects (DAO) always keep track of changes to collections. As you add objects to collections and delete objects from collections, Microsoft Jet knows about these changes. However, in a multiuser setting, other users may be modifying collections by adding new tables or queries to the database, or deleting existing objects. In this case, DAO doesn’t automatically keep track of changes to collections.

As an example, consider the following code, which displays all the TableDef objects in the current database:

Sub ShowAllTables(dbs As Database)
    Dim tdf As TableDef

    For Each tdf In dbs.TableDefs
        Debug.Print tdf.Name
    Next tdf
End Sub

When this code runs, it takes a snapshot of the TableDefs collection as it exists at that time. It iterates through each TableDef object and displays its name. Assume you have to run this code twice. The first time you run it, ten table names are displayed. But before you can run it again, another user on the network who also has the database open adds a new table. Now you run the code again. It still displays ten table names. This is because DAO doesn’t automatically know about changes made to the collections by other users. To solve this problem, use the Refresh method. The Refresh method forces Microsoft Jet to re-inventory the database and update the collections with the most recent changes.

See Also For more information about DAO, see Chapter 2, “Introducing Data Access Objects.”

To change the previous code to always display the most current list of TableDef objects, add a Refresh method, as follows:

Sub BetterShowAllTables(dbs As Database)
    Dim tdf As TableDef

    dbs.TableDefs.Refresh
    For Each tdf In dbs.TableDefs
        Debug.Print tdf.Name
    Next tdf
End Sub


Requerying Data

If your multiuser application presents data to the user in a visual form, such as in a window or form, you may want to update the user’s view with the most current data. While this functionality is automatically available to applications using the Microsoft Access user interface, an application based on Visual Basic or some other language must explicitly requery data to get the most current view of other users’ changes.

To obtain the most current view of data in a Recordset object, you must first determine if the Recordset object supports the Requery method. You can do this by checking the value of the Restartable property of the Recordset object. If the value is True, you can refresh the Recordset object’s contents by using the Requery method. This causes Microsoft Jet to repopulate the Recordset object with the most current data.

If the Recordset object doesn’t support the Requery method, you must open the Recordset object again with the OpenRecordset method. The following code illustrates how to generically requery a Recordset object. The OpenRecordsetForRequery procedure opens a dynaset-type Recordset object on the Orders table. It then tries to refresh the Recordset object’s contents by calling the RequeryIfYouCan function. If this fails, it reopens the Recordset object.

Sub OpenRecordsetForRequery(strDbPath As String)
    Dim dbs As Database
    Dim rst As Recordset

    ' Open database in shared mode.
    Set dbs = OpenDatabase(strDbPath, False)
    ' Open dynaset-type recordset.
    Set rst = dbs.OpenRecordset("Orders", dbOpenDynaset)

    ' Call RequeryIfYouCan function.
    If RequeryIfYouCan(rst) Then
        Debug.Print "Recordset has been refreshed."
    Else
        ' If recordset cannot be requeried, reopen it.
        Set rst = dbs.OpenRecordset _
            ("Orders", dbOpenDynaset)
        Debug.Print "Recordset has been reopened."
    End If

    rst.Close
    dbs.Close
End Sub

Function RequeryIfYouCan(rst As Recordset) As Boolean
    ' Check recordset's Restartable property.
    If rst.Restartable Then
        ' Requery recordset.
        rst.Requery
        RequeryIfYouCan = True
    Else
        RequeryIfYouCan = False
    End If
End Function

In general, most Recordset objects are restartable, which means they can be requeried. The exceptions are recordsets based on pass-through queries and on crosstab queries that contains variable-length fields. These types of recordsets cannot be requeried and must be reopened to get the most current state of the data.


Using Transactions in the Multiuser Environment

You can use Microsoft Jet transactions to group updates in units that can be committed or rolled back as a whole. Because transactions save updates in a temporary file instead of in real tables, they also have useful effects in multiuser environments. The most common use of transactions in multiuser environments is to make sure users don’t see an incomplete view of shared data as it’s being changed.

For example, assume your application is running code that is updating data, and another user is simultaneously running a report on that data. If you don’t wrap your updates in a transaction, the user running the report could receive inconsistent data if some records have been updated by your code, and some have not been. If you wrap your updates in a transaction, however, the other user can’t receive inconsistent data because all records are updated at once.

Try not to keep a transaction open too long. All locks that result from your edits with the transaction are kept in place until the transaction is committed or rolled back. This can diminish the concurrency of your multiuser application.

See Also For more information about transactions, see Chapter 5, “Working with Records and Fields.”


Auto-Increment Fields

There are issues with auto-increment fields and locking when using Microsoft Jet 2.x. Because the value of the next Counter number is stored in the table-header page of the database file, when adding a record to a table that has a Counter data type (called an AutoNumber data type in Microsoft Access 95 and 97), Microsoft Jet locks that page. This occurs in Microsoft Jet 2.x when inserting a record into a table, but is not a problem when using Microsoft Jet 3.0 and 3.5.


Identifying Users

In your multiuser application, you may want to programmatically identify the user currently logged on to the system. This is useful for administrative functions such as storing the user’s name with edited records to create an audit trail. Several methods are available to achieve this functionality. One involves Microsoft Jet user-level security, another requires you to maintain your own user information, and another requires using utilities on the CD-ROM included with this book.

When you establish user-level security for your database, you force the user to log on to your application with a predefined user name and password. The user name is then available to your application through the UserName property of the Workspace object. The following procedure writes the user’s name and the current date and time to fields. In this example, strDbPath is the path to the database.

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
' Open recordset.
Set rst = dbs.OpenRecordset("AuditTrail")
With rst
    ' Add new record.
    .AddNew
    ' Write name of current user to table.
    !UserLastModified = Workspaces(0).UserName
    ' Write time to table.
    !DateLastModified = Now
    .Update
End With

See Also For more information about users and passwords, see Chapter 10, “Managing Security.”

If you don’t want to implement security, but still want to identify users, you can have your application prompt the user for a name and password at startup and store those values in code variables or a temporary table. This way, you still have access to the user’s name, but don’t have to implement a secured database.

A new (currently unsupported) way to get this information is to use the graphical 32-bit-only Ldbview.exe or programmatic 32-bit Msldbusr.dll utilities. These are found on the CD-ROM that comes with this guide.

See Also For information about how to use Ldbview.exe and Msldbusr.dll to view information about users, see the “Understanding Microsoft Jet Locking” white paper in the Papers folder on the companion CD-ROM. Its file name is Jetlock.doc.


Optimizing Multiuser Applications

In most scenarios, the very act of implementing a database system in a multiuser environment incurs a substantial performance penalty. Few networks in the world supply “across the wire” performance that approaches local disk access performance. Given this, your biggest tasks in optimizing your multiuser application are minimizing network traffic and identifying and eliminating bottlenecks in your local area network (LAN). Most of the following suggestions are based on this supposition that decreasing network traffic has the largest effect on your application’s performance.


Minimizing Network Traffic for Data

Most of the optimizations you perform for data access in a single-user environment create equal, if not greater, improvements in multiuser environments. When you optimize a single-user application, you’re concerned only with that application’s performance on a local machine. When you optimize a multiuser application, reductions in network traffic help not only the individual user, but all users of the application.

For example, consider the case where your application has to search for a record in a table that has no indexes. In this scenario, Microsoft Jet must perform a sequential scan of the entire table to find a match. This means that the entire table must be sent from the server to the workstation’s copy of the engine. If the table is large, this can be a quite lengthy operation. The problem is further compounded because the user running the search is waiting a long time for a response, and the high volume of network traffic is in turn slowing down other users. This situation can be alleviated by adding indexes to the table for the fields on which your application searches. However, keep in mind that having indexes may cause increased network traffic and may reduce concurrency and efficient performance.

See Also For more information about when to use indexes, see “Other Optimization Tips and Techniques” in Chapter 13, “Optimizing Performance.”

In another example, assume that your application uses a set of tables containing lookup data that represents postal codes or lists of part numbers — data that doesn’t change often. By moving this type of data to the local workstation, you can eliminate network access that may be made hundreds of times during the application’s typical execution.


Minimizing Network Traffic for Static Objects

As discussed earlier in this chapter, the location of objects in a multiuser application has a direct effect on the application’s performance. Consider the following worst-case scenario.

A user has a workstation with 8 MB of RAM and a 200 MB hard disk, of which 100 MB is unused. The software on this machine consists of MS-DOS® and the necessary network software to connect to the file server. The user runs your multiuser application completely from the network. This means that he or she has to load Microsoft Windows, Microsoft Jet, and your application from the network. Additionally, every byte of data access, whether it’s from a live data table or from a static object such as a query, has to come across the network. Finally, the user is using a temporary swap file mapped to a network drive.

Obviously, this user is not going to be happy with your application’s performance. In fact, it would be surprising if the user could even run your application. To understand how to minimize network traffic for static objects, it’s useful to work backward from this worst-case scenario and address the individual problems.

You know that the user has the necessary disk space to allow you to move your application to the local level. Start by moving the support software to the user’s workstation. This includes installing the operating system, such as Microsoft Windows, on the workstation, setting a permanent swap file on the local drive, and installing the Microsoft Jet components locally. Next, consider whether or not to install the workgroup information file for your application at the local level.

Then, install your application on the user’s local drive. If your application is based on Microsoft Access, install it locally. If your application is based on Visual Basic or some other language, install the necessary resources such as executable files and dynamic-link libraries locally. Figure 6.6 illustrates this optimization technique.

Figure 6.6 Optimizing a workstation

Finally, investigate bottlenecks occurring on your LAN that are due to hardware. You might want to try the following:

  • Segment the LAN into subnets. Typically, a server has only one network interface card (NIC) with many nodes connected to it. To reduce this bottleneck, add multiple NICs to the server and spread the nodes across those segments. This is probably the cheapest and biggest performance win because it substantially reduces collisions on the wire and gives each station a larger slice of bandwidth to communicate with the server.

  • Add a better NIC to the server. Practically all Pentium systems now come with PCI slots. By using PCI NICs you can experience significantly faster throughput while keeping the CPU utilization lower. Also, many PCI NICs offer a multi-ported design, thus allowing you to do multiple segments from one PCI NIC. An example of this is a server with two PCI slots that uses two four-port PCI NICs. This creates eight segments that go to 36 nodes to reduce collisions on the wire and increase the bandwidth that each node has when communicating with the server. This would allow you to focus on performance issues that are software-related instead of hardware-related.

    Another option with PCI NICs is the 100 MB Ethernet standard. While the 100 MB hubs required to accompany the 100 MB NICs are expensive, they do provide an alternative to finding workarounds for performance issues over the wire, and they can use existing cabling.

  • Add faster disk drives and host adapters to the server. Again, moving to PCI- or EISA-based host adapters can have a significant impact on performance. To eliminate this bottleneck, one approach is to configure the server to use an EISA SCSI RAID controller using eight 1-GB SCSI drives. This increases performance because the EISA card reduces CPU processing by handling some processing itself, and because the RAID configuration allows reads and writes to be spread across the eight disk drives, thus reducing contention issues when trying to access a drive. This is probably the most expensive solution, but it makes a big performance improvement over a one ISA card-one disk solution.

See Also For more information about other performance optimizations, see Chapter 13, “Optimizing Performance.”


Sharing Microsoft Jet Databases on the Web

You can also create simple client/server applications that the use a Web browser, such as Microsoft Internet Explorer, as a front-end used to access a Microsoft Jet database. It is beyond the scope of this book to discuss this in great detail, but this section will point you to resources that document how to do this.

You can use the Active Server Pages (ASP) components of Microsoft Internet Information Server 3.0 on Windows NT Server version 4.0 to access a Microsoft Jet database. You can also install and use the Active Server Pages components from a workstation used as a Web server with Microsoft Peer Web Services 3.0 on Windows NT Workstation version 4.0 or Microsoft Personal Web Server version 1.0a on Windows 95.

For information about:

  • Microsoft Internet Information Server 3.0 and installing the Active Server Pages components, see the Internet Information Server Web site at http://www.microsoft.com/iis/

  • Microsoft Peer Web Server 3.0, see the Peer Web Services Web site at http://www.microsoft.com/ntworkstation/info/aapeerweb.htm

  • Microsoft Personal Web Server 1.0a, see the Internet Explorer Starter Kit Web site at http://www.microsoft.com/ie/iesk/pws.htm

After you have installed Active Server Pages on any of the three Web servers, you can view the Active Server Pages online documentation. On the Windows Start menu, point to Programs, point to your Web server’s submenu, and then click Active Server Pages Roadmap. From the Roadmap, you can also view the Adventure Works sample Web site, which demonstrates many of the features of Active Server Pages scripting and is an excellent source of sample code. You can also view the Roadmap by typing the following address: http://yourserver/iasdocs/aspdocs/roadmap.asp, where yourserver is the domain name of your server. The Roadmap documentation requires either Microsoft Internet Explorer version 3.0 or later, or a browser that supports frames. You will get the best results with Microsoft Internet Explorer.

The Database Access component of Active Server Pages uses ActiveX Data Objects (ADO) to provide easy access to information stored in a database that complies with the Open Database Connectivity (ODBC) standard. To access a Microsoft Jet database, you use the Microsoft Access ODBC Driver. For information about how to create an Active Server Pages application that opens a database, open the Active Server Pages Roadmap documentation as described above, and then in the Contents frame, open Tutorial, Module 2, Lesson 3, “Using the Database Access Component.”


Chapter 7 Database Replication

Database replication is the process of sharing data or database design changes between databases in different locations without having to copy the entire database. Replication involves copying a database so that two or more copies of a single database remain synchronized. The original database is called a Design Master and each copy of the database is called a replica. Together, the Design Master and the replicas make up a replica set. There is only one Design Master in a replica set. The Design Master is the only member of the replica set where you can make design changes to the objects in the database. Each member of the replica set contains a common set of replicable objects, such as tables, queries, forms, reports, macros, or modules. Each member of the replica set can also contain nonreplicated — or local — objects.

Synchronization is the process of ensuring that every copy of the database contains the same objects and data. When you synchronize the replicas in a replica set only the data that has changed is updated. You can also synchronize changes made to the design of the objects in the Design Master.

If you design your applications for multiple users, database replication, in the appropriate circumstances, can improve the way your users share data. By using database replication, you can make copies of a database so that two or more users can work on their own copy of the database at the same time and each user’s changes are passed on to other members of the replica set through synchronization.

Why Use Database Replication?

Implementing Database Replication

Microsoft Jet Replication Architecture

Replica Set Topology

Replicating Your Database

Making Additional Replicas

Synchronizing Replicas

Synchronization Conflicts and Errors

Advanced Design Considerations

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

The companion CD-ROM also includes three databases that you can use to study database replication. These samples are located in the JetBook\Samples\Replicas subfolder. The databases include SampleDesignMaster.mdb, the Design Master for the replica set; SampleReplica.mdb, a replica created from the Design Master; and SamplePartialReplica.mdb, a partial replica created from the Design Master.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


Why Use Database Replication?

Imagine that a client has asked you to develop a contact-management application that the company’s field sales staff can use to monitor sales and orders. Each sales representative has a laptop computer that can be connected to the company’s network.

A traditional approach to building this application is to separate the tables from the other objects in the database so that the data can reside in a back-end database on a network server, or on the Internet or an intranet, while the queries, forms, reports, macros, and modules reside in a separate front-end database on the user’s computer. The objects in the front-end database are based on tables that are linked to the back-end database. When sales representatives want to retrieve or update information in the database, they use the front-end database.

Database replication enables you to take a new approach to building this application by creating a single database that contains both the data and objects, and then making replicas of the database for each sales representative. You can make replicas for each user and synchronize each replica with the Design Master on a network server. Sales representatives update the replicas on their computers during the course of a work session, and users synchronize their replicas with the Design Master on the server as needed.

In addition, you can choose to replicate only a portion of the data in the Design Master, and you can replicate different portions for different users by creating partial replicas. In the scenario involving sales representatives using replica databases, each individual salesperson typically needs only the sales data related to his or her own territory. Replicating all sales data for all sales representatives would involve unnecessary processing and duplication of data. By using partial replicas, you can duplicate only the data that each sales person actually needs. A complete set of data is still contained in the Design Master, but each replica handles only a subset of that data.

Database replication is well suited to business applications that need to:

  • Share data among remote offices You can use database replication to create copies of a corporate database to send to each satellite office across a wide area network (WAN). Each location enters data in its replica, and all remote replicas are synchronized with the replica at corporate headquarters. Individual replicas can maintain local tables that contain information not included in the other replicas in the set.

  • Share data among dispersed users New information that is entered in the database while sales representatives are out of the office can be synchronized any time the sales representatives establish an electronic link with the corporate network. As part of their workday routine, sales representatives can dial in to the network, synchronize the replica, and work on the most current version of the database. Because only the incremental changes are transmitted during synchronization, the time and expense of keeping up-to-date information are minimized. By using partial replicas, you can synchronize only specified parts of the data.

  • Make server data more accessible If your application doesn’t need to have immediate updates to data, you can use database replication to reduce the network load on your primary server. Introducing a second server with its own copy of the database improves response time. You determine the schedule for synchronizing the replicas and can adjust that schedule to meet the changing needs of your users. Replication requires less centralized administration of the database while offering greater access to centralized data.

  • Distribute application updates When you replicate your application, you automatically replicate not only the data in your tables, but also your application’s objects. If you make changes to the design of the database, the changes are transmitted during the next synchronization; you don’t have to distribute complete new versions of the software.

  • Back up data At first glance, database replication might appear to be very similar to copying a database. However, while replication initially makes a complete copy of the database, thereafter it simply synchronizes that replica’s objects with the original objects at regular intervals. This copy can be used to recover data if the original database is destroyed. Furthermore, users at any replica can continue to access the database during the entire backup process.

  • Internet or intranet replication You can configure an Internet or intranet server to be used as a hub for propagating changes to participating replicas.

Although database replication can solve many of the problems inherent in distributed-database processing, it is important to recognize the situations in which replication is less than ideal. You may not want to use replication if:

  • There are a large numbers of record updates at multiple replicas Applications that require frequent updates of existing records in different replicas are likely to have more record conflicts than applications that simply insert new records in a database. Record conflicts occur when any changes are made to the same record by users at different locations at the same time. Applications with many record conflicts require more administrative time because the conflicts must be resolved manually. This is true even if different fields are updated within the same record.

  • Data consistency is critical at all times Applications that rely on information being correct at all times, such as funds transfer, airline reservations, and the tracking of package shipments, usually use a transaction method. While transactions can be processed within a replica, there is no support for processing transactions across replicas. The information exchanged between replicas during synchronization is the result of the transaction, not the transaction itself.

See Also For more information about replication, see the Microsoft Access and Microsoft Replication Manager Help files, and see the white paper “Database Replication in Microsoft Jet,” located in the Papers folder on the companion CD-ROM.


Implementing Database Replication

You can implement Microsoft Jet database replication by using:

  • Microsoft Access menu commands

  • Briefcase replication in Windows 95

  • Microsoft Replication Manager

  • Data Access Objects (DAO) programming

The first three replication tools provide an easy-to-use visual interface, while the last enables programmers to build replication directly into their applications code.

Partial replicas are available only by using the Partial Replica Wizard in Microsoft Access or through DAO programming. The Partial Replica Wizard is not part of the retail version of Microsoft Access 97, but is available at no cost from the Microsoft Access Developer Forum Web site at http://www.microsoft.com/accessdev/accwhite
/prtlrepl.htm. This wizard is also included in the PrtlReplWiz folder on the companion CD-ROM.

Replication can be used on a variety of computer networks, including Microsoft Windows 95 peer-to-peer networks, Microsoft Windows NT Server or Workstation version 3.51 or later networks, and Novell NetWare versions 3.x and 4.x network servers.


Microsoft Access Replication

Microsoft Access provides replication commands that you can use while working in your databases. By using the Replication command on the Tools menu, you can:

  • Create a complete replica

  • Synchronize your complete or partial replica with another member of the replica set

  • Resolve synchronization conflicts by using the Conflict Resolver

  • Recover the replica set’s Design Master, if necessary

Microsoft Access Users If you have Microsoft Access, one of the easiest ways to become familiar with the concepts and procedures associated with database replication is to experiment with the Microsoft Access replication commands. Open an existing Microsoft Jet database in Microsoft Access, point to Replication on the Tools menu, and then click Create Replica to both convert your database to a Design Master and create a replica. You can then explore the changes made to the design of your database (these are explained in detail in the “Changes Made to Your Database When You Use Replication” section later in this chapter) and the similarities of the replica to the Design Master. Next, make a change to the data in the replica and a change to the design of a table in the Design Master, and then click Synchronize Now. You can then open the replica and Design Master to confirm that the changes appear in both members of the replica set.


Briefcase Replication

Windows 95 or Windows NT Server or Workstation version 4.0 Briefcase replication is perhaps the easiest way to implement replication. To use Briefcase replication, drag a Microsoft Access database file from a shared network folder to the My Briefcase icon on your laptop’s desktop. You can disconnect the laptop from the network and make changes to the data in the replica. When you are finished working on the files on the laptop computer, reconnect to the network, double-click the My Briefcase icon, and then click Update All on the Briefcase menu to automatically synchronize any changes between the database on your laptop and the Design Master on the network.

When Briefcase replication is installed (through the Custom installation option in the Microsoft Access Setup program), it registers with the Windows 95 or Windows NT Server or Workstation 4.0 Briefcase a special class ID (CLSID) for Microsoft Jet 3.5 .mdb files. When a file with the .mdb extension is dragged into Briefcase, the replication code converts the database into a replica. Before the conversion takes place, however, you are prompted to make a backup copy of the original database file. The backup copy has the same file name as the original, except that it has a .bak file extension. The copy is kept in the same folder as the original database file.

The replication code then converts the database into a replicable form, leaves the Design Master at the source, and places a replica in the Briefcase. You also have the option of putting the Design Master in your Briefcase and leaving a replica on the desktop.

You can take your replica with you into the field and then synchronize the changes made to your replica with the Design Master on the desktop or network. When you click Update All or Update Selection on the Briefcase menu, replication code merges the changes to the two members of the replica set. Unlike Microsoft Replication Manager, Briefcase replication does not have the ability to set a schedule for synchronizing other members of the replica set. Synchronization occurs only at the time the Update command is clicked and only between the current member and the specified member.

There are some limitations to using the Briefcase for replicating your databases. Briefcase replication works only under Windows 95 and Windows NT Server or Workstation 4.0 and only if the Briefcase Reconciler option was selected when the Microsoft Access Setup program was run. There is no programmatic access available to the Briefcase itself, the Briefcase can’t distinguish between databases of different versions, and partial replicas can’t be created or synchronized by using the Briefcase. Finally, there is no way to schedule automatic synchronization among replicas by using the Briefcase.

Note If some users of your database need to continue using it in its nonreplicated form, make a copy of your database before converting it to a replicable database.


Microsoft Replication Manager

If you have a copy of Microsoft Office 97, Developer Edition, you can use the visual interface of Replication Manager to convert databases (create the first replica, or Design Master, of a replica set), create additional replicas, view the relationships between replicas, and set properties for replicas. You can also use Replication Manager to schedule synchronizations so they occur at specified times and can be completed unattended. Replication Manager gives you the greatest control over the sequence and timetable for synchronization. You can use Replication Manager to:

  • Manage a large number of replicas.

  • Support laptop users who are not always connected to a network. Laptop users can specify a network file location, or dropbox folder, where synchronization information is deposited for later processing.

  • Create complete replicas of more than one database.

  • Set the schedules for synchronizing replicas. Replication Manager provides a graphical user interface for scheduling synchronizations between replicas, as well as allowing on-demand synchronization by using a single menu command.

  • Configure synchronizations to send data, receive data, or send and receive data.

  • Access additional tools for troubleshooting.


Replication by Using DAO

The DAO programming interface provides methods and properties you can use to implement replication and manipulate replicas. DAO provides access to all the replication functionality available from the Briefcase or from within Microsoft Access, as well as partial replicas, to any application that supports Microsoft Jet and DAO.

Note Not all applications that support Microsoft Jet use its features in the same way. For example, although Microsoft Excel cannot replicate a database, it can be used to update a database that has been made replicable by another product. Any changes made to the database through Microsoft Excel will be correctly synchronized with other replicas.

You can use DAO to:

  • Convert a database to a Design Master.

  • Create and populate additional replicas.

  • Create partial replicas.

  • Synchronize replicas.

  • Get and set specific properties of a replicated database.

  • Handle or resolve conflicts or errors.


Choosing Microsoft Replication Manager or DAO

When you’re building your replication-based application, you can use either Replication Manager, DAO, or both. Deciding which one is best for your application depends on a few factors.

Replication Manager offers the fastest way to create a replication system. You can use its graphical interface to quickly define a replication topology and set replication schedules — without programming. If possible, you should use Replication Manager to incorporate replication into your application.

In some cases, DAO may be a better solution than Replication Manager. Although DAO requires programming, it gives you the ability to customize your replication system. Generally, DAO should be used under the following circumstances:

  • When you need to synchronize replicas when certain events occur. For example, you might want to trigger synchronization whenever updated product pricing information is received from headquarters. Because Replication Manager does not support event-based replication, you must use DAO to achieve this.

  • When you need to distribute your replicated application to users who have minimal computer expertise. In this case, you could use DAO to design a simplified replication interface or make replication completely transparent to users. Although you can distribute Replication Manager to your customers on a royalty-free basis, it might not be suitable for users with minimal knowledge of computer concepts.

  • When you need to create a partial replica. For example, you may want to create a replica that contains only a subset of the data contained in the Design Master.


Microsoft Jet Replication Architecture

The Microsoft Jet replication engine consists of four major components:

  • Tracking layer

  • Microsoft Replication Manager

  • Synchronizer

  • Briefcase reconciler

The following diagram shows the relationship among these components and between the components and Microsoft Jet.

Figure 7.1 The structure of Microsoft Jet database replication

This section describes each of these components and identifies the files associated with each component.


Tracking Layer

The ability to track changes in the database resides within the Jet database engine. As its name implies, every action executed by Microsoft Jet in one replica is tracked and recorded in preparation for its transmission to other replicas. Both data and design changes are replicated. The main replication services, including the creation of replicable databases and replica synchronization are included in the files MSRepl35.dll, MSRclr35.dll, and MSJet35.dll.


Microsoft Replication Manager

As described earlier, Replication Manager, which is included with Microsoft Office 97, Developer Edition, provides the management tools necessary to support replicated applications in dispersed locations. It can also be used to generate reports about the synchronization activity between replicas, which can be used to make sure the distributed application is performing as designed. Microsoft Replication Manager is consists of the files ReplMan.exe and MSRemr35.dll.


Synchronizer

For database replication to be useful, it is not enough that there just be multiple replicas of the same database. The replicas must communicate with one another in order to remain synchronized. As mentioned earlier, synchronization is the act or process of making the replicas identical, both in terms of the data they contain and how they are designed; changes made to the existing records in one replica are communicated to each of the other replicas that have that same record. Similarly, records added to or deleted from one member of the replica set are communicated to each of the other members of the set.

If you use Briefcase or Microsoft Access to synchronize your replicas, Microsoft Jet handles the direct exchange of information between members.

If you use Replication Manager, a separate utility called the Synchronizer (formerly known as the Transporter) monitors the changes to a replica and handles the actual exchange of data between replicas.

Note Although you can have more than one Synchronizer assigned to the same replica, this approach is not recommended because it can cause undesirable side effects.

The Synchronizer performs either direct or indirect synchronizations between two members of a replica set. Direct synchronization occurs when the two members can be opened simultaneously by either the Synchronizer or the Briefcase reconciler. If both members are on the same computer or are available over a common network, the Synchronizer can apply the changes to one member directly to the other member. Indirect synchronization occurs when the target replica set member is not in a shared folder, the other computer is temporarily disconnected from the network, the network itself is down, or there is another situation that prevents a direct connection.

When the Synchronizer is instructed to initiate a synchronization, it first attempts a direct connection with the target member of the replica set. If it can’t establish a direct connection, the Synchronizer for the source replica leaves a message for the Synchronizer of the target replica in a dropbox folder on the network. The shared folder serves as a dropbox location for the target replica and stores all the messages sent by all other Synchronizers in the replica set while the target member is unavailable.

Direct synchronizations on a local area network (LAN) are usually processed faster than indirect synchronizations because direct synchronizations allow the first Synchronizer to bypass the second Synchronizer completely. In indirect synchronizations, the return message to the first Synchronizer might take a few minutes, hours, or days, depending upon how often the second member connects to the network. Indirect synchronization over a WAN or dial-up connection can be significantly faster than a direct synchronization. When you are using a WAN topology, you can force indirect synchronization by making sure your replicas are not in a shared folder that would permit a direct synchronization. For more information about synchronizing over WANs, see the “Synchronizing Data in Wide Area Networks” section later in this chapter.

Because a replica set member’s availability can change over time, the Synchronizer might complete a direct synchronization one time and an indirect synchronization the next time. To determine whether a synchronization was direct or indirect, click Synchronization History on the View menu in Replication Manager and then click the Details button in the resulting dialog box, or look at the exchange details in the MSysExchangeLog system table.

Note The Synchronizer can run only on a computer that is running either Windows 95 or Windows NT Server or Workstation 3.51 or later. If a member of the replica set is stored on a non-Windows-based server, such as a Novell network, the Synchronizer must be located on a different computer.

You configure the Synchronizer through the Replication Manager user interface. When Replication Manager is first configured on a computer running Windows 95 or Windows NT Server or Workstation 3.51 or later, you are asked for the network folders to be used by the Synchronizer and whether the Synchronizer should be started automatically each time the computer is started. If you select automatic startup, a shortcut to the Synchronizer program is placed in the Windows 95 Start menu or the Windows NT Startup group. To change the Synchronizer startup to manual mode, you should move the Synchronizer icon to the Microsoft Access folder or program group. The file names for the Synchronizer are MSTran35.exe and MSTrani35.exe (used when synchronizing over the Internet or an intranet).

See Also For information about restarting the Synchronizer if it stops, see the Microsoft Replication Manager Help file.

The status of the Synchronizer is reported through the Replication Manager user interface. In the status bar in the Replication Manager window, there is an image of two computers with a connection. The connection between the computers changes as the status of the Synchronizer changes. A legend explains the status of the Synchronizer.

Note You cannot schedule synchronization over the Internet or an intranet by using Replication Manager because Replication Manager can’t control the communication link to your Internet service provider. To work around this problem, you can write VBA code to link to your Internet service provider, execute synchronization, and then drop the communications link.


Briefcase Reconciler

The Briefcase reconciler (MSRclr35.dll) is a special utility that is automatically executed when Windows 95 or Windows NT Server or Workstation 4.0 Briefcase is used, or from Microsoft Access when synchronizations through the Synchronizer are not explicitly requested. The reconciler ensures that a database is replicable and manages the merging of updates between the Briefcase (local replica) and the target replica.


Replica Set Topology

An important part of incorporating replication into your application is planning the appropriate topology for synchronizing data and design changes. A replication topology defines the communication that exists among members of the replica set, along with the logic that determines how synchronization occurs among the various members of the replica set.

If you’re using Microsoft Replication Manager to perform replication, all members ultimately update one another by using the topology that you create when you graphically link the replicas in the Replication Manager window. When you create a replication topology with Replication Manager, the latency in the chaining can be important. If A and B synchronize, and then B and C synchronize, and then A and B synchronize again, the time lag between synchronizations can be important. For example, if A and B synchronize, and then A and C synchronize, and then B and C synchronize, A is provided with the current information sooner. The programmer and database administrator must work together to determine the order in which updates are dispersed throughout the database.

If you’re using only DAO programming to implement replication, your code must ensure that all members of the replica set are synchronized. This might involve creating a custom replication schedule and synchronizing each member according to the schedule. One way to do this is by storing a list of all replica set members in a table or initialization file.

If you are creating partial replicas, either with the Partial Replica Wizard or through DAO programming, you can only synchronize partial replicas with other replicas that contain all the replicated data, such as the Design Master or another full replica. You cannot synchronize a partially replicated database with another partial replica since each partial replica contains only a subset of the replicated data.

The following diagram shows alternative topologies that can be created:

Figure 7.2 Replica set topologies

Other types of topologies exist. As the hybrid topology in the diagram suggests, there is an almost limitless variety of combinations and configurations. The type of topology you pick usually depends on your application; however, the star topology is usually the best for most applications.


Designing Synchronization Control

When designing a replication topology, you’ll need to decide how synchronization is initiated and which replica set member initiates it. In simple applications, a single replica might be selected as the member that initiates and controls synchronization. This replica is called the controlling replica. For example, in a simple star topology, the replica on the hub workstation might control all synchronization that occurs between members of the replica set. It is possible for a single replica to control all synchronization because the two databases specified in the DAO Synchronize method can be any two replicable databases in the same replica set.

Having a single controlling replica has the primary advantage of simplicity and ease of programming. However, if you are using a single controlling replica and that replica is damaged or unavailable, synchronization cannot occur. This design is suitable for applications where the reliability of replication is not a critical element of the application’s operation.

In some applications, the reliability of replication is critical. To increase reliability in these cases, you might want to design your replication topology so that more than one replica can control synchronization. There are several ways to do this. For example, your replication code could run on more than one replica, each of which periodically verifies whether the “default” controlling replica is available. One way to do this is to have a replica use the OpenDatabase method to see if the controlling replica and its computer are available. If the default controlling replica is unavailable, one of the other replicas could assume control.

A second way of determining if the controlling replica is available is to have the controlling replica maintain a log of all synchronizations that occur. Periodically, other replicas check the log to verify that a scheduled synchronization has occurred. If a synchronization does not appear in the log, one of the replicas temporarily promotes itself to the status of controlling replica and initiates the synchronization process. For safety, you should replicate the log so you have a backup in case the log for the controlling replica becomes unusable.

Another way to provide redundancy is to always have more than one controlling replica that performs synchronizations. This is the case with the fully connected topology, in which every replica is by definition a controlling replica. Although this can produce significant overhead, it is more reliable.

Note Because so many different factors are involved, it is difficult to predict which topology will be the best solution for a specific application. You might want to experiment with a few different topologies in your replication scheme to determine which one produces the best results.


Staggered Synchronization Schedules

If you’re implementing your replication topology with more than one controlling replica, consider using staggered schedules at each of the controlling replicas. If two members of the replica set both attempt to synchronize with a third member at approximately the same time, one of the synchronizations will succeed and the other may encounter a temporary lock failure. The member that attempted — but failed — to synchronize continues to attempt to synchronize, thereby generating additional network traffic and overhead. Using a staggered schedule helps make sure that the target member is not “busy” when the synchronization is initiated.

See Also For more information about synchronizing, see the “Synchronizing Replicas” section later in this chapter.


Star Topology

In the star topology (see Figure 7.2), a single hub periodically synchronizes with each of its satellite replicas. In this topology, all data is shared among replicas through a single, centralized database. The star topology reduces data latency time among members of the replica set because data has to travel at most two “hops” to synchronize with any other member of the set. If the average quantity of data to exchange during synchronization is relatively small, the star topology can be a fast and efficient solution.

One of the potential disadvantages of the star topology, however, is that the first replica associated with the hub does not receive any new data from the other replicas and the last replica receives all the new data from all the other replicas. To make sure that all satellites are properly synchronized, you must make two synchronization passes around the star.

A second potential disadvantage of the star is that the hub is involved with every synchronization and therefore must be able to handle a greater load than any of the replicas associated with it. As a result, the star topology has a practical limit to the number of satellite replicas it can support because so much activity is centered at the hub. An effective way of handling this is to create additional stars and connect their hubs. Or, you can place the hub on a dedicated high-end computer capable of handling a heavy load. However, if your application exchanges only a small amount of data with each synchronization, the hub might be able to support a relatively large number of workstations.

Another potential disadvantage of the star is that it contains a single point of failure — the hub. If the controlling replica is located at the hub, and the hub fails, synchronization cannot continue among any members of the replica set. This problem can be handled in code by reassigning the controlling replica.

If you use the star topology, consider locating the Design Master on a computer other than the hub. This has several advantages. First, you can better protect the Design Master by making it unavailable to users who have permission to modify the design of database objects. Second, you can modify the Design Master and test these modifications offline. This prevents a set of incomplete changes from being replicated to other workstations during synchronization. When you’ve completed your design modifications, you can synchronize with the hub, which in turn synchronizes with other members of the replica set.

The star topology is a good starting point for many applications. It is reasonably simple to implement and can be designed to be both efficient and reliable. If you’re implementing replication for the first time, the star topology is recommended. For more information, see the “Synchronizing the Star Topology” section later in this chapter.


Ring Topology

In the ring topology, (see Figure 7.2) A synchronizes with B, B synchronizes with C, C synchronizes with D, and D synchronizes with A. The main advantage of the ring is that each computer handles approximately the same load. For more information, see “Synchronizing the Ring Topology” later in this chapter.

Latency times for a ring topology can be greater than latency times for a star topology. This is because data might have to travel multiple “hops” before it disperses to every replica. For a large number of replicas, it might take a while to disperse data throughout the replica set.

Another potential problem with simple ring implementations is that if any of the databases is unavailable, data fails to disperse throughout the application. This can be handled in your code, however, by routing information around the failure point instead of through it.

Ring topologies can be expanded by adding additional rings and connecting two or more of the replicas within each ring.


Fully Connected Topology

As its name suggests, the fully connected topology involves a replica set in which each replica synchronizes with every other replica. One way of conceptualizing the fully connected topology is as a star topology where every replica acts as a hub.

The fully connected topology has several benefits. One of the most significant benefits is the low latency time for data propagation. Because each replica synchronizes with every other replica, data is sent directly to all replicas without having to indirectly disperse through a series of replicas.

Another benefit of the fully connected topology is its high degree of redundancy and reliability. In the fully connected topology, each replica is a controlling replica that initiates synchronization. As a result, the impact of computer or replica failure on the entire application is minimized.

The fully connected topology can be useful in applications where data is changed frequently in most or all of the replicas and it’s important to disperse these changes as quickly as possible.

Despite its benefits, the fully connected topology can be inefficient because it involves many more synchronizations than other topologies and therefore requires more overhead and network traffic. If your application involves frequent synchronization among a large number of replicas, the fully connected topology might generate a prohibitive amount of overhead. In a fully connected scenario with n replicas, the number of synchronizations that occur is n times n. For example, 30 replicas would involve 900 separate synchronizations. If you’re using a fully connected topology, it might be wise to stagger the replication schedules to reduce the amount of record locks that could occur with simultaneous synchronizations.


Linear Topology

The linear topology is similar to the ring topology, but a loop through all the replicas is not completed. That is, the first and last replicas do not synchronize with each other.

The linear topology is simple to implement, but it is usually not as efficient as some of the other topologies. The main problem with the linear topology is the long latency time for data dispersion.

To illustrate this, consider four replicas, A, B ,C, and D, that are synchronized by using a linear scheme, with A as their controlling replica. If a change is made to data in replica D, but to none of the other replicas, it would take six synchronizations, starting at A, before all other replicas have D’s changes. These synchronizations are: A-B, B-C, C-D, A-B, B-C, A-B. If this were set up as a ring topology, A and D would be directly connected, so it would take only four synchronizations for D’s changes to be fully dispersed: A-B, B-C, C-D, D-A.

When designing a topology for your application, it is a good idea to count the number of synchronizations that must occur in different situations to fully disperse data throughout the replica set. Although the number of synchronizations is not the only measure of a topology’s effectiveness, it can tell you something about the efficiency of your replication architecture.


Replicating Your Database

The first step in making your database replicable is to create a Design Master. You can create a Design Master by using any of the methods discussed in the “Implementing Database Replication” section earlier in this chapter.

After you have created the Design Master, identify the objects in the database you don’t want replicated. In Microsoft Access, you can keep an object from being replicated by clearing the Make Replicable check box in the Save As dialog box when you save an object in the Design Master. You can also convert a local object to a replicable object by right-clicking the object in the Database window, clicking Properties on the shortcut menu, and then selecting the Replicable check box. All objects saved in a replica (not the Design Master) will automatically be local, nonreplicable objects.

To create local objects programmatically, set their KeepLocal property to the string value “T” (the quotation marks are part of the string). You can only set the KeepLocal property of database objects before you create the Design Master. The KeepLocal property is read-write in nonreplicated databases and read-only in replicated databases. To create a local object in a database that has already been replicated, set the object’s ReplicableBool property to False. To make your database replicable, set its ReplicableBool property to True. For more information about making additional replicas of your database, see the “Making Additional Replicas” section later in this chapter.

Important The Microsoft Jet database engine does not allow you to protect a replicable database with a database password. Before you begin using replication, remove any database password protection from the database you will be making replicable. Permissions defined for user-level security do not interfere with replica synchronization. For more information about how to remove a database password and how to define user-level security, see Chapter 10, “Managing Security.”


Keeping Objects Local

When you create a Design Master, all of the objects in the database are converted to replicable objects. If you don’t want all the objects in your database dispersed throughout the replica set, you can append and set the KeepLocal property for any objects you don’t want replicated before you create the replica. For example, if your database has a table containing confidential salary information, initialization information, or names of users who log on to the database, you might want to keep that information only in the Design Master. You can set the table’s KeepLocal property to “T” to keep it local while all other objects are replicated when the database is converted to a Design Master.

If you replicate an object with your database, you can still set that object’s ReplicableBool property to False so that the object will not be propagated to the replica set when you synchronize. The KeepLocal property is most useful when you want to make sure that an object is never made replicable at all. For example, when you replicate a table, new fields are added to the table, and any AutoNumber fields are no long simple linear sequences but instead become globally unique identifiers (GUIDs). To prevent these changes to a table, you can set its KeepLocal property before you make the database replicable.

To set the KeepLocal property of an object, you must first create the property and append it to the object’s Properties collection if it does not already exist in the collection. For a TableDef or QueryDef object, you create the KeepLocal property in the TableDef or QueryDef object’s Properties collection. For a Microsoft Access form, report, macro, or module, you create the KeepLocal property in the Properties collection of the Document object that is associated with the object you want to keep local.

The following code shows how to set the KeepLocal property for an object. If the KeepLocal property does not exist in the object’s Properties collection, the procedure creates the property, sets its value to “T”, and appends it to the object’s Properties collection. Note that the object passed in for the obj argument must be of the type TableDef, QueryDef, or Document:

Function MakeObjectLocal(obj As Object) As Boolean
    Const conErrPropNotFound = 3270

    On Error GoTo Err_MakeObjectLocal
    ' Does the KeepLocal property exist for this object?
    obj.Properties("KeepLocal").Value = "T"
    MakeObjectLocal = True

Exit_MakeObjectLocal:
    Exit Function

Err_MakeObjectLocal:
    ' If KeepLocal property does not exist, create the KeepLocal property
    ' and set it to "T".
    If Err = conErrPropNotFound Then
        ' Add the KeepLocal property to the Properties collection by
        ' using the SetCustomProperty function defined in Chapter 2.
        If SetCustomProperty(obj, "KeepLocal", dbText, "T") = True Then
            Resume Next
        End If
    Else
        MsgBox "Error: " & Err.Number & " - " & Err.Description
    End If
    MakeObjectLocal = False
    Resume Exit_MakeObjectLocal
End Function

Important If the object on which you are setting the KeepLocal property has already inherited that property from another object, the value set by the other object has no effect on the behavior of the object you want to keep local. You must directly set the property for each object.

You cannot set the KeepLocal property for objects after they have been converted into a replicable form. However, to prevent an object from being propagated through the replica set, you can set its ReplicableBool property to False.

If you have created a relationship between two tables in your database, you must set the KeepLocal property the same for both tables — both tables must be local or both must be replicable. If the property is not set the same for both tables, an error occurs. The KeepLocal property cannot be set while the relationship is in effect. Before creating and setting the KeepLocal property, delete the relationship between the tables. After setting the KeepLocal property, add the relationship back to the tables and proceed with converting the database.


Creating a Replica Set

When you create a new replica set, the database you convert becomes the Design Master. You can have only one Design Master in a replica set. Any changes to the design of the database, or to the design any of the replicable tables, queries, forms, reports, macros, and modules in the database can be made only in the Design Master. This prevents users at multiple replicas from making conflicting changes to the database’s design and objects. You can still make changes to local (nonreplicable) objects in a non-Design Master replica, but those changes will remain local to the replica in which they were made.

You can convert your database from a nonreplicable form to a replicable form in one of four ways:

  • Using the Create Replica command in Microsoft Access

  • Dragging the database file to the My Briefcase icon

  • Using the Convert Database To Design Master command in Microsoft Replication Manager

  • Using DAO code

This section describes how to convert your database by using DAO code.


Making a Database Replicable

Before you convert a database, you might want to determine whether the database is already replicable. You can check whether the Replicable or ReplicableBool property exists in the Properties collection of the Database object. If a database has been made replicable, both properties exist in the Properties collection of the Database object.

Note that you may want to make a copy of the nonreplicable database before you make the database replicable. Microsoft Jet doesn’t do this for you automatically.

The following code makes a database replicable by setting its ReplicableBool property to True. If the ReplicableBool property doesn’t exist, it is first created and appended and then set to True:

Function MakeDbReplicable(strDbPath As String) As Boolean
    Dim dbs As Database
    Const conErrPropNotFound = 3270

    On Error GoTo Err_MakeDbReplicable

    ' To create a replica, the database must be opened in exclusive mode.
    ' You open the database in exclusive mode by setting the options
    ' argument of the OpenDatabase method to True. If you try to open the
    ' current database exclusively in Microsoft Access, and you already have
    ' it open in shared mode, you'll get an error. Close the current database
    ' and reopen it in exclusive mode.
    ' To determine whether the database can be opened exclusively, use the
    ' CanOpenDbExclusively function defined in Chapter 2.
    If CanOpenDbExclusively(strDbPath) Then
        Set dbs = OpenDatabase(strDbPath, True)
    Else
        MsgBox "Database must be open in exclusive mode to make replicable."
        Exit Function
    End If
    ' Does the ReplicableBool property exist?
    dbs.Properties("ReplicableBool") = True
    MakeDbReplicable = True

Exit_MakeDbReplicable:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Function

Err_MakeDbReplicable:
    If Err = conErrPropNotFound Then
        ' Add the ReplicableBool property to the database's Properties
        ' collection and set its value to True. Use the SetCustomProperty
        ' function defined in Chapter 2.
        If SetCustomProperty(dbs, "ReplicableBool", dbBoolean, True) Then
            Resume Next
        End If
    End If
    MsgBox "Error: " & Err.Number & " - " & Err.Description
    MakeDbReplicable = False
    Resume Exit_MakeDbReplicable
End Function

To create a Design Master by using DAO code, you must set the options argument of the OpenDatabase method to True to open the database exclusively. You cannot make a database replicable if it is currently open. During the conversion process, Microsoft Jet maintains all the property settings of your original database.

Once you have created a Design Master, you can create additional replicas by using the MakeReplica method of the database object. For example, the following code fragment creates a database defined by the strPathAndName variable that is a replica of the Design Master represented by the dbs database variable.

strPathAndName = "C:\JetBook\Samples\NewReplica.mdb"
dbs.MakeReplica strPathAndName


Creating a Single-Master Replica Set

Programmers who are familiar with the use of a single-master database on a server might want to implement the same topology with Microsoft Jet replication. The single-master topology establishes the Design Master as the only member of the replica set that accepts changes to either the design of a database or the contents of the database. All other replicas in the set are read-only, even if you specify otherwise when creating replicas by using DAO code.

The only way to create a single-master replica set is to use Replication Manager; you cannot implement this topology through DAO programming.

Note that performance is always better if the replicas in a replica set are read-only, whether you create them through code or by creating a single-master replica set by using Replication Manager. When replicas are read-only, synchronization occurs in only one direction, from the Design Master to the replica, rather than in both directions.

Û To create a single-master replica set

  1. Start Replication Manager.

  2. On the Tools menu, click Convert Database To Design Master.

  3. Select (highlight) the database you want to convert and click Open.

  4. On the fifth screen of the Convert Database to Design Master Wizard, click I Want All Replicas To Be Read-Only and Data Changes To Be Made Only At The Design Master.

  5. Complete the remainder of the wizard screens.


Creating Partial Replicas

A partial replica is a database that contains a subset of the data contained in the Design Master. You create a partial replica by using either the Partial Replica Wizard or DAO code. It may be easier to create a partial replica by using the wizard and then make modifications to it as needed.

Note The Partial Replica Wizard is available at no cost from the Microsoft Access Developer Forum Web site at http://www.microsoft.com/accessdev/accwhite/prtlrepl.htm. This wizard is also included in the PrtlReplWiz folder on the companion CD-ROM.

The Partial Replica Wizard is a Microsoft Access add-in that displays several dialog boxes to prompt you through the steps required to create a partial replica. You run the add-in from within the Design Master or a full replica that contains the data you want to filter.

The data in a partial replica is defined by an SQL WHERE clause (without the WHERE keyword) on a single table in the Design Master. You can filter records from multiple tables only by using DAO code. When you define the criteria that will make up the WHERE clause, the following restrictions apply:

  • Enclose date values with the number sign (#). For example:
    [Order Date] > #12/31/95# AND [Order Date] < #1/1/97#
  • Surround the contents of Text or Memo fields with quotation marks. For example:
    FirstName = "Nancy" AND LastName = "Davolio"
  • Enter numeric values using the field name and the value. For example:
    CategoryID = 1
  • You cannot use user-defined or aggregate functions nor can you prompt the user at run-time to enter a parameter value.

Using DAO code to create a partial replica gives you much more flexibility and much more control over how you filter the data in the Design Master. Creating a partial replica by using DAO code involves four basic steps:

  1. Create an empty partial replica database. When this step is completed, you have a “template” database, but no data. You create an empty partial replica by using the MakeReplica method of the Database object that refers to the Design Master database, specifying dbRepMakePartial in the options argument. When you create a partial replica in this manner, the partial replica at first contains all the replicable tables in the Design Master but none of the data.

    In the following code, strDMPath is the path to the Design Master database, and strDbPartial is the path to the new partial replica:

    Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDMPath)
dbs.MakeReplica Name:=strDbPartial, Options:= dbRepMakePartial
  2. Define filters for the partial replica. There are two types of filters. One type is an SQL WHERE clause (without the WHERE keyword) that contains the selection criteria that defines the records that will appear in a specific table in the partial replica. Define the filter by using the ReplicaFilter property of a TableDef object in the new partial replica. For example, the following code fragment uses the ReplicaFilter property to define a filter based on the EmployeeID field in the Orders table:
    Set tdf = dbs.TableDefs("Orders")
tdf.ReplicaFilter = "EmployeeID = 3"

    You can use intersection expressions in a single ReplicaFilter property setting. For example, you can create a partial replica containing all orders that fall within a specified starting and ending date. The following code fragment defines a filter that returns all records that occurred between 1/1/95 and 1/31/95:

    tdf.ReplicaFilter = _
    "OrderDate >= #1/1/96# AND OrderDate <= #12/31/96#"

    You can create these types of filters on one or more tables in the database. Synchronizing partial replicas will be significantly faster when you create filters that reference indexed fields. For example, in most circumstances you should define the criteria for the ReplicaFilter property based on a table’s primary key. In the previous example, you’d want to make sure that the OrderDate field is indexed.

    The other type of filter is a Boolean. You set the ReplicaFilter property to True to get all the records and to False to get none.

  3. Decide which records you want from related tables. There are two scenarios that you should take into consideration: the relationships in which your filtered table is on the “many” side, and those in which your table is on the “one” side. Note that this applies only to enforced relationships between tables.

    If your filtered table is on the “many” side of a relationship, Microsoft Jet automatically includes records from tables when required by referential integrity. For example, suppose you set the ReplicaFilter property on the Orders table, as shown in the previous example. The Orders table participates in one-to-many relationships with the Employees, Customers, and Shippers tables; the Orders table is on the “many” side of each of these relationships. Because you cannot have a record in the Orders table that refers to an employee, a customer, or a shipper that doesn’t exist, Microsoft Jet automatically includes all the employees, customers, and shippers in their respective tables for the orders that your filter specifies.

    If your filtered table is on the “one” side of a relationship, referential integrity does not require that records be included from tables on the “many” side. In the case where you have set a filter on the Orders table, which is on the “one” side, Microsoft Jet does not require any of the records in the Order Details table, which is on the “many” side, to be present. However, when you’re creating a partial replica, it’s likely that you’ll want records from the Order Details table as well.

    To include records from the Order Details table, you can set the PartialReplica property on the relationship between the Orders and Order Details tables. The PartialReplica property alerts Microsoft Jet to include records from the “many” side of a one-to-many relationship, if there’s a filter on the “one” side. You must set the PartialReplica property for every enforced relationship for which you want to include records from the table on the “many” side.

    For example, the following code fragment sets the PartialReplica property on the relationship between the Orders table and the Order Details table. In this example, rel is a Relation object:

    If rel.Table = "Orders" And rel.ForeignTable = "Order Details" Then
    rel.PartialReplica = True
End If
  4. Fill the partial replica with data. You use the PopulatePartial method of the Database object that represents the partial replica database. If the database is not opened exclusively, an error occurs when you try to execute this method. Use this method to populate the new partial replica with data before synchronizing for the first time and after you change any partial replica filters. The following code fragment fills a partial replica with data from the full replica specified in the strDMPath argument, which contains the name of the Design Master. In this example, strDbPartial is the path to the partial replica:
    Set dbs = OpenDatabase(strDbPartial, True)
dbs.PopulatePartial strDMPath

    The PopulatePartial method works only with direct connections. It does not support indirect, or Internet/intranet synchronization. If you change the filter for a partial replica, you must use the PopulatePartial method to remove records from the partial replica that matched the old filter and add all records that comply with the new filter.

The following example shows one way to create a partial replica and fill it with data from a Design Master. When you run this function, you’ll see that all replicable tables in the Design Master exist in the partial replica, but that the Orders table contains only those orders entered by the employee whose employee ID is 3, and that this employee is the only employee in the Employees table. The tables whose data is required for referential integrity contain data. Those whose data isn’t required for referential integrity don’t contain data.

To call this function, pass in the name of the Design Master and the name for the new partial replica:

Function CreatePartialReplica(strDMPath As String, _
        strDbPartial As String) As Boolean
    Dim dbs As Database
    Dim tdf As TableDef
    Dim rel As Relation

    On Error GoTo Err_CreatePartialReplica

    ' Set a reference to the Design Master.
    Set dbs = OpenDatabase(strDMPath)
    ' Create a new partial replica and then close the Design Master.
    With dbs
        .MakeReplica strDbPartial, _
            "Partial replica of " & strDMPath, dbRepMakePartial
        .Close
    End With
    Set dbs = Nothing

    ' Create a reference to the new partial replica.
    Set dbs = OpenDatabase(strDbPartial, True)
    Set tdf = dbs.TableDefs("Orders")
    ' Set the filter for the new partial replica.
    tdf.ReplicaFilter = "EmployeeID = 3"

    ' Specify that Microsoft Jet include records from the "many" side
    ' of a one-to-many relationship.
    For Each rel In dbs.Relations
        If rel.Table = "Employees" And rel.ForeignTable = "Orders" Then
            rel.PartialReplica = True
            Exit For
        End If
    Next rel

    ' Fill the partial replica with data.
    dbs.PopulatePartial strDMPath
    CreatePartialReplica = True

Exit_CreatePartialReplica:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Function

Err_CreatePartialReplica:
    MsgBox "Error: " & Err.Number & " - " & Err.Description
    CreatePartialReplica = False
    Resume Exit_CreatePartialReplica
End Function


Changes Made to Your Database When You Use Replication

When you convert a nonreplicable database into a replicable database, Microsoft Jet:

  • Adds new fields to each existing table in your database.

  • Adds new tables to your database.

  • Adds new properties to your database.

  • Changes the behavior of AutoNumber fields.

  • Increases the physical size of your database file as a result of the additional fields and tables.


New Fields

The conversion process adds several new system fields to each existing table in a database. Among these fields are a unique identifier, a generation indicator, and a lineage indicator. All replication fields are read-only.

Unique Identifier (GUID)

During the conversion of a database, Microsoft Jet first looks at the existing fields in a table to determine if any field uses both the dbGUID data type (in Microsoft Access, the AutoNumber data type) and either the dbAutoIncrField or the dbSystemField attribute constant (in Microsoft Access, the Replication ID field size). If no field uses that data type and one of those attributes, Microsoft Jet adds the s_GUID field, which stores the AutoNumber that uniquely identifies each record. The AutoNumber for a specific record is identical across all replicas.

The AutoNumber is a 16-byte GUID (globally unique identifier, sometimes referred to as a UUID, or universally unique identifier) that appears in the following format:

{2FAC1234-31F8-11B4-A222-08002B34C003}

The hyphens and braces are used for display only and are not stored as part of the GUID.

Autogenerated GUID fields are identified by the dbGUID data type and either the dbAutoIncrField or the dbSystemField attribute constant. There can be more than one field in a table with the dbGUID data type, but there can be only one autogenerated field with the dbGUID data type in a table (regardless of whether it is replicable). If an autogenerated GUID field is added to a table, GUID values are generated for all records in the table. The value of an autogenerated GUID cannot be changed or deleted.

Generation Identifier

During the conversion process, Microsoft Jet also adds a field called s_Generation to each table in the database. The s_Generation field is used to determine which records are sent during synchronization. This allows the sending replica to avoid sending records that have not been updated since the last synchronization. Whenever a record is modified, its generation field is set to 0.

In general, during a synchronization all records with an s_Generation field set to 0 are sent, and then the generation field for the record is incremented to 1 more than the last generation, which now becomes the new highest generation. The sending replica knows the last generation sent to that specific receiving replica, and sends only records with generation 0 and generations higher than the previous generations.

The receiving replica does not apply generations received out of sequence. In some cases, Microsoft Jet might determine that there are too many records to be sent in a single message and split a synchronization into more than one message.

Generally, there is a single generation field per record. To optimize synchronizations for databases that contain Memo (Long Text) or OLE Object (Long Binary) fields (sometimes referred to as a BLOBs, or binary large objects), an extra generation field is associated with each BLOB. If the BLOB is modified, this generation value is set to 0 so that the BLOB is sent during the next synchronization. If a record modifies other fields, but not the BLOB, the BLOB’s generation field is not set to 0 and the BLOB is not sent.

This extra generation field is named Gen_xxxx, where xxxx is the BLOB field’s name (truncated, if necessary). If this name duplicates an existing field name, the prefix is changed, one character at a time, until a unique name is found. A ColGeneration property is set on the BLOB field to identify the added Gen_xxxx field. One of these fields is set for each BLOB field.

Lineage Identifier

During the conversion process, Microsoft Jet also adds a field named s_Lineage to each table in the database. The s_Lineage field is used to determine which replicas have already received an update and also determine the winner if a conflict occurs. It contains a list of nicknames for replicas that have updated the record and the last version created by each of those replicas. The version field is an integral field that is incremented when the record is changed. The first pair of values in the lineage reflects the current version and the name of the last member of the replica set to update the record. This is a binary field and is not usually readable by users.


Replication System Tables

Microsoft Jet adds several system tables to the database when the database is replicated. These tables are system tables, which are not normally visible to users and cannot be manipulated by programmers. The following table describes the system tables added to the database during replication.

Name | Description
MSysErrors | Identifies where and why errors occurred during data synchronization. This table appears in all members of the replica set and is empty when all errors have been resolved.
MSysExchangeLog | Stores information about synchronizations that have taken place between this member and other members of the replica set. This is a local table.
MSysGenHistory | Stores a history of generations of updates. It contains a record for each generation that a replica knows about. It is used to avoid sending common generations during synchronizations and to resynchronize replicas that are restored from backups. This table appears in all members of the replica set, but it is merged by a process slightly different from that used with normal replicated tables.
MSysOthersHistory | Stores a record of generations received from other replicas. It contains one generation from every message seen from other replicas.
MSysRepInfo | Stores information about the entire replica set, including the identity (GUID) of the Design Master. It contains a single record. This table appears in all members of the replica set.
MSysRepLock | Stores information about locking conflicts encountered during synchronization.
MSysReplicas | Stores details (such as the replica ID and path) of all replicas in the replica set. This table appears in all members of the replica set.
MSysSchChange | Stores design changes that have occurred in the Design Master so that they can be dispersed to any member of the replica set. The records in this table are deleted periodically to minimize the size of the table.
MSysSchedule | Stores information for scheduled synchronizations. The Synchronizer for a local replica set member uses this table to determine when the next synchronization with another Synchronizer should take place, and how to synchronize data and design changes with the other Synchronizer.
MSysSchemaProb | Identifies errors that occurred while synchronizing the design of the replica. This table exists only if a design conflict has occurred between the user’s replica and another member of the replica set. This is a local table.

Name | Description
MSysSidetables | Identifies the tables that experienced a conflict and the name of the conflict table that contains the conflicting records. This table exists only if a conflict occurs between the user’s replica and another member of the set. This is a local table.
MSysTableGuids | Stores the name of all replicated tables and their GUIDs. TableGUIDs are used in tables such as MSysTombstone as a reference to a table name stored in this table. This allows efficient renaming of tables. In addition, this table includes the level number used for ordering tables so that updates can be processed efficiently when referential integrity is enforced. This is a local table that is updated by the tracking layer in the Design Master and, as part of the processing of design changes, in all other members of the replica set.
MSysTombstone | Stores information about deleted records, and allows deletes to be dispersed to other replicas. This table appears in all members of the replica set.
MSysTranspAddress | Stores addressing information for the Synchronizer and defines the set of Synchronizers known to this replica set. This table appears in all members of the replica set.
MSysTranspCoords | Stores the display layout of the Synchronizers and replicas in the synchronization window of the Replication Manager.


Replication Properties

Microsoft Jet adds new properties to your database when it becomes replicable: ReplicableBool, Replicable, ReplicaID, and DesignMasterID. The ReplicableBool property replaces the Replicable property used in prior versions of Microsoft Jet. The Replicable property is still available but is used only to maintain backward compatibility and should not be used. The ReplicableBool property is set to True, indicating the database is now replicable. After the property is set to True, however, it cannot be changed. Setting the property to False returns an error. The ReplicaID property is a 16-byte value that uniquely identifies the Design Master or replica.

The DesignMasterID property can be used to make a replica the new Design Master for the replica set. This property is set for the Design Master when the database is initially made replicable. Under extreme circumstances — for example, the loss of the original Design Master — you can set the property for the current replica. However, setting this property for a replica, when there is already another Design Master, might partition your replica set into two irreconcilable sets and prevent any further synchronization of data. If you determine that it is necessary to set this property for a replica, first synchronize the replica with all other replicas in the set.

Caution Never create a second Design Master in a replica set. The existence of a second Design Master can result in the loss of data.

There are two new properties that you can append and set on the individual tables, queries, forms, reports, macros, and modules in your database: KeepLocal and ReplicableBool. To prevent an object from being copied to other replicas in a replica set, you can set the KeepLocal property of the object to “T” prior to converting the database into a replicable form. After the database is converted, you can set the ReplicableBool property to True to make a local object replicable. For more information about these two properties, see the “Replicating Your Database” section earlier in this chapter.

Microsoft Jet also adds the ColGeneration property to Memo (Long Text) and OLE Object (Long Binary) fields to identify the name of the field used to track generations for these fields.


Increases in the Physical Size of Your Database

The addition of new fields during the process of creating a Design Master database imposes two limitations on your tables:

  • Microsoft Jet allows a maximum of 2048 bytes (not counting Memo or OLE Object fields) in a record. Replication uses a minimum of 28 bytes to store unique identifiers and information about changes to the record. If the record contains either Memo or OLE Object fields, replication uses an additional 4 bytes for each of those fields. The total number of bytes available in a record in a replicated table can be calculated as follows:

    2048 bytes
    – 28 bytes for replication overhead
    – (4 bytes * the number of Memo fields)
    – (4 bytes * the number of OLE Object fields)
    = the maximum number of bytes available

  • Microsoft Jet allows a maximum of 255 fields in a table, of which at least three fields are used by replication. The total number of fields available in a replicated table can be calculated as follows:

    255 fields
    – 3 system fields
    – the number of Memo and OLE Object fields
    = the number of fields available

Few well-designed applications use all the available fields in a table or characters in a record. However, if you have a large number of Memo fields or OLE Object fields in your table, you should be aware of your remaining resources.

In addition to setting limits on the available number of characters and fields, Microsoft Jet replication also limits the number of nested transactions allowed. You can have a maximum of seven nested transactions in a nonreplicable database, but a replicated database can have a maximum of six nested transactions.

Just as the addition of three new fields to your tables adds to the size of each record, the addition of new system tables also adds to the size of your database. Many of these new tables contain only a few records, but some of the new tables can grow significantly, depending upon the frequency of synchronization between replicas.

The size of your database file is significant for two reasons:

  • Microsoft Jet supports files up to a maximum of 1 GB in size, but many users have limited space available on their hard disks and the creation of a replicable database that is larger than the original file could exceed available disk space.

  • If you choose to make a backup copy of the nonreplicable form of your database, you must have sufficient space on your hard disk for both the replicated database and the backup copy.


Changes to the Behavior of AutoNumber Fields

When you create a Design Master, any AutoNumber fields with a NewValues property set to Increment are changed to Random; however, the AutoNumber field’s FieldSize property isn’t changed. All existing AutoNumber fields in existing records retain their values, but new values for inserted records are random numbers with values between – 2,000,000,000 and +2,000,000,000. Random AutoNumber fields are not meaningful because they aren’t in any particular order and the highest value doesn’t belong to the record inserted last. When you open a table with a random AutoNumber key, the records appear in the order of ascending random numbers, not in chronological order. With random AutoNumber fields it is possible — although highly unlikely — for records inserted in different replicas to be assigned the same value. If this happens, a unique key error would occur that would require user intervention to resolve. If you experience such problems, consider setting the AutoNumber field’s FieldSize property value to Replication ID or consider using the s_GUID field as the primary key. Because all numbers in the s_GUID field are unique, each record has a different ID.

Before you convert your nonreplicable database into a replicated database, determine if any of your applications or users rely on the order and incremental nature of the AutoNumber field. If so, you can use an additional Date/Time field to provide sequential ordering information.

If your database contains tables that don’t need to be made replicable, set the KeepLocal property to True for those tables. This will prevent the tables from ever being made replicable, and the AutoNumber fields in those tables will not be affected.


Database Security

Replicable databases use the same security model as nonreplicable databases: Users’ permissions on the database are determined at the time they start Microsoft Access and log on. As a programmer, it’s up to you to make sure the same security information is available at each location where a replica is used. You can do this by making the identical workgroup information file (the file that stores security information) available to users at each location where a replica is used. The default security workgroup information file is called System.mdw. The System.mdw file cannot be replicated, but it can be manually copied to each location. Another way to make the same workgroup information file available to all your users is to re-create the entries for users and groups at each location in the local system database by entering the same user and group names with their associated PIDs at each location. Note that making modifications to permissions is a design change and can only be done in the Design Master.

Security permissions control certain aspects of database replication. For example, a user must have Administer permission on the database to:

  • Convert a nonreplicable database into a replicable database.

  • Make a local object replicable or make a replicable object local.

  • Designate a replica as the new Design Master. You can perform this action in any replica in the set; however, you should never have more than one Design Master in a set at one time.

See Also For more information about database security, see Chapter 10, “Managing Security.”


Making Additional Replicas

Although changes to the design of the database can be made only in the Design Master, additional replicas can be made from any member of the set. In fact, the only way for new copies of the database to be included in the replica set is for them to be created from an existing member. Once created, all new replicas become part of the replica set. All the members of a replica set have a unique identity and can communicate and synchronize with one another. Each replica set is independent from all other replica sets, and replicas in different sets cannot communicate or synchronize with each other.

Important Never try to make additional replicas from the original, nonreplicable database. The result would not be an additional replica, but rather a new replicable Design Master database and replica set.

When you convert your database by setting its ReplicableBool property to True, you have only one member (the Design Master) in the replica set. You can make your first replica, and subsequent replicas, by using the MakeReplica method. For example, to make a replica, you can use the following code:

Sub MakeAdditionalReplica(strReplicableDB As String, strNewReplica As String)
    Dim dbs As Database

    ' Open a replica in replica set.
    Set dbs = OpenDatabase(strReplicableDB)
    ' Make new read-only replica.
    dbs.MakeReplica strNewReplica, " Replica of " & _
        strReplicableDB, dbRepMakeReadOnly
    dbs.Close
End Sub

Note that the value passed for the description argument of the MakeReplica method shows up only in Microsoft Replication Manager. It is not available anywhere else.

If you include the dbRepMakeReadOnly constant, the replicable elements of the newly created replica cannot be modified. Otherwise, users will be able to make changes to the data in the new replica. As Microsoft Jet creates the new replica, all data definition language property settings of the source replica are included in the new replica. You can make subsequent replicas from either the Design Master or any full replica in the set. If you attempt to create a subsequent replica from a partial replica, an error occurs.

When you are using the MakeReplica method, make sure that the objects you are replicating are not locked, or the method will fail. Microsoft Jet locks objects while they are open in design mode or being updated during a synchronization. Programmers might easily overlook this requirement and attempt to make a replica from the database that has locked objects.

Note When you make a new replica, you copy all of the replicable objects and properties from the source replica to the new replica. Although you copy all linked tables, the path to a linked table might no longer be accurate because of the new replica’s location on the network. Be sure to test the new replica to determine if you need to establish a new path for any of the linked tables.


Using Replicas Instead of Backups

With database replication, it is no longer necessary to make a separate backup copy of your database. Each member of the replica set serves as a backup of the replicable portion of the database. If the Design Master is corrupted or unusable, do not copy or restore an older version of the Design Master; instead, make another member of the replica set the Design Master. To make a replica the Design Master, either use the DesignMasterID property as described in the “Moving the Design Master” section later in this chapter, or use the Microsoft Access menu command by pointing to Replication on the Tools menu and then clicking Recover Design Master.

Caution Do not to back up and restore members of the replica set as you would ordinary files. If you back up and restore the Design Master, you could lose critical information about changes to the design of the database as well as the ability of the Design Master to synchronize with the other replicas in the set.


Synchronizing Replicas

For database replication to be useful, the members of the replica set must communicate with one another in order to remain up to date. Synchronization is the process of making the design and data in all the members identical. Changes made to the existing records in one member are periodically communicated to each of the other members that have that same record. Similarly, records added to or deleted from any one member are communicated to all the other members of the replica set.

You can synchronize one member with another by using either Microsoft Access, Briefcase, Replication Manager, or DAO.

You can keep the members of the replica set synchronized by using the Synchronize method. By specifying the target database’s file name, you can synchronize one user’s replica with another member of the set. You can also perform one-way or two-way synchronizations. For example, you might use the following code to perform a two-way synchronization between members:

Sub SynchronizeDbs(strDbPath As String, strSyncTargetDb As String)
    Dim dbs As Database

    Set dbs = OpenDatabase(strDbPath)
    ' Synchronize replicas (bidirectional exchange).
    dbs.Synchronize strSyncTargetDb, dbRepImpExpChanges
    dbs.Close
End Sub

If you do not provide a constant value for the exchange argument, the synchronization is bidirectional (import and export).

When Microsoft Jet synchronizes two members of the replica set, it always synchronizes design changes before it synchronizes changes to data. The design of both members must be at the same design version level before data can be synchronized. For example, even if you use dbRepExportChanges to specify that data changes flow only from the current members to the designated target, design changes could be made to the current member if it has a lower design version number than the target member.

If you want to prevent users from making changes to the design of your database, do not put the Design Master on the network server. Instead, keep the Design Master at a network location that is accessible only to you. As you make changes to your application, you can synchronize with a replica on the server and rely on it to pass these changes on to other replicas in the replica set.


“Cleaning” Records Before Synchronizing

In some applications, a certain percentage of records are discarded because of bad or missing information. For example, in an application where information on hard copy forms is entered in a database, a certain percentage of these records must be discarded because they are incomplete. In other applications, some data might need “fixing” before it can be used. For example, a field in a table might be designed to hold a string of comma-separated numbers, but might instead contain non-comma separators, letters, or other mistakes.

When data is synchronized, all updated data is dispersed, regardless of whether it is useful to a particular application. Bad data in one database becomes bad data in multiple databases when synchronization occurs. Therefore, when building a replication system for your application, you might want to consider writing procedures that either purge or fix data before it is replicated to other systems.


Synchronizing the Star Topology

As described in the “Replica Set Topology” section earlier in this chapter, you can implement a variety of topologies for synchronizing members of the replica set. The following code example shows how to synchronize the star topology.

The following code provides a simple example of implementing synchronization by using a star topology. The procedure opens a replica and checks to see whether it is the hub. If the replica is the hub, it synchronizes with every other replica in the star. If it is not the hub, it synchronizes with the hub only.

The example assumes that a list of replica paths and file names is stored in a table named PathsStar in each replica. The PathsStar table contains the following key fields: PathID, which is the numeric identifier for each record and the primary key; Path, which is a text field containing the path and file name of a replica; and Hub, a Boolean field that specifies the replica serving as the hub for the star.

To try this code from the sample applications, run the StarSync function from the JetBook\Samples\JetSamples.mdb database, and pass in the name of one of the databases in the JetBook\Samples\Replicas subfolder — either SampleDesignMaster.mdb or SampleReplica.mdb. Or you can import the PathsStar table into your own replicable database, and run the code from there.

Function StarSync(strDbPath As String) As Boolean
    Dim dbs As Database, dbsHub As Database
    Dim rst As Recordset

    On Error GoTo Err_StarSync

    ' Open database.
    Set dbs = OpenDatabase(strDbPath)
    Set rst = dbs.OpenRecordset("PathsStar", dbOpenDynaset)

    With rst
        ' Check to see whether passed-in database is listed
        ' in list of replicas.
        .FindFirst "[Path] = """ & strDbPath & """"
        ' If it's not, then add it to list.
        If .NoMatch Then
            .AddNew
            !Path = strDbPath
            .Update
        End If

        ' Search table to find name of database marked as hub.
        .FindFirst "[Hub] = True"
        ' If match is found, check which database is marked as hub.
        If Not .NoMatch Then
            ' Open the original hub database.
            Set dbsHub = OpenDatabase(!Path)
        Else
            ' If there is no hub, display error and exit.
            MsgBox "Hub does not exist in replica set. " & _
                "Can't synchronize at this time."
        End If
    End With

    ' Check to see if passed-in database is the hub.
    If dbsHub.Name = dbs.Name Then
        ' Synchronize all members of the replica set with the hub.
        ' Since error trapping is turned off, the program continues to
        ' synchronize with other replicas if the Synchronize method fails
        ' for any replica.
         rst.MoveFirst
        Do Until rst.EOF
            ' Hub can't synchronize with itself.
            If rst!Path <> dbs.Name Then
                Debug.Print rst!Path
                dbs.Synchronize rst!Path
            End If
            rst.MoveNext
        Loop
    Else
        dbs.Synchronize dbsHub.Name
    End If

    StarSync = True

Exit_StarSync:
    On Error Resume Next
    rst.Close
    dbs.Close
    dbsHub.Close
    Exit Function

Err_StarSync:
    ' If an error occurred while synchronizing with the current
    ' replica, log an error that stores which replica failed to
    ' synchronize. The LogError function is a custom procedure
    ' for logging errors to a text file.
    If Err.Number <> 0 Then
        ' If recordset is not open, error is not a synchronization error.
        If rst Is Nothing Then
            MsgBox "Error: " & Err.Number & " - " & Err.Description
            StarSync = False
            Resume Exit_StarSync
        Else
            LogError Err, rst!Path
            MsgBox ("Synchronization failure at " & rst!Path)
            Err.Clear
            Resume Next
        End If
    End If
End Function

If an error occurs during synchronization, the following function is called to log the error:

Function LogError(errX As ErrObject, Optional strPath As String)
    Dim intX As Integer

    intX = FreeFile(0)
    Open "ErrLog.txt" For Append As #intX
    Write #intX, strPath
    Write #intX, errX.Number
    Write #intX, errX.Description
    Write #intX, Now
    Write #intX,
    Close #intX
End Function


Synchronizing the Ring Topology

The following code provides a simple example of implementing synchronization by using a ring topology. The procedure opens one of the replicas in a ring topology and attempts to synchronize this replica with the next replica in the ring. In this procedure, each replica has only one synchronization partner and synchronizes with only that partner. This code runs in each replica, so that each replica can synchronize with its partner independent of the other replicas in the set. The last replica in the list has the first replica in the list as its synchronization partner.

The example assumes that the list of replica paths and file names is stored in a table named PathsRing in each of the replicas in the ring. The PathsRing table contains the following key fields: PathID, which is the numeric identifier for each record and the primary key; and Path, which is a text field containing the path and file name of a replica.

To try this code from the sample applications, run the RingSync function from the JetBook\Samples\JetSamples.mdb database, and pass in the name of one of the databases in the JetBook\Samples\Replicas subfolder — either SampleDesignMaster.mdb or SampleReplica.mdb. Or you can import the PathsRing table into your own replicable database, and run the code from there.

Function RingSync(strDbPath As String) As Boolean
    Dim dbs As Database
    Dim rst As Recordset

    On Error GoTo Err_RingSync

    ' Open a replica in the ring.
    Set dbs = OpenDatabase(strDbPath)

    ' Open recordset to access path information for replicas.
    Set rst = dbs.OpenRecordset("PathsRing", dbOpenSnapshot)

    On Error Resume Next
    rst.FindFirst "[Path] = """ & dbs.Name & """"

    With rst
        ' If passed-in database is not in the list, add it.
        If .NoMatch Then
            .AddNew
            !Path = strDbPath
            .Update
            ' Move to newly created record.
            .Bookmark = .LastModified
        End If
        ' Move to next record to find synchronization partner.
        .MoveNext
        ' Check for end of recordset.
        If .EOF Then
            ' If at end of recordset, move to first record.
            .MoveFirst
        End If
        ' Synchronize passed-in database with its partner.
        dbs.Synchronize !Path
    End With

    RingSync = True

Exit_RingSync:
    On Error Resume Next
    rst.Close
    dbs.Close
    Exit Function

Err_RingSync:
    MsgBox "Error: " & Err.Number & " - " & Err.Description
    RingSync = False
    Resume Exit_RingSync
End Function

You could write this function so that if a replica’s synchronization partner cannot be found, the function opens one of the other replicas in the PathsRing table and synchronizes with it.


Synchronizing Data in Wide Area Networks

The way in which you manage synchronization within your application can depend on your network. Synchronization strategies for computers connected by a single local area network (LAN) are usually quite different from synchronization strategies for computers connected using wide area networks (WANs) and modems. For computers connected through a single LAN, synchronization can occur frequently because telecommunication overhead does not have to be considered.

With WANs, telecommunication overhead can be a major factor because of longer data transmission times and higher transmission rates. In this section, two different WAN topologies are discussed, along with possible ways to handle synchronization.

The following diagram shows a company that has an order-entry department in Denver and headquarters in Boston. Each day, customers call an 800 number to order products from the company’s catalog, and these orders are entered in a custom Microsoft Jet database application.

Figure 7.3 Ring topology synchronization over a WAN

Note Networks have topologies too, and these should not be confused with replication topologies. For example, a network topology can be a ring, a star, or a straight line. The ring (circle) shown in the diagram above refers to the replication topology. The underlying network topology is not shown and can be of almost any type.

Once a day, sales transactions entered in Denver must be transferred to headquarters, where managers can run queries against the data to track sales totals, geographic sales distribution, and other statistics. Conversely, updated product information such as new products and discounts on existing products must be sent from headquarters to the sales office as soon as it is available. Computers in the order-entry department make up a single LAN, and computers at headquarters make up another LAN. The two LANs are connected through a modem.

In this environment, how can replication be configured efficiently to meet the company’s requirements? Configuring replication within each LAN is a fairly straightforward task. For example, the databases in Denver could be configured to synchronize data every 30 minutes by using a ring topology. In a ring topology, computer A updates computer B, which updates computer C, and so on. This update chain occurs repeatedly at specified intervals and can be set by using Microsoft Replication Manager or by using DAO methods and the Timer event. The databases in Boston can be configured to regularly synchronize data in a similar manner.

The main concern is the bottleneck created by the modem link between Denver and Boston. For a large number of sales transactions, the telecommunications overhead of frequently synchronizing data between the two cities would be high. But because headquarters needs information only once a day, a good strategy would be to wait until night to synchronize one of the computers in Denver with one of the computers in Boston. The computer in Boston and the computer in Denver must be fully synchronized within their respective rings before they synchronize between the two rings. After the information is synchronized between the two computers, the periodic synchronization that occurs between computers in the same city disperses the information to all computers.

Because product information in Boston must be sent to Denver only when new information is available, replication can be configured to occur only when necessary. This might occur once or twice a week. To handle this, a user could click a button on a form to synchronize the databases in Denver and Boston whenever product information changes. A more sophisticated solution would be to set a value in a table each time information in one of the product tables changes. A procedure linked to a Timer event would periodically check this value and invoke the Synchronize method whenever the value is set.

Caution It is highly recommended that you take special precautions when performing exchanges over WANs. When “direct” exchanges are performed over dial-up lines, the potential for database corruption increases substantially. This is due to the fact that the local Synchronizer or reconciler can be asked to make many lengthy updates to the remote database over slower, and often unreliable, switched lines. If the line is dropped, or interrupted, while the local process is updating the remote replica, corruption can occur because exchanges are not transactionally secure. For this reason it is recommended that you make exchanges often to minimize the duration of the exchanges and do what you can to ensure the integrity of your dial-up connection. Alternatively, you can use Replication Manager to set up indirect synchronization. In this configuration, updates are first transferred to the remote site and then are applied locally on the same machine.


Synchronizing Partial Replicas

You can synchronize a partial replica in the same way that you synchronize a full replica; however, a partial replica can be synchronized only with a full replica.

Whenever you create or change the filter for a partial replica, you must use the PopulatePartial method before you attempt to synchronize the replica. If you attempt to synchronize a partial replica that has a new filter without first using the PopulatePartial method, a trappable run-time error occurs.

Note that when you use the PopulatePartial method, all existing data is removed from the partial replica and new data is written into the replica.


Synchronizing over the Internet or an Intranet

When a replica has been placed on an Internet or intranet server and the Synchronizer has been configured correctly, users can synchronize by using a standard HTTP connection. Note, however, that you cannot schedule synchronization over the Internet or an intranet with Replication Manager, because Replication Manager cannot control the communication link to your Internet service provider. As an alternative, you can write DAO code to link to your Internet service provider, execute a synchronization, and then drop the communications link.

You need three things to use synchronization over the Internet or an intranet: a client computer with a replica, an Internet or intranet server that is also configured as a Microsoft Jet replication Synchronizer, and a second replica on the same computer as the Internet server.

The client computer starts synchronization by doing the following:

  • Identifying the changes to be sent to the replica on the Internet or intranet server and bundling them in a message.

  • Connecting to the Internet or intranet server and getting the address of the server’s FTP folder.

  • Sending the bundled changes to the FTP address.

Once the changes are stored on the server, the server continues the synchronization by notifying the Synchronizer that there is data in the FTP folder. The Synchronizer then takes the following actions:

  • Picks up the data in the FTP folder and applies it to the replica on the Internet or intranet server.

  • Queries the replica on the Internet or intranet server to see if there is any data to send back to the replica on the client computer.

If there are no changes to send to the replica on the client computer, the synchronization is complete. If there are changes to send back to the replica on the client computer, the following process occurs:

  • The Synchronizer places the data in the FTP folder and notifies the Internet or intranet server that there is data for the client computer.

  • The server sends a message to the client computer that there is data to collect.

  • The client computer collects the data and applies it to the client replica.

Internet or intranet synchronization works only between replicas on different machines. If you attempt to synchronize two replicas — one managed with an Internet Synchronizer and one unmanaged — on the same machine, you will get a message saying that the exchange was unsuccessful because the Internet is slow or because there is a problem with the Internet or intranet server. However, if you have the same two replicas on two different computers, the Internet or intranet exchange succeeds.


Preparing for Internet or Intranet Synchronization

Before you can synchronize over the Internet or an intranet, you must properly configure your Internet or intranet server and Replication Manager. The following steps discuss how to prepare for Internet or intranet synchronization:

  1. Set up your Internet or intranet server with operating system and server software. For example, you can use Windows NT Server 4.0 with Microsoft Internet Information Server (IIS).

  2. Install Microsoft Access and Replication Manager on the Internet or intranet server. Replication Manager runs on Intel® server platforms only and supports the Microsoft Internet Information Server and Netscape server platforms. Internet proxy servers are not supported.

  3. Create a replica on the Internet or intranet server and manage it by using Replication Manager. This will “stamp” the replica with information about the Internet or intranet server’s HTTP address and other internal system information.

  4. Create a copy of the replica (you can use the Windows Explorer to do this) and distribute this copy to your users. When they open the replica in Microsoft Access and synchronize, they will be able to synchronize back to the replica on the Internet or intranet server by using the automatically configured HTTP address.

Note Some Internet providers may require you to stop and restart Replication Manager and/or the communications connection if synchronization is canceled before it is complete.

The procedure above describes the easiest way to confirm that you have set up your Internet or intranet server and your Microsoft Access application for replication. Once you have the system working, you may decide you do not need to install the full version of Microsoft Access on the Internet or intranet server, but only the run-time version of Microsoft Access that is included with Microsoft Office 97, Developer Edition (ODE). If you decide to use only the run-time version, you can install it on the Internet or intranet server by using the ODE Setup Wizard to create a custom Setup program for the Microsoft Access application that you want to replicate. When you’re creating the Setup program, make sure you select the Microsoft Replication Manager and Microsoft Access Run-Time Version redistributable components. When you run the custom Setup program on the Internet or intranet server, it will extract only the files you need and create the correct registry settings.

Note The ODE Setup Wizard is the recommended tool for installing your application on an Internet or intranet server because it automatically updates the system for you. For example, the Setup program created by the Setup Wizard updates registry settings for you. Updating these entries manually is complex and prone to error, particularly with regard to Internet settings and versions of system DLLs that interact with other applications. If you manually copy your application and associated files to the server, you must also update system settings such as registry entries. For this reason, we strongly recommend you use the ODE Setup Wizard to create a Setup program for the application.

See Also For more information about preparing for Internet or intranet synchronization, see “Setting Up an Internet or Intranet Server for Replication” in the Microsoft Replication Manager Help file.


Synchronizing over the Internet or an Intranet by Using DAO

The DAO Synchronize method includes a new constant, dbRepSyncInternet, that supports Internet synchronization. You cannot use the dbRepSyncInternet constant independently; you use it in combination with another constant that specifies which type of synchronization you want to use. Here are three statements that illustrate how you can use this constant:

dbs.Synchronize "domainname/replicaname", dbRepImpChanges Or dbRepSyncInternet
dbs.Synchronize "domainname/replicaname", dbRepExpChanges Or dbRepSyncInternet
dbs.Synchronize "domainname/replicaname", dbRepImpExpChanges Or dbRepSyncInternet

Note DAO provides dbRepImpExpChanges as a default constant only if the user does not specify any arguments at all.

In the examples above, the domainname/replicaname argument consists of the Internet server’s domain name together with the name of the replica without a path. To synchronize over an intranet, use servername/replicaname, where servername is the name of the intranet server and replicaname is the name of the replica, without a path.


Synchronization Conflicts and Errors

When using database replication, you may occasionally encounter synchronization conflicts or synchronization errors. Synchronization conflicts occur when the same record is updated in two members of the replica set, and Microsoft Jet attempts to synchronize the two versions. The synchronization succeeds, but the changes from only one of the members are applied to the other. Synchronization errors occur when a data change in one replica set member cannot be applied to another member because it would violate a constraint such as a referential integrity rule or uniqueness assertion. The synchronization succeeds, but the content of the database in different members starts to diverge.

Synchronization errors and design errors are much more significant problems than synchronization conflicts because the replica set members no longer share a common design or identical data. This section describes the factors contributing to conflicts and errors, and suggests ways to prevent or resolve them.


Synchronization Conflicts

Whenever you synchronize two members of a replica set, there is always the possibility of conflict between versions because the same record might have been updated at both locations. During synchronization, if Microsoft Jet detects that the same record was changed in both members, Microsoft Jet treats it as a synchronization conflict even if the changes made in one replica set member were in different fields than the changes made in the other member.

When a synchronization conflict occurs, Microsoft Jet does not attempt to resolve the conflict based on the content of the records or the changes made to the data. Instead, it uses a simple algorithm to select one of the record versions to be the official change and writes the data from the other record version into a conflict table. It is then the responsibility of the programmer, database administrator, user, or a custom resolution program to review each conflict to determine whether the correct information was applied to the database.

The algorithm Microsoft Jet uses to select one record version over the other is based on the record’s version number, which is stored in the table’s s_Lineage field. Each time a change is made to the data in a record, the version number increases by 1. For example, a record with no changes has a version number of 0. A change to data updates the version number to 1. A second change to the same data, or a change to different data in the record, changes the version number to 2. If the record in one replica set member has been changed once and the same record in the second member has been changed three times, the record in the second member has a higher version number than the record in the first member. When Microsoft Jet compares the version numbers for the same record, it assumes that the version that has changed the most is the more correct of the two versions. If both records have the same version number, Microsoft Jet examines the value of the ReplicaID property and selects the replica set member with the lowest ReplicaID value.

While this algorithm and its underlying assumption might not be the most appropriate in all applications, it allows Microsoft Jet to quickly and consistently complete the synchronization process and preserves all the information from both versions for later review. The data from the record version that Microsoft Jet did not select is stored in a conflict table in the member where the change was made. Conflict tables derive both their names and fields from the underlying tables. Conflict table names are in the form table_Conflict, where table is the original table name. For example, if the original table’s name is Customers, the conflict table’s name is Customers_Conflict. Because conflicts are reported only to the replica set member that originated the losing update, conflict tables are not replicated.


Resolving Conflicts by Using DAO

After synchronizing two replicas, you should review each conflict to determine whether the correct information was applied to the database. You can determine if a conflict has occurred for a specific table by using the ConflictTable property. This property returns the name of the conflict table that contains the database records that conflicted during synchronization. For example, to find the name of the conflict table and examine and resolve each record conflict, you can use the following code. This is a generic function into which you can plug your own custom conflict-resolution code:

Sub ResolveConflicts(strDbPath As String)
    Dim dbs As Database
    Dim tdf As TableDef, rst As Recordset

    Set dbs = OpenDatabase(strDbPath)
    For Each tdf In dbs.TableDefs
        If (Len(tdf.ConflictTable) > 0) Then
            Set rst = dbs.OpenRecordset(tdf.ConflictTable)
            ' Process each record.
            With rst
                .MoveFirst
                Do Until .EOF
                    ' Perform conflict resolution.
                    ' Remove conflicting record when finished.
                    .Delete
                    .MoveNext
                Loop
                .Close
            End With
        End If
    Next tdf
End Sub

If there is no conflict table, or if the database is nonreplicable, the ConflictTable property returns a zero-length string.

As you review each conflict in the conflict table, you should take the appropriate action. If the record version selected by Microsoft Jet was the correct version and no further action is necessary, you can delete the record from the conflict table. If the record version selected by Microsoft Jet was not the correct version, you might want to:

  • Manually enter the data from the conflict table in the database.

  • Develop a custom conflict-resolution routine that always assigns a higher priority to changes in one specific replica over another replica.

  • Consider whether procedural changes regarding how data is entered or changed in records are necessary.

The following code is an example of a custom conflict-resolution procedure. This function compares the conflicting records and determines which one was entered most recently. The most recently entered record wins the conflict. For this procedure to work, each table must have a MostRecentUpdate field that is updated to the current time each time the record is updated.

You would run this procedure after a replica has been synchronized with another replica in the replica set. At this point Microsoft Jet will have performed its conflict resolution, so that the record that was changed the most times or that has the lowest ReplicaID property value “wins” the conflict and is written into the source table in both replicas. The losing record is written into the conflict table in the replica in which the record was originally entered.

This function iterates through each TableDef object in the database, looking to see if it has a conflict table by checking the TableDef object’s ConflictTable property. When a conflict table is found, it opens up recordsets on both the source table and the conflict table. The procedure then moves through each record in the conflict table, and uses the Seek method on the s_GUID field, which exists as a unique index in both tables, to compare the source table record and the conflicting record. It checks the MostRecentUpdate field in both tables, and if the source table (which contains the winning record) was not updated last, this record is replaced by the (former) losing record in the conflict table. Regardless of whether it wins or loses, the conflict record is then deleted.

Note that in order to use this function to resolve conflicts on a table in your replicable database, you must add a Date/Time field named MostRecentUpdate to the table. You must also ensure that every time a user updates a record, the time at which the update occurred is stored in the MostRecentUpdate field.

Function ConflictResolver()
    Dim dbs As Database
    Dim tdf As TableDef
    Dim fld As Field
    Dim rstConflict As Recordset
    Dim rstSource As Recordset

    Set dbs = DBEngine(0)(0)

    For Each tdf In dbs.TableDefs
        ' Check to see if table has a conflict table.
        If (tdf.ConflictTable <> "") Then
            Set rstConflict = dbs.OpenRecordset(tdf.ConflictTable, dbOpenTable)
            Set rstSource = dbs.OpenRecordset(tdf.Name, dbOpenTable)
            rstSource.Index = "s_GUID"
            rstConflict.MoveFirst
            ' Scroll through conflict table's records.
            Do While Not rstConflict.EOF
                rstSource.Seek "=", rstConflict![s_GUID]
                ' Make sure match was retrieved in source.
                If Not rstSource.NoMatch Then
                    ' Check to see if loser was more recent.
                    If rstSource!MostRecentUpdate < rstConflict!MostRecentUpdate Then
                        On Error Resume Next
                        rstSource.Edit
                        ' Scroll through fields to update.
                        For Each fld In rstSource.Fields
                            rstSource(fld.Name) = rstConflict(fld.Name)
                        Next fld
                        rstSource.Update
                        On Error GoTo 0
                    End If
                End If
                ' Delete conflict record.
                rstConflict.Delete
                rstConflict.MoveNext
            Loop
            rstConflict.Close
            rstSource.Close
        End If
    Next tdf
End Function

When this function copies the conflict table data over the source table data, it disables error handling and uses a For Each...Next loop to iterate through all the fields. Because all the replication system fields are read-only, the function can’t update them and would return an error if error handling were enabled.

There are two problems with resolving conflicts so that the most recently updated record is the winner. The first problem is that computer clocks are not synchronized on a network, so you can’t perform a completely accurate comparison to determine which record was updated last. The second problem occurs when your users have replicas in different time zones. If this scheme doesn’t work for your application, there are a variety of different conflict-resolution schemes that you can implement. You can use the function above as a starting point, and develop your own conflict-resolution paradigms, depending on your needs.


Resolving Conflicts by Using Microsoft Access

If you handle replication through the Microsoft Access user interface, Microsoft Access automatically notifies you of a synchronization conflict each time you open a database that contains conflicts. Microsoft Access ships with a Conflict Resolver to assist you in displaying a list of all tables that have synchronization conflicts and choosing whether to apply the data from the conflict table. You run the Conflict Resolver by clicking Yes when asked if you want assistance with resolving the conflicts. You can also launch the Conflict Resolver at any time by pointing to Replication on the Microsoft Access Tools menu, and then clicking Resolve Conflicts.

You can replace the Conflict Resolver with a custom function for reviewing and processing the records in a conflict table. If you want to use a custom function instead of the Conflict Resolver, create the ReplicationConflictFunction property and set its value to the name of the custom function. The custom function must be stored in a module in the Design Master (not as a separate dynamic-link library), and the module must be replicable.

Û To replace the Microsoft Access Conflict Resolver

  1. On the Microsoft Access File menu, click Open Database.

  2. Select (highlight) the Design Master that you want to have use the custom conflict-resolution function and click Open.

  3. On the File menu, click Database Properties.

  4. Click the Custom tab.

  5. In the Name box, type ReplicationConflictFunction (do not put spaces between the words).

  6. In the Value box, enter the name of the custom function followed by ( ).

  7. Click OK to close the dialog box.

If you want to return to using the default Conflict Resolver after using a custom function, set the ReplicationConflictFunction property’s value to " " (an empty string). If you want to disable the default Conflict Resolver, but you don’t want to implement a custom one, you can set the ReplicationConflictFunction property to call a function that doesn’t do anything.


Synchronization Errors

You need be aware of at least four sources of potential synchronization errors when building your application:

  • Table-level validation (TLV) rules

  • Duplicate keys

  • Referential integrity constraints

  • Record locks


Table-Level Validation Rules

Microsoft Jet allows you to establish table-level validation (TLV) rules to restrict the value or type of data entered in a table. However, if you implement TLV rules in the Design Master without determining that any existing data does not violate the validation rule, you might encounter a synchronization error in the future. For example, assume that you apply a TLV rule to a field that already contains data and you then test to make sure that the existing values meet the rule. There might be new or updated records in other replica set members that do not satisfy the new rule. If you do not test the existing values in the other members and change the values that fail the rule, the next synchronization will fail to correctly update the records. When Microsoft Jet applies the new or updated records from the other member to the Design Master, Microsoft Jet tests each insertion or update against the rule. If the value fails, the update fails and an error is written to the MSysErrors table in the receiving member. To correct the error, you must correct the invalid values in the sending replica set member. You can avoid the error by synchronizing all members of the replica set with the Design Master before applying a TLV rule.


Duplicate Keys

Duplicate keys can occur when users of different replicas simultaneously insert a new record and use the same primary key for their respective records. When the replicas are synchronized, the synchronization succeeds, but Microsoft Jet writes a duplicate key error to the MSysErrors table for each of the records that could not be inserted or updated. To correct a duplicate key error, change the value of one of the keys or delete the duplicate record.


Referential Integrity Constraints

Referential integrity preserves the relationship between tables when you are adding or deleting records. Enforced referential integrity prevents you from adding a record to or deleting a record from a related table if there is no corresponding record in the primary table. In some situations, enforced referential integrity might result in synchronization errors. For example, if a user deletes a record in the Design Master that has a primary key and another user inserts a record in a replica that references that key, the next synchronization of the two members generates two errors. During synchronization, Microsoft Jet attempts to delete the primary key record in the replica but cannot. The reference to a record or records in a related table in the replica prevents the deletion from succeeding. Microsoft Jet also attempts to add the referencing record in the Design Master but cannot because the new record references a primary key that no longer exists in the Design Master. To correct the two errors, you must delete from the replica the record that references the deleted primary key.

The outcome of the preceding scenario changes if you enforce cascading deletes in your application. With cascading deletes in effect, Microsoft Jet deletes both the primary key record and all referencing records in the Design Master. When the Design Master is synchronized with the replica, the referencing record fails to synchronize because there is no longer a valid primary key; Microsoft Jet records the error in the MSysErrors table. In this situation, however, the error will correct itself during the next synchronization when the replica is notified that the primary key has been deleted from the record.


Record Locks

If a record is locked when Microsoft Jet attempts to update it during a synchronization, Microsoft Jet retries the update several times. If the record remains locked after repeated attempts, the synchronization fails and Microsoft Jet records an error in the MSysErrors system table. Although this type of error is exceedingly rare, it might occur in certain multiuser applications. Errors caused by locked records can be ignored because Microsoft Jet will again try to update the record during the next synchronization. Because it is unlikely that the same record will be locked during the next synchronization, the record is updated and the error is removed from the MSysErrors table.


Correcting Errors

Synchronization errors are recorded in the MSysErrors table and replicated to all members of the replica set. This table includes information about the:

  • Table involved.

  • Record that encountered the errors.

  • Replica or replicas where the error was detected.

  • Replica that last changed the record.

  • Type of operation that failed.

  • Reason it failed.

Errors must be corrected as soon as possible, because they indicate that the data in different replicas might be diverging. You should be especially careful to correct synchronization errors before moving or renaming your database, because the error is recorded against the ReplicaID property value at the time the error occurred. If the ReplicaID property value is changed it will not be possible for Microsoft Jet to automatically remove the error records during a subsequent synchronization. If the error record is not removed, you will continue to receive notification of the error each time you open the database in Microsoft Access even if you have corrected the problem.

In many circumstances, errors are self-correcting during the next synchronization. For example, during an attempted synchronization, an update will be rejected if another user has the record locked. Microsoft Jet records an error and attempts to reapply the update at a later time. If the subsequent update succeeds, the error record is removed. As a general rule, always synchronize all members of the replica set before manually correcting synchronization errors. Due to the nature of bidirectional synchronizations, it might take more than one synchronization to clear the error record from MSysErrors after the error is corrected. However, all corrected errors should be cleared from the MSysErrors table after two bidirectional synchronizations.


Correcting Errors by Using Microsoft Access

Just as Microsoft Access automatically notifies you of a synchronization conflict each time you open a database that contains conflicts, it also notifies you of any synchronization errors. The default Conflict Resolver that ships with Microsoft Access first displays a list of all the tables that contain synchronization errors. After you select a table, the Conflict Resolver displays the following information for each error: the replica set member, the path, the table name, the record ID, the attempted operation, and the reason the operation failed. You can review each error individually and take the appropriate action to correct the error.


Advanced Design Considerations

Although Microsoft Jet makes database replication easy to implement through DAO programming or through Microsoft Access in combination with Microsoft Replication Manager and Briefcase, replication can be complicated by a number of special considerations. These considerations include:

  • Moving the Design Master.

  • Compacting your database before synchronizing.

  • Synchronizing all members of the replica set before making design changes.

  • Closing tables before synchronizing.

  • Using list boxes tied to replicated tables.

  • Distributing Microsoft Replication Manager with your applications.

  • Making a database nonreplicable.

This section presents these additional factors that must be included in your planning, design, and implementation of database replication.

Moving the Design Master

The Design Master is the most important member of a replica set because it is the only member in which you can make changes to the structure of the database. Under certain circumstances, you might have to transfer the Design Master status to a replica. For example, you might have the Design Master on your computer while another member of your development team has a replica on his or her computer. While you are on vacation, you want the other programmer to be able to make changes to the database. You can use the DesignMasterID property to accomplish the transfer and synchronize the old and new Design Masters, as shown in the following code:

Sub SetNewDesignMaster(strOldDM As String, strNewDM As String)
    Dim dbsOldDM As Database, dbsNewDM As Database

    ' Open current Design Master in exclusive mode.
    Set dbsOldDM = OpenDatabase(strOldDM, True)

    ' Open database that will become the new Design Master.
    Set dbsNewDM = OpenDatabase(strNewDM, True)

    dbsOldDM.DesignMasterID = dbsNewDM.ReplicaID
    dbsOldDM.Synchronize strNewDM, dbRepImpExpChanges
    dbsOldDM.Close
    dbsNewDM.Close
End Sub

You must have the Design Master open in order to make another replica the new Design Master. The Design Master is, by definition, read-write. If you make a replica that is designated read-only into the Design Master, the target replica is made read-write; the old Design Master also remains read-write.

If the Design Master is erased or corrupted, you can designate a replica to become the new Design Master. However, remember that you can have only one Design Master at a time.

If you decide to make your own replica the new Design Master for the set, synchronize it with all the replicas in the replica set before setting the DesignMasterID property in your replica. You must have your replica open in exclusive mode to make it the Design Master.

Caution Avoid using Windows or MS-DOS to copy or move replicas because using them will modify the internal information of the replicas. Moving a Design Master with Windows or MS-DOS will cause the Design Master to become a simple replica and may result in the loss of the record of conflicts associated with the replica, if any. If you are using Microsoft Replication Manager, you can use the Move Replica command on the File menu to move a replica or Design Master to another location.

Compacting Your Database Before Synchronizing

Each time you change the design of your replicable database, Microsoft Jet stores information about the change in the MSysSchemaChg system table. If you change the same object more than once before synchronizing, Microsoft Jet continues to store the newest design in preparation for the synchronization. Frequent compacting of the database after you make design changes helps to reduce the overhead required for storing changes and reduces the information sent to the replicas. You might find it most efficient to compact your database daily and again just before synchronization.

Important Before compacting a replicated database, see “Important Considerations When Compacting Replicated Databases” in Chapter 13, “Optimizing Performance.”

Synchronizing Before Making Design Changes

When two members of a replica set are synchronized, Microsoft Jet applies the design changes to each member before applying the data changes. You can reduce the amount of processing during synchronization and reduce the potential for conflicts if you synchronize all the replicas in the set before making design changes to the Design Master.

Closing Tables Before Synchronizing

Microsoft Jet must open tables exclusively before design changes are applied to them. If you are using a table in a database at the time you want to synchronize, Microsoft Jet suggests that you first close the table. If you use a “Synchronize Now” button on a form so users can easily click the button whenever they need to synchronize, be sure the button does not have an associated table open — either directly or through a query — in the database that is being synchronized.

Using List Boxes with Replicated Tables

If you tie list boxes to replicable tables, under certain circumstances the list box might display system fields in addition to your regular data fields. If the option to display system objects is turned on, the system fields in the replication system tables are also displayed. After they’re visible, they might appear in the related list box.

To avoid displaying system tables, either clear the System Objects check box on the View tab of the Microsoft Access Options dialog box (Tools menu), or base the list box on a query that selects only the fields you want displayed.

Distributing Microsoft Replication Manager with Your Application

You can distribute your Microsoft Access database application to others by including Replication Manager. Setting up replicated databases with Replication Manager is a cooperative effort involving the programmer and the site administrator. To prepare your database for replication and distribution, you need information from the site administrator, and you need to assist the administrator — preferably in person at the customer’s site.

The recommended topology for using Replication Manager with your application is a star topology, shown in Figure 7.4. You should place the Design Master on a satellite computer along with Replication Manager, but you should not have it on a synchronization schedule. (You, or the administrator at the site, can synchronize it with the hub replica when a structural change to the database is needed.) The replica on the hub becomes the synchronization partner for each of the other replicas in the organization. In addition to the Design Master and the hub replica, a base replica is used with the Setup program to copy subsequent replicas to users’ machines. After a copy is made on a user’s computer and it is synchronized with the hub, the copy becomes a true replica with its own unique identification.

Figure 7.4 Star topology for distributing your application

See Also For more information about distributing your applications with Replication Manager, see “Distributing Replicated Databases” in the Microsoft Replication Manager Help file.

Making a Database Nonreplicable

After you’ve converted a database into a replicable database, you can’t convert it back to its former status as a nonreplicable database. However, if you no longer want to use replication and want to decrease the size of a replicated database, you can create a new, nonreplicable database that contains all of the objects and data in your replicated database without the additional system fields, tables, and properties associated with replication.

Note The Replica to Regular Database Wizard, which is located in the ReplWizard folder on the companion CD-ROM, enables you to convert a Design Master to its original, nonreplicated state.

Û To make a nonreplicable database from a replicable one by using Microsoft Access

  1. Create and open a new database.

  2. On the File menu in the new database, point to Get External Data, and then click Import.

  3. Import the objects (except tables) that you want in the new database.

  4. Close the new database and open the replicated database.

  5. Create a query for each table in the replica that takes all the data in the old table and puts it in a new table.

    Important If the s_GUID field is a primary key (and foreign keys in other tables refer to it), you’ll want the s_GUID field in the new table. If the s_GUID field is not used as a primary key, there is no reason to preserve it in the new table. The s_Lineage and s_Generation fields should not be copied over to the new table.

    To create the queries, open query Design view, and click Make-Table Query on the Query menu. In the Table Name box, select the current table. Click Another Database, and add the name of the database that you created in step 1. Run the query.

  6. For each table in the new database, create the same index used in the replica table and the relationships that existed for the replica table.

  7. Compact, repair, and save the new database.


Chapter 8 Accessing External Data

You can use the Microsoft Jet database engine to access other popular desktop database files, spreadsheets, Microsoft Exchange and Microsoft Outlook folders and address books, tables in HTML files, and textual data stored in tabular format. One way to access external data is through installable indexed-sequential access method (IISAM) drivers. These are called ISAM databases because Microsoft Jet accesses them using IISAM drivers. The IISAM drivers expose the desktop database to Data Access Objects (DAO) as a Microsoft Jet database, supporting most of the querying, searching, and data-definition functionality of a native Microsoft Jet database file.

All external data sources are considered either Open Database Connectivity (ODBC) databases or desktop databases. With DAO 3.5, you can connect to ODBC databases either by using the Microsoft Jet database engine or through an ODBCDirect workspace.

See Also For information about how to access external data through ODBC and how to use ODBCDirect, see Chapter 9, “Developing Client/Server Applications.”

Supported desktop database IISAM formats include older versions of Microsoft Jet, Microsoft FoxPro, dBASE, and Paradox. Supported spreadsheets include Microsoft Excel and Lotus 1-2-3. Additional support is provided for Microsoft Exchange and Outlook folders and address books, HTML tables and lists, and text files in character-delimited and fixed-length formats.

This chapter discusses general strategies and techniques for accessing external data through IISAM drivers and linked ODBC tables and includes specific examples. In addition, it provides detailed information on managing access to text data.

Understanding Data Sources and Access Methods

Establishing Access to External Data

Maintaining Links

Creating an External Table

Performance Optimization and Troubleshooting Tips

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

Examples in this chapter also use files in external data formats. The dBASE sample files are located in the JetBook\Samples\dBASE folder. The Microsoft Excel sample files are located in the JetBook\Samples\Excel folder. The Microsoft FoxPro sample files are located in the JetBook\Samples\FoxTables folder. The HTML sample files are located in the JetBook\Samples\HTML folder. The Paradox sample files are located in the JetBook\Samples\Pdx folder. The text sample files are located in the JetBook\Samples\Text folder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


Understanding Data Sources and Access Methods

This section discusses the database formats you can access through the Microsoft Jet database engine, as well as the differences between the data access techniques that you can use. It also discusses the resources you need to connect to external data sources, the objects and methods that you specifically cannot use when you are working with external data, and the transaction methods available on supported databases.


Supported External Data Sources and Versions

Microsoft Jet can work with data from any of the following external data sources:

  • Microsoft Jet databases such as those created by Microsoft Access, Microsoft Visual Basic, Microsoft Visual C++, or Microsoft Excel. (This includes databases created with previous versions of Microsoft Jet.)

  • Microsoft FoxPro, versions 2.0, 2.5, 2.6, 3.0, and Database Container (DBC).

  • Microsoft SQL Server and other ODBC databases.

  • dBASE III, dBASE IV, and dBASE 5.0.

  • Paradox, versions 3.x, 4.x, and 5.x.

  • Microsoft Excel 3.0, 4.0, 5.0, 7.0, and 8.0 worksheets.

  • Lotus 1-2-3 WKS, WK1, WK3, and WK4 spreadsheets.

  • Microsoft Exchange and Outlook folders and address books.

  • Tables and lists contained in HTML files.

  • ASCII text files in tabular format.

Note Before you can access any external data sources, you need to make sure that you have installed the appropriate drivers. Microsoft Access users can click the Custom/Complete button in Microsoft Office Setup or Microsoft Access Setup and select the Data Access check box in the Options list box and then select the Database Drivers selection and click the Change Option button. Microsoft Visual Basic 5.0 users can click the Custom button in Microsoft Visual Basic 5.0 Setup and select the Data Access check box and then click the Change Option button. Then click ISAM Drivers in the Options list box.

Your Microsoft Jet application can read from and write to external data sources (the FoxPro 3.0, DBC, Microsoft Exchange, Outlook, HTML, and Lotus WK4 formats are read-only). You can view and edit external tables with your application without affecting their accessibility from their native applications. If you choose to link an external data source to a Microsoft Jet database, you can use it just as you would any other table in the database. You can create queries on the data and create forms and reports that use the data. If the external data source permits multiuser read-write access, you can even view and update the data while other users access the external data source through its native application.


Linking, Opening, and Importing External Tables

You can access external data tables in three ways: by linking the table to a Microsoft Jet database; by opening the table directly; or by importing the data in the table into a Microsoft Jet database table. The answers to the following questions will determine the data access method you choose:

  • Do you need to maintain access to the data from another application?

    If you need to maintain access to the data from another application, such as the native application used to create the database, you should link or open the external table rather than import the data. All of the IISAM drivers provide import or export capabilities.

  • Does the data reside on an ODBC data source?

    If the table you want to access resides on an ODBC data source and you are not creating a client application where queries will be executed on the server, then the best performance will occur when you link the table rather than attempt to open it directly.

  • Will you need to establish the connection to the data frequently or just once?

    If you need frequent access to the data source, use a link, because linked tables maintain all connection information between sessions. If you only need occasional access to the data source, you can reduce the size of your Microsoft Jet database by opening the external table directly in code rather than storing the connection information in a link. If you need to access the data source only one time to migrate the data to Microsoft Jet format, use the import capabilities of the IISAM driver (if available).

See Also For information about how to access external data through ODBC and how to use ODBCDirect, see Chapter 9, “Developing Client/Server Applications.”


Differences Between Linked and Opened Tables

If you need to maintain data in its original format (rather than importing it into a Microsoft Jet database), you must choose whether to link the table or open it directly. In most cases, you’ll find that linking makes sense with ODBC tables, as it is usually faster; directly opening the table is usually faster for all data sources accessed through IISAM drivers. There are some fundamental differences in the behavior of both access methods. This section describes these differences.

Important You cannot use the Seek method with linked tables.

See Also For information about how to access external data through ODBC and how to use ODBCDirect, see Chapter 9, “Developing Client/Server Applications.”


Linking a Table

A linked table appears and behaves just like any other table in your Microsoft Jet database (although there are slight performance differences associated with connecting to and retrieving remote data). All of the information necessary to establish and maintain a connection to the remote data source is stored within the table definition. You can also use the OpenRecordset method in Microsoft Access or Visual Basic for Applications (VBA) to open a linked table. The following example demonstrates how to link an external table, where strDbPath is the path to the database that will contain the linked table:

Dim dbs As Database
Dim tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
' Create new table in Microsoft Jet database.
Set tdf = dbs.CreateTableDef("EmployeeSales")
' Set Connect and SourceTableName properties for table.
With tdf
    .Connect = "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales"
    .SourceTableName = "EmpSales"
End With
dbs.TableDefs.Append tdf

For information about updating link information when tables are moved, see the “Maintaining Links” section later in this chapter.


Opening a Table

In contrast, when you open a table directly, you must supply the connection information at the beginning of each session to establish a connection to the data source. None of the information needed to establish a connection to the remote data source is stored in your Microsoft Jet database. To open a table directly, you must use the OpenDatabase method in Microsoft Access or VBA, and you must supply connection information (such as the data source, user name, password, and database name), as in the following example:

Dim dbs As Database
Dim tdf As TableDef
Dim fld As Field
Dim rst As Recordset

' Open FoxPro database.
Set dbs = OpenDatabase("C:\JetBook\Samples\FoxTables\Sales", False, _
    False, "FoxPro 3.0")
' Return a reference to FoxPro table.
Set tdf = dbs.TableDefs("EmpSales")
' Open a recordset on the table and
' print the values in the Debug window.
Set rst = tdf.OpenRecordset
With rst
    Do Until .EOF
        For Each fld In .Fields
            Debug.Print "Name = " & fld.Name & " Value = " & fld.Value
        Next fld
        .MoveNext
    Loop
End With


External Data Access Requirements

Before you can connect your application to a remote data source, you must make sure that the remote data is accessible to your application’s users and that your application is properly designed to handle remote data source security challenges. You must also make sure that your application interacts correctly with case-sensitive data sources and that the IISAM driver is correctly initialized for the data source you want to access. Finally, you must check, especially when converting existing Microsoft Access or VBA code, that your code doesn’t use objects or methods that are specific to Microsoft Jet data sources when accessing non-Microsoft Jet data sources.


Accessing Data over a Secured Network

As with any other resource on a network, your application must have access to the server and share where the external data resides. You must determine if your application needs read-only or read-write permissions on the share, and you should establish the permissions according to your needs.

In addition, you must make sure that your application has proper access to the table or spreadsheet through the host application’s security mechanisms. You can provide password information to the host application as part of the connection string.

For database servers accessed as ODBC data sources, you may need to provide authentication information at additional levels (such as at the table or view level as well as at the database level). You should test remote data on ODBC data sources for accessibility by common users of your application rather than by those with system-level or administrative privileges. Your code should also handle requests for additional information from the remote data source (such as user name and password requests).


Overview of IISAM Driver Windows Registry Settings

Each IISAM driver has a corresponding registry setting that configures the driver upon initialization. During the installation process for the IISAM drivers, the Setup program creates keys in the registry for the following data sources:

  • Microsoft FoxPro, versions 2.0, 2.5, 2.6, 3.0, and DBC

  • dBASE III, dBASE IV, and dBASE 5.0

  • Paradox, versions 3.x, 4.x, and 5.x

  • Microsoft Excel versions 3.0, 4.0, 5.0, and 8.0 worksheets

  • Lotus 1-2-3 WKS, WK1, WK3, and WK4 spreadsheets

  • ASCII text files in tabular format

  • Microsoft Exchange and Outlook folders and address books

  • HTML

  • Microsoft Jet, versions 2.x

During this process, the Microsoft Office 97, Microsoft Access, or Visual Basic Setup program also assigns a set of default values to each IISAM driver. Your application can alter these values to change the behavior of certain aspects of each driver. For example, you can select a collating sequence for the FoxPro IISAM driver that changes the case-sensitive behavior of the driver, as discussed in the following section.

Note The Setup program does not write many of the settings for the Microsoft Jet 2.x and Microsoft Jet 3.0 and 3.5 IISAM drivers, or for the ODBC drivers. If you add registry settings for any IISAM driver or for ODBC, you must shut down and restart Microsoft Jet so that the new settings can take effect.

See Also For a complete listing of registry settings and their uses, see Appendix C, “Registry Settings.”


Accessing Data on Case-Sensitive Data Sources

Your application must be designed to support operations, such as searching, on external, case-sensitive data sources. Some desktop database products and database servers are case-sensitive by default. These include FoxPro, dBASE, and Paradox. In addition, some database servers accessible through ODBC can be configured as case-sensitive.

If you need to search on a case-sensitive data source, you can design your queries using OR clauses to capture all possible combinations (all uppercase, all lowercase, or proper case). There are, however, three cases where products that are typically case-sensitive are never case-sensitive:

  • If you’re connecting to an Xbase database, such as FoxPro or dBASE, check the CollatingSequence setting in the \HKEY_LOCAL_MACHINE\SOFTWARE
    \Microsoft\Jet\3.5\Engines\Xbase key of the registry. If this setting is set to ASCII, all operations are case-sensitive; if it is set to International, all operations are not case-sensitive.

  • If you’re connecting to a Paradox database, check the CollatingSequence setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Paradox key of the registry. If this setting is set to ASCII, all operations are case-sensitive; if it is set to International, Norwegian-Danish, or Swedish-Finnish, all operations are not case-sensitive.

  • If you’re connecting to a database server through ODBC, case-sensitivity will be determined by the server’s configuration. Check with your database server administrator to determine if your server is configured as case-sensitive or not case-sensitive.

Note If a case-sensitive query is performed across multiple data sources, the collating sequence in effect on the database that hosts the query will determine the query’s case-sensitivity.


Unsupported Objects and Methods

Microsoft Jet database applications cannot use Microsoft Jet-specific objects and methods with external data sources. The following objects and methods are not supported through the IISAM drivers.

Unsupported object or method | Type
CompactDatabase | DAO method
Container | DAO object
CreateDatabase | DAO method
CreateField | DAO method (This method is supported on new tables but cannot be used to expand tables with existing fields.)
CreateQueryDef | DAO method
DeleteQueryDef | DAO method
OpenQueryDef | DAO method
ListParameters | DAO method
ListTables | DAO method
SetDefaultWorkspace | DAO method
Document | DAO object
QueryDef | DAO object
Relation | DAO object
RepairDatabase | DAO method


Transaction Support

Although some methods and objects aren’t supported through IISAM drivers, the IISAM drivers do support transactions on external databases that support transactions. You can use the Transactions property of a Database, Connection, or Recordset object to determine if DAO transaction methods are supported. The following table lists the transaction methods that are available on supported databases.

DAO transaction method | Description
BeginTrans | Begins a transaction on the selected data source.
CommitTrans | Completes a transaction on the selected data source by saving changes.
Rollback | Undoes a transaction on the selected data source.


Establishing Access to External Data

Now that you’re familiar with the different methods of accessing data and external data source limitations, you’re ready to learn how to establish a connection and link or open external tables. This section illustrates how to link tables and open tables directly with various external data sources. In addition, this section describes procedures for updating and deleting links and creating a table in an external data format.


Establishing a Connection

Before you can establish a connection to an external data source, you must decide whether to link or open the target table. The connection information you supply is essentially the same in either case — but the techniques are slightly different. The following sections explain how to establish connections for linking and opening external tables.


Supplying Connection Information for Linking an External Table

Supplying Connection Information for Linking
an External Table

When you link an external table by creating or modifying a TableDef object, you must supply information indicating the type and location of the external database as well as the name of the table you want to link. You can accomplish this by doing one of the following:

  • Set the SourceTableName and Connect properties of the TableDef object. This technique has the added advantage of working with existing TableDef objects.

  • Pass the source and connect values as arguments in the CreateTableDef method.
Setting Connection Information with the SourceTableName
and Connect Properties

The following code fragment sets the connection information with the Connect and SourceTableName properties and assumes you want to connect to the table Q1Sales in a FoxPro 3.0 database named Sales. In this example, strDbPath is the path to the Microsoft Jet database:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
' Create a new TableDef object in a Microsoft Jet database.
Set tdf = dbs.CreateTableDef("FirstQuarterSales1996")
' Set the Connect and SourceTableName properties to link the TableDef object.
With tdf
    .Connect = "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales"
    .SourceTableName = "Q1Sales"
End With
' Append the linked TableDef object.
dbs.TableDefs.Append tdf

Important Use a semicolon (;) to separate arguments specified in the Connect property. Don’t include any spaces on either side of the semicolon.

Setting Connection Information When You Create the TableDef Object

The following code fragment sets the connection information when you create the TableDef object and assumes you want to connect to the table Q1Sales in a FoxPro 3.0 database named Sales. In this example, dbs is a Database object and tdf is a TableDef object. Note that you must include the value of 0 for the attributes argument:

Set tdf = dbs.CreateTableDef("FirstQuarterSales1996", _
    0, "Q1Sales", "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales;")

You can also connect to a data source on an HTTP or FTP server. For example, the following code fragment creates a TableDef object linked to an external FoxPro table on an FTP server:

Set tdf = dbs.CreateTableDef("FirstQuarterSales1996", _
    0, "Q1Sales", "FoxPro 3.0;DATABASE=FTP://WebServer/YTDSales/Sales;")


Supplying Connection Information for Opening an External Table

Supplying Connection Information for Opening
an External Table

When you open an external table directly, you must supply a database path and a source database type as arguments in the OpenDatabase method. The source database name argument, dbname, is described in detail in the next section, “Specifying Database Names.”

The following code fragment assumes you want to connect to the FoxPro 3.0 database named Sales:

Set dbs = OpenDatabase("C:\JetBook\Samples\FoxTables\Sales", _
    False, False, "FoxPro 3.0")

In general, you can use the following methods and properties in VBA code to specify connection information:

  • OpenDatabase method. Use the connect argument of the OpenDatabase method to specify the source database type, and use the dbname argument to specify the database name. For ODBC data sources, use the connect argument of the OpenDatabase method to specify the data source name (DSN).

  • CreateTableDef method. Use the connect argument of the CreateTableDef method to specify the source database type or the database path, and use the sourcetablename argument to specify the source table name.

  • OpenRecordset method. Use the source argument of the OpenRecordset method to specify the source table name.

  • Connect property. Use the Connect property to specify the source database type with the database specifier setting and use the database name with the database path setting. For ODBC data sources, use the Connect property to specify connection information including the DSN, user ID, password, and database name. An ODBC data source connection string uses the same syntax whether you are connecting through an ODBCDirect workspace object or a Microsoft Jet workspace object.

For examples illustrating how to supply connection information for opening an external table, see the “Putting It All Together: Building a Connection String” section later in this chapter.


Specifying Database Names

When you specify a database name for a data source supported by an IISAM driver, you can include the fully qualified or relative path to the file. On a local drive, you must include the drive letter, directory path, and file name; on a network drive, you must include the server name, share, directory path, and file name. You can also map a server, share, and directory path to a logical drive and indicate the database name as the drive letter followed by the file name. The following examples illustrate each of these situations.

To specify the FoxPro database Sales in the JetBook\Samples\FoxTables directory on the local C drive, use the following database name:

C:\JetBook\Samples\FoxTables\Sales

To specify the FoxPro database Region1 on the Regional share on the Sales server, use the following database name:

\\Sales\Regional\Region1

To specify the FoxPro database Region1 on the Regional share on the Sales server where the network path \\Sales\Regional has been assigned to drive G, use the following database name:

G:\Region1

Note ODBC data sources use a different specifier, called a DSN, to indicate the database name. For all ODBC data sources, use the empty string "" as the database name. The ODBC DSN is described in the following section, “Specifying Data Source Names.”


Specifying Data Source Names

ODBC data sources use a different syntax for specifying database names and connection information. You can add the data source name, or DSN, by using the ODBC Data Source Administrator application (accessed through the Windows Control Panel), the RegisterDatabase method of the DBEngine object, or the ODBC application programming interface (API). Data source names are stored in the \HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\Odbc.ini registry key.

Each entry in the ODBC.INI subkey assigns a logical DSN to a set of attributes that includes the name of the ODBC database server; the name of the database on the server, if multiple databases are supported; the type of server (for example, SQL Server); a description of network and connection arguments; and additional information, such as character set conversions. Because connections to ODBC data sources rely on this information, your application must make sure users have the appropriate DSN in their registry before you attempt to access remote data.

See Also For more information about creating a DSN through the ODBC Data Source Administrator application, see the ODBC Data Source Administrator Help file.


Specifying Source Database Types

Each external ISAM format has an associated database type that must be specified when using the driver. Each IISAM driver also has a registry subkey that configures the behavior of the driver. When you build a connection string, as discussed in “Putting It All Together: Building a Connection String” later in this chapter, you must include a source database type so that Microsoft Jet knows how to handle the external data.

Note All ODBC data sources use the source database type ODBC.

The following table lists the source database types.

Data source | Source database types
dBASE | dBASE III dBASE IV dBASE 5.0
Microsoft Excel | Excel 3.0 Excel 4.0 Excel 5.0* Excel 8.0
FoxPro | FoxPro 2.0 FoxPro 2.5 FoxPro 2.6 FoxPro 3.0 FoxPro DBC
Lotus | Lotus WK1 Lotus WK3 Lotus WK4
ODBC | ODBC
Paradox | Paradox 3.x Paradox 4.x Paradox 5.x
HTML | HTML Import HTML Export
Microsoft Exchange | Exchange 4.0**
Text | Text
* The Excel 5.0 source database type string is used to specify Microsoft Excel 5.0 and 7.0 workbooks. ** The Exchange 4.0 source database type string is used to specify Microsoft Exchange 4.0, 5.0, and Outlook folders and address books.

Important You must enter the source database type strings exactly as they appear in the table, including spaces and punctuation.


Specifying a Password

Microsoft Jet honors the security system in all external data sources that support security. When the external data source is secured by a password, your application must supply a password to establish a connection. You can do this by prompting the user for this information and building it into the connection string, or you can hard-code the password into your application.

Note If you store sensitive data in the external data source, you shouldn’t hard-code passwords in your Microsoft Jet application.

Use the PWD argument to pass the password to the external data source, as in the following example. You must follow the password argument with a semicolon.

Set tdf = dbs.CreateTableDef("FirstQuarterSales1996", 0, "Q1Sales", _
    "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales;PWD=RollsRoyce;")

Note You can use the password argument to specify database passwords only; you cannot use the password to log on to a network. If your application requires network access, you must establish these connections before attempting to access the external data.

You cannot use the PWD argument to decrypt Microsoft Excel worksheets or workbooks; you must unprotect and save the worksheet or workbook in Microsoft Excel before you can open it with the Microsoft Excel IISAM driver.

Saving ODBC Passwords Between Sessions

If you don’t want to hard-code sensitive ODBC database passwords into your application or require your users to enter a password each time they start the application, you can set the Attributes property of the TableDef object to dbAttachSavePWD. This saves the ODBC password provided by the user as part of the table definition. Each time the user subsequently opens the remote table through the TableDef object, the password information is automatically applied. The following line of code sets the Attributes property to save a password with a TableDef object:

tdf.Attributes = dbAttachSavePWD

If your remote data is particularly sensitive, you may not want to allow users to save their passwords. To change this behavior, you must modify the MSysConf table in your remote database to disallow local storage of login IDs and passwords (by default, local storage is allowed). If your application has already been deployed with password saving enabled, you can still disallow local user ID and password saving with MSysConf; ODBC will automatically prompt users for any missing authentication information when they attempt to open the remote database.

See Also For more information about the MSysConf table, see Chapter 9, “Developing Client/Server Applications.”


Putting It All Together: Building a Connection String

Before you create a connection string, you need to know the data source you will connect to and the DAO object you will use with the data source. An external data source will be either an ODBC database or an ISAM (desktop) database.

The recommended way to connect to an ODBC database is through an ODBCDirect workspace object. For more information about ODBCDirect, see Chapter 9 “Developing Client/Server Applications.” However, there may be situations where you will connect to an ODBC data source through a Microsoft Jet workspace; for example, to take advantage of specific Microsoft Jet features such as heterogeneous joins or indexed searches. If you are using a version of DAO earlier than 3.5, you must connect to an ODBC data source through a Microsoft Jet workspace.

You can use the DAO Connection, Database, QueryDef, and TableDef objects to connect to an external data source. The following table shows which objects can connect to which database type and workspace type.

Object | ODBC data source (via ODBCDirect) | ODBC data source (via Microsoft Jet) | ISAM data source
Connection | Yes | N/A | N/A
Database | Yes | Yes | Yes
QueryDef | No | Yes | No
TableDef | No | Yes | Yes

A complete connection string includes all information necessary to connect to and open an external data source on a local drive, network, or ODBC-compliant database server. Connection strings consist of a database type followed by a database name or a DSN (ODBC data sources only). If the external data source requires additional information, such as a password, you can include this in the connection string as well. You can set a connection string by using the OpenDatabase method, the CreateTableDef method, or the Connect property.

For ODBCDirect workspaces, you can also set the connection string by using the OpenConnection method. An ODBC connection string uses the same syntax, regardless of which object you connect it to and regardless of whether you connect through an ODBCDirect or Microsoft Jet workspace.

Connecting to an ISAM database can only be done through a Microsoft Jet workspace. The IISAM drivers expose the database to Microsoft Jet as a Microsoft Jet database supporting all the functionality of a native Microsoft Jet database. Any IISAM driver (except Microsoft Exchange or an external Microsoft Jet database) can use FTP or HTTP protocols to connect to a file on an intranet or the Internet.

All IISAM drivers accept a connection string that uses the same basic syntax:

"identifier; source; options"

The identifier argument is a keyword that specifies the type of file being opened. The “Specifying Source Database Types” section earlier in this chapter contains a table with the identifier for each ISAM database listed as “Source Database Types.” No identifier argument is used when you are connecting to an external Microsoft Jet database.

The source argument contains a drive letter or transfer protocol (such as HTTP), a fully qualified path, and (if needed) a file name that is the file (or the directory containing the file) you want to open. The source argument (except for Microsoft Jet databases) always begins with DATABASE=, for example:

strConnect = "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables"

The options argument is used to specify additional, optional arguments to the IISAM driver. For example, you can specify a password or indicate whether the first record in a spreadsheet file contains column header information. Multiple arguments are separated by semicolons.

The connection string is slightly different when you are opening or creating an external Microsoft Jet database file. There are two IISAM drivers for Microsoft Jet databases: one for databases created by using Microsoft Jet 2.5 or earlier and another for databases created by using Microsoft Jet 3.0 or later. When you are creating a connection string for an external Microsoft Jet database, the identifier argument is left blank (but the semicolon is included). The source argument is the drive, path, and file name to the .mdb file. The options argument may contain, where needed, the password associated with a password-protected .mdb file. For example, the following code fragment creates a connection string for an external Microsoft Jet database. You could use this code to link the Employees table in the NorthwindTables database to the JetSamples database:

Dim dbs As Database, tdf As TableDef

' Create new TableDef object in a Microsoft Jet database.
Set tdf = dbs.CreateTableDef("LinkedEmployees")
' Link table from another Microsoft Jet database.
With tdf
    .Connect = ";DATABASE=C:\JetBook\Samples\NorthwindTables.mdb"
    .SourceTableName = "Employees"
End With
dbs.TableDefs.Append tdf

The following code fragment sets the connection string by using the OpenDatabase method:

Dim dbs As Database

Set dbs = OpenDatabase("C:\JetBook\Samples\FoxTables\Sales", _
    False, False, "FoxPro 3.0")

Note that the connection information appears in a slightly different format when it is specified in the arguments to the OpenDatabase method. The identifier and options arguments are passed in the connect argument (the fourth argument in the example above), while the source argument is supplied in the first argument without prepending the DATABASE= information.

The following code fragment sets the connection string with the CreateTableDef method; connection information is supplied as arguments in the method. In this example, strDbPath is the path to the Microsoft Jet database:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.CreateTableDef("Q1Orders1996", 0, "Q1Ord96", _
    "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales;")
dbs.TableDefs.Append tdf

The following code fragment sets the connection string by using the Connect property, CreateTableDef method, and SourceTableName property:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)
Set tdf = dbs.CreateTableDef("EmployeeSalesTotals")
With tdf
    .Connect = "Paradox 3.X;DATABASE=C:\JetBook\Samples\pdx"
    .SourceTableName = "PDXSales"
End With
dbs.TableDefs.Append tdf

The following code fragment sets the connection string by using the OpenConnection method of an ODBCDirect workspace object:

Dim wrk As Workspace, cnn As Connection

' Create an ODBCDirect workspace.
Set wrk = CreateWorkspace("NewWS", "admin", "", dbUseODBC)
' Open a connection on the workspace.
Set cnn = wrk.OpenConnection("NorthwindConnection", , , _
    "ODBC;DSN=NorthwindSQLDSN;DATABASE=NorthwindSQL;UID=sa;PWD=;")


Opening an External Table

You need to perform two steps to open an external table directly:

  1. Use the OpenDatabase method of the Workspace object to open the Microsoft Jet database and provide connection information for the external table. Arguments in the OpenDatabase method specify the database name, whether the external data source is opened as exclusive, whether the external data source is opened as read-only, and the source database type.

  2. Use the OpenRecordset method of the Database object to create a new Recordset object for the external table. Use the source argument of the OpenRecordset method to supply the name of the table to open.

Note The dbname argument of the OpenDatabase method is used to specify the database name, and the source argument of the OpenRecordset method is used to specify the table name for all IISAM drivers. For external data sources that store one table per file, the dbname argument should contain the full directory or network path to the file, and the source argument should contain the data file name with no extension. For external data sources that store multiple tables in a file, dbname should contain the full directory or network path to the file, including the file name and extension; source should contain the table name.

You can specify a type argument for the OpenRecordset method if you want more control over the Recordset object’s type. For more information about the types of Recordset objects you can open, see Chapter 5, “Working with Records and Fields.”

Important The dbOpenTable constant is not a valid argument for ODBC databases.

After you’ve opened the table, you can access the Recordset object you created just as you would any other Recordset object in your Microsoft Jet database. Keep in mind, however, that you must re-establish your connection to the external data source and re-create the Recordset object for each session.

The following code example opens a table that is stored in a Microsoft Excel 8.0 workbook. The example opens a single worksheet in a workbook.


Example: Opening a Table in a Microsoft Excel 8.0 Workbook

This example steps you through opening a table that is stored in a Microsoft Excel 8.0 workbook. The example follows the steps outlined in the previous section, “Opening an External Table.”

The first step is to open the Microsoft Excel 8.0 workbook:

Dim dbs As Database, rst As Recordset

Set dbs = OpenDatabase("C:\JetBook\Samples\Excel\Products97.xls", _
    False, False, "Excel 8.0;HDR=NO;")

Note that the example uses HDR=NO to suppress the column header row from the count of records. If you don’t suppress headers in this example, the RecordCount property value displayed in the message box will incorrectly include the first row of column headings in the count of records.

The second step is to create a Recordset object for the external data by using code similar to the following:

Set rst = dbs.OpenRecordset("Products97$")

The argument for the OpenRecordset method is the name of a particular worksheet in the Microsoft Excel workbook. The dollar sign character following the sheet name tells the IISAM driver that you’re referencing the entire sheet, rather than just a range on the sheet.

The following example establishes the connection, creates a Recordset object, and displays the total number of records in a message box:

Sub OpenExcelWorkbook()
    Dim dbs As Database
    Dim rst As Recordset

    ' Open the Microsoft Jet sample database.
    Set dbs = OpenDatabase("C:\JetBook\Samples\Excel\Products97.xls", _
        False, False, "Excel 8.0;HDR=NO;")

    ' Create a Recordset object for the Microsoft Excel Products worksheet.
    Set rst = dbs.OpenRecordset("Products$")

    ' Move to the last record and display the RecordCount property value.
    With rst
        .MoveLast
        MsgBox "There are " & .RecordCount & " records in this worksheet."
        .Close
    End With
    dbs.Close
End Sub

See Also The sample workbook discussed in this chapter can be found in the JetBook\Samples\Excel sample file folder on the companion CD-ROM.


Creating a Link to an External Table

The following procedure outlines the four steps to creating a link to an external table. (This explanation uses the CreateTableDef method and the Connect and SourceTableName properties.)

Û To create a link to an external table

  1. Use the OpenDatabase method of the Workspace object to open the Microsoft Jet database that will store the linked table definition.

  2. Use the CreateTableDef method of the Database object to create a table definition for the linked table. You can assign a unique, descriptive name to the TableDef object, which can be any valid Microsoft Jet table name.

  3. Use the Connect and SourceTableName properties of the TableDef object to set the connection information for the linked table. This step establishes the connection and provides authentication services, such as user name and password presentation to the remote data source, if needed.

  4. Use the Append method of the TableDefs collection to append the table definition for the linked table to the TableDefs collection.

After you’ve created the link, you can access the external table as you would any other table in your Microsoft Jet database. The link remains current unless you delete the link or delete or move the external table. You can update the link information to an external table that has been moved by refreshing the link.

For information about removing and refreshing links, see the “Maintaining Links” section later in this chapter.


Example: Linking a FoxPro Database

The following example steps you through linking a FoxPro database to a Microsoft Jet database. The example follows the steps outlined in the previous section, “Creating a Link to an External Table.”

Note If you link a FoxPro 3.0 table to a Microsoft Jet database and then view the connection string in table Design view in Microsoft Access, the specifier in the connection string appears as FoxPro 2.0. Microsoft Access uses the FoxPro 2.0 specifier for both FoxPro 2.x and 3.0 tables.

The first step is to open a Microsoft Jet database. In the following line of code, strDbPath is the path to the database that will contain the linked table:

Dim dbs As Database, tdf As TableDef

Set dbs = OpenDatabase(strDbPath)

The second step is to create the table definition:

Set tdf = dbs.CreateTableDef("EmployeeSalesQ1")

The third step is to set the connection information:

tdf.Connect = "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales"
tdf.SourceTableName = "EmpSales"

Note You can use the Connect property to specify the database name and the SourceTableName property to specify the table name for all IISAM drivers. For external data sources that store one table per file, the Connect property should contain the full directory or network path to the file, and the SourceTableName property should contain the data file name with no extension. For external data sources that store multiple tables in a file, the Connect property should contain the full directory or network path to the file including the file name and extension; the SourceTableName property should contain the table name.

The fourth and final step is to append the TableDef object to the TableDefs collection:

dbs.TableDefs.Append tdf

The following example establishes the connection, appends the linked table, and tests the data to make sure that it can be accessed. In this example, strDbPath is the path to the database that will contain the linked table:

Dim dbs As Database
Dim tdf As TableDef, qdf As QueryDef, rst As Recordset

Set dbs = OpenDatabase(strDbPath)
' Create a table definition for the FoxPro table.
Set tdf = dbs.CreateTableDef("EmployeeSalesQ1")
With tdf
    ' Provide source database type and name with Connect property.
    .Connect = "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables\Sales"
    ' Provide name of FoxPro table.
    .SourceTableName = "EmpSales"
End With

' Append TableDef object to the TableDefs collection to create the link.
dbs.TableDefs.Append tdf

' Run a simple query to ensure the data is accessible.
Set qdf = dbs.CreateQueryDef("")
qdf.SQL = "SELECT Sum(Sales) As TotalSales FROM " _
    & "EmployeeSalesQ1;"
Set rst = qdf.OpenRecordset()
MsgBox "Total Sales = " & Format(rst!TotalSales, "Currency")

See Also The FoxPro database discussed in this chapter can be found in the JetBook\Samples\FoxTables sample file folder on the companion CD-ROM.


Special Considerations for Desktop Databases

The following sections describe special information that will help you understand how Microsoft Jet interacts with specific ISAM databases.


Working with dBASE Databases

Borland’s dBASE product line includes the following versions supported by the Microsoft Jet Xbase IISAM driver: dBASE III, dBASE IV, and dBASE 5.0. dBASE databases differ from Microsoft Jet databases in four major areas: data types, handling of deleted records, specification of indexes, and storage of Long Text (Memo) fields. The following sections describe these differences.

Converting dBASE Data Types to Microsoft Jet Data Types

Microsoft Jet translates each dBASE data type into the corresponding Microsoft Jet data type when your application reads the data. The following table shows the one-to-one correspondence between data types.

dBASE data type | Microsoft Jet data type
Character | Text
Numeric, Float | Double
Logical | Boolean
Date | Date/Time
Memo | Memo
OLE | Long Binary (OLE Object)

Handling Deleted Records

When you delete a record from a dBASE table through your application, the record is marked as deleted in the table. The record is not, however, removed from the table because dBASE allows users to recover deleted records until the table is packed. Because you cannot pack a dBASE table through Microsoft Jet, deleted records will continue to appear in your application’s data set.

To force Microsoft Jet to filter out deleted records in a dBASE table, you must set a setting in the registry. You can do this by setting the Deleted setting under the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Xbase key to 01 (True) and restarting Microsoft Jet. This will hide deleted dBASE records from your application.

Handling dBASE Indexes

You can speed up Microsoft Jet access to dBASE tables by specifying the dBASE .ndx or .mdx index files to use. Index files are specified in an .inf file, which you can create either by linking your dBASE table through the Microsoft Access user interface or by creating a text file with the same file name as your dBASE database and appending an .inf extension. The following procedure describes how to create an .inf file for the hypothetical dBASE database Sales.dbf.

  1. Create the text file Sales.inf.

  2. On the first line, add the database type identifier enclosed in square brackets. For example, to specify dBASE III indexes, type the following:
    [dBASE III]
  3. Assign an index number to the first index for the database. Number the first index NDX1, the second NDX2, and so on. If you want to specify multiple-index (.mdx) files for dBASE IV or dBASE 5.0 databases, use index numbers of the form MDX1, MDX2, and so on. You can specify unique indexes by prepending a “U” to the index number.

  4. Follow the index number with an equal sign and the file name of the index you want to specify, including the extension. Don’t put any spaces in this entry. For example, to specify the index Custname.ndx as the first index and the unique index Custno.ndx as the second index, add the following entries:
    NDX1=CUSTNAME.NDX
UNDX1=CUSTNO.NDX
  5. Repeat steps 3 and 4 until you’ve specified all indexes for the database table. Your completed .inf file should look similar to the following:
    [dBASE III]
NDX1=CUSTNAME.NDX
UNDX1=CUSTNO.NDX
  6. Save the .inf file to the directory that contains your dBASE database. Alternatively, you can create an INFPath setting in the registry to point to the path where the .inf file is stored. This is useful if you cannot store the .inf file in the same directory as the database, (for example, if the file is on a CD-ROM or a read-only network server), or if you want to keep all .inf files in one shared location for easy maintenance.

    To create an INFPath setting in your registry, you must manually add the registry setting in the Registry Editor as a string value. To add the setting, open the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Xbase key in the registry. On the Edit menu, point to New, and then click String Value. Type INFPath as the name, and type the full directory or network path to the .inf file in the data field. Save the registry changes and restart Microsoft Jet to use this setting.

Keep in mind that Microsoft Jet will need to periodically update your index files as you change data from within your dBASE database. If you associate index files with a dBASE database that is linked or opened from a Microsoft Jet database, you need to make sure the indexes are available to your Microsoft Jet database as well.

Important Don’t delete or move the index files or the .inf file without updating associated references (such as the registry). If Microsoft Jet doesn’t have access to up-to-date index information, it cannot correctly process queries.

Handling Memo Fields

Memo fields for dBASE databases are stored in the file system instead of in the database. If you want to link or open a dBASE database that has Memo fields, make sure the Memo field files are stored in the same directory as the database files.


Working with FoxPro Databases

The Microsoft FoxPro product line includes the following versions supported by the Microsoft Jet Xbase IISAM driver: FoxPro 2.0, FoxPro 2.5, FoxPro 2.6, FoxPro 3.0, and FoxPro DBC. FoxPro databases differ from Microsoft Jet databases in four major areas: data types, handling of deleted records, specification of indexes, and storage of Long Text (Memo) fields. The following sections describe these differences.

Converting FoxPro Data Types to Microsoft Jet Data Types

Microsoft Jet translates each FoxPro data type into the corresponding Microsoft Jet data type when your application reads the data. The following table shows the one-to-one correspondence between data types.

FoxPro data type | Microsoft Jet data type
Character | Text
Numeric, Float | Double
Logical | Boolean
Date | Date/Time
Memo | Memo
General | Long Binary (OLE Object)
Double* | Double
Currency* | Currency
Integer* | Long
DateTime* | Date/Time
* Supported in Microsoft FoxPro 3.0 and Microsoft FoxPro DBC only

Handling Deleted Records

When you delete a record from a FoxPro table through your application, the record is marked as deleted in the table. The record is not, however, removed from the table because FoxPro allows users to recover deleted records until the table is packed. Because you cannot pack a FoxPro table through the Microsoft Jet database engine, deleted records will continue to appear in your application’s data set.

To force Microsoft Jet to filter out deleted records in a FoxPro table, you must set a value in the registry. You can do this by setting the Deleted setting under the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Xbase key to 01 (True) and restarting the Microsoft Jet database engine to hide deleted records from your application.

Handling FoxPro Indexes

You can speed up Microsoft Jet access to FoxPro tables by specifying which FoxPro .idx or .cdx index files to use. Index files are specified in an .inf file, which you can create by linking your FoxPro table through the Microsoft Access user interface or by creating a text file with the same file name as your FoxPro database and appending an .inf extension. If there is no .inf file but there is a .cdx index file in the same directory as the database file, the IISAM driver will automatically use that index file. The following procedure describes how to create an .inf file for the hypothetical FoxPro database Sales.dbf.

  1. Create the text file Sales.inf.

  2. On the first line, add the database type identifier enclosed in square brackets. For example, to specify FoxPro 2.0 indexes, type the following:
    [FoxPro 2.0]
  3. Assign an index number to the first index for the database. Number the first index IDX1, the second IDX2, and so on. If you want to specify combined-index (.cdx) files for FoxPro databases, use index numbers of the form CDX1, CDX2, and so on. You can specify unique indexes by prepending a “U” to the index number.

  4. Follow the index number with an equal sign and the file name of the index you want to specify, including the extension. Do not put any spaces in this entry. For example, to specify the index Custname.idx as the first index and the unique index Custno.idx as the second index, add the following entries:
    IDX1=CUSTNAME.IDX
UIDX1=CUSTNO.IDX
  5. Repeat steps 3 and 4 until you have specified all indexes for the database table. Your completed .inf file should look similar to the following:
    [FoxPro 2.0]
IDX1=CUSTNAME.IDX
UIDX1=CUSTNO.IDX
  6. Save the .inf file to the directory that contains your FoxPro database. Alternatively, you can create an INFPath setting in the registry to point to the path where the .inf file is stored. This is useful if you cannot store the .inf file in the same directory as the database or if you want to keep all .inf files in one shared location for easy maintenance.

    To create an INFPath setting in your registry, you must manually add the registry setting in the Registry Editor as a string value. To add the setting, open the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Xbase key in the registry. On the Edit menu, point to New, and then click String Value. Type INFPath as the name and type the full directory or network path to the .inf file in the data field. Save the registry changes and restart Microsoft Jet to use this setting.

Keep in mind that Microsoft Jet will need to periodically update your index files as you change data from within your FoxPro database. If you associate index files with a FoxPro database that is linked or opened from a Microsoft Jet database, you must make sure the indexes are available to your Microsoft Jet database as well.

Important Don’t delete or move the index files or the .inf file without updating associated references (such as the registry). If Microsoft Jet doesn’t have access to up-to-date index information, it cannot correctly process queries.

Handling Memo Fields

Memo fields for FoxPro databases are stored in the file system rather than in the database. If you want to link or open a FoxPro database that has Memo fields, make sure the Memo field files are stored in the same directory as the database files.


Working with Paradox Databases

The Paradox product line includes the following versions supported by the Microsoft Jet IISAM driver for Paradox: Paradox 3.x, Paradox 4.x, and Paradox 5.x. Paradox databases differ from Microsoft Jet databases in three major areas: data types, handling of indexes and keys, and dependency on network resources. The following sections describe these differences.

Converting Paradox Data Types to Microsoft Jet Data Types

Microsoft Jet translates each Paradox data type into the corresponding Microsoft Jet data type when your application reads the data. The following table shows the one-to-one correspondence between data types.

Paradox data type* | Microsoft Jet data type
Alphanumeric | Text
Number | Double
Short number | Integer
Currency | Double
Date | Date/Time
Memo | Memo
OLE | Long Binary (OLE Object) (Note that the Microsoft Jet database engine recognizes the object but will not let you open it.)
Logical (Paradox 5.x) | Boolean
Integer (Paradox 5.x) | Long
Timestamp (Paradox 5.x) | Date/Time
Binary (Paradox 5.x) | Binary
BCD (Paradox 5.x) | Double
Time (Paradox 5.x) | Date/Time
Autoinc (Paradox 5.x) | AutoNumber
* Paradox Graphic, Binary, and Formatted memo types aren’t supported and won’t appear when you display external Paradox tables.

Handling Paradox Indexes and Keys

Paradox uses a .px index file to store primary key information for each table that has a primary key. Microsoft Jet must be able to locate the .px index file each time you attempt to use the table. If the .px file is unavailable, Microsoft Jet will be unable to open the Paradox table.

You cannot update data in a Paradox table that has no primary key. Also, you cannot open, in shared mode, a Paradox table that has no primary key.

Understanding the ParadoxNetPath, ParadoxUserName, and ParadoxNetStyle Settings

Microsoft Jet uses three settings to control user access to Paradox tables that reside on a file server. You can set each of these settings in the \HKEY_LOCAL_MACHINE
\SOFTWARE\Microsoft\Jet\3.5\Engines\Paradox key in the registry.

The first setting, ParadoxNetPath, indicates the location of the network control file Paradox.net (for Paradox 3.x databases) or the file Pdoxusrs.net (for Paradox 4.x and 5.x databases). This setting must be identical for all users who are accessing a particular Paradox database.

The second setting, ParadoxUserName, is a string value that is displayed in Paradox to an interactive Paradox user when the user attempts to place a lock that is incompatible with the lock held by the Microsoft Jet Paradox IISAM driver.

The third setting, ParadoxNetStyle, indicates whether users are accessing the Paradox database using the Paradox 3.x or Paradox 4.x locking method.

For more information about Paradox network dependencies and locking methods, see the documentation for Paradox.

See Also For more information about Paradox-specific settings in the registry, see the Paradox section of Appendix C, “Registry Settings.”


Special Considerations for Spreadsheets

The following sections describe special information that will help you understand how Microsoft Jet interacts with Lotus 1-2-3 spreadsheets and Microsoft Excel worksheets and workbooks. Both products have one common feature worth mentioning: the HDR argument in the connection string. Additional considerations for each product are covered separately in the sections that follow.

Using the HDR Argument to Suppress Headers

The registry keys that manage behavior in Lotus 1-2-3 spreadsheets and Microsoft Excel worksheets and workbooks contain a setting that determines whether the IISAM driver should use the data in the cells of the first row of the sheet as field names in the Microsoft Jet database. This setting, FirstRowHasNames, is found in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Lotus key for Lotus 1-2-3 and the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5
\Engines\Excel key for Microsoft Excel. When the value of FirstRowHasNames is 01 (True, the default), all sheets of that product family are handled as if the first row contained field name information.

To override this registry setting on a sheet-by-sheet basis, you must use the HDR argument in the options portion of the connection string. Set HDR to True (HDR=YES) to instruct the IISAM driver to use the data in the first row of the sheet as field names, regardless of the registry setting. Set HDR to False (HDR=NO) to instruct the IISAM driver to treat the first row of the sheet as data, regardless of the registry setting.

The following procedure opens a worksheet in a Microsoft Excel 8.0 workbook and computes the number of records in the Products worksheet contained in the Products97.xls workbook. The HDR argument is set according to the value of the blnHDRParam argument passed to the procedure. When the HDR argument in the connection string is set to NO, the count is 80 records; when it’s set to YES, the count is 79.

Sub TestHDRConnectParameter(blnHDRParam As Boolean)
    Dim dbs As Database
    Dim rst As Recordset
    Dim strHDRParam As String

    If blnHDRParam = True Then
        strHDRParam = "YES"
    Else
        strHDRParam = "NO"
    End If

    ' Open the Microsoft Jet sample database.
    Set dbs = OpenDatabase("C:\JetBook\Samples\Excel\Products97.xls", _
        False, False, "Excel 8.0;HDR=" & strHDRParam & ";")

    ' Create a Recordset object for the Microsoft Excel Products worksheet.
    Set rst = dbs.OpenRecordset("Products$")

    ' Move to the last record and display the RecordCount property value.
    With rst
        .MoveLast
        MsgBox "There are " & .RecordCount & " records in this worksheet."
        .Close
    End With
    dbs.Close
End Sub


Working with Lotus 1-2-3 Worksheets

Lotus Development Corporation’s 1-2-3 product line includes the following worksheet versions supported by the Microsoft Jet IISAM driver for Lotus: Lotus WKS, Lotus WK1, Lotus WK3, and Lotus WK4. Specific versions of Lotus worksheets allow different amounts of access to the data when linked or opened through the Lotus IISAM driver:

  • Lotus .wks and .wk4 files allow read-only access to the data. You cannot add, delete, or modify rows in worksheets created with these versions.

  • Lotus .wk1 and .wk3 files allow read and insert access to the data. You can read all rows and add new rows but you cannot delete or modify existing rows in worksheets created with these versions.
Specifying Sheets and Ranges in Lotus Worksheet Files

You can specify a subset of the available data when you open a Lotus 1-2-3 single- or multiple-sheet worksheet file. In single-sheet files, you can open the entire sheet, a named range of cells, or an unnamed range of cells. In a multiple-sheet file, you can open a single sheet, a named range anywhere in the multiple-sheet file, or an unnamed range in a single sheet. The following table lists the conventions for the DATABASE and source connection string arguments you must supply to access each of these objects.

To access this object | In this Lotus version | Use this syntax
Entire sheet in a single-sheet file | WKS and WK1 | Set the DATABASE argument to the fully qualified network or directory path, without a file name. Set the OpenRecordset method’s source argument to the file name as filename#wks or filename#wk1.
Entire sheet in a multiple-sheet file | WK3 and WK4 | Set the DATABASE argument to the fully qualified network or directory path to the multiple-sheet file, including the multiple-sheet file name. Set the OpenRecordset method’s source argument to sheetname:, where sheetname is the name of the worksheet. Important You must follow the worksheet name with a colon (:).
Named range of cells in a single- or multiple-sheet file | WKS, WK1, WK3, and WK4 | Set the DATABASE argument to the fully qualified network or directory path to the single- or multiple-sheet file, including the file name. Set the OpenRecordset method’s source argument to an existing named range as namedrange.
Unnamed range of cells in a single-sheet file | WKS and WK1 | Set the DATABASE argument to the fully qualified network or directory path to the worksheet file, including the worksheet’s file name. Set the OpenRecordset method’s source argument to the range of cells you want to access, such as A1..Z256.
Unnamed range of cells in a multiple-sheet file | WK3 and WK4 | Set the DATABASE argument to the fully qualified network or directory path to the multiple-sheet file, including the file name. Set the OpenRecordset method’s source argument to sheetname:range. For example, to access cells A1 through D12 in worksheet named 1996 Sales, you would set the source argument to 1996 Sales:A1..D12.

Note You cannot specify a value in a range that exceeds the maximum number of rows, columns, or sheets for the worksheet. See your Lotus 1-2-3 documentation for these values.

For examples that illustrate the concepts discussed in this section, see the “Microsoft Excel Sheet and Range Specification Examples” section later in this chapter.


Working with Microsoft Excel Worksheets and Workbooks

The Microsoft Excel product line includes the following single-sheet worksheet and multiple-sheet workbook versions supported by the Microsoft Jet IISAM driver for Microsoft Excel: Microsoft Excel 3.0 and 4.0 (single-sheet) and Microsoft Excel 5.0, 7.0, and 8.0 (multiple-sheet workbooks). There are a few operations that you cannot perform on Microsoft Excel worksheets or workbooks through the Microsoft Excel IISAM driver:

  • You cannot delete rows from Microsoft Excel worksheets or workbooks.

  • You can clear data from individual cells in a worksheet, but you cannot modify or clear cells that contain formulas.

  • You cannot create indexes on Microsoft Excel worksheets or workbooks.

  • You cannot read encrypted data through the Microsoft Excel IISAM driver. The PWD argument in the connection string cannot be used to open an encrypted worksheet or workbook, even if you supply the correct password. You must decrypt all Microsoft Excel worksheets or workbooks through the Microsoft Excel user interface if you plan to link or open them in your Microsoft Jet database.
Specifying Sheets and Ranges in Microsoft Excel Worksheet and Workbook Files

You can specify a subset of the available data when you open a Microsoft Excel worksheet or workbook. In worksheet files, you can open the entire sheet, a named range of cells, or an unnamed range of cells. In a workbook file, you can open a single worksheet, a named range anywhere in the workbook, or an unnamed range in a single worksheet. The following table lists the conventions for the DATABASE and source connection string arguments you must supply to access each of these objects.

To access this object | In this version of Microsoft Excel | Use this syntax
Entire sheet in a worksheet file | Microsoft Excel 3.0 and 4.0 | Set the DATABASE argument to the fully qualified network or directory path to the worksheet file, without a file name. Set the OpenRecordset method’s source argument to filename#xls, where filename is the name of the worksheet.
Entire worksheet in a workbook file | Microsoft Excel 5.0, 7.0, and 8.0 | Set the DATABASE argument to the fully qualified network or directory path to the workbook file, including the workbook’s file name. Set the OpenRecordset method’s source argument to sheetname$, where sheetname is the name of the worksheet. Important You must follow the worksheet name with a dollar sign ($).
Named range of cells in a worksheet or workbook file | Microsoft Excel 3.0, 4.0, 5.0, 7.0, and 8.0 | Set the DATABASE argument to the fully qualified network or directory path to the worksheet or workbook file, including the worksheet’s or workbook’s file name. Set the OpenRecordset method’s source argument to namedrange, where namedrange is the name previously assigned to the range in Microsoft Excel.
Unnamed range of cells in a worksheet file | Microsoft Excel 3.0 and 4.0 | Set the DATABASE argument to the fully qualified network or directory path to the worksheet file, including the worksheet’s file name. Set the OpenRecordset method’s source argument to the range of cells you want to access, such as A1:D12.
Unnamed range of cells in a single worksheet in a workbook file | Microsoft Excel 5.0, 7.0, and 8.0 | Set the DATABASE argument to the fully qualified network or directory path to the workbook file, including the workbook’s file name. Set the OpenRecordset method’s source argument to the sheet you want to link or open as sheetname$range. For example, to access cells A1 through D12 in a worksheet named 1996 Sales, you would use the following in the source argument: 1996 Sales$A1:D12.

Note You cannot specify a value in a range that exceeds the maximum number of rows, columns, or sheets for the worksheet or workbook. See your Microsoft Excel documentation for these values.

Microsoft Excel Sheet and Range Specification Examples

The following code examples illustrate how to specify an entire worksheet, a named range, and an unnamed range of cells in a Microsoft Excel 4.0 worksheet and in a Microsoft Excel 8.0 workbook.

This first example opens a Microsoft Excel 4.0 worksheet from a Microsoft Jet database three times to demonstrate how to open an entire worksheet, how to open a named range in a worksheet, and how to open an unnamed range in a worksheet. The ranges are tested by counting the number of records that appear in the Recordset object.

Sub Excel4SheetAndRangeTest()
    Dim dbs As Database
    Dim rst As Recordset

    ' Open the Microsoft Excel 4.0 sample spreadsheet.
    Set dbs = OpenDatabase("C:\JetBook\Samples\Excel\Supplier.xls", _
        False, False, "Excel 4.0;HDR=NO;")
    ' Create a Recordset object for the worksheet Supplier.xls.
    Set rst = dbs.OpenRecordset("Supplier$")
    ' Move to the last record and then count the records.
    rst.MoveLast
    MsgBox "There are " & rst.RecordCount & " rows in this worksheet."
    rst.Close

    ' Open an existing named range.
    Set rst = dbs.OpenRecordset("FirstTenRows")
    ' Move to the last record and then count the records.
    rst.MoveLast
    MsgBox "There are " & rst.RecordCount & " rows in named range FirstTenRows."
    rst.Close

    ' Create a Recordset object for the range A1 through G5
    ' in the worksheet Supplier.xls.
    Set rst = dbs.OpenRecordset("A1:G5")
    ' Move to the last record and then count the records.
    rst.MoveLast
    MsgBox "There are " & rst.RecordCount & " rows in this range."

    rst.Close
    Set rst = Nothing
    dbs.Close
    Set dbs = Nothing
End Sub

This next example opens a worksheet in a Microsoft Excel 8.0 workbook from a Microsoft Jet database three times to demonstrate how to open a worksheet in a workbook, how to open a named range in a workbook, and how to open an unnamed range in a worksheet in the workbook. The ranges are tested by counting the number of records that appear in the Recordset object.

Sub Excel8SheetAndRangeTest()
    Dim dbs As Database
    Dim rst As Recordset

    ' Open the Microsoft Excel 97 sample spreadsheet.
    Set dbs = OpenDatabase("C:\JetBook\Samples\Excel\Products97.xls", _
         False, False, "Excel 8.0;HDR=NO;")
    ' Create a Recordset object for the Products worksheet.
    Set rst = dbs.OpenRecordset("Products$")
    ' Move to the last record and then count the records.
    rst.MoveLast
    MsgBox "There are " & rst.RecordCount & " rows in this worksheet."
    rst.Close

    ' Create a Recordset object for the named range "SecondTenRows".
    Set rst = dbs.OpenRecordset("SecondTenRows")
    ' Move to the last record and then count the records.
    rst.MoveLast
    MsgBox "There are " & rst.RecordCount & " rows in this named range."
    rstSales.Close

    ' Create a Recordset object for the range A1 through E5 in the
    ' Products worksheet.
    Set rst = dbs.OpenRecordset("Products$A1:E5")
    ' Move to the last record and then count the records.
    rst.MoveLast
    MsgBox "There are " & rst.RecordCount & " rows in this range."

    rst.Close
    Set rst = Nothing
    dbs.Close
    Set dbs = Nothing
End Sub


Special Considerations for Text Files

You can use the Microsoft Jet text IISAM driver to link and open character-delimited and fixed-length text files. Commas, tabs, or user-defined delimiters are valid in the source file.

Microsoft Jet recognizes Null values in character-delimited files by the presence of two consecutive delimiting characters. Microsoft Jet recognizes Null values in fixed-length files by the absence of data (spaces) in the data column.

Microsoft Jet determines the format of the text file by reading the file directly or by using a schema information file. The schema information file, which is always named Schema.ini and is always kept in the same location as the text data source, provides the IISAM driver with information about the general format of the file, the column name and data type information, and a number of other data characteristics. A Schema.ini file is always required for accessing fixed-length data; a Schema.ini file is recommended when your text table contains Date/Time, Currency, or Decimal data or any time you want more control over the handling of the data in the table.

Important Microsoft Jet doesn’t support multiuser access to text files. When you open a text file through Microsoft Jet, you have exclusive access to the file.

The following table lists the few limitations to the size of text tables and objects.

Object | Maximum size per text file
Field | 255
Field name | 64 characters
Field width | 32,766 characters
Rows | 65,000 bytes


Understanding Schema.ini Files

Schema.ini files provide schema information about the records in a text file. Each Schema.ini entry specifies one of five characteristics of the table: the text file’s name; the file format; the field names, widths, and types; the character set; and special data type conversions. The following sections discuss these characteristics.

Specifying the File Name

The first entry in the Schema.ini file is always the name of the text source file enclosed in square brackets. The following example illustrates the entry for the file Sample.txt:

[SAMPLE.TXT]
Specifying the File Format

The Format option in the Schema.ini file specifies the format of the text file. The text IISAM driver can read the format automatically from most character-delimited files. You can use any single character as a delimiter in the file except the double quotation mark. The Format option in Schema.ini overrides the Format setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Text key of the registry on a file-by-file basis. The following table lists the valid values for the Format option.

Format specifier | Table format
TabDelimited | Fields in the file are delimited by tabs.
CSVDelimited | Fields in the file are delimited by commas (comma-separated values).
Delimited(*) | Fields in the file are delimited by asterisks. You can substitute any character for the asterisk except the double quotation mark.
FixedLength | Fields in the file are of a fixed-length.

For example, to specify a format of comma-delimited, you would add the following line to the Schema.ini file:

Format=CSVDelimited
Specifying the Fields

You can specify field names in a character-delimited text file in two ways: either include the field names in the first row of the table and set the ColNameHeader option in Schema.ini to True; or specify each column by number and designate the column name and data type. For fixed-length files, you must specify each column by number and designate the column name, data type, and width.

Note The ColNameHeader option in Schema.ini overrides the FirstRowHasNames setting in the registry on a file-by-file basis.

If you use the ColNameHeader option to specify field names in a character-delimited file, you can also instruct Microsoft Jet to guess the data types of the fields. Use the MaxScanRows option to indicate how many rows Microsoft Jet should scan when guessing the column types. If you set MaxScanRows to zero, Microsoft Jet scans the entire file. The MaxScanRows option in Schema.ini overrides the MaxScanRows setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines
\Text key of the registry on a file-by-file basis.

The following example shows how to indicate that Microsoft Jet should use the data in the first row of the table to determine field names and should examine the entire file to determine the data types used:

ColNameHeader=True
MaxScanRows=0

The next example shows how to designate fields in a table by using the column number (Coln) option, which is optional for character-delimited files and required for fixed-length files. The example shows the Schema.ini entries for two fields, a 10-character CustomerNumber text field and a 30-character CustomerName text field:

Col1=CustomerNumber Text Width 10
Col2=CustomerName Text Width 30

The syntax of the Coln entry follows:

Col n=columnname type [Width #]

The following table describes each part of the Coln entry.

Argument | Description
columnname | The text name of the column. If the column name contains embedded spaces, it must be enclosed in double quotation marks.
Type | Value types are: Microsoft Jet data types: Bit (Boolean) Byte Short (Integer) Long Currency Single Double Date/Time Text Memo ODBC Text Driver data types: Char (same as Text) Float (same as Double) Integer (same as Short) LongChar (same as Memo) Date date format
Width | The literal string value Width. Indicates that the following number designates the width of the column (optional for character-delimited files, required for fixed-length files).
# | The integer value that designates the width of the column (required if Width is specified).

Selecting a Character Set

You can select from two character sets: ANSI and OEM. The following example shows the Schema.ini entry for an OEM character set. The CharacterSet option in the Schema.ini file overrides the CharacterSet setting in the \HKEY_LOCAL_MACHINE
\SOFTWARE\Microsoft\Jet\3.5\Engines\Text key of the registry on a file-by-file basis. The following example shows the Schema.ini entry that sets the character set to ANSI:

CharacterSet=ANSI
Specifying Data Type Formats and Conversions

The Schema.ini file contains a number of options that you can use to specify how data is converted or displayed when read by Microsoft Jet. The following table lists each of these options.

Option | Description
DateTimeFormat | Can be set to a format string indicating dates and times. This entry should be specified if all Date/Time fields in the import or export are handled with the same format. All of the Microsoft Jet formats except AM and PM are supported. In the absence of a format string, the short date picture and time options in the Windows Control Panel are used.
DecimalSymbol | Can be set to any single character that is used to separate the integer from the fractional part of a number. If this entry is absent, the default value in the Control Panel is used.
NumberDigits | Indicates the number of decimal digits in the fractional portion of a number. If this entry is absent, the default value in the Control Panel is used.
NumberLeadingZeros | Specifies whether a decimal value less than 1 and greater than – 1 should contain leading zeros; this value can either be False (no leading zeros) or True.
CurrencySymbol | Indicates the currency symbol to be used for currency values in the text file. Examples include the dollar sign ($) and Dm. If this entry is absent, the default value in the Control Panel is used.
CurrencyPosFormat Can be set to any of the following values:
  • Currency symbol prefix with no separation ($1)

  • Currency symbol suffix with no separation (1$)

  • Currency symbol prefix with one character separation ($ 1)

  • Currency symbol suffix with one character separation (1 $)

If this entry is absent, the default value in the Control Panel is used.

Option | Description
CurrencyDigits | Specifies the number of digits used for the fractional part of a currency amount. If this entry is absent, the default value in the Control Panel is used.
CurrencyNegFormat | Can be one of the following values: ($1) $1 $–1 $1– (1$) –1$ 1–$ 1$– –1 $ –$ 1 1 $– $ 1– $ –1 1– $ ($ 1) (1 $) The dollar sign is shown for the purposes of this example, but it is replaced with the appropriate CurrencySymbol value in the actual program. If this entry is absent, the default value in the Control Panel is used.
CurrencyThousandSymbol | Indicates the single-character symbol to be used for separating currency values in the text file by thousands. If this entry is absent, the default value in the Control Panel is used.
CurrencyDecimalSymbol | Can be set to any single character that is used to separate the whole from the fractional part of a currency amount. If this entry is absent, the default value in the Control Panel is used.


Examples of Schema.ini Files

This section shows two examples of Schema.ini files. The first example is the schema information file for a comma-delimited text file, and the second example is for a fixed-length file.

The first example is the Schema.ini file for the text table Sample1.txt in the JetBook\Samples\Text\CommaDelimited directory. This file does not contain header information in the first row and is in comma-delimited format. The character set is ANSI and there are five columns. Each of the columns is designated by name, along with the corresponding data type, but only three columns include a width designator. (Width designators are optional for character-delimited files.)

[SAMPLE1.TXT]
ColNameHeader=False
Format=CSVDelimited
CharacterSet=ANSI
Col1=ProductID Short
Col2=ProductName Char Width 30
Col3=QuantityPerUnit Char Width 30
Col4=UnitPrice Currency
Col5=Discontinued Bit

The second example is the Schema.ini file for the text table Sample2.txt in the JetBook\Samples\Text\FixedLength directory. This file does not contain header information in the first row and is in fixed-length format. The character set is ANSI and there are five columns. Each of the columns is designated by name, along with the corresponding data type, and each includes a width designator, as required.

[SAMPLE2.TXT]
ColNameHeader=False
Format=FixedLength
CharacterSet=ANSI
Col1=CategoryID long width 11
Col2=CategoryName text width 15
Col3=Description memo width 513


Linking a Comma-Delimited Text File

The procedures for linking a comma-delimited file are similar to those for linking other database tables. If you want Microsoft Jet to use the information in the Schema.ini file, however, you must place the data file and the Schema.ini file in the same directory. The following example illustrates how to link a comma-delimited text file. In this example, strDbPath is the path to the database that will contain the linked table:

Dim dbs As Database
Dim tdf As TableDef
Dim qdf As QueryDef
Dim rst As Recordset

' Open database.
Set dbs = OpenDatabase(strDbPath)

' Create a table definition for text table.
Set tdf = dbs.CreateTableDef("TextProducts")

' Provide source database type, database name with Connect property.
tdf.Connect = "TEXT;DATABASE=C:\JetBook\Samples\Text\CommaDelimited"

' Provide name of text table with the SourceTableName property.
tdf.SourceTableName = "Sample1.txt"

' Append TableDef object to the TableDefs collection to create the link.
dbs.TableDefs.Append tdf

' Run a simple query to ensure the data is accessible.
Set qdf = dbs.CreateQueryDef("")
qdf.SQL = "SELECT Sum(UnitPrice) AS Tally FROM TextProducts;"
Set rst = qdf.OpenRecordset()
MsgBox "Total Sales = $" & rst!Tally
rst.Close
dbs.Close


Special Considerations for Microsoft Exchange and Microsoft Outlook Folders and Address Books

If you have installed the Microsoft Jet IISAM driver for Microsoft Exchange and Outlook, you can use DAO objects and methods to link or import data stored in Microsoft Exchange and Outlook folders to Microsoft Access tables. The Microsoft Exchange IISAM driver lets you access data stored remotely on a Microsoft Exchange server or data stored locally in offline folder (.ost), personal folder (.pst), or personal address book (.pab) files.

To install the most current version of the driver, download and run Wzmapi80.exe from http://www.microsoft.com/AccessDev/AccWhite/ExchWiz.htm. The Wzmapi80.exe file will install and register the most current version of Msexch35.dll. It also installs new Microsoft Exchange and Outlook wizards that you can use from the Microsoft Access user interface to import and link Microsoft Exchange and Outlook data. The Microsoft Exchange and Outlook wizards work only in Microsoft Access 97.

Note The Wzmapi80.exe file is also located in the ExchWizard folder on the companion CD-ROM.

Using the Microsoft Exchange IISAM driver to access Microsoft Exchange and Outlook data is primarily useful for reading information from message folders, public folders, address books, and other items. You cannot modify any existing items. And, while it is possible to write new items to Microsoft Exchange and Outlook message stores, not all fields are available or updatable, which limits your ability to create some items. For more information about these issues, see the white paper “Accessing Microsoft Exchange and Microsoft Outlook Data Using Visual Basic,” located in the Papers folder on the companion CD-ROM.


Creating the Connection String

The following sections contain the information you need to build a connection string to access data in Microsoft Exchange and Outlook folders.

Understanding the Elements of the Connection String

The following table contains a description of the required and optional elements used to create the identifier, source, and options portions of a connection string for a Microsoft Exchange or Outlook external data source.

Element | Description
Source | Exchange 4.0 (required.) The setting works with Microsoft Exchange 4.x, 5.0, and Outlook data.
Table name | MAPILEVEL=storage|folders (required). The storage entry is the exact name of a mailbox on a server, a personal folder, or public folder; and folders is the path to the folder immediately above the folder you want to connect to. The storage entry and the pipe character (|) are always required. The folders entry is required to access folders below the top-level of folders within storage. When you are listing nested folders, separate each folder name with a backslash (\).
Source table type | TABLETYPE=0 (for folders) (default) TABLETYPE=1 (for address books; required)
Database name | DATABASE=path (required). The path entry is the path to a Microsoft Access database (.mdb) in which to store system tables used by the driver (usually the current database).
Profile name | PROFILE=profile (optional; if not specified, the default profile is used). The profile entry is the name of the Microsoft Exchange or Outlook profile to use.
Password | PWD=password (optional; not required if your network logon password is passed to your Microsoft Exchange server). The password entry is the Microsoft Exchange or Outlook logon password.

Building the Connection String

All IISAM drivers accept a connection string that uses the same basic syntax:

"identifier, source, options"

When you are building a connection string to a Microsoft Exchange or Outlook data source, the source argument will always be Exchange 4.0; even if you are working with Microsoft Exchange 5.0 or Outlook. The source argument consists of the table name, source table name, and the database name. The options argument, if used, can consist of settings for either or both the profile name and the password.

The following code sample illustrates a connection string used to link a Microsoft Exchange folder named SampleMessages, which is a subfolder of the Inbox folder, to a Microsoft Jet table named SampleMessages. In this example, strDbPath is the path to the database that will contain the linked table:

Dim dbs As Database
Dim tdf As TableDef
Dim strConnect As String

' Open a Microsoft Jet database.
Set dbs = OpenDatabase(strDbPath)
' Create connection string.
strConnect = "Exchange 4.0;MAPILEVEL=Mailbox - " & _
    "David Jones|Inbox;TABLETYPE=0;" & _
    "DATABASE=" & dbs.Name & ";"
' Create TableDef object in Microsoft Jet database.
Set tdf = dbs.CreateTableDef("SampleMessages")
' Link TableDef object to Microsoft Exchange data source.
With tdf
    .Connect = strConnect
    .SourceTableName = "SampleMessages"
End With
dbs.TableDefs.Append tdf

In the example above, the connection string identifier is Exchange 4.0; and the source is MAPILEVEL="Mailbox - Dave Jones|Inbox;TABLETYPE=0;DATABASE=" & dbs.Name & ";".

The MAPILEVEL= portion of the connection string specifies the path to the folder immediately above the folder you want to access. The SourceTableName property specifies the name of the folder you want to access. The information that appears to the left of the pipe character ( | ) can be any valid mailbox name or any valid local personal folder file. You must preserve any spaces and capitalization that appear in the name.

The DATABASE= portion of the connection string must contain the path and name of an existing Microsoft Access 97 database. Although you may specify any database here, it is preferable to use the current database for this purpose. The specified database is used to store system tables that contain information about the structure of the Microsoft Exchange or Outlook folder.

In the options argument, if the PROFILE argument is omitted, the default profile is used. If the PWD argument is omitted, your network logon password is used. If, for some reason, either of these default values is invalid, you are prompted to enter the correct information.


Connecting to a Microsoft Exchange or Outlook Folder

You can open a Microsoft Exchange or Outlook folder directly, link a folder to a Microsoft Access 97 table, or import the data in a folder to an existing Microsoft Access table. The following sections describe these methods of accessing data stored in Microsoft Exchange or Outlook folders.

Opening a Folder

To open a Microsoft Exchange or Outlook folder directly, use the connection string as an argument to the OpenDatabase method. Even though you are opening the folder directly, you must still specify a Microsoft Access database file that will contain the system files necessary to maintain the connection. This technique is useful when you don’t need to maintain a persistent connection to the data in a folder.

The following code fragment uses the OpenDatabase method to open the personal folder store, and prints the name of each top-level folder to the Debug window. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef
Dim strConnect As String

strConnect = "Exchange 4.0;MAPILEVEL=Personal Folders|;"
' Open Microsoft Exchange folder.
Set dbs = OpenDatabase(strDbPath, 0, 0, strConnect)

' Open each top-level folder.
For Each tdf In dbs.TableDefs
    Debug.Print tdf.Name
Next tdf
Debug.Print "Done"
dbs.Close
Linking to a Folder

When you create a link to a Microsoft Exchange or Outlook folder, the folder is treated as a linked table. The following example creates a linked table in the current database that contains all the entries in the Inbox folder. In this example, strDbPath is the path to the database:

Dim dbs As Database
Dim tdf As TableDef
Dim strConnect As String

' Open database.
Set dbs = OpenDatabase(strDbPath)

' Build the connection string.
strConnect = "Exchange 4.0;MAPILEVEL=Mailbox - David Jones" _
    & "|;TABLETYPE=0;" _
    & "DATABASE=" & dbs.Name & ";"

' Create a TableDef object. The name specified for the
' TableDef object is displayed as the name of the link
' in the Database window.
Set tdf = dbs.CreateTableDef("Linked Exchange Folder")
With tdf
    ' Set connection information and specify a folder.
    .Connect = strConnect
    .SourceTableName = "Inbox"
End With
' Append the TableDef object to create the link.
dbs.TableDefs.Append tdf
Importing a Folder

To import the data in Microsoft Exchange or Outlook folders to an existing Microsoft Access table, you need to make sure that the table that will receive the data contains the fields you want to import. Once you have a table in the correct format, you establish a temporary link to the folder and run an append query to add records to the specified table.

See Also For more information about creating a table and importing data from a Microsoft Exchange or Outlook folder, see the white paper “Accessing Microsoft Exchange and Microsoft Outlook Data Using Visual Basic,” located in the Papers folder on the companion CD-ROM.


Special Considerations for HTML Files

Hypertext Markup Language (HTML) is a standard for presenting information over an intranet or the Internet. HTML files are text files that contain tags that specify how information in the file is displayed. You can use the HTML table tags (<TD> and </TD>) to embed one or more tables in an HTML file. The Microsoft Jet HTML IISAM driver can access information in HTML files that is formatted as tabular data or data in HTML files that is formatted as list data.

You can directly open, link to, or import data stored in HTML tables. The methods for linking or importing HTML data are similar to those used for other external data sources.

Understanding the Elements of the Connection String

The following table contains a description of the required elements used to create the identifier, source, and options portions of a connection string for an HTML data source.

Element | Description
Source | HTML Import; (required).
Database name | The fully qualified path to the HTML file that contains the data you want to import.
Source table name | The table named by <CAPTION> tags (if they exist) within the HTML file that contains the data you want to import.

The Microsoft Jet HTML IISAM driver assumes that the first row of table data does not contain field names. You can override this behavior by adding the HDR=YES element to the connection string to indicate that the first row of data does contain field names.

Table Naming Conventions

The source table name element of the source argument is the caption of the table within the HTML file, if the table has a caption. If the table does not have a caption, and it is the only table in the file, use the title of the HTML file to refer to the table. If more than one table exists and none of the tables has a caption, you refer to them sequentially as Table1, Table2, and so on. The IISAM driver interprets these references as the first unnamed table in the HTML file, the second unnamed table in the HTML file, and so on.

Column Names and Data Types

The Microsoft Jet HTML IISAM driver reads the data in the HTML table and chooses the data type for the data in the table columns by interpreting the data contained in the table cells. For example, if any of the data in a column is text, the IISAM driver interprets the column data type as text, with a field size of 255 characters. If the data in a column is numeric, the IISAM driver interprets the column data type as Long or Double, depending on whether most of the values are integer or floating point. If the data in a column is a combination of numeric and text values, the column data type is interpreted as text.

You can force the Microsoft Jet HTML IISAM driver to interpret column data as a specific data type by creating a Schema.ini file that contains information about the data type for each column of data. The only difference between a Schema.ini file created for an HTML table and one created to import text file data is that the section heading contains the name of the table, not the name of the file. For more information about Schema.ini files, see the “Examples of Schema.ini Files” section earlier in this chapter.

Linking HTML Files

The following code fragment shows how you can link to the table named Sales in an HTML file named Sales_1.html. In this example, strDbPath is the path to the database:

Sub LinkToHTMLTable()
    Dim dbs As Database
    Dim tdf As TableDef
    Dim strConnect As String

    Set dbs = OpenDatabase(strDbPath)
    ' Specify connection string.
    strConnect = "HTML Import;" & _
        "DATABASE=C:\JetBook\Samples\HTML\Sales_1.html;"
    ' Create new TableDef object.
    Set tdf = dbs.CreateTableDef("LinkedHTMLTable")
    ' Link table to HTML data source.
    With tdf
        .Connect = strConnect
        .SourceTableName = "Sales"
    End With
    dbs.TableDefs.Append tdf
End Sub

The following code fragment shows how to set the connection string to an HTML file on an HTTP server:

strConnect = "HTML Import;DSN=Sales_1 " & _
    "Link Specification;HDR=NO;IMEX=2;" & _
    "DATABASE=HTTP://WebServer/YTDSales/Sales_1.html;"


Maintaining Links

Links to external tables are convenient because they store all the connection information you need to access the table in the future. Connection information for linked tables resides in the Connect property of the TableDef object for that table in your Microsoft Jet database. If you move or delete the external table, you must update the corresponding TableDef object to reflect this change.


Updating Links to Tables That Have Moved

If you move a table that you previously linked, you need to refresh the link before you can access the table again. To refresh a link, you must reset the Connect property of the TableDef object to point to the table’s new location. Then, use the RefreshLink method of the TableDef object to update the link information.

The following procedure shows how to re-create the link to a FoxPro table that was moved to another directory after it was linked to a Microsoft Jet database. The TableDef object LostLink is linked to the FoxPro table LostLink.dbf, which was originally in the JetBook\Samples\FoxTables\Sales directory but is now in the JetBook\Samples\FoxTables\LostLink directory. When you try to open the linked table, Microsoft Jet reports that it couldn’t find the object “LostLink.”

  1. Try to open the table LostLink.dbf through Microsoft Access. Microsoft Access displays the message “Couldn’t find object ‘LostLink’.”

  2. Run the following code, where strDbPath is the path to the Microsoft Jet sample database:
    Dim dbs As Database
Dim qdf As QueryDef
Dim rst As Recordset

Set dbs = OpenDatabase(strDbPath)
' Update connection string to include new location.
dbs!LostLink.Connect = "FoxPro 3.0" & _
    ";DATABASE=C:\JetBook\Samples\FoxTables\LostLink"
' Refresh link.
dbs.TableDefs!LostLink.RefreshLink

' Run a simple query to ensure the data is accessible.
Set qdf = dbs.CreateQueryDef("")
qdf.SQL = "SELECT Sum(Sales) As Tally FROM LostLink;"
Set rst = qdf.OpenRecordset()
MsgBox "Total Sales = $" & rst!Tally
rst.Close
dbs.Close
  3. Try to open the table LostLink.dbf through Microsoft Access once again. Microsoft Access opens the linked table.


Deleting Links to External Tables

You can also delete the TableDef object associated with an external table. This removes the table’s icon from your Microsoft Jet database along with the associated linking information. When you delete a TableDef object that defines a linked table from your Microsoft Jet database, the external table is unaffected. You use the Delete method of the TableDefs collection object to delete a table from a Microsoft Jet database. For example, the following code fragment deletes the LostLink table from the database:

dbs.TableDefs.Delete "LostLink"


Creating an External Table

You can create a table in an external data format through Microsoft Access by exporting the structure and data from a Microsoft Jet object to the external data format.

You create a table in an external format by using the TransferDatabase method of the DoCmd object.

Important This example uses the TransferDatabase method, which is only available in Microsoft Access; you cannot create a database or database table in an external data format by using VBA.

After you’ve created the table, you can access it from the table’s native application or you can link or open it as you would any other external table. The following example demonstrates how to create a structure in a Microsoft Access table and use that structure as the basis for a FoxPro table:

Sub CreateExternalFoxProTable()
    Dim dbs As Database
    Dim tdf As TableDef
    Dim fldContact As Field, fldPhone As Field
    Dim rst As Recordset
    Const conObjectExists = 3012

    On Error GoTo Err_CreateExternalFoxProTable
    ' Create a Database object pointing to the current database. This
    ' code should be run from within a Microsoft Access database.
    Set dbs = CurrentDb
    ' Create a table definition for the Microsoft Access table that will
    ' provide the structure information for the FoxPro table.
    Set tdf = dbs.CreateTableDef("AccessTable")
    With tdf
        ' Create two text fields in the Microsoft Access table.
        Set fldContact = .CreateField("Contact", dbText)
        fldContact.Size = 30
        Set fldPhone = .CreateField("Phone", dbText)
        fldPhone.Size = 25
        ' Append the newly created fields to the Microsoft Access table.
        .Fields.Append fldContact
        .Fields.Append fldPhone
    End With
    ' Append the TableDef object to the TableDefs collection.
    dbs.TableDefs.Append tdf

    ' Use the TransferDatabase method to export the Microsoft Access
    ' table's structure to a FoxPro table; this creates a new FoxPro
    ' table in the specified directory.
    DoCmd.TransferDatabase acExport, "FoxPro 3.0", _
        "C:\JetBook\Samples\FoxTables", acTable, "AccessTable", "FoxTable"
    ' Delete the TableDef object for the Microsoft Access table.
    dbs.TableDefs.Delete "AccessTable"

    ' Link the new table and test it by inserting a few records.
    Set tdf = dbs.CreateTableDef("LinkedFoxTable")
    With tdf
        .Connect = "FoxPro 3.0;DATABASE=C:\JetBook\Samples\FoxTables;"
        .SourceTableName = "FoxTable"
    End With
    dbs.TableDefs.Append tdf
    Set rst = dbs.OpenRecordset(tdf.Name)
    With rst
        .AddNew
        !Contact = "Nancy Davolio"
        !Phone = "555-9857"
        .Update

        .AddNew
        !Contact = "Andrew Fuller"
        !Phone = "555-9482"
        .Update

        .AddNew
        !Contact = "Janet Leverling"
        !Phone = "555-3412"
        .Update

        .AddNew
        !Contact = "Margaret Peacock"
        !Phone = "555-8122"
        .Update
        .Close
    End With

Exit_CreateExternalFoxProTable:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Sub

Err_CreateExternalFoxProTable:
    If Err = conObjectExists Then
        dbs.TableDefs.Delete tdf.Name
        Resume
    Else
        MsgBox "Error: " & Err.Number & " - " & Err.Description
        Resume Exit_CreateExternalFoxProTable
    End If
End Sub


Performance Optimization and Troubleshooting Tips

When you link or open external tables in a Microsoft Jet database, it’s easy to forget that you’re accessing the data through an additional service layer (the IISAM or ODBC driver). When you consider the amount of overhead involved in maintaining a connection, transferring and translating data, and maintaining a remote index in a foreign format, you can see why certain Microsoft Jet operations are significantly slower when executed against an external data source. The first part of this section discusses one way to dramatically increase performance when you only need to read data in a single pass. The remainder of the chapter focuses on two common problems: connections and temporary space.


Using Forward-Only-Type Recordset Objects

If your application only needs to read through records in an external table in a single pass, you can dramatically increase the performance of your application by using the forward-only-type Recordset object. Although this type of Recordset object limits the direction of travel through the recordset and restricts the use of a few other methods, it offers perhaps the greatest performance improvement possible when used with large external tables. In an ODBCDirect workspace, the default Recordset object is the forward-only type. The following methods are unavailable when you use the dbOpenForwardOnly constant to create a forward-only-type Recordset object. The Bookmark property is also unavailable.

Unavailable methods with forward-only-type Recordset objects
Clone | FindFirst | FindLast
FindNext | Move (with any value other than 1) | MoveFirst
MoveLast | MovePrevious | OpenRecordset

See Also For more information about the forward-only-type Recordset object, see Chapter 5, “Working with Records and Fields.”


Resolving Connection Problems

If your application cannot connect to the external data source, check the following:

  • Make sure you can access the external table through the file system. If the table is stored on a network share, for example, verify that you have sufficient privileges to access that share.

  • Check that the external table is accessible through its native application. For example, if you’re trying to open a Microsoft Excel worksheet, verify that you can open the worksheet in Microsoft Excel.

  • Check the code that establishes the connection to the external table carefully to make sure that it follows the guidelines in the “Establishing a Connection” section earlier in this chapter.

  • If you were able to connect to the external table through your application before but you cannot now, make sure that the table is still in its original location. If the table is linked in your Microsoft Jet database and it has been moved, see the “Maintaining Links” section earlier in this chapter.

  • If you receive the message “Unable to find installable ISAM,” check the source database type value in the table in the “Specifying Source Database Types” section earlier in this chapter. (Remember that Excel 7.0 is not a valid source database type; use Excel 5.0 instead.)

  • If the source database type matches one of the values in the table in the “Specifying Source Database Types” section, make sure that the IISAM driver resides in the directory specified in the registry.

  • If you are supplying arguments to a case-sensitive data source, make sure that the arguments are of the correct form and case.

  • Check the system on which the external data source resides for sufficient disk space.


Understanding Temporary Space Needs

Microsoft Jet creates temporary indexes on your local machine when you query a database. This occurs even when the database is remotely located and in a different file format. These indexes can be as small as a few kilobytes or as large as several megabytes, depending on the data source.

You must make sure users of your application have sufficient resources to accommodate these temporary space needs. All users should have a defined value for the TEMP environment variable and enough disk space to store the indexes that Microsoft Jet may create.

Important If users lack the disk space necessary to store temporary indexes, your application may behave unpredictably.


Chapter 9 Developing Client/Server Applications

This chapter introduces you to the concepts of client/server development using the Microsoft Jet database engine. Microsoft Jet contains a number of features that enable you to create client/server applications with any application that can use the Data Access Objects (DAO) programming interface, such as Microsoft Access, Microsoft Visual Basic, or Microsoft Excel. Microsoft Jet is tightly integrated with the Open Database Connectivity (ODBC) standard, which enables access to the most popular and widely used SQL database servers. With DAO 3.5, you can also access ODBC data sources without loading Microsoft Jet by using an ODBCDirect Workspace object.

This chapter discusses the differences between using Microsoft Jet and ODBCDirect in client/server applications, and gives the advantages of each. It describes the ODBCDirect object model, and provides information on how to implement your client/server application using the objects, methods, and properties of both Microsoft Jet and ODBCDirect Workspace objects. The code examples in this chapter work with Microsoft SQL Server 6.5, and you can easily modify them to work with other ODBC databases.

Most of the other chapters in this guide assume you are already familiar with the basic concepts of database design. However, because client/server computing is a new concept to most developers, the first section of this chapter contains introductory material.

Introduction to Client/Server Design

Using DAO to Connect to ODBC Data Sources

Creating Client/Server Applications Using Microsoft Jet

The Object Model for ODBCDirect Workspaces

Using ODBCDirect

Managing Connection Resources

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

Some of the examples shown in this chapter exist in the BookSales and BookSalesOptimized projects. These Visual Basic 5.0 projects are located in the JetBook\Samples\BookSales and JetBook\Samples\BookSalesOptimized subfolders. To use these applications, you must first perform the following steps:

  1. Create a data source name (DSN) that refers to the Pubs database on your own SQL Server.

  2. Change the conConnect constant to connect to your SQL Server. Include the name of the new DSN.

  3. Re-link the ODBC tables in JetBook\Samples\BookSalesOptimized \Pubs.mdb so that they point to the Pubs database on your SQL Server.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface. For more information about the BookSales and BookSalesOptimized applications, see the Readme.doc file in the JetBook\Samples\BookSales subfolder.


Introduction to Client/Server Design

In a file-server system, a copy of Microsoft Jet is running on each workstation. Microsoft Jet performs most processing on the workstation; only a minimal amount is performed on the server. The server responds to requests for data from files.

A Microsoft Jet file-server application must move a fair amount of data back and forth across the network to satisfy a user’s requests for data. For example, to edit a record on a file server, Microsoft Jet needs to read an index, retrieve the data, read and write to the lock file, and then update the index and the database itself. The performance and simplicity of the file-server architecture make it ideal for prototyping large multiuser systems and creating small- to moderate-sized applications.

See Also For more information on how the file-server environment works, see Chapter 6, “Creating Multiuser Applications.”

There are certain applications that exceed the capabilities of file-server systems. For example, once an application exceeds a certain number of concurrent users or becomes mission-critical, there are performance, security, and data integrity issues that arise that no file-server system can accommodate fully. The client/server architecture meets these requirements, but it requires you to think about your database development efforts in new ways.

A Microsoft Jet and DAO client/server application uses an ODBC driver to connect to a remote database stored on an ODBC server such as Microsoft SQL Server 6.5. In a client/server application, a front-end (or client) application, such as a Visual Basic or Microsoft Access application, runs on a local workstation and uses Microsoft Jet (or ODBCDirect) and DAO to send requests to a back-end database stored on an ODBC server. These requests take the form of SQL statements that are translated to the server’s dialect of SQL by the ODBC driver (when using Microsoft Jet or ODBCDirect), or passed directly to the server (when using pass-through queries). In a well-written client/server application, most of the processing occurs on the server itself before data is returned to the client application, which reduces network traffic. Additionally, the computer used to run an ODBC server is often more powerful than a typical file server, so it can handle more requests and process each request more quickly.

To get a general idea of some of the differences between file-server architecture and client/server architecture, consider the simple task of opening a table. The following DAO code shows how to open a table and a Recordset object in a file-server system. In this code, strDbPath is the path to the NorthwindTables database:

Dim dbs As Database
Dim rst As Recordset
Dim strName As String

Set dbs = OpenDatabase(strDbPath)
Set rst = dbs.OpenRecordset("Employees")
strName = rst!LastName
Debug.Print strName

The preceding code performs the following operations:

  1. Microsoft Jet opens the file NorthwindTables.mdb, a very inexpensive operation in terms of time and resources.

  2. The Microsoft Jet dynamic-link library (DLL) looks inside the database file for information about a table named Employees. This results in several relatively inexpensive disk reads and some memory manipulations on your user’s machine.

  3. Microsoft Jet loads details about the Employees table into the memory on your user’s workstation, such as how many fields it has, what the data types are, and which indexes are available, as well as information about exactly where in the database file the first data page can be found. Because an .mdb file is the native file format for Microsoft Jet, this operation is performed relatively quickly.

  4. Microsoft Jet reads the data from disk and makes it available to the user’s application.

In the file-server example, the OpenDatabase method is used to access data in a database. In a client/server system, there are several ways to access ODBC server data. You can:

  • Open an .mdb file and retrieve data using a linked table in the database.

  • Open an ODBC data source using the OpenDatabase method of a Microsoft Jet Workspace object.

  • Open an ODBC data source using the OpenConnection method of an ODBCDirect Workspace object. This method works directly with the ODBC driver and doesn’t require loading the Microsoft Jet engine into memory.

  • Send a pass-through query to the ODBC server using Microsoft Jet.

In a client/server system, the following steps occur when you open a linked table or a table in a remote data source:

  1. When you access data in an ODBC data source using one of the methods previously described, a connection is made to the ODBC server. This is a relatively expensive operation, which may take anywhere from a half second to several seconds.

  2. When the ODBC server receives a connection request, it sets aside memory and other resources and prepares for other requests. It then replies with a connection.

  3. Microsoft Jet or DAO uses the connection to issue a request to the server for the table data.

  4. The server returns the requested data and makes it available to the application’s user.

If you read these steps carefully, you can see the fundamental difference between file-server and client/server architectures. In a file-server system, the database engine is completely responsible for physically retrieving the data from the database. It knows explicitly where in the file to find the data, and returns it directly to the user.

In a client/server system, the client (the local front-end application) formats a request for a specific set of data and passes this request to the database server using an ODBC driver. The client serves no other function in retrieving the data. It’s up to the database server to process the request and pass the data back to the client.


Advantages of Client/Server Applications

With client/server applications, you can request exactly what you want. With file-server applications, you have a much lower degree of granularity; for example, even if you only select one record from one table in a file-server database, Microsoft Jet must read at least a 2K page. Also, by isolating all database files under the control of one database server and one physical machine, the server can provide advanced features that can’t be furnished by a file-server architecture. For example:

  • Online backup Use an automatic scheduler to back up your database without having to exclude users from the database.

  • Durable transactions Updates made within a transaction can always be recovered or rolled back if either the client or the server computers fail.

  • Better reliability and data protection If either a workstation or file server fails while a Microsoft Jet database (.mdb) file is being written to, the database may be damaged. You can usually recover a damaged database using the Repair method, but you must have all users close the database before doing so. This rarely happens with a server database such as Microsoft SQL Server.

  • Advanced hardware support Uninterruptable power supplies, hot swappable disk drives, and multiple processors can all be added to the server with no changes to the client workstations.


General Principals in Client/Server Design

The differences between file-server and client/server systems mean that you have to design your applications in a fundamentally different way to take full advantage of the client/server architecture. A design that works well as a file-server application generally needs to be modified to work well as a client/server application. This section outlines the general principals you should follow while developing a successful client/server application.


Sets of Records

ODBC servers deal with sets of records. These are almost always the result of a SELECT statement that has been sent to the server, either explicitly by your program, or by Microsoft Jet on your behalf. Once the server has returned these records to the client, it doesn’t typically perform operations on that set of records again. For example, operations like finding a particular record, or even scrolling backwards to a previous record, are not supported by most servers. Microsoft Jet will make those operations work for you; however, these operations do come with a price attached as you’ll see in the following sections.


Network Traffic and Round Trips

When your client program is working with a server, one of the main bottlenecks is usually the network traffic that gets generated between your client application and the server application. A round trip is a conversation with the server in which Microsoft Jet asks the server for something and receives an answer. A well-written client/server application tries to minimize round trips. Indeed, Microsoft Jet itself does a lot to minimize round trips by caching results locally to avoid requesting the same information from the server multiple times. You can help Microsoft Jet minimize round trips by structuring your application in certain ways that are discussed throughout this chapter.


Converting a Microsoft Access Application Using the Upsizing Wizard

If you are converting an existing Microsoft Access application, or plan on prototyping your application in Microsoft Access before converting it to a client/server application, consider using the Upsizing Wizard to perform your initial conversion. The Upsizing Wizard is part of the Microsoft Access Upsizing Tools, which you can find on the companion CD-ROM or on the Microsoft Web site at http://www.microsoft.com/AccessDev/AccInfo/AUT97dat.htm.

The Upsizing Wizard runs as a Microsoft Access 97 add-in. The Upsizing Wizard works with Microsoft SQL Server versions 4.21, 6.0, and 6.5. It performs the following tasks:

  • Re-creates the structure of all Microsoft Access tables on your server

  • Creates indexes on your server

  • Creates as many validation rules as possible on your server

  • Replaces incrementing AutoNumber fields with Identity columns (for Microsoft SQL Server 6.0 and 6.5) or with server triggers, a special form of stored procedure that is automatically executed by the server when data in a specified table is modified (for Microsoft SQL Server 4.21 or later)

  • Creates triggers (for Microsoft SQL Server 4.21 or later) or uses Declarative Referential Integrity (for Microsoft SQL Server 6.0 and 6.5) to enforce referential integrity on your server

  • Renames server tables and columns as necessary to conform to Microsoft SQL Server naming rules

  • Exports your Microsoft Access data to your server

  • Creates links to Microsoft SQL Server tables in your Microsoft Access application

  • Creates queries as necessary to map changed SQL Server column and table names back to their original names in your Microsoft Access application

The Upsizing Wizard won’t turn your Microsoft Access application into a fully optimized client/server application, but it enables you to prototype your database’s structure using Microsoft Access design tools, and makes it much simpler to re-create your database on Microsoft SQL Server. You can then concentrate on optimizing your client/server application using the principles and techniques discussed in this chapter.


Using DAO to Connect to ODBC Data Sources

There are two different ways to use DAO to connect to ODBC data sources: through Microsoft Jet, or by means of a new technology called ODBCDirect. If you’re working with a database created with Microsoft Jet or in an external format supported by an installable ISAM, all DAO operations are processed through Microsoft Jet. If you’re working with an ODBC data source, you can either process DAO operations through Microsoft Jet, or you can use ODBCDirect to work directly with the data in the ODBC data source without using Microsoft Jet.

Whether you use DAO with Microsoft Jet or with ODBCDirect to work with an ODBC data source depends on what kind of operations you need to perform on the data source. You can use DAO with Microsoft Jet when you need to take advantage of the unique features for ODBC operations in Microsoft Jet, such as the ability to join data from different database formats.

You can use ODBCDirect when you need to run queries or stored procedures against a back-end server, such as Microsoft SQL Server, or when your client application needs only the specific capabilities of ODBC, such as batch updates or asynchronous queries. ODBCDirect can also make certain client/server operations significantly faster.

Because not all DAO features are available with ODBCDirect, DAO still supports ODBC through the Microsoft Jet database engine. You can use ODBC through Microsoft Jet, ODBCDirect, or both, with a single ODBC data source.

Which of these two methods you can use to access an ODBC data source is determined by what type of workspace you’re working in. A workspace, represented by a Workspace object, is an active session for a particular user account. A session marks a sequence of database operations. A session begins when a particular user logs on and ends when that user logs off. The operations that a user can perform during a session are determined by the permissions granted to that user.

The security system that controls permissions is determined by the kind of workspace you are using. In a Microsoft Jet workspace, the Microsoft Jet database engine enforces security for local objects, and the server you are connecting to, such as Microsoft SQL Server, enforces security for its objects. In an ODBCDirect workspace, the server you are connecting to enforces security. A workspace also provides mechanisms for performing multiple operations against a database as a single transaction that can be rolled back if it fails. If you don’t specifically create a workspace, then DAO creates a default workspace for you.

See Also For more information on transactions, see “Microsoft Jet Transactions” in Chapter 5, “Working with Records and Fields.”

Note Each type of workspace has its own object model. For more information on the object model for Microsoft Jet workspaces, see Chapter 2, “Introducing Data Access Objects.” For information on the object model for ODBCDirect workspaces, see “The Object Model for ODBCDirect Workspaces” later in this chapter.

When you’re working with an ODBC data source, you’ll need to decide whether you should use ODBC with Microsoft Jet, ODBCDirect, or both. This section discuss the advantages of both ODBC with Microsoft Jet and ODBCDirect. It also explains how to register an ODBC data source, whether you’re working with a Microsoft Jet workspace or with an ODBCDirect workspace.


Accessing ODBC Data with a Microsoft Jet Workspace

The following capabilities are supported in Microsoft Jet workspaces, but not in ODBCDirect workspaces:

  • Updatable Joins You can generally update data in Recordset objects based on multiple-table joins. Only certain multiple-table joins in ODBCDirect are updatable. For example, for a one-to-many join in ODBCDirect, users will receive errors if they attempt to update the same primary key value from two different records at the same time.

  • Support for Linked Tables You can store persistent links to server data in a local Microsoft Jet database. When you link a table, you can cache information about the table’s structure, including field and index information, in your local database. The next time you access that table, the connection is quicker because you don’t need to retrieve the structural information from the data source again.

  • Support for the Find Methods You can use the FindFirst, FindNext, FindPrevious, and FindLast methods with Recordset objects in a Microsoft Jet workspace.

  • Partial Failures of Update Queries You can set the FailOnError property of an update query so that if the query fails for some reason, the query stops, giving you the opportunity to decide whether or not you want to commit the changes made up to the point of failure.

    See Also For more information about the FailOnError property, search the Microsoft Access Help index for “FailOnError property” and the DAO Help index for “Execute method.”

  • User-Defined Properties You can customize DAO objects by adding persistent properties to existing objects. For example, you can add a Description property to an object so that you can store descriptive text about the object.

    See Also For more information about user-defined properties, see “Types of Properties” in Chapter 2, “Introducing Data Access Objects.”

  • Crosstab Queries You can use the SQL TRANSFORM statement to create crosstab queries that summarize data.

    See Also For more information, see Chapter 4, “Queries.”

  • Heterogeneous Data Access You can work with server data, native Microsoft Jet database (.mdb file) data, and external installable ISAM data such as FoxPro, Paradox, and dBASE data. You can perform joins on tables in different data sources.

    See Also For more information, see Chapter 8, “Accessing External Data.”

  • Modify Database Structure Using DAO You can use DAO to perform operations that affect the structure of your database. For example, you can create, delete, and modify tables.

    See Also For more information, see Chapter 3, “Data Definition and Integrity.”

  • Form and Control Binding If your application requires that forms or controls be bound to data in an ODBC data source, you must use Microsoft Jet. Data accessed within an ODBCDirect workspace cannot be bound to forms or controls because ODBCDirect does not support linked tables.


Accessing ODBC Data with an ODBCDirect Workspace

With ODBCDirect, you can access server data by using the existing DAO object model directly on top of the ODBC application programming interface (API). ODBCDirect implements a thin code layer over the ODBC API that establishes connections, creates cursors, and runs complex procedures using minimal workstation resources, without going through Microsoft Jet. ODBCDirect offers the following advantages:

  • Direct Access Your application can access ODBC data sources directly. You can improve performance, reduce network traffic, and take advantage of the server’s capabilities by processing more data on the server.

  • Reduced Resource Requirements You don’t have to go through Microsoft Jet, so your application requires fewer resources at the workstation. If you’re using ODBCDirect from Microsoft Access, keep in mind that Microsoft Access always loads Microsoft Jet, even though ODBCDirect operations don’t go through Microsoft Jet.

  • Improved Access to Server-Specific Functionality You can take advantage of features specific to the ODBC server that aren’t available if you’re using ODBC through Microsoft Jet. For example, in an ODBCDirect workspace, you can specify where cursors are located — on the client or on the server — for servers that support different types of cursors.

  • Asynchronous Queries You can run a query and perform other operations without waiting for the query to finish. You can then check properties to keep track of the query’s progress. You can enhance concurrency and optimize performance with asynchronous queries.

  • Batch Optimistic Updating With batch optimistic updating, you can cache recordset changes locally and then submit these changes to the server in a single batch.

  • Flexible Stored Procedure Execution You can specify input parameters and check return values from stored procedures — operations that are not possible in a Microsoft Jet workspace.

Note In an ODBCDirect workspace, you can’t modify your database’s structure using DAO methods such as the CreateTableDef method, but you can run SQL data definition language (DDL) statements using the Execute method to modify the structure of the database.

See Also For more information about these ODBCDirect features, see “The Object Model for ODBCDirect Workspaces” and “Using ODBCDirect” later in this chapter.


Registering an ODBC Data Source

Before you can use ODBC in a Microsoft Jet workspace or in an ODBCDirect workspace, you must register the ODBC data source. Registering the data source stores information about the data source in the Windows Registry and makes this information available to applications. You can register a data source from the ODBC Data Source Administrator or from Visual Basic.

Û To register a SQL Server data source by using the ODBC Data Source Administrator

  1. In Windows Control Panel, double-click the 32bit ODBC icon. In Microsoft Windows NT Workstation 3.51 and 4.0, this icon is named ODBC.

  2. Click the tab for the kind of data source name (DSN) you want to define:
    • To create a DSN that is visible only to the user currently logged on to the current computer, click the User DSN tab.

    • To create a DSN that is available to any user logged on to the current workstation, click the System DSN tab.

    • To create a disk file that contains DSN information and that can be distributed with your application or accessed from a network location, click the File DSN tab. This option is only supported for the Microsoft SQL Server driver. Microsoft Jet will return an error if you try to use a .dsn file created for other drivers.
  3. Click Add and then in the Create New Data Source dialog box, double-click the ODBC driver for the data source you want to access. For example, double-click SQL Server.

  4. In the Data Source Name box, type a data source name. This can be any string, such as SalesDB or Pubs. The string doesn’t have to correspond to the actual name of a database or table you want to access.

  5. In the Description box, type a description of the database, such as Sales Data for 1996. You can enter any text.

  6. In the Server box, type the name of the network server where your data source resides. Do not include a double-backslash (\\) before the name. If you don’t know the name of the server, you must get the exact name from your network administrator. If you are using Microsoft Windows NT Workstation, the names of registered servers will be available in the drop-down list.

  7. Click Options, and then type the exact name of the database you want to access in the Database Name box. For example, to specify the Microsoft SQL Server Pubs sample database, type Pubs.

Note This procedure describes the steps for registering a Microsoft SQL Server data source. The steps for registering other ODBC data sources may vary because the data source driver requires a different set of information. If the dialog box for the data source you selected has values not described in the preceding steps, click the Help button for more information.

In some cases, you may want to register the data source in Visual Basic code instead of relying on users to register it with the ODBC Data Source Administrator. To do this, use the RegisterDatabase method of the DBEngine object. The following example registers a data source named Pubs.

Function RegisterDB() As Boolean
    Dim str As String, errLoop As Error

    On Error GoTo Error_RegisterDB
    ' Build keywords string.
    str = "Description=SQL Server on Server Publishers" & _
        vbCr & "OemToAnsi=No" & _
        vbCr & "Network=(Default)" & _
        vbCr & "Address=(Default)" & _
        vbCr & "Server=Publishers" & _
        vbCr & "Database=Pubs"
    ' Register database.
    DBEngine.RegisterDatabase "Pubs", "SQL Server", True, str
    RegisterDB = True

Exit_RegisterDB:
    Exit Function

Error_RegisterDB:
    ' Notify user of any errors that result from
    ' invalid data.
    For Each errLoop In DBEngine.Errors
        MsgBox "Error number: " & errLoop.Number & _
            vbCr & errLoop.Description
    Next errLoop
    RegisterDB = False
    Resume Exit_RegisterDB
End Function


Creating Client/Server Applications Using Microsoft Jet

Now that you’re familiar with some of the basic concepts involved in client/server design, you probably want to see how you can use Microsoft Jet to implement your application. This section provides additional details on how the engine works in client/server architectures.

Some of the code examples in this section are from the BookSales and BookSalesOptimized sample applications, which are provided on the companion CD-ROM. The BookSales application performs a set of functions on a database, and the BookSalesOptimized application performs the same functions in a more efficient manner. For information about using these sample applications, see “Using the Code Examples in This Chapter” earlier in this chapter.

Note Although most of the techniques discussed in this section apply only to using Microsoft Jet Workspace objects, some also apply to using ODBCDirect Workspace objects. For information on using ODBCDirect Workspace objects, see “The ODBCDirect Workspace Object Model” later in this chapter.


Limiting Returned Data

Some of the strategies for converting a file-server application to client/server are easy to implement. Others may require rethinking or redesigning the way your application works. The concept of limiting the data returned by the server is one of the strategies that may require some extensive work in your conversion process. However, the performance gains are potentially very large, and the more data you have, the bigger the payoff. This optimization strategy applies to both Microsoft Jet and ODBCDirect Workspace objects.

As an example, consider the following code that opens a Recordset on the Orders table:

Set rst = dbs.OpenRecordset("Orders")

In a stand-alone or file-server environment, this command is a perfectly acceptable way of opening a table in a Microsoft Jet database. Because Microsoft Jet reads data at a page level, selecting all fields is no less efficient than selecting a single field.

However, when you open the Orders table on an ODBC server this way, the server starts sending the entire contents of the Orders table to your workstation. This can be a very slow and resource-intensive task.

If you look closely at what your application really needs, you may find that you rarely need all records from a table, or all fields in each record. In a client/server system, you want to return only the data that is absolutely necessary for the part of your application that is currently executing. The less data you request, the less time is spent getting the data. In some cases, you may even set limits on what your users can retrieve. For example, if you give users the ability to do searches and then display what data is found, before retrieving the data, you may want to check the number of records being retrieved. If it’s an extremely large number, you can prompt users to limit searches, or display a message that there may be some delays if they proceed.

The remainder of this section will discuss the three main ways to limit the amount of returned data:

  • Limiting the number of fields you select from the database

  • Limiting the number of records you select from the database

  • Limiting the number of records that can be returned from the database


Limiting the Number of Fields Selected

To limit the amount of data traveling across a network, limit the number of fields you select from the database. Although SELECT * is quick and easy to type, it may not be quick and easy for the network to return all the data. For example, consider a table with fifteen fields, one being an integer key value, and the rest being Memo and Text fields. If you are interested only in the key value, select only that field. Otherwise, your application will be subject to a performance penalty caused by retrieving unnecessary fields.

The BookSales and BookSalesOptimized sample applications don’t apply the technique of limiting fields because almost all the fields in the tables are necessary. Also, because there are no Memo or Long Binary (OLE Object) fields that could potentially slow down the application, there is no need to exclude such fields. However, if the tables used by your application have many fields, consider retrieving just the few you need to display. If the user needs to see more detailed information in a different context, your application can request additional fields using a separate query.


Limiting the Number of Records Selected

You can limit the number of records you select from a database by using a WHERE clause in your query. If the user is supplying the criteria for the WHERE clause, you can use a SELECT COUNT(*) statement with that WHERE clause to find out how many records will be returned, and then decide if you want prompt your user to limit the criteria they are using.

For example, the starting version of the BookSales sample application (JetBook
\Samples\BookSales\BookSales.vbp) loads books into the Books list box (on the BookList form) by returning all of the books, and then walking through the recordset using the FindFirst and FindNext methods to find the records that belong in the list box. Changing this to a WHERE clause as shown in the BookSalesOptimized sample application (JetBook\Samples\BookSalesOptimized\BookSalesOptimized.vbp) makes this 4.5 times faster, even on this small set of 100 records.

A sample SELECT statement with a WHERE clause looks like this:

SELECT au_id, au_lname, au_fname FROM authors WHERE state = 'CA' ORDER BY au_lname

This is a good way to structure the query if your application needs all three fields that are returned. It also illustrates the concept of limiting the number of returned records by specifying a WHERE clause. Note that the statement also orders the data before returning it by using an ORDER BY clause. Although Microsoft Jet does provide the ability to sort a recordset, for efficiency reasons, it’s always better to let the server do the sort.


Limiting the Number of Records Returned

When working with an ODBC data source, you can limit the number of records returned to a QueryDef object by setting the MaxRecords property. For example, the following code fragment creates a QueryDef object against the Microsoft SQL Server Pubs sample database and sets the MaxRecords property to limit the maximum number of records returned to 20. In this example, strDbPath is the path to the Microsoft Jet database:

Dim dbs As Database, qdf As QueryDef
Dim rst As Recordset

Set dbs = OpenDatabase(strDbPath)
' Create a QueryDef object.
Set qdf = dbs.CreateQueryDef("")

' Set the properties of the new query to retrieve data from
' the Microsoft SQL Server Pubs database, limiting the number
' of returnable records to 20.
With qdf
    .Connect = "ODBC;DATABASE=pubs;UID=sa;PWD=;DSN=Pubs"
    .SQL = "SELECT * FROM titles"
    .ReturnsRecords = True
    .MaxRecords = 20
End With

' Open a recordset against this query to work
' with the records returned.
Set rst = qdf.OpenRecordset()

See Also For more information about limiting returned data, see Chapter 4, “Queries.”


Using SQL Pass-Through Queries

SQL pass-through queries are SQL statements that are sent directly to the database server without interpretation by the Microsoft Jet database engine. SQL pass-through queries provide your application with the ability to use the features of your database server directly. This section describes the common uses of SQL pass-through queries as well as how to design and integrate them into your application.

SQL pass-through query objects are composed of two pieces of information: the SQL statement and the ODBC connection string. The SQL statement is interpreted only by the database server and must use the keywords and syntax of your server’s SQL dialect. Pass-through SQL statements cannot contain Microsoft Jet-specific elements or Visual Basic commands or functions.

See Also For information about operations that cannot be performed by your database server, see “Unsupported Operators and Functions” later in this chapter, or your database server documentation.

You typically use SQL pass-through queries to:

  • Perform data definition language (DDL) operations on the server.

  • Take advantage of server-specific features such as stored procedures.

  • Run an SQL query using the database server’s syntax.

  • Run an SQL query that returns multiple result sets.

  • Maintain user accounts or perform other system administration tasks.

  • Run maintenance operations like Microsoft SQL Server’s database consistency checker (DBCC). The DBCC is used to check the logical and physical consistency of a database, check memory usage, decrease the size of a database, check performance statistics, and so on.

  • Execute multiple INSERT or UPDATE statements in a single batch operation.

When your application executes SQL pass-through queries, Microsoft Jet opens a new connection to the database or uses an existing connection. Then, Microsoft Jet passes the SQL statement directly to the ODBC driver, without interpreting it as a Microsoft Jet SQL query (in other words, the SQL statement can contain commands that are unavailable in Microsoft Jet SQL, provided they are valid in your database server’s SQL language).

The database server interprets the statement and returns the query results (if applicable) and any error or informational messages that arise during query execution. As with other queries, if there are any results to retrieve, they are retrieved on demand as required by the application; this reduces network traffic and ensures that data is only transferred when needed.

Although SQL pass-through queries bypass the Microsoft Jet query processor, they use the Microsoft Jet Recordset processor to create and manage result sets that may be generated by your query. This lets you use foreign-dialect SQL statements to take advantage of your database server’s query processing capabilities while maintaining the power and ease-of-use of the Microsoft Jet Recordset model after the data has been retrieved.

You can create and execute an SQL pass-through query by creating a QueryDef object or by using the Execute method on the Database object.

Note If your SQL pass-through query returns a result set, the Microsoft Jet database engine creates a snapshot-type Recordset object for the results.


Using an SQL Pass-Through Query with a Stored QueryDef Object

One way to create and execute an SQL pass-through query is to create a QueryDef object.

Û To create and execute an SQL pass-through query using a QueryDef object

  1. Use the CreateQueryDef method of the Database object to create a new QueryDef object. If you plan to use the QueryDef object again, save it in the database by assigning a name; if you plan to use the query one time, pass an empty string ("") for the name argument.

  2. Set the Connect property of the QueryDef object to a valid ODBC connection string. This identifies the query as an SQL pass-through query to the Microsoft Jet database engine. Microsoft Jet will not parse the SQL code in the query when you specify an ODBC connection string.

  3. Set the SQL property of the QueryDef object to the SQL statement you want to execute against the remote database. The syntax of the SQL statement must conform to the syntax of the remote database server. If the remote database server cannot execute the statement, a trappable error occurs.

  4. If your query returns a set of records, you must also set the ReturnsRecords property of the QueryDef object to True. This instructs Microsoft Jet to prepare a Recordset object to hold the query results. If you don’t set the ReturnsRecords property to True for a query that returns records, a trappable error results.

    Note You can return multiple result sets from a single query. For information, see “Processing Multiple Result Sets” later in this chapter.

  5. Use the OpenRecordset method of the QueryDef object to open the result set.

This example shows how to create and execute a simple SQL pass-through query named Total Orders. It uses the VerboseErrorHandler function to display any errors that occur, including all ODBC errors.

Sub SQLPassThroughQueryDef(strDbPath As String)
    Dim dbs As Database, qdf As QueryDef
    Dim rst As Recordset, intN As Integer

    Set dbs = OpenDatabase(strDbPath)

    On Error GoTo Err_SQLPassThroughQueryDef
    Set qdf = dbs.CreateQueryDef("Total Orders")

    ' Set QueryDef's Connect, SQL, and ReturnsRecords properties.
    With qdf
        .Connect = "ODBC;DATABASE=pubs;UID=sa;PWD=;DSN=Pubs"
        .SQL = "SELECT * FROM Sales"
        .ReturnsRecords = True
    End With
    ' Open snapshot on query.
    Set rst = qdf.OpenRecordset(dbOpenSnapshot)

    intN = rst.RecordCount
    MsgBox intN & " records were returned by this query."

Exit_SQLPassThroughQueryDef:
    On Error Resume Next
    rst.Close
    dbs.Close
    Set dbs = Nothing
    Exit Sub

Err_SQLPassThroughQueryDef:
    ' Call error handling function to display ODBC errors.
    VerboseErrorHandler
    Resume Exit_SQLPassThroughQueryDef
End Sub

The VerboseErrorHandler function is a simple error handler that exposes errors at each level in the client/server system. This function handles errors in many of the code examples found throughout this chapter.

Function VerboseErrorHandler()
    ' Handles single or multiple Jet errors.
    Dim errX As Error

    ' Check whether ODBC errors have occurred.
    If Errors.Count > 1 Then
        ' Loop through Errors collection.
        For Each errX In Errors
            MsgBox "Error " & errX.Number & _
                    ": " & errX.Description
        Next errX
    Else
        ' Display single VBA error.
        MsgBox "Error " & Err.Number & _
            ": " & Err.Description
    End If
End Function


Executing SQL Pass-Through Queries with the Execute Method

Another way to create and execute an SQL pass-through query is to use the Execute method on the Database object.

Û To execute an SQL pass-through query by using the Execute method

  1. Open the remote database, supplying a valid connect string for the connect argument.

  2. To run the query, use the Execute method of the Database object and specify the dbSQLPassThrough constant in the options argument.

This example shows how to execute an SQL pass-through query from code by using the Execute method.

Sub SQLPassThroughExecute()
    Dim dbs As Database, strSQL As String

    On Error GoTo Err_SQLPassThroughExecute
    ' Open remote database.
    Set dbs = OpenDatabase("", False, False, _
        "ODBC;DATABASE=pubs;UID=sa;PWD=;DSN=Pubs")
    strSQL = "UPDATE Titles SET Royalty = 33 WHERE Type = 'Business'"
    ' Call Execute method with pass-through flag.
    dbs.Execute strSQL, dbSQLPassThrough

Exit_SQLPassThroughExecute:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Sub

Err_SQLPassThroughExecute:
    VerboseErrorHandler
    Resume Exit_SQLPassThroughExecute
End Sub

Note that you can also execute an SQL pass-through query by opening a Microsoft Jet database and setting the Connect property of the Database object to point to the remote database server you want to query. Then call the Execute method of the Database object with the dbSQLPassThrough option. For example, you could rewrite the previous procedure with the following code, where strDbPath is the path to a Microsoft Jet database:

Dim dbs As Database, strSQL As String

' Open local Microsoft Jet database.
Set dbs = OpenDatabase(strDbPath)
dbs.Connect = "ODBC;DATABASE=pubs;UID=sa;PWD=;DSN=Pubs"
strSQL = "UPDATE Titles SET Royalty = 33 WHERE Type = 'Business'"
' Call Execute method with pass-through flag.
dbs.Execute strSQL, dbSQLPassThrough

The Microsoft Jet database may or may not contain linked ODBC tables that point to tables on the remote database server.

The data loading section of the BookSales sample application (located in the Dataload.bas module) performs several cleanup tasks on the server tables before loading new data. It deletes all the records in the Sales, TitleAuthor, Authors, Titles, and Stores tables that have been added since the instance of the application began running (it does not remove the original data in the Pubs SQL Server database). If this application was originally .mdb-based, it might delete these records by opening a Recordset and stepping through each record using the Delete method, but using a delete query is faster than programmatically stepping through individual records and deleting them.

When using SQL statements with a Microsoft Jet Workspace object, you can tell Microsoft Jet to assign more of the processing burden to the server. To do this, use the dbSQLPassThrough constant in the options argument of the Execute method. This instructs Microsoft Jet to send the command directly to the server without doing any of its own processing. For example:

dbs.Execute "DELETE stores", dbSQLPassThrough

Note that the DELETE statement is not valid Microsoft Jet SQL syntax. If you run this query without using the dbSQLPassThrough constant, a run-time error will occur. When sending a query directly to the server as a pass-through query, you must use the syntax and keywords of the server’s dialect of SQL, not Microsoft Jet SQL. Microsoft Jet will not translate a pass-through query.

Note All SQL queries are passed directly to an ODBC server when using an ODBCDirect Workspace object. For information on how to use ODBCDirect features, see “The Object Model for ODBCDirect Workspaces” later in this chapter.


Processing Multiple Result Sets

SQL pass-through queries can return more than one result set. When you assign the result set to a Recordset object or display the results using a Microsoft Visual Basic Data control or a Microsoft Access datasheet, form, or report, only the first result set from the query is processed. To remedy this situation, you must use a make-table query, that is, a SELECT INTO SQL statement, to capture the additional result sets into Microsoft Jet tables.

When Microsoft Jet returns the result sets from your query, it writes the results into new tables. The names of these new tables are derived from the table name you specify in the make-table query with ascending integer values following the initial table name. For example, if your query returns two result sets to the Results table, the first table would be named Results and the second would be named Results1. The following code fragment creates and runs a query that returns two result sets. This function calls the PrintRecordset procedure to print the recordset to the Debug window.

Sub ProcessMultipleResultSets(strDbPath As String)
    Dim dbs As Database, qdf As QueryDef, rst As Recordset

    Set dbs = OpenDatabase(strDbPath)
    ' Create saved QueryDef.
    Set qdf = dbs.CreateQueryDef("GetAuthorsTitles")

    ' Set query properties.
    With qdf
        .Connect = "ODBC;DSN=Pubs;UID=sa;PWD=;DATABASE=pubs"
        .ReturnsRecords = True
        .SQL = "SELECT * FROM authors; SELECT * FROM titles;"
    End With

    ' Execute SQL statement to create new tables.
    dbs.Execute "SELECT * INTO Results FROM GetAuthorsTitles"

    ' Open recordset on first results table.
    Set rst = dbs.OpenRecordset("Results")
    ' Call the PrintRecordset function to print the recordset.
    PrintRecordset rst
    Debug.Print

    ' Open recordset on second results table.
    Set rst = dbs.OpenRecordset("Results1")
    ' Call the PrintRecordset function to print the recordset.
    PrintRecordset rst
End Sub

Function PrintRecordset(rst As Recordset) As Boolean
    Dim fld As Field

    On Error GoTo Err_PrintRecordset

    With rst
        Do Until .EOF
            For Each fld In .Fields
                Debug.Print fld.Value
            Next fld
            Debug.Print
            .MoveNext
        Loop
    End With
    PrintRecordset = True

Exit_PrintRecordset:
    Exit Function

Err_PrintRecordset:
    MsgBox "Error: " & Err & vbCrLf & Err.Description
    PrintRecordset = False
    Resume Exit_PrintRecordset
End Function

The tables returned support all the normal table operations. Normally you would use these tables to browse the data returned and then delete them in preparation for the next time you run the SELECT INTO query. Note that you only need to create the GetAuthorsTitles query once. For this reason, you may choose to save the SELECT INTO query rather than having it embedded in code. Also, you must use "SELECT *" to have multiple tables returned — you can’t select specific fields, even if they occur in all the result sets.

Note The previous example uses a SELECT INTO statement to create temporary tables in a Microsoft Jet database. If you use a similar procedure that creates temporary tables on the server database instead, the ODBC connection to the server may hang. This happens if you use Microsoft SQL Server version 6.0 with the Select Into/Bulk Copy option set for the server database and the DSN that is used to connect to the database has the Generate stored procedure for prepared statements option set. To work around this problem, use the ODBC Data Source Administrator to modify the DSN and clear the Generate stored procedure for prepared statements option. This isn’t a problem when using a Microsoft SQL Server 6.5 database.

See Also When using an ODBCDirect Workspace object, you can use the NextRecordset method to loop through multiple result sets returned from your server. For more information, see “Retrieving Multiple Result Sets” later in this chapter.


Using Stored Queries Instead of Dynamic SQL

A common requirement of applications is the ability to build an SQL string based on values that the user provides. This technique is often referred to as using dynamic SQL. Dynamic SQL is easy to use, and you don’t have to store queries in the local Microsoft Jet database. However, performance may suffer. Using dynamic SQL requires that Microsoft Jet perform numerous steps, especially when using ODBC data, as described here:

  1. Microsoft Jet parses the SQL string.

  2. Microsoft Jet compiles the SQL string, determining which parts go to the ODBC server.

  3. Microsoft Jet builds a server-specific query and sends it to the ODBC server.

  4. The ODBC server parses and compiles the SQL string.

  5. The ODBC server retrieves the results and returns them to Microsoft Jet.

  6. DAO evaluates the results and builds the appropriate Recordset and Field objects.

The alternative to using dynamic queries is to save the queries as stored queries in the local Microsoft Jet database. After a query has been saved and compiled once, Microsoft Jet can reduce the number of steps required to execute that query.

In comparison to dynamic queries, the Microsoft Jet engine performs the same steps, except steps 1 and 2 only need to be done the first time a query is executed. After that, only steps 3 through 6 need to take place.

If you need to pass criteria to a saved query, you can create a parameter query and re-execute the query to use the criteria as new parameter values. This yields substantial savings, especially for client/server applications. If you don’t need to update the results and the query only uses server data, you can use a saved SQL pass-through query.

It’s also important to note the difference between dynamic queries and stored queries when your application is used on workstations with less than ideal memory configurations. When Microsoft Jet executes a dynamic query, it has to load several different engine modules to parse, optimize, and execute the query. Each of these modules takes up memory. When you create and store a query, Microsoft Jet parses it and stores it in a compiled state. This eliminates the need to load the modules responsible for parsing and optimization, and reduces the memory requirements of Microsoft Jet.


Using Stored Procedures

If you are using an existing client/server database, you’ll likely find that the administrator has created many stored procedures that you can use. Stored procedures are compiled code written in the server’s native programming language (generally the server-specific dialect of SQL) stored on the server that are used for a wide variety of operations. Their main purpose is to build logic into the processing of SQL statements. For instance, if you want to select data from one of two tables on the server, depending upon a value from a third table, a stored procedure would be an ideal candidate. Pass-through queries allow you to use stored procedures.

Stored procedures on the server are generally faster than dynamic SQL statements, just as stored queries in Microsoft Jet are faster than dynamic SQL. That is, a stored procedure is usually a precompiled execution plan. No parsing needs to occur, only execution.

When your application is waiting for user input, it isn’t worth the added complication of stored procedures to increase the speed, because neither the user nor the server benefits from the increased speed. Stored procedures really become useful when more complex logic is required — for example, when satisfying a business rule that a particular book has to be in stock and then checking to see if more books should be ordered on each purchase.

You can add records by using DAO code that uses the Edit or AddNew method along with the Update method on a dynaset-type Recordset object. When combined with the dbAppendOnly constant in the options argument of the OpenRecordset method, this is an efficient and simple technique. For specialized scenarios, however, you may find that it’s worth creating your own stored procedures.

The BookSalesOptimized sample application creates the following stored procedure on the SQL Server when you run it:

CREATE PROCEDURE InsertSale(@stor_id char(4),
@ord_num varchar(20),
@ord_date datetime,
@qty smallint,
@payterms varchar(12),
@title_id varchar(6))
AS
BEGIN
INSERT INTO sales VALUES (@stor_id, @ord_num, @ord_date, @qty, @payterms, @title_id)
RETURN
END

This stored procedure adds a new record to the Sales table. You can call this procedure by using the Execute method of a Database object with the dbSQLPassThrough constant specified in the options argument, and supplying a value for each of the six parameters. The values you supply for parameters are inserted into fields in the table.

For example, you could call the InsertSales stored procedure as follows, where dbs is a Database object representing the remote database, and strStoredProcCall is a string variable:

strStoredProcCall = "InsertSale '6380', 'SALES2', '5/6/97 7:16:12 PM'", _
    & "1, 'Net 30', 'PS2091'"
dbs.Execute strStoredProcCall, dbSQLPassThrough

See Also For more information about queries, see “Using SQL Pass-Through Queries” earlier in this chapter. You can use input parameters and return values with stored procedures when using ODBCDirect QueryDef objects. For more information, see “The Parameter Object and the Parameters Collection” and “Working with Stored Procedures” later in this chapter.


Choosing a Recordset Type

This section discusses the types of Recordset objects available through Microsoft Jet and how each type affects the performance and operation of your application. For information on ODBCDirect Recordset objects, see “Using Cursors in ODBCDirect Workspaces” later in this chapter.

There are four types of Recordset objects: table-type, which are a representation of actual database tables; dynaset-type, which can be updated; snapshot-type, which cannot be updated; and forward-only-type, which can’t be updated and can only be scrolled forward. Since you cannot create a table-type Recordset object based on a remote data source through ODBC, the remainder of the discussion focuses on dynaset-type, snapshot-type, and forward-only-type Recordset objects.

If you do not explicitly specify what type of Recordset objects to use in your code, by default Microsoft Jet uses the one with the most functionality. For remote databases, this is a dynaset-type Recordset object. If you don’t need to update your data, you will get better performance by explicitly creating a snapshot-type Recordset object.

The decision to use a dynaset-type versus a snapshot-type or forward-only-type Recordset object is based on resource, speed, and performance considerations in addition to the basic needs of the application (such as the ability to update remote data). For example, if your application doesn’t need update capabilities and the Recordset object you want to use contains fewer than 500 records, a snapshot-type Recordset object improves the overall data access speed of your application. However, if the Recordset object you’re using is large or contains Memo or Long Binary (OLE Object) fields, using a dynaset may be more efficient. Also, if you only need to move forward through the Recordset object, you can achieve even better performance by using a forward-only-type Recordset object.


Dynaset-Type Recordsets

In its simplest form, a dynaset-type Recordset object is a collection of bookmarks that allow each record in a server database table to be uniquely identified. Each bookmark corresponds to one record on the server. Normally, the value of the bookmark corresponds to the primary key value for that record. For example, if you have an Orders table on the server database with a primary key on its OrderID field, then internally to Microsoft Jet, the dynaset contains all the OrderID values corresponding to the records that satisfy the query that was issued.

See Also For information on how these bookmarks are gathered, and in particular when they are gathered, see “Recordset Population” later in this chapter.

Note that if you try to examine the contents of the Bookmark property of a dynaset, it will not contain the value in the primary key field. Exactly what it contains varies with the implementation of the recordset. Although you should never rely on it, the Bookmark property will probably contain the page number in the temporary Microsoft Jet database where the primary key value is stored. The reason the engine does not use primary key field values as bookmark values is that dynasets can be based on a multiple-table join. In such a case, there are several primary key values stored by Microsoft Jet — one for each record in each table involved in the join.

When you access the data in dynaset fields, Microsoft Jet uses the bookmark for the record to issue a query in the following form:

"SELECT field1, field2,... FROM ORDERS WHERE ORDERID=bookmark"

This statement is then sent to the server. (For performance reasons, Microsoft Jet includes up to ten bookmarks in the WHERE clause.) Then, as you request the data, Microsoft Jet calls the ODBC SQLGetData function on a field-by-field basis to return the data from each field.

One of the most important implications of this field-by-field behavior is that data for a field is not retrieved from the server unless that field is explicitly retrieved by your application’s code. For example, only if your code contained a line such as

strNew = rstOrders!Photo

would the Photo field be retrieved from the server. This behavior is of particular interest if the table you are accessing has Long Binary (OLE Object) or Memo fields that contain large amounts of data. If you don’t reference those large fields in your code, Microsoft Jet does not have to retrieve them from the server.

Once you have the bookmarks you need, the most time-consuming part of using a dynaset is retrieving the data. To speed this up, you can tell a Recordset to cache the data that it retrieves and keep it in memory so that if you need it again you can avoid retrieving it from the server again. You can control dynaset caching by using the FillCache method and the CacheStart and CacheSize properties of the Recordset object.

See Also For more information, see “Caching Remote Data” later in this chapter.


Snapshot-Type Recordsets

When compared with dynasets, snapshots are fairly simple to understand — they are a complete copy of all the requested fields in your query. As you move through a snapshot-type Recordset for the first time, all data is copied first into memory and then, if need be, into a temporary Microsoft Jet database in the temporary directory on the user’s workstation. The resulting set of data is read-only and you can scroll through it forward and backward.

See Also For information about when the data is retrieved, see “Recordset Population” later in this chapter.


Forward-Only-Type Recordsets

By default, when you request a dynaset- or snapshot-type Recordset object, you can scroll through the records in both directions. Forward-only-type Recordset objects have an additional feature that can conserve connection resources by allowing scrolling in only one direction. This is helpful when you only need to make a single pass through the Recordset.

You can specify a forward-only-type Recordset object by specifying the dbOpenForwardOnly constant in the type argument the OpenRecordset method. This retrieves the result set and places it in a non-scrollable buffer, which conserves system memory. Because you can only scroll forward through the records, you cannot use the MovePrevious or MoveFirst methods, or the Move method with a negative value in the rows argument. Less obviously, you also cannot use the MoveLast method because it implies having a set of records — with forward-only-type Recordset object, only one record exists at any given time.

A forward-only-type Recordset object does not save any data from previous records that you have retrieved. Thus, you avoid the in-memory copy and temporary database creation operations, often with considerable performance savings.


Performance Characteristics of Dynasets and Snapshots

Dynaset-type Recordset objects differ from snapshot-type and forward-only-type Recordset objects in the way they retrieve and cache data. They also differ in performance and data accessibility. Frequently, the needs of your application will dictate which Recordset type you can use. Ask yourself the following questions about your application and use the answers that follow as a guide to help you choose the right Recordset type for your application and to help you understand the tradeoffs between the two types.

  • Do you need to update the remote data?

    If you need to update the remote data, you must use a dynaset-type Recordset object. If you don’t need to update data or see changes made by other users, use a snapshot-type or forward-only-type Recordset object. Snapshot-type and forward-only-type Recordset objects are faster to open and scroll through than dynaset-type Recordset objects.

  • Is your result set large?

    If your result set contains a large number of records, a dynaset-type Recordset object is faster and more efficient than a snapshot-type or forward-only-type Recordset object. The performance difference in large Recordset objects occurs because your application must retrieve all records in a snapshot-type or forward-only-type Recordset object when you move from the first record to the last. In a dynaset-type Recordset object, your application only needs to retrieve the bookmark fields initially. As each record, or group of records, in the dynaset-type Recordset object is visited, the application retrieves the data from the server that corresponds to the bookmarks.

    If your result set contains a small number of records, a snapshot-type or forward-only-type Recordset object is faster and more efficient than a dynaset-type Recordset object. The performance difference in small Recordset objects occurs because your application can retrieve and hold a few hundred complete records more easily than it can retrieve and hold a few hundred bookmarks and subsequently retrieve the data from the server that corresponds to the bookmarks at the application’s request. This is especially true of scrolling speed: Snapshot-type or forward-only-type Recordset objects are faster because all the data is cached locally.

  • Do you need to retrieve data from all fields in the remote table?

    If your application doesn’t need all fields in the remote table, you can improve opening and scrolling performance in all types of Recordset objects by limiting the fields retrieved from the server.

    See Also For more information about limiting the fields retrieved from the server, see “Limiting Returned Data” earlier in this chapter.

  • Does your Recordset contain Long Binary (OLE Object) or Memo fields?

    For dynaset-type Recordset objects, if your application retrieves records that include Long Binary (OLE Object) or Memo fields and you don’t need to view or update these objects, you can improve performance by ensuring that they aren’t retrieved. Microsoft Jet only retrieves Long Binary (OLE Object) and Memo records if they are visible through the user interface of your application (a Microsoft Access form or report, for example) when the rest of the record is retrieved. You can remove the objects from the form and add a button to expose the Long Binary (OLE Object) or Memo records as needed. This will let your users retrieve the Long Binary (OLE Object) or Memo records at will, without the burden of displaying this information when it isn’t needed.

    It’s also important to understand that Memo fields are cached in a snapshot-type or forward-only-type Recordset object. Long Binary (OLE Object) fields are not.


Recordset Population

Regardless of which type of Recordset object you use, they are populated in the same way. When you first call the OpenRecordset method, Microsoft Jet returns control to your program as soon as the first record is available. At that stage, no other records have been retrieved. In other words, in the case of a dynaset-type Recordset object, only one primary key value has been retrieved from the server, and in the case of a snapshot-type or forward-only-type Recordset, only one record of data has been retrieved. If you close the Recordset at this stage, that is the sum total of all data that is retrieved.

You probably want more than one record, however. Therefore, each time you move the cursor forward (for instance, by using the MoveNext method, or the Move method with a positive value in the rows argument), Microsoft Jet retrieves either bookmarks (dynaset-type) or record data (snapshot-type or forward-only-type) until it reaches the end of the requested records. Issuing a MoveLast method forces Microsoft Jet to retrieve all data.

If you are using a Microsoft Access form or the Microsoft Visual Basic Data control, then some background retrieval is done for you. For example, while the first record is being displayed on a form, Microsoft Access is retrieving the bookmark (dynaset-type) or data (snapshot-type or forward-only-type) for subsequent records while checking the keyboard to see if the user has typed anything. The Visual Basic Data control does the same thing. You can adjust the size and rate of these retrievals with the MSysConf table described in “Using the MSysConf Table to Configure Microsoft Jet” later in this chapter. For Visual Basic, you can additionally control the dynaset cache size by setting the CacheSize property of the Recordset and then assigning the Recordset to the Data control’s Recordset property. By default, the Data control caches 50 records.


Criteria for Using the Find Methods

The Find methods (FindFirst, FindLast, FindNext, FindPrevious) locate the first, last, next, or previous record in a dynaset- or snapshot-type Recordset object that satisfies the specified criteria. A common error that developers make when converting code to work with an ODBC database is to try to replace Seek method calls with Find method calls. As a general rule, this code will be slow.

When trying to find an individual record, the best approach is to create a new Recordset with a WHERE clause that contains the criteria for the records you want to find. This will return all records that match the criteria. Because this is a single operation on the server, this approach is much more efficient than using the Find methods, especially if the number of records returned is small.


Using the Find Methods with Dynasets

If your program can’t be modified to eliminate the use of Find methods, then understanding exactly how they work will help you make the best use of them. The Find methods are fairly efficient when the search criteria is a match against fields that are indexed on the server.

Microsoft Jet recognizes these cases and executes a query on the server that returns primary keys for all the records that match the search criteria. If matching records are found, it then moves through the keys it has stored for the dynaset-type Recordset object until it finds a match. If the local recordset is small, this is a relatively quick operation.

One result of this strategy is that unsuccessful Find operations will be as fast as the server allows. This isn’t something you’ll be able to take advantage of in all scenarios, but it may be useful in some.


Using the Find Methods with Snapshots

The Find methods are also reasonably efficient with snapshot-type Recordset objects that have a small number of records. In this case, the find criteria are resolved by searching the data that has already been downloaded into the snapshot on the local machine. Of course, if the snapshot is not yet fully populated, the time required to retrieve the data increases the search time. This means that Find operations that fail will be slow if all the records need to be retrieved (unlike the previous dynaset case in which no further retrieval occurs if the Find method fails).


Conditions That Cause the Find Methods to Work Slowly

The Find methods are usually slow for dynaset-type Recordset objects on an ODBC data source that have more than 100 records. The algorithm used to resolve the criteria in this case is to retrieve and examine each record until either a match is found or all records have been examined.


Caching Remote Data

If you are using a Microsoft Jet workspace, you can use the Microsoft Jet CacheSize and CacheStart properties to store all or part of the data contained in a dynaset-type Recordset object in local memory. Local caching of records dramatically speeds up operations when moving through dynaset records in both directions, and shows significant improvements even when moving in a forward-only direction.

To use caching, specify the number of records to be stored using the CacheSize property. The range of values you can use for the CacheSize property is between 5 and 1200 records. If the size of the cache exceeds available memory, the excess records spill into a temporary disk file. Typically, you set the value of the CacheSize property to 100. To recover the cache memory, set the CacheSize property to 0. Specify the beginning record by setting the CacheStart property to the value of a bookmark using the Bookmark property. Then use the FillCache method to retrieve every value in the specified cache range and fill the cache with server data. For example, you might use the following code when you open your form:

With rst
    .CacheSize = 20
    .CacheStart = rst.Bookmark
    .FillCache
End With

This code will preload an internal buffer with 20 records of data. As the user moves through those records, they display quickly because they won’t have to be retrieved from the server.

This is more efficient than filling the cache as each record is retrieved, so if you know ahead of time that all records in the cache range will be visited, call the FillCache method every time you change the setting of the CacheStart property. Retrievals within the cache boundary occur locally, speeding display of the cached records in a datasheet or in a continuous form.

As you move through records, the data you retrieve will be cached until you move out of the defined range. Once you’ve hit the end of the range defined by the CacheSize and CacheStart properties, you need to change the CacheStart property setting to stay synchronized with the set of records you’re working with. Then, caching will continue with the new range, reusing values appropriately if the new cache range overlaps the old.

Using a cache can provide significant performance benefits. If the application requires backward scrolling within the cached region, the performance improvements will be even greater. Depending on your scenario, using a cache may be faster than using a read-only, forward-only-type Recordset object, especially if it contains Memo or Long Binary (OLE Object) fields that are referenced only occasionally.

See Also For more information about forward-only-type Recordset objects, see “Forward-Only-Type Recordsets” earlier in this chapter.

The size of the cache you use is determined by the application’s needs. For example, if you are displaying these records to the user, then you might use a cache size determined by the number of records permitted on the screen. If the code has no user interaction, then you can make a tradeoff between local memory availability, network traffic, and record size.

Notes

  • One important distinction with caching ODBC data is that the cached data will include Memo fields but not Long Binary fields (such as OLE Object fields). Hence, the cache can get large if you have those types of fields in your result set. If it becomes too large, then the cache overflows onto disk. Although more expensive than in-memory cache, this is still usually better then generating more round-trips to the server. If you are using other Microsoft Jet data sources, then only a pointer to the database page containing the Memo or Long Binary field is stored in the cache, and the actual contents are only retrieved if the field itself is retrieved.

  • You can’t use recordset caching with an ODBCDirect Recordset object, but you can change the default cache size by setting the CacheSize property of an ODBCDirect QueryDef object.


Inserting and Updating Data on a Remote Data Source

When you insert records or update data on a remote data source, you must be aware of a few characteristics of the database server that may affect your application’s behavior. This section discusses simple record insertions, the way Microsoft Jet handles updates, speeding up inserts and updates with bulk operations, and refreshing the data in your recordset with the latest data from the database server.


Inserting Records

You can insert new records into a remote database table by opening a linked table and inserting the records with the AddNew and Update methods, as you would with any other Microsoft Jet table. Whenever you insert records using DAO operations, you should use the dbAppendOnly constant in the options argument of the OpenRecordset method. This causes the engine to optimize its operations for adding data.

For example, the BookSales and BookSalesOptimized sample applications include a bulk data loader that creates the sample data used by the rest of the application. You can find this code in the DataAlterSales procedure of the Dataload.bas module. Using the Recordset approach, the code to add a single record is as follows. In this example, dbs is a Database object representing the remote data source, rst is a Recordset object, and conSalesID is a constant numeric value:

Set rst = dbs.OpenRecordset("Sales", dbOpenDynaset, dbAppendOnly)
With rst
    .AddNew
    !stor_id = "6380"		' All linked to the first store.
    !ord_num = conSalesID & Format(intC, "0000")
    !ord_Date = Now
    !qty = 5
    !payterms = "Net 60"
    !title_id = "BU2075"
    .Update
    .Close
 End With

Although this is a simple method, using a pass-through query with an SQL INSERT statement can be much faster in the client/server environment. The same code, implemented using a pass-through query, becomes:

Dim strInsert As String
strInsert = "INSERT INTO Sales VALUES ('6380', '" _
    & conSalesID & Format(intC, "0000") _
    & "', '" & Now & "', 5, 'NET 60', 'BU2075')"
dbs.Execute strInsert, dbSQLPassThrough

Building an INSERT statement from scratch like this can be tricky if your data has embedded quotation marks ("). If you’re only doing single updates, the time saved is generally not worth the effort, especially if the update is tied to a form.


Handling Inserts and Updates to Remote Data

When a user inserts a record into a remote table, Microsoft Jet sends values for every field in the recordset to the remote database. Similarly, when a user updates records in the remote database, the engine sends the values that changed during the update.

You should be aware of three special cases when using Microsoft Jet to insert or update remote data:

  • If the remote database server supplies default values when you insert a new record, Microsoft Jet honors those defaults. You can override these values if needed. Microsoft Jet uses the server defaults only when no value is specified.

  • If the remote database contains a trigger that is set to activate when a user changes data in a specific field, the trigger will not be invoked unless the user has changed the data in that field. When Microsoft Jet updates a record, it does not provide values for every field; it only provides values for the fields that have changed.

  • Microsoft Jet does not overwrite the data in a field that the ODBC API SQLSpecialColumns identifies as a SQL_ROWVER. These values are generated and maintained by the remote database server, and Microsoft Jet does not allow you to modify these fields.


Speeding Up Inserts and Updates with Bulk Operations

You can perform bulk operations, such as multiple-record insert, update, and delete operations, in one of two ways: You can use an SQL pass-through query or you can use the Microsoft Jet query processor. The Microsoft Jet query processor provides additional functionality, but can be somewhat slower than the SQL pass-through method for bulk operations.

The difference between using the Microsoft Jet query processor and an SQL pass-through query is in the way Microsoft Jet prepares for a bulk operation. When executing a bulk operation through its own query processor, Microsoft Jet builds a keyset for all records that will be affected by the operation. Then, the engine executes the operation for each record in the keyset, one record at a time.

The benefits of using the Microsoft Jet query processor are:

  • It allows for partially successful bulk queries.

  • It allows you to perform some bulk operations that may not be available on the remote database server.

The only drawback is the speed of the operation. You have to determine if the advantages offered by the Microsoft Jet query processor outweigh the possible performance penalties. If you find that performance is your highest goal, consider using pass-through queries.

See Also For information on SQL pass-through queries, see “Using SQL Pass-Through Queries” earlier in this chapter.


Refreshing Remote Data

Another important consideration when working with remote data is whether your copy of the data is up-to-date. Before you attempt to insert, update, or delete records on the remote database server, you should make sure that your copy of the data is current.

Although you can create a new Recordset object to capture the most recent data, there is a much faster method. Use the Requery method of the Recordset object to refresh the data. This method works with snapshot- and dynaset-type Recordset objects. You can use this method as a first step before an insert or update operation or any time you suspect the remote data may have changed.


Understanding Client/Server Query Behavior

Query operations in your client/server application are handled either locally or remotely. With local processing, Microsoft Jet handles the query. With remote processing, the remote database server handles the processing.

To successfully implement a client/server application, you should be aware of the differences between these two types of processing. This section discusses some key differences between local and remote query processing and lists operators and functions that may not be supported by the remote database server. This section also explains how to trace SQL operations.


Remote vs. Local Processing

When you create and execute a query, Microsoft Jet must decide which portion of the query to process locally through its own query processor, and which portion to send to the remote database server for processing. Microsoft Jet always sends the entire query to the server unless it contains items the server cannot process.

Because Microsoft Jet’s query processor supports heterogeneous joins, queries based on other queries, arbitrary expressions, and user-defined functions, your query may contain elements that the database server cannot interpret. To maximize the performance of queries in a client/server application, you should familiarize yourself with the capabilities of the server and design queries that use those capabilities. Use standard query optimization techniques for your server and use Microsoft Jet-specific query enhancements only when needed. Your goal should be to make it as easy as possible for Microsoft Jet to send your complete query to the server for processing.

Keep the following conditions in mind when constructing a query for the remote database server:

  • If your query joins data from multiple data sources, Microsoft Jet has to perform at least one part of the query locally.

  • If your query cannot be expressed in a single SQL statement, Microsoft Jet has to perform part of the query locally.


Effects of Asynchronous Queries

If your ODBC driver, database server, and network operating system support asynchronous operations, Microsoft Jet executes ODBC queries asynchronously. This allows your application to cancel a long-running query or to switch to another task while the query runs on the server. Microsoft Jet interrogates the server at the interval specified by the value of the AsyncRetryInterval setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\ODBC key in the Windows Registry. By default, this value is set to 500 milliseconds.

If your application cancels an asynchronous query (or closes the query before all results have been retrieved), Microsoft Jet calls the ODBC SQLCancel function. The SQLCancel function discards any pending results and returns control to the application. Some database servers or network operating systems respond quickly to query cancellations; others may require your application to wait several seconds.

In addition, Microsoft Jet cancels a query when the interval specified by the value of the QueryTimeout setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\ Microsoft\Jet\3.5\Engines\ODBC key in the Windows Registry is reached. By default, this value is set to 60 seconds. A timed-out query isn’t necessarily an indicator of trouble on the server; it simply means that the query did not return results in the time allotted. If you plan to execute a lengthy query, you can set the query’s ODBCTimeout property to override the value of the QueryTimeout setting in the Windows Registry.

See Also For more information on setting the connection timeout, see “Setting Connection Timeout” later in this chapter. For more information on Registry settings, see Appendix C, “Registry Settings.”


Unsupported Operators and Functions

When you construct a query to send to the server, Microsoft Jet determines which parts of the query can be handled by the server and which parts must be handled locally. Microsoft Jet uses the ODBC call SQLGetInfo to ask the ODBC driver which operators and functions are available on the server. When Microsoft Jet encounters a statement containing an operator or function that cannot be processed by the server, that statement must be evaluated locally.

The following tables list operators and functions that Microsoft Jet sends to the server in SQL statements if they are supported by your server. See your server documentation for further details.

General operators
= | - | Is Null
< > | * | Is Not Null
< | / | Like
> | & | Mod
< = | And | Not
> = | In | Or
+ |  |

Math functions
Abs | Fix | Sgn
Atn | Int | Sin
Cos | Log | Sqr
Exp | Rnd | Tan

String functions
Asc | Left | Space
Chr | Len | Str
InStr | Mid | String
Lcase | RTrim | Trim
LTrim | Right | Ucase

Aggregate functions
Avg | Min | Sum
Count | Max |

Conversion functions
Ccur | CLng | CVDate
CDbl | CSng |
Cint | CStr |

Date and time functions
Date | Minute | Time
DatePart | Month | Weekday
Day | Now | Year
Hour | Second |

Most servers don’t support string expressions surrounded by double quotation marks (") in SQL statements such as:

SELECT * FROM Authors WHERE State = "CA"

When using a Microsoft Jet workspace, Microsoft Jet translates double quotation marks into single quotation marks (') before sending the statement to the server. However, when using an ODBCDirect workspace, no translation is performed and an error will occur. To write code that is easily portable between either kind of workspace, make sure to use single quotation marks around strings in SQL statements as shown in the following example:

SELECT * FROM Authors WHERE State = 'CA'

In addition, some servers don’t support the following features:

  • Outer joins

  • Expressions, or nonfield references, in the ORDER BY clause

  • The Like operator when applied to Text, Memo, or binary fields

  • Unfamiliar wildcards in Like expressions

  • Expressions that use a mixture of incompatible operators and functions, such as:
    A LIKE B * C

The following features are unsupported on all known ODBC-accessible servers.

  • Aggregation of reports constructed in Microsoft Access that contain multiple levels of grouping and totals

  • User-defined functions that use variable arguments

  • Microsoft Jet-specific SQL extensions, such as TOP or TRANSFORM


Tracing SQL Operations

If you want to get a better picture of the SQL statements that Microsoft Jet sends to the remote database server through ODBC, you can trace the SQL operations. To enable SQL tracing, create a TraceSQLMode setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\ODBC key in the Windows Registry and set the value to 1. When you close and restart Microsoft Jet, the engine will log all ODBC calls in the text file Sqlout.txt, which is created in the current directory on your hard disk.

See Also For complete information on Microsoft Jet Registry settings, see Appendix C, “Registry Settings.”

Microsoft Jet always appends new SQL statements to the file while TraceSQLMode is enabled. If you want to disable SQL logging, set the value of TraceSQLMode to 0 and restart Microsoft Jet.

The following table lists the SQL output strings that can appear in the Sqlout.txt file.

Log entry | Description
SQLExecDirect: <SQL-string> | The calling application executed a non-parameter user query.
SQLPrepare: <SQL-string> | The calling application prepared a parameter user query.
SQLExecute: (PARAMETERIZED QUERY) | The calling application executed a prepared, parameter user query.
SQLExecute: (GOTO BOOKMARK) | The calling application retrieved a single record based on a bookmark.
SQLExecute: (MULTI-RECORD FETCH) | The calling application retrieved multiple records based on multiple bookmarks.
SQLExecute: (MEMO FETCH) | The calling application retrieved a Memo field for a single record based on a bookmark.
SQLExecute: (GRAPHIC FETCH) | The calling application retrieved a Long Binary (OLE Object) field for a single record based on a bookmark.
SQLExecute: (RECORD-FIXUP SEEK) | The calling application retrieved a single record based on an index key (not necessarily the bookmark index).
SQLExecute: (UPDATE) | The calling application updated a single record based on a bookmark.
SQLExecute: (DELETE) | The calling application deleted a single record based on a bookmark.
SQLExecute: (INSERT) | The calling application inserted a single record in dynaset mode.
SQLExecute: (SELECT INTO insert) | The calling application inserted a single record in export mode.

If you are trying to track specific query behavior or diagnose a problem, you may want to archive or delete the Sqlout.txt file before running the query. After you run the query, scan the file for the first instance of SQLPrepare or SQLExecDirect, which should correspond to your first query.


Linking Remote Tables

There are two basic ways to work with data stored on a server when using a Microsoft Jet Workspace object. You can:

  • Open the database directly using the OpenDatabase method, specifying a connection string to identify the server, database, and connection information.

  • Create a permanent link to tables through a Microsoft Jet database file.

Although opening the database directly sounds faster than linking, that is not the case. When you access data through the OpenDatabase method, every time the table is accessed, Microsoft Jet must retrieve information about tables, such as field names and data types. When you link a table, the engine stores this information permanently in the local Microsoft Jet database. This effectively caches the information, making it quickly accessible each time the table needs to be opened. In effect, you have eliminated a round trip to the server.

When you link the tables to your application’s Microsoft Jet database, the tables appear as if they were native Microsoft Jet tables in your application’s database. Besides convenience, one of the main advantages to linking is that Microsoft Jet caches information about the remote table in the stored link. Each time you open the linked table, information about its location, indexes, and other attributes are quickly accessible to Microsoft Jet, and opening is therefore faster.

Microsoft Jet also supports linking to SQL views on the remote data source, but with some restrictions, as discussed in “Updating Tables with No Unique Index” later in this chapter.

The BookSales and BookSalesOptimized sample applications compute the time it takes to perform various operations against the Pubs SQL Server database. The BookSales application opens the SQL Server database directly each time, using the following code:

' Note that you must alter the ODBC connection string before using
' this code with your application, so that it refers to the Pubs database
' on your SQL Server.

Set dbs = OpenDatabase("", False, False, _
    "ODBC;DSN=Pubs;UID=sa;PWD=;DATABASE=Pubs")

The BookSalesOptimized application opens a local Microsoft Jet database that contains tables linked to the Pubs SQL Server database. This database is Pubs.mdb, located in the JetBook\Samples\BookSalesOptimized subfolder:

' Note that you must re-link tables in Pubs.mdb before running the
' application so that they point to the Pubs database on your SQL server.

Set dbs = OpenDatabase("C:\JetBook\Samples\Pubs.mdb")

Linking tables may provide a dramatic performance improvement. Also, small recordsets, such as those in the BookSales and BookSalesOptimized sample applications, perform better than larger recordsets because the speed improvement you see from linked tables only affects the initial opening of the recordset. Note that small recordsets are typical of a well-written client/server application.

Note You can’t use linked tables from an ODBCDirect Workspace object, but ODBCDirect provides other performance advantages. For information on how to use ODBCDirect features, see “The Object Model for ODBCDirect Workspaces” later in this chapter.


How Microsoft Jet Links Remote Tables

Once you have established a connection to a remote data source, Microsoft Jet calls a series of ODBC API functions to determine the available objects (such as tables) on the remote data source. First, Microsoft Jet calls the SQLTables ODBC function to retrieve a list of all tables, views, synonyms, and similar objects. This is to ensure that the table you want to link exists.

Next, to get information on the requested table, Microsoft Jet calls ODBC informational functions such as SQLColumns (to determine the field names in the table) and SQLStatistics (to determine a table’s indexes). Finally, Microsoft Jet determines how to best represent the data types in the remote table with Microsoft Jet data types. This process is called data type mapping.

See Also For information about data type mapping, see “Mapping Data Types” later in this chapter.


How Microsoft Jet Determines If a Remote Table Can Be Updated

When Microsoft Jet opens a Recordset object, it attempts to use a dynaset-type Recordset object, which allows your application to update the data in the remote table. This is only possible if the remote table is uniquely indexed. The unique key values on each record of a remote table allow direct access to the records for all updates. The fields that comprise the unique keys are called bookmarks.

When you link a table, Microsoft Jet determines the bookmark field or fields by choosing the first unique index returned by the SQLStatistics function as the primary index (if any unique indexes exist on the table). The SQLStatistics function always returns indexes in a specific order: clustered indexes, followed by hashed indexes, followed by other indexes. Furthermore, the SQLStatistics function alphabetizes the indexes within each group. Therefore, if you want Microsoft Jet to select a particular index, you can name the index so that it appears first in the alphabetical listing.

Note Microsoft Jet does not attempt to determine the optimal set of fields that uniquely identifies a record in the table through the SQLSpecialColumns ODBC function call. Furthermore, Microsoft Jet does not attempt to use a database server’s native record number identifier as a unique index due to the potentially ambiguous nature of such identifiers in a multiuser environment.

If Microsoft Jet is unable to identify a unique index on the remote table, it must open a snapshot-type Recordset object, which can’t be updated. You can get around this behavior by creating a pseudo index, as discussed in the next section.


Updating Tables with No Unique Index

You can link a server view to your application’s database just as you would link a table with no unique index. Microsoft Jet will treat the linked view as a snapshot-type Recordset object, unless you create a pseudo index on the link (not on the server view). Therefore, you can think of linking to an SQL view the same as you would link to any other data source with a non-unique index.

To make a server view or other table with no index updatable, select a group of fields that uniquely identify each record in the view. Then, create a data definition query on the linked table. Do not use an SQL pass-through query because you aren’t trying to create an index on the server tables or the server view.

This gives Microsoft Jet enough information to uniquely identify records on the linked view and allows your application to use all operations normally associated with dynaset-type Recordset objects (such as INSERT, UPDATE, and DELETE operations).

Note You can’t link server-based stored procedures using this technique.

See Also To see examples of how to create pseudo indexes, see “Indexes and ODBC Views” in Chapter 3, “Data Definition and Integrity.”


Using Transactions

This section describes the behavior of Microsoft Jet transactions in a client/server environment. This section also discusses the limitations of transactions and what you should avoid when using transactions in your application.

Note The information in this section applies to both Microsoft Jet and ODBCDirect Workspace objects.

See Also For information on Microsoft Jet transactions in non-client/server environments, see Chapter 5, “Working with Records and Fields.”


Transaction Behavior

Often, to enforce data integrity, a set of operations must be considered as a single unit. For example, the transfer of funds from one bank account to another consists of two operations: entering a debit in one account and a matching credit in the other account. In practice, either both operations must succeed or neither operation should be processed.

In this situation, the debit and credit operations are a single unit, or a transaction. A transaction is a set of operations that are committed (saved) if and only if all of the operations succeed. If any of the operations fail, all of the operations that succeeded are rolled back (canceled), and the data is returned to the state it was in before the transaction began.

DAO supports three transaction methods: one for starting, one for committing, and one for rolling back a transaction:

  • The BeginTrans method begins a new transaction.

  • The CommitTrans method commits all changes made to data by the most recent BeginTrans method.

  • The Rollback method rolls back, or cancels, all changes made to data by the most recent BeginTrans method.

BeginTrans, CommitTrans, and Rollback are all methods of the Workspace object.

On most servers, transactions hold locks that prevent other users from updating or even reading data affected by the transaction until it’s committed or rolled back. You should therefore keep your transactions as short as possible and avoid beginning a transaction and then waiting for the user’s response before completing it.

The following code shows a transfer using two common update operations, a withdrawal and a deposit. The transfer occurs in the context of a transaction. If either operation fails, the transaction makes sure that the operation as a whole fails.

Sub IncreaseRoyalty(strDbPath As String)
    Dim wrk As Workspace, dbs As Database
    Dim qdf As QueryDef

    On Error GoTo Err_IncreaseRoyalty

    Set wrk = DBEngine.Workspaces(0)
    Set dbs = wrk.OpenDatabase(strDbPath)

    ' Begin the transaction.
    wrk.BeginTrans

    ' Create a temporary SQL pass-through query.
    Set qdf = dbs.CreateQueryDef("")
    ' Set QueryDef properties and execute.
    With qdf
        .Connect = "ODBC;DSN=Pubs;UID=sa;PWD=;DATABASE=Pubs"
        .ReturnsRecords = False
        .SQL = "UPDATE Roysched SET Royalty = " & _
            "Royalty + 2 WHERE HiRange < 6000"
        ' Execute method will fail if error occurs.
        .Execute dbFailOnError
    End With
    ' Commit the transaction.
    wrk.CommitTrans

Exit_IncreaseRoyalty:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Sub

Err_IncreaseRoyalty:
    ' If one operation fails, roll them all back.
    wrk.Rollback
    VerboseErrorHandler
    MsgBox "Transaction rolled back!"
    Resume Exit_IncreaseRoyalty
End Sub
How Microsoft Jet Uses ODBC Transactions

When your DAO code does not use transaction methods explicitly, Microsoft Jet will use ODBC in auto-commit mode. In this mode, each SQL statement sent to the server has an implicit transaction around it; that is, the SQL statement has an immediate effect on the ODBC data and cannot be rolled back. Microsoft Jet stops using auto-commit mode and uses explicit ODBC transactions with:

  • SQL statements that modify data (for example, UPDATE, INSERT, or APPEND).

  • Explicit DAO transaction methods.

When using bulk operation SQL statements such as UPDATE, INSERT, and APPEND, Microsoft Jet places a transaction around the operation so that it can succeed or fail as a single entity. This is necessary because one such Microsoft Jet SQL statement may correspond to many individual server statements.

DAO Transactions

Multiple concurrent transactions on Recordset objects against a single server are actually a single transaction because a single connection is being used to service updates for both dynasets. You should structure your transactions so that they do not overlap; transactions are intended to be atomic units.

If the server supports transactions at all, Microsoft Jet assumes only single-level support; that is, no nesting of transactions. Therefore, if your application code nests transactions, only the outermost BeginTrans, CommitTrans, and Rollback methods are sent to the server; nested transactions are ignored without warning.

These transaction semantics also apply to SQL pass-through queries that modify server data, so explicit transactions within the pass-through queries cannot be used.

See Also For more information on SQL pass-through queries, see “Using SQL Pass-Through Queries” earlier in this chapter.


Transaction Limitations

Although transactions can improve the functionality and performance of your client/server applications, they have several limitations, as described in the following sections.

Transactions Cannot Be Nested

Although Microsoft Jet supports nesting transactions on its own native data sources, ODBC doesn’t support nesting transactions. Because of this, Microsoft Jet doesn’t support any transaction nesting against ODBC data and will ignore attempts to nest transactions. This is a common code change you need to make when converting code that runs against local data sources to work with ODBC data sources.

Some Queries Are Not Allowed in Transactions

Some queries may not be allowed within a transaction. For example, SQL Server doesn’t allow data definition, permission, or backup-related statements within transactions. Check your server’s documentation to determine these limits.

Server Transaction Commands Are Not Allowed

Microsoft Jet often conserves connections by sharing them between queries. If you use server-specific transaction commands in pass-through queries, they can confuse the internal tracking of server transactions performed by Microsoft Jet, as well as provide you with unexpected results. For example, on SQL Server, don’t use the SQL Server command BEGIN TRAN. Instead, use the BeginTrans method. Microsoft Jet translates DAO transaction methods into the equivalent server commands.

Server Limits on Locks

Locks may be placed on the server for every operation you perform inside a transaction. Be aware of the number of locks you’re requesting and make sure your server is configured to handle them.

Note You can control the maximum number of locks Microsoft Jet places against a shared database using the MaxLocksPerFile registry setting. For more information, see “Setting the Maximum Number of Locks” in Chapter 13, “Optimizing Performance.”

Isolating Transactions

For efficiency reasons, remote data used within Workspace objects does not have an isolated transaction space. For example, if you use two Workspace objects to open a table attached to an ODBC data source, transaction methods on one Workspace object will affect transactions on the other. Because it’s unusual to have two different sets of transactions used concurrently, this isn’t likely to affect much code.

If you want to have multiple concurrent transactions on your server, you can force each workspace to have a distinct remote transaction space by setting the IsolateODBCTrans property of the Workspace object to True. This prevents the workspace from sharing connections with other workspaces, which guarantees transaction isolation. The IsolateODBCTrans property can’t be used with an ODBCDirect Workspace object.


Dividing Data Efficiently

One of the key elements in successful client/server design is deciding where to put the data. The key to making this decision lies in recognizing and utilizing the strengths of each system. You are really designing two databases and establishing links between them. Although there are many techniques available to increase Microsoft Jet performance against ODBC data sources, the initial design of the data model will have the greatest effect on performance.

Note The information in this section applies to both Microsoft Jet and ODBCDirect Workspace objects.


Client-Side Data Considerations

On the client side of the client/server data model, you can store lookup tables and “master” tables. The Microsoft Jet database on each client machine can also store all of the queries needed to access and manipulate remote data.

Why use local lookup tables?

It’s more efficient for order entry personnel to use lookup tables to choose from a list of available products than for them to type in the name or stock number of each item in an order, but why should you store the lookup tables on the local machine? One reason is performance; local lookup tables respond to queries much more quickly.

Another reason is that the values in a lookup table are relatively static. Although the items in an organization’s product offerings may periodically change, this is usually an infrequent occurrence that can be planned for ahead of time, and the local lookup tables can be updated easily. When it’s time to update the lookup table of product offerings, you can post the updated table to the server and have all users run a query to update the local tables.

Why store master tables on the client?

Master tables are often good candidates for client-side tables. For example, a salesperson might maintain a local table of customers and each customer might have a number of orders. By storing the customer data locally, each salesperson has access to order information for his or her respective customers. The order information itself remains in the detail table on the server where order entry personnel can update the information, accounting personnel can gather billing information, and shipping personnel can determine which orders need to be filled. Master tables can also be indexed for faster performance.


Server-Side Data Considerations

You should use the database server primarily for storing centralized, detailed data. Most database servers are very efficient at storing large amounts of data in a secured environment while also providing high-speed concurrent access to multiple users. For example, this makes them perfect candidates for detail tables in order entry systems that provide data for many different groups within an organization (such as salespeople, shipping personnel, and accounting staff).


Handling Server-Generated Errors

When you receive an ODBC error from Microsoft Jet, the error message does not contain the specific error that occurred on the server. This message is stored in the Errors collection. As described in Chapter 2, “Introducing Data Access Objects,” the Errors collection is a zero-based collection of error messages that starts with the first error that occurs. You can walk through the Errors collection to display all messages logged by the system. The VerboseErrorHandler function, shown earlier in this chapter, gives information on all errors in the Errors collection.

Note The information in this section applies to both Microsoft Jet and ODBCDirect Workspace objects.


Mapping Data Types

Most database servers support a rich set of data types, including a number of floating-point types of varying scale and precision. The set of data types supported by Microsoft Jet may not exactly match many of the data types on a specific database server. To allow transparent data access, however, Microsoft Jet must select an equivalent data type for each field in a remote table. This section discusses the rules that Microsoft Jet uses to select local data types to represent standard ODBC data types and vice versa.


Floating-Point Data

Techniques for handling floating-point data (any numeric data that does not have a fixed number of digits after the decimal point) vary among database servers. Furthermore, when remote floating-point data is put into a numeric format acceptable to Microsoft Jet (as in the case of linked tables), minor numeric data accuracy loss may occur. Although these accuracy losses usually occur in very large or very small floating-point numbers and have no appreciable effect on common mathematical operations, they can cause a problem when used for exact comparisons. One case that is particularly important is that of floating-point data in fields that are part of a unique index.

When fields containing floating-point values are used as part of a unique index and any variation (even a very small one) exists between the numeric data on the local and remote data sources, Microsoft Jet may mark the record containing the discrepancy as deleted. This occurs because Microsoft Jet requests the record from the server by presenting its key value. Because of the loss of accuracy, the server table doesn’t contain a record with key values that exactly match the values requested by Microsoft Jet, therefore it appears to Microsoft Jet that another user has deleted the record.

To remedy this situation, you can force Microsoft Jet to select another unique index (if available) that doesn’t use any floating-point values. Rename the new index (that is, the index that doesn’t contain floating-point values) so that it appears alphabetically before the original index and re-establish the link to the remote table. Microsoft Jet automatically updates the link with the new index as the primary index on the remote table.


ODBC to Jet Data Type Mapping

Microsoft Jet calls the ODBC SQLColumns function to gather information on the fields in the remote table. The SQLColumns function returns (among other things) the following information for each field in the table, which determines how the data type of each field will be mapped to a Microsoft Jet data type.

Argument | Description
fSqlType | The fSqlType argument returns the SQL data type of the field. Valid SQL data types include a minimum set, which provides basic ODBC conformance, a core set, which is a superset of the minimum set and adds X/Open and SAG CAE (92)-compliant types, and an extended set, which includes the minimum set, the extended set, and additional data types that support specific database servers.
LPrecision | The lPrecision argument returns the maximum number of digits used by the data type represented in the field.
WScale | The wScale argument returns the maximum number of digits to the right of the decimal point used by the data type represented in the field.

Microsoft Jet uses the fSqlType, lPrecision, and wScale values to determine an appropriate local data type, on a field-by-field basis, and uses that data type to represent the remote data in a linked table. Each time Microsoft Jet executes an action or parameter query against the remote data source, the information stored in the linked table is used to ensure that ODBC calls are made with a valid ODBC data type. The following table lists ODBC data types and the corresponding Microsoft Jet data types.

ODBC data type | Microsoft Jet data type
SQL_BIT | Yes/No
SQL_TINYINT
SQL_SMALLINT 		Number (Integer)
SQL_INTEGER | Number (Long Integer)
SQL_REAL | Number (Single)
SQL_FLOAT
SQL_DOUBLE 		Number (Double)
SQL_TIMESTAMP
SQL_DATE 		DateTime
SQL_TIME | Text
SQL_CHAR
SQL_VARCHAR 		Varies based on the lPrecision value:
  • For data types where the lPrecision is less than or equal to 255, the Microsoft Jet data type is Text and the FieldSize is equal to the value of lPrecision.

  • For data types where the lPrecision is greater than 255, the Microsoft Jet data type is Memo.
SQL_BINARY
SQL_VARBINARY 		Varies based on the lPrecision value:
  • For data types where the lPrecision is less than or equal to 255, the Microsoft Jet data type is Binary and the FieldSize is equal to the value of lPrecision.

  • For data types where the lPrecision is greater than 255, the Microsoft Jet data type is Long Binary (OLE Object).
SQL_LONGVARBINARY | Long Binary (OLE Object)
SQL_LONGVARCHAR | Memo

ODBC data type Microsoft Jet data type
SQL_DECIMAL
SQL_NUMERIC 		Varies based on a combination of the lPrecision and wScale values:
  • For data types where the wScale equals 0 and lPrecision is less than or equal to 4, the Microsoft Jet data type is Number (Integer).

  • For data types where the wScale equals 0 and lPrecision is less than or equal to 9, the Microsoft Jet data type is Number (Long Integer).

  • For data types where the wScale is greater than or equal to 0, and lPrecision is less than or equal to 15, the Microsoft Jet data type is Number (Double).

  • For data types where the wScale is greater than 0 and less than or equal to 4, and lPrecision is less than or equal to 15, the Microsoft Jet data type is Number (Double); and the FieldSize is equal to the value of lPrecision; if wScale is greater than 4, the Microsoft Jet data type is Double.

Special Mappings for Microsoft SQL Server

  • For data types where the wScale equals 4 and lPrecision equals 19, the Microsoft Jet data type is Currency.

  • For data types where the wScale equals 4 and lPrecision equals 10, the Microsoft Jet data type is Currency.

See Also For a complete description of data types recognized by ODBC, as well as a complete explanation of precision and scale in ODBC, see the Microsoft ODBC 3.0 Software Development Kit Programmer’s Reference.


Microsoft Jet to ODBC Data Type Mapping

When you send data from a Microsoft Jet client to a remote data source, the Microsoft Jet engine must find an appropriate ODBC data type to receive the data. This data type mapping can involve rather complex decisions for Microsoft Jet because many database servers implement only a subset of the ODBC data types. Therefore, Microsoft Jet tries a prioritized list of data types and maps the local data to the first data type supported on the remote data source. Note that some Microsoft Jet data types are unsupported on some database servers.

Microsoft Jet data type | ODBC data type
Yes/No (Boolean) SQL_BIT
SQL_SMALLINT
SQL_INTEGER
SQL_VARCHAR(5)
Number (Byte) SQL_SMALLINT
SQL_INTEGER
SQL_VARCHAR(10)
Number (Integer) SQL_SMALLINT
SQL_INTEGER
SQL_VARCHAR(10)
Number (Long Integer) SQL_INTEGER
SQL_VARCHAR(20)
Currency SQL_DECIMAL(19,4) (Microsoft SQL Server and Sybase SQL Server only)
SQL_FLOAT
SQL_VARCHAR(30)
Number (Single) SQL_REAL
SQL_FLOAT
SQL_VARCHAR(30)
Number (Double) SQL_FLOAT
SQL_VARCHAR(40)
DateTime SQL_TIMESTAMP
SQL_VARCHAR(40)
Text (fieldsize) | SQL_VARCHAR(n), where n is the smaller of the Microsoft Jet field size and the database server’s maximum size for a VARCHAR.
Binary (fieldsize) SQL_VARBINARY(n), where n is the smaller of the Microsoft Jet field size and the database server’s maximum size for a VARBINARY.
If SQL_VARBINARY is not supported on the database server, the query fails.
Memo SQL_LONGVARCHAR if the server supports it.
Otherwise, SQL_VARCHAR(n), where n is the server’s maximum size for a VARCHAR if the maximum size is greater than 2000.
If neither case is supported on the database server, the query fails.
Long Binary
(OLE Object) 		SQL_LONGVARBINARY if the server supports it.
Otherwise, SQL_VARBINARY (n), where n is the database server’s maximum size for a VARBINARY if the maximum size is greater than 2000.
If neither case is supported on the database server, the query fails.


Using the MSysConf Table to Configure Microsoft Jet

The MSysConf table is a special table that you create in your remote database. You can use it to:

  • Prevent users from storing the logon ID and password locally for a linked SQL table.

  • Set the number of records that are retrieved at one time.

  • Set the number of seconds of delay between each retrieval.


Creating the MSysConf Table

Create the MSysConf table in each remote database you want to work with. When you create the table, give all users of your application permission to use the SELECT statement on this table; the system administrator is the only person who needs permission to use INSERT, UPDATE, and DELETE statements. If your remote database is case-sensitive, you must type the table name and field names exactly as they appear in the following table.

Field name | Data type | Allows Null?
Config | Any valid server data type that corresponds to a 2-byte integer | No
chValue | VARCHAR(255) | Yes
nValue | Any valid server data type that corresponds to a 4-byte integer | Yes
Comments | VARCHAR(255) | Yes


Specifying Settings in the MSysConf Table

The MSysConf table currently recognizes three configuration settings. These settings are activated by adding a record to the table for each setting. Currently, only the Config and nValue fields are used; the chValue and Comments fields are reserved for future use and are ignored in this release.

The following table describes the values you can use in the nValue field for each configuration specified in the Config field of your MSysConf table.

Config field value | nValue field value | Description
101 | 0 or 1 | When this value is set to 0, the system doesn’t allow local storage of the logon ID and password in linked tables; when the value is set to 1, the system allows local storage of the logon ID and password in linked tables. The default value is 1.
102 | D | This value, D, designates the delay, in seconds, between each retrieval operation performed by background population of a Recordset object. Setting a higher delay time decreases network traffic, but increases the amount of time that read-locks are held on data (if the server uses read-locks). The default value is 10 seconds.
103 | N | This value, N, designates the number of records retrieved during the background population of a Recordset object. The default value is 100 records.

Important If you create an MSysConf table, it must be set up properly or you will not be able to connect to the SQL database table at all.


The Object Model for ODBCDirect Workspaces

The object model for an ODBCDirect workspace includes a subset of the objects in a Microsoft Jet workspace, with the addition of a new object, the Connection object. Figure 9.1 shows the object hierarchy for ODBCDirect workspaces; the subsequent sections describe the objects themselves, to the extent that they differ from the objects in the Microsoft Jet object hierarchy.

See Also For more information comparing the features of the Microsoft Jet and ODBCDirect object models, search the DAO Help index for “data access overview.” In the “Data Access Objects Overview” Help topic, click See Also for additional Help topics. For information about the Microsoft Jet object model, see Chapter 2, “Introducing Data Access Objects.”

Figure 9.1 The DAO object hierarchy for ODBCDirect workspaces


The DBEngine Object

The DBEngine object contains both Microsoft Jet and ODBCDirect workspaces. The DefaultType property of the DBEngine object determines what type of Workspace object is created by default when you use the CreateWorkspace method. If you set the DefaultType property to dbUseODBC, then the default workspace is an ODBCDirect workspace. When you’re creating a workspace, you can override the setting for this property by specifying either dbUseJet or dbUseODBC as the type argument of the CreateWorkspace method. For example, if the DefaultType property is set to dbUseJet and you want to create an ODBCDirect workspace, specify the dbUseODBC constant as the type argument of the CreateWorkspace method. Conversely, if the DefaultType property is set to dbUseODBC and you want to create a Microsoft Jet workspace, specify the dbUseJet constant as the type argument of the CreateWorkspace method.

Note If you’re programming in Microsoft Access, be careful about setting the DefaultType property to dbUseODBC. Because Microsoft Access uses DAO and Microsoft Jet for many types of operations, setting the DefaultType property to dbUseODBC may cause unexpected results.

The following code creates an ODBCDirect workspace.

Dim wrkODBC As Workspace
Set wrkODBC = DBEngine.CreateWorkspace("NewODBCWrk", "Admin", "", dbUseODBC)

Because you can use both Microsoft Jet and ODBCDirect workspaces in your code, you may need to determine the type of a Workspace object after it is created. You can do this by using the Type property of the Workspace object. The Type property is read-only once it is set and returns either dbUseJet or dbUseODBC.


The Workspace Object and the Workspaces Collection

The Workspace object represents an ODBCDirect workspace. The Workspaces collection contains the set of all active ODBCDirect workspaces. The following table shows the relationship between the Workspace object and the Workspaces collection and other objects and collections in an ODBCDirect workspace.

Object or collection | Is contained by | Contains
Workspace object | Workspaces collection | Connections collection
 |  | Databases collection
 |  | Properties collection
Workspaces collection | DBEngine object | Workspace objects

The first step in using ODBCDirect is to create an ODBCDirect workspace with the CreateWorkspace method. The ODBCDirect workspace routes calls directly to the ODBC application programming interface (API), as opposed to the Microsoft Jet workspace, which first routes calls to the Microsoft Jet database engine, and then to the ODBC API when you’re using ODBC.


The Connection Object and the Connections Collection

After you’ve created an ODBCDirect workspace, you can connect to an ODBC data source. To connect to an ODBC data source, you can use the OpenConnection method to open a new Connection object, or you can use the OpenDatabase method to open a new Database object. This section explains how to use the Connection object. For information on how to use a Database object, see the following section, “The Database Object and the Databases Collection.”

A Connection object represents a connection to an ODBC database in an ODBCDirect workspace. The Connections collection contains all currently open Connection objects. When you open a Connection object, it is automatically appended to the Connections collection of the Workspace object. When you close a Connection object with the Close method, it is removed from the Connections collection.

Note Before you close a Connection object, close all open Recordset objects within it.

When you open a Connection object, a corresponding Database object is also created and appended to the Databases collection in the same workspace. This allows you to use the features of both the Connection and Database objects. For information on how to use the Database object from an ODBCDirect workspace and how to switch between Connection and Database objects, see the following section “The Database Object and the Databases Collection.”

The Connection object provides the following advantages for accessing ODBC data:

  • Asynchronous Connection Your application can connect to an ODBC data source asynchronously. Rather than pausing execution while the connection is established, your code can continue to perform other operations, and can later check to determine whether the connection was made successfully.

  • Asynchronous Queries Your application can run queries against your ODBC data source asynchronously. Rather than pausing execution while a long query runs, your code can perform other tasks, and then check later to determine whether the query has run successfully.

  • QueryDef Objects You can define QueryDef objects that represent queries in the ODBC data source.

Use the OpenConnection method to create a Connection object. The syntax of the OpenConnection method is:

Set connection = workspace.OpenConnection (name, options, readonly, connect)

The connection argument is the name of the new Connection object. The workspace argument is the name of an ODBCDirect Workspace object from which you’re creating the new Connection object.

The name argument indicates the name of the registered data source. You can reference the new Connection object by using either the data source name (DSN) or the Connection object’s ordinal position within its collection. The options argument determines if and when to prompt the user to establish the connection, and whether or not to open the connection asynchronously. The readonly argument controls the updatability of the data accessed through the connection. Set this argument to True to prevent updates; set it to False to allow updates.

The connect argument is a valid connection string that supplies parameters to the ODBC Driver Manager. These parameters can include user name, password, default database, and DSN, which overrides the value provided in the name argument.

The connection string must start with "ODBC;", and must contain a series of values needed by the driver to access the data. The actual connection string can vary depending on the data source you’re trying to access; different ODBC data sources require different parameters in the connect argument. Usually, the minimum requirement is a user ID, a password, and a DSN, as shown in the following example:

ODBC;UID=JamesK;PWD=OpenSesame;DSN=MasterData

When the ODBC driver processes the connection string and one or more of the parameters required by the data source is missing, the driver displays a dialog box that asks for the information. If you don’t want this dialog box displayed, you must make sure that the connection string has all the required information.

Note If you are trying to connect to a Microsoft SQL Server database that uses integrated security, omit the user ID (UID) and password (PWD) values because your Windows NT user name and password are automatically used. For example, the connection string may look something like the following:

ODBC;UID=;PWD=;DATABASE=Pubs;DSN=Pubs

See Also For more information about formatting connection strings, see Chapter 8, “Accessing External Data.”

The following example illustrates how to use the OpenConnection method to open a new Connection object.

Function OpenPubsConnection() As Boolean
    Dim wrk As Workspace, cnn As Connection, rst As Recordset, fld As Field
    Dim strConnect As String, strSQL As String

    On Error GoTo Err_OpenPubsConnection
    ' Create connect string.
    strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
    ' Create SQL string.
    strSQL = "SELECT * FROM Authors WHERE State = 'CA';"

    ' Create ODBCDirect workspace.
    Set wrk = DBEngine.CreateWorkspace("NewODBCDirect", "Admin", "", dbUseODBC)
    ' Open connection.
    Set cnn = wrk.OpenConnection("Pubs", dbDriverNoPrompt, False, strConnect)
    ' Open recordset on connection.
    Set rst = cnn.OpenRecordset(strSQL, dbOpenDynaset)
    ' Print values in recordset.
    Do Until rst.EOF
        For Each fld In rst.Fields
            Debug.Print fld.Name, fld.Value
        Next fld
        Debug.Print
        rst.MoveNext
    Loop
    OpenPubsConnection = True

Exit_OpenPubsConnection:
    On Error Resume Next
    rst.Close
    cnn.Close
    Exit Function

Err_OpenPubsConnection:
    VerboseErrorHandler
    OpenPubsConnection = False
    Resume Exit_OpenPubsConnection
End Function

After you’ve created a Connection object, you can open Recordset objects and run queries using the Connection object.

Note Most servers don’t support string expressions surrounded by double quotation marks (") in SQL statements such as:

SELECT * FROM Authors WHERE State = "CA"

When using a Microsoft Jet workspace, Microsoft Jet translates double quotation marks into single quotation marks (') before sending the statement to the server. However, when using an ODBCDirect workspace, no translation is performed and an error will occur. When writing SQL statements to be executed using an ODBCDirect connection, make sure to use single quotation marks around strings in SQL statements as shown in the following example:

SELECT * FROM Authors WHERE State = 'CA'


Opening Connections Asynchronously

In some cases, opening connections to data sources can take a long time, making it necessary for users to wait until the connection completes or an error occurs. To reduce the amount of time users must wait, you can open a connection asynchronously. This means that your application can complete other tasks while the connection is being established. To open a connection asynchronously, specify the dbRunAsync constant for the options argument of the OpenConnection method, as shown in the following example.

Dim wrk As Workspace, cnn As Connection, strConnect As String

Set wrk = DBEngine.CreateWorkspace("NewODBCDirect", "Admin", "", dbUseODBC)
strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
Set cnn = wrk.OpenConnection("", dbDriverNoPrompt + dbRunAsync, False, strConnect)

You can use the StillExecuting property of the Connection object to see if the connection has been established, or use the Cancel method of the Connection object to cancel the connection attempt if it takes too long.


The Database Object and the Databases Collection

You can also connect to an ODBC data source by using the OpenDatabase method to open a Database object. However, the Database object in an ODBCDirect workspace doesn’t support all of the functionality of a Connection object. Specifically, if you’re using a Database object, you can’t connect asynchronously, run queries asynchronously, or define QueryDef objects that represent queries in the ODBC data source.

To connect to an ODBC data source with the OpenDatabase method in an ODBCDirect workspace, specify a valid connection string for the connect argument of the OpenDatabase method, as shown in the following example.

Dim wrk As Workspace, dbs As Database
Dim strConnect As String

strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
Set wrk = DBEngine.CreateWorkspace("NewODBCDirect", "Admin", "", dbUseODBC)
Set dbs = wrk.OpenDatabase("Pubs", dbDriverNoPrompt, False, strConnect)


Switching Between Connection and Database Objects

With ODBCDirect, you can open a Database object and a Connection object against the same ODBC data source, and use both in your code. You can then take advantage of each object for its different capabilities.

Alternatively, you may want to create a single object and then switch to the other type when needed. To do this, use the Connection property of the Database object or the Database property of the Connection object. You can use these properties to create Connection objects from Database objects and to create Database objects from Connection objects. This is especially useful for adding ODBCDirect capabilities to existing applications that only use Database objects.

For example, you can use a Database object for most of your ODBC data access needs, but when you need to run an asynchronous query, you can create a Connection object from the Database object and then run the query on the Connection object. The following example illustrates this technique.

Sub DeleteWashingtonRecords()
    Dim dbs As Database, cnn As Connection
    Dim strSQL As String, strConnect As String

    strConnect = "ODBC;DSN=Pubs;DATABASE=Pubs;UID=sa;PWD=;"
    strSQL = "DELETE FROM Authors WHERE State = ""WA"""

    ' Open database in default workspace.
    Set dbs = OpenDatabase("", False, False, strConnect)

    ' Try to create Connection object from a Database object. If workspace is an
    ' ODBCDirect workspace, the query runs asynchronously. If workspace is a
    ' Microsoft Jet workspace, an error occurs and the query runs synchronously.

    Err.Clear
    On Error Resume Next
    Set cnn = dbs.Connection
    If Err.Number = 0 Then
        cnn.Execute strSQL, dbRunAsync
    Else
        dbs.Execute strSQL
    End If
End Sub


The QueryDef Object and the QueryDefs Collection

The QueryDef object represents a temporary definition of a query in an ODBCDirect workspace. The QueryDefs collection contains all QueryDef objects that currently exist in the workspace. The following table shows the relationship between the QueryDef object and the QueryDefs collection and other objects and collections in an ODBCDirect workspace.

Object or collection | Is contained by | Contains
QueryDef object | QueryDefs collection | Parameters collection
 |  | Properties collection
QueryDefs collection | Connection object | QueryDef objects

Unlike QueryDef objects created in a Microsoft Jet workspace, QueryDef objects created in an ODBCDirect workspace are always temporary — they are not saved within the data source before they run, even if you assign them a name.


Running Asynchronous Queries

Creating and running queries in an ODBCDirect workspace is similar to creating and running queries in a Microsoft Jet workspace. You create the query by invoking the CreateQueryDef method on a Connection object, and then use the Execute or OpenRecordset methods on the resulting query.

You can use asynchronous queries so that users can continue using your application while the query runs. You can also give users the ability to cancel asynchronous queries if they are taking too long. The following example runs an asynchronous query.

Function DeleteLargeSales() As Boolean
    Dim wrk As Workspace, rst As Recordset
    Dim cnn As Connection, qdf As QueryDef
    Dim strConnect As String, strSQL As String
    Dim errObj As Error

    On Error GoTo Err_DeleteLargeSales
    ' Create ODBCDirect workspace.
    Set wrk = DBEngine.CreateWorkspace("ODBC", "Admin", "", dbUseODBC)
    ' Create connection string.
    strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
    ' Open connection on workspace.
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)

    ' Create QueryDef.
    Set qdf = cnn.CreateQueryDef("DeleteLargeSales")
    strSQL = "DELETE FROM sales WHERE qty > 100"
    qdf.SQL = strSQL

    ' Run query asynchronously.
    qdf.Execute dbRunAsync

    Do Until Not qdf.StillExecuting
    ' Additional code runs here while query runs.
    ' Check StillExecuting property to determine whether query has finished.
    Loop

    DeleteLargeSales = True

Exit_DeleteLargeSales:
    On Error Resume Next
    cnn.Close
    wrk.Close
    Exit Function

Err_DeleteLargeSales:
    For Each errObj In Errors
        Debug.Print errObj.Number, errObj.Description
    Next errObj
    DeleteLargeSales = False
    Resume Exit_DeleteLargeSales
End Function

The preceding example uses a QueryDef object on a Connection object to run an asynchronous query. You can also use the Execute method directly on the Connection object, as shown in the following example.

Dim wrk As Workspace, cnn As Connection
Dim strConnect As String

' Create ODBCDirect workspace.
Set wrk = DBEngine.CreateWorkspace("ODBC", "Admin", "", dbUseODBC)
strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
' Open connection.
Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)
' Execute SQL statement.
cnn.Execute "DELETE FROM sales WHERE qty > 100", dbRunAsync
cnn.Close

When you run a query asynchronously, you can use the StillExecuting property to determine if the query has completed. If the value of the StillExecuting property is True, the query has not yet completed. If you want to cancel an asynchronous query, use the Cancel method, as shown in the following example.

Function CancelAsynchQuery() As Boolean
    Dim wrk As Workspace, cnn As Connection, strConnect As String
    Dim errObj As Error

    On Error GoTo Err_CancelAsynchQuery
    Set wrk = DBEngine.CreateWorkspace("ODBCDirect", "Admin", "", dbUseODBC)
    strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)

    ' Start transaction in order to roll back if needed.
    wrk.BeginTrans
    cnn.Execute "DELETE FROM sales WHERE qty > 100", dbRunAsync

    ' Perform other operations.
        .
        .
        .

    ' If query is still running, cancel and roll back.
    If cnn.StillExecuting Then
        cnn.Cancel
        wrk.Rollback
    ' If query is complete, commit transaction.
    Else
        wrk.CommitTrans
    End If
    CancelAsynchQuery = True

Exit_CancelAsynchQuery:
    On Error Resume Next
    cnn.Close
    wrk.Close
    Exit Function

Err_CancelAsynchQuery:
    For Each errObj In Errors
        Debug.Print errObj.Number, errObj.Description
    Next errObj
    CancelAsynchQuery = False
    Resume Exit_CancelAsynchQuery
End Function

You can use the StillExecuting property and the Cancel method with QueryDef, Connection, and Recordset objects.

A Connection object can support only one asynchronous operation at a time. Also, you can’t perform another DAO operation, such as recordset manipulation, on a Connection object while an asynchronous query runs on the same Connection object. After an asynchronous query is complete, you can then begin running another asynchronous query on the same Connection object. You must first test the value of the StillExecuting property to determine whether you can start the next asynchronous operation. Depending on the capabilities of your servers, to run multiple asynchronous queries at the same time, you may need to create separate Connection objects and run each asynchronous query on its own Connection object.

When running an action query as an asynchronous query, you’ll generally want to run it as part of a transaction. Be aware, however, that if you call the CommitTrans method while the asynchronous query is still running, your code will pause at the CommitTrans method until the query finishes. For this reason, it is more efficient to periodically check the StillExecuting property and continue to perform other work while the query runs. Once the StillExecuting property returns False, you can then call the CommitTrans method. This prevents your code from pausing at the CommitTrans method.

Note If you cancel an action query that is not part of a transaction, the query updates records up to the point where you called the Cancel method. The operation will be partially complete and will not be rolled back. For this reason, you should use the Cancel method on an action query only within the scope of a transaction. Additionally, if you start an asynchronous query in a procedure and the procedure exits before the query has completed, the query will continue to run.

To improve performance when you’re retrieving data from an ODBC data source, you can cache records locally. A cache is a space in local memory that holds the data most recently retrieved from the server. If you’re performing repeated operations on a set of data, caching that data makes those operations faster because you don’t have to retrieve the data from the server each time you need it.

In ODBCDirect queries, use the CacheSize property of the QueryDef object to specify the number of records to cache. The default cache size is 100 records. The following example shows how to reset the cache size to 200 records.

Sub SetCacheSize()
    Dim wrk As Workspace, qdf As QueryDef, rst As Recordset
    Dim cnn As Connection, strConnect As String

    Set wrk = CreateWorkspace("ODBCDirect", "Admin", "", dbUseODBC)
    strConnect = "ODBC;DSN=Pubs;UID=SA;PWD=;DATABASE=Pubs"
    ' Open connection.
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)
    ' Create temporary QueryDef.
    Set qdf = cnn.CreateQueryDef("tempquery")
    With qdf
        .SQL = "SELECT * FROM roysched"
        .CacheSize = 40
        Set rst = .OpenRecordset()
    End With
    ' Print recordset to Debug window.
    PrintRecordset rst
    rst.Close
    cnn.Close
End Sub


The Parameter Object and the Parameters Collection

The Parameter object in an ODBCDirect workspace is similar to the Parameter object in a Microsoft Jet workspace, with a few differences. In an ODBCDirect workspace, you can change the setting of the Type property, which is read-only in a Microsoft Jet workspace. You can also use the Direction property to indicate whether a parameter is an input parameter, an output parameter, or both, or the return value from the procedure. The following example specifies parameters for a query in an ODBCDirect workspace.

Function RunStoredProc() As Boolean
    Dim wrk As Workspace
    Dim qdf As QueryDef, rst As Recordset
    Dim cnn As Connection, strConnect As String, strSQL As String

    ' Create ODBCDirect workspace.
    Set wrk = CreateWorkspace("ODBCDirect", "Admin", "", dbUseODBC)
    strConnect = "ODBC;DSN=Pubs;UID=sa;PWD=;DATABASE=Pubs"
    ' Open connection.
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)

    strSQL = "CREATE PROCEDURE tamram @lolimit money AS " _
        & "SELECT pub_id, type, title_id, price " _
        & "FROM titles WHERE price >@lolimit"
    ' Execute SQL statement to create stored procedure.
    cnn.Execute strSQL

    ' Create new QueryDef.
    Set qdf = cnn.CreateQueryDef("RunStoredProc")
    With qdf
        ' Set SQL property to call stored procedure.
        .SQL = "{ call tamram (?) }"
        ' Provide value for parameter.
        .Parameters(0).Value = CCur(10)
        ' Open recordset on querydef.
        Set rst = .OpenRecordset()
        ' Print recordset to Debug window.
        PrintRecordset rst
    End With
End Function


The Recordset Object and the Recordsets Collection

The Recordset object represents the records that result from running a query on a Connection object or a Database object in an ODBCDirect workspace. The Recordsets collection contains all currently open Recordset objects on a Connection object or a Database object. The following table shows the relationship between the Recordset object and the Recordsets collection and other objects and collections in an ODBCDirect workspace.

Object or collection | Is contained by | Contains
Recordset object | Recordsets collection | Field objects
 |  | Properties collection
Recordsets collection | Connection object | Recordset objects
 | Database object |

The types of Recordset objects supported in an ODBCDirect workspace include the dynaset-type, snapshot-type, forward-only-type, and dynamic-type Recordset objects. For more information on all of these Recordset objects except the dynamic-type Recordset object, see “Dynaset-Type Recordsets,” “Snapshot-Type Recordsets,” “Forward-Only-Type Recordsets” earlier in this chapter. The following section describes dynamic-type Recordset objects.

Note When you use an ODBCDirect workspace, you can’t open a Recordset object using a QueryDef object name as you can from a Microsoft Jet workspace. For example:

Set rst = cnt.OpenRecordset("qrySelectEmps")

Instead, you should perform the OpenRecordset method directly from a QueryDef object like this:

Set qry = cnt.CreateQueryDef("qrySelectEmps", "SELECT * FROM Employees")
Set rst = qry.OpenRecordset()

Or you can specify the SQL statement as the source argument of the OpenRecordset method, as follows:

Set rst = qry.OpenRecordset("SELECT * FROM Employees", dbOpenDynamic)

This is because QueryDef objects in ODBCDirect are not permanent objects as they are in a Microsoft Jet workspace.


Dynamic-Type Recordset Objects

An additional type of Recordset object, the dynamic-type Recordset object, is available in ODBCDirect workspaces. Dynamic-type Recordset objects behave like dynaset-type Recordset objects, but they are updated dynamically as other users make modifications to the underlying tables. To create a dynamic-type Recordset object, specify the dbOpenDynamic constant for the type argument of the OpenRecordset method.

Dynamic-type Recordset objects are available only if you’re using an ODBC driver that supplies its own cursors. Because not all ODBC drivers supply their own cursors, you need to determine whether yours does before you try to open a dynamic-type Recordset object. If your ODBC driver doesn’t supply its own cursors, then you should open a snapshot-type or forward-only-type Recordset object instead.

See Also For more information on cursors, see “Using Cursors in ODBCDirect Workspaces” later in this chapter.

The advantage of using a dynamic-type Recordset object is that the recordset will immediately reflect any changes to the data, including added or deleted records. For example, if you open a dynamic-type Recordset object and another user edits a record in one of the underlying tables, that change will be reflected in the Recordset object you opened. In order to do this, however, DAO must constantly requery the data source, which may slow performance considerably. Therefore, avoid using dynamic-type Recordset objects except in situations where it’s crucial to have the most up-to-date data at all times.


Opening Recordset Objects Asynchronously

In addition to running queries asynchronously, you can open Recordset objects asynchronously. To do so, specify the dbRunAsync constant for the options argument of the OpenRecordset method. You can then use the Cancel method and the StillExecuting property directly on the Recordset object. For example, if you open a Recordset object asynchronously, and it takes a long time to open because more records are returned than expected, you can give users the option of canceling the operation in order to specify more restrictive criteria that returns fewer records.

If you cancel an OpenRecordset method, the Recordset object becomes invalid and you must reopen it to retrieve a valid Recordset object.

Because moving to the last record in a Recordset object can take a long time, the MoveLast method of a Recordset object supports asynchronous operation. To perform an asynchronous MoveLast operation, use the dbRunAsync constant with the MoveLast method. Be sure to check the StillExecuting property to determine when this operation is complete.


The Field Object and the Fields Collection

In an ODBCDirect workspace, the Field object represents a field in a QueryDef object or a Recordset object. When you’re performing batch updates, you can use the Value, VisibleValue, and OriginalValue properties of a Field object to verify successful completion of a batch update.

See Also For more information, see “Using Batch Optimistic Updating” in the following section.


Using ODBCDirect

The following sections explain how to perform some common operations in an ODBCDirect workspace: using batch optimistic updating, working with cursors, and working with stored procedures.


Using Batch Optimistic Updating

In many client/server applications, optimistic updates occur on a record-by-record basis. This usually happens with the following series of events:

  1. A user edits a record.

  2. The user tries to save the record.

  3. The server attempts to place a lock on that record, and if successful, the record is updated. Otherwise, a lock violation is handled by the application.

  4. The user moves to another record and the entire process is repeated.

Although this process works well for many applications, it is often more efficient to have the user edit multiple records that are cached locally and then submit these records to the server in a single batch for updating. This process is called batch optimistic updating. Note that this differs from performing transactions with the BeginTrans and CommitTrans methods. When using the BeginTrans and CommitTrans methods, data is sent to the server to be cached every time you use the AddNew or Update methods within the transaction. Then, data is written to the server database from the server’s cache when you use the CommitTrans method to commit the transaction.

Û To use batch optimistic updating

  1. Create an ODBCDirect workspace.

  2. Set the DefaultCursorDriver property of the workspace to dbUseClientBatchCursor.

  3. Open a Connection or Database object from the ODBCDirect workspace.

  4. Use the OpenRecordset method on the Connection or Database object to open a Recordset and specify the dbOptimisticBatch constant in the lockedits argument.

    Note When you don’t use the options argument, which precedes the lockedits argument, you must specify 0 for options.

  5. Perform any edits to the Recordset object. All edits are cached locally.

  6. When you are ready to update the data source, call the Update method on the Recordset object, specifying dbUpdateBatch for the type argument.

Note If you attempt a batch update while a record in that Recordset object is being edited by the user, the record being edited will automatically be updated before the batch update begins.

The following example illustrates how to use batch optimistic updating.

Function RunInBatch()
    Dim wrk As Workspace, cnn As Connection, rst As Recordset
    Dim strConnect As String

    ' Create ODBCDirect workspace.
    Set wrk = DBEngine.CreateWorkspace("ODBCDirect", "Admin", "", dbUseODBC)
    ' Set default cursor driver to dbUseClientBatchCursor.
    wrk.DefaultCursorDriver = dbUseClientBatchCursor
    ' Create connection string.
    strConnect = "ODBC;DSN=Pubs;DATABASE=Pubs;UID=sa;PWD=;"
    ' Open connection.
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)
    ' Open recordset on connection.
    Set rst = _
    cnn.OpenRecordset("SELECT * FROM sales", dbOpenDynaset, 0, dbOptimisticBatch)

    ' Change all records in local recordset.
    With rst
        Do Until .EOF
            .Edit
            !qty = !qty + 1
            .Update
            .MoveNext
        Loop
        ' Update all records in data source.
        .Update dbUpdateBatch
    End With
End Function

If multiple records have been edited locally, and you want to update the current record before you perform the batch update, you can call the Update method and specify the dbUpdateCurrentRecord constant for the type argument. This writes the current record to the data source without writing any other batch updates. This is illustrated in the following example.

' Edit and update first record.
    ' Only first record is written back to data source.
    With rst
        .MoveFirst
        .Edit
        !qty = !qty + 2
        .Update dbUpdateCurrentRecord
        ' Update remaining records in data source.
        .Update dbUpdateBatch
    End With


Handling Collisions

When you attempt to update a group of records in a single batch operation, it is possible that other users are editing one or more records you are trying to update, causing a collision. A collision occurs when a batch update attempts to update a record after another user has changed the record.

When you call the Update method, you can choose either to force all the changes you have made into the database automatically, or to examine each conflicting record and determine whether to overwrite the changed value with your new value, keep the changed value, or return to the original value that the record had before it was edited. To force all changes, call the Update method with the force argument set to True. To examine each conflicting record and decide which value to keep, call the Update method with the force argument set to False (the default value). You can then use the BatchCollisions and BatchCollisionCount properties to handle any collisions.

If you’ve decided to handle collisions, examine the BatchCollisions property on the Recordset object. The BatchCollisions property returns an array that stores bookmarks pointing to records in the Recordset object on which a collision occurred. Each time a collision occurs during a batch update, a bookmark for the record is added to the array returned by the BatchCollisions property. You can then move to each of these bookmarks and examine the following properties of the Field object of the current record.

Property | Description
Value | The current value of the field in your Recordset object. This corresponds to the value of the field after the Update method was called.
OriginalValue | The value of the field in your Recordset object before the Update method was called.
VisibleValue | The value of the field as it is stored in the database.

Caution Calling the Update method and specifying the dbUpdateBatch constant for the type argument and True for the force argument forces all your changes into the data source and overwrites any changes that other users made to the records. For this reason, it is safer to call the Update method without specifying the force argument, and then resolve collisions individually by using the array returned by the BatchCollisions property along with the Value, OriginalValue, and VisibleValue properties.

The following example shows how to use the array returned by the BatchCollisions property to examine conflicting records after a batch update.

Function BatchNoForceChanges()
    Dim wrk As Workspace
    Dim rst As Recordset, cnn As Connection
    Dim fld As Field
    Dim strConnect As String, intI As Integer
    Dim varCollision As Variant

    ' Create ODBCDirect workspace.
    Set wrk = DBEngine.CreateWorkspace("ODBCDirect", "Admin", "", dbUseODBC)
    ' Set default cursor driver to dbUseClientBatchCursor.
    wrk.DefaultCursorDriver = dbUseClientBatchCursor
    ' Create connection string.
    strConnect = "ODBC;DSN=Pubs;DATABASE=Pubs;UID=sa;PWD=;"
    ' Open connection.
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)

    ' Open recordset for batch optimistic updating.
    Set rst = _
        cnn.OpenRecordset("SELECT * FROM sales", dbOpenDynaset, 0, dbOptimisticBatch)
    ' Change all records in local recordset.
    Do Until rst.EOF
        rst.Edit
        rst!qty = rst!qty + 1
        rst.Update
        rst.MoveNext
    Loop

    ' Update with force argument of False (default).
    ' If another user has made changes since the updated records were
    ' edited, update fails, and the BatchCollisionCount and BatchCollisions
    ' properties will indicate the number of conflicts and the conflicting records.
    rst.Update dbUpdateBatch, False

    ' Check for collisions.
    For intI = 0 To rst.BatchCollisionCount - 1
        rst.Bookmark = rst.BatchCollisions(intI)
            For Each fld In rst.Fields
                If fld.OriginalValue <> fld.VisibleValue Then
                    Debug.Print fld.Value
                    Debug.Print "OriginalValue: " & fld.OriginalValue
                    Debug.Print "VisibleValue: " & fld.VisibleValue
                    Debug.Print "Value: " & fld.Value
                    .
                    .	' Handle the collision as you want to.
                    .
                End If
        Next fld
    Next intI
End Function

Note If a collision occurs because the primary key field has been modified by another user, using the OriginalValue and VisibleValue properties to view the field value will fail and return errors. This is because querying these properties implicitly re-queries the recordset and marks the copy of the record as deleted in the client batch cursor.


Using Cursors in ODBCDirect Workspaces

A cursor indicates the current record position in a result set. Most types of cursors contain a representation of the data in the data source, and are not updatable. Keysets are cursors that contain actual data, and are updatable.

You work with a cursor through the DAO Recordset object. When you open a Recordset object through DAO, ODBCDirect creates the corresponding cursor. Each type of Recordset object, except for the table-type Recordset object, corresponds to a different type of cursor.


Characteristics of Cursors

You can use cursors to work with sets of data on an ODBC data source. Cursors can:

  • Represent some or all records in a single table.

  • Represent some or all records in a multiple-table join.

  • Represent no records.

  • Be read-only or updatable at either the cursor or the field level.

  • Be fully scrollable, meaning that you can move forward and backward through the records, or they can be forward-only scrolling.

  • Exist on either the client or the server.


Client-Side Cursors vs. Server-Side Cursors

A cursor requires temporary resources to hold its data. These resources can be in the form of RAM, a paging file such as the virtual memory feature of Microsoft Windows, or temporary files or databases. If these resources are stored on the client machine, the cursor is called a client-side cursor. With this type of cursor, the server sends the data that the cursor represents across the network to the client, along with the data required by the cursor itself. The client manages the temporary resources needed by the cursor.

Some server database engines, such as Microsoft SQL Server version 6.0, support an additional type of cursor known as server-side cursors. With this cursor type, the server manages the result set with resources located on the server itself. The server returns only the requested data to the client over the network. Using this type of cursor can result in significant performance improvements compared to client-side cursors, especially in situations where excessive network traffic or inadequate network bandwidth is a problem. However, because RAM and disk space resources are needed at the server, you must plan accordingly and ensure that your server hardware is capable of managing all cursors requested by clients.


Choosing a Cursor Type

When you open a Recordset object on a non-ODBC data source, you can specify a constant for the type argument of the OpenRecordset method that determines what type of Recordset object is opened. When you open a Recordset object on an ODBC data source, you use this same argument to specify the type of cursor that the Recordset object represents. Each type of cursor corresponds to a type of Recordset object. The following table shows the four constants you can use for the type argument, the type of Recordset object that is created on a non-ODBC data source, and the type of cursor that is created on an ODBC data source.

Constant | Recordset type | Cursor type
dbOpenDynamic | Dynamic-type | Dynamic
dbOpenDynaset | Dynaset-type | Keyset
dbOpenSnapshot | Snapshot-type | Static
dbOpenForwardOnly | Forward-only-type | Forward-only scrolling (this is the default)

Note Table-type Recordset objects aren’t supported in ODBCDirect workspaces, so they have no corresponding cursor.

See Also For more information about ODBC cursors, see the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference.

The DefaultCursorDriver property of a Workspace object specifies where ODBCDirect creates the cursor — on the client or on the server. You can set the DefaultCursorDriver property to any of the constants listed in the following table.

Constant | Description
dbUseODBCCursor | Use client-side cursors. Client-side cursors give better performance for small result sets, but degrade quickly for larger result sets.
dbUseServerCursor | Use server-side cursors. For most large operations, server-side cursors provide better performance, but may cause more network traffic. Not all ODBC data sources support server-side cursors.
dbUseDefaultCursor | Use server-side cursors if the server supports them; otherwise, use client-side cursors.
dbUseClientBatchCursor | Use client batch cursors. Required for batch updates.
dbUseNoCursor | Open all Recordset objects as forward-only-type, read-only, with a rowset size of 1.

Note As of the time this book was printed, if you set the DefaultCursorDriver property of the workspace to dbUseClientBatchCursor, and then use the Move method on a Recordset object using a value of 2 or more for the rows argument, the cursor is moved forward one more row than expected.


Record Locking

When you open a Recordset object, you can also specify the type of record locking you want to use by setting the lockedits argument of the OpenRecordset method to the appropriate constant. The following table lists the five constants you can use for the lockedits argument of the OpenRecordset method, and describes the ODBC cursor lock type to which they correspond.

Constant | ODBC cursor lock type
dbOptimistic | Uses optimistic locking to determine how changes are made to the Recordset object in a multiuser environment. The page containing the record that is being edited is locked only while the record is being updated by the Update method.
dbPessimistic | Uses pessimistic locking to determine how changes are made to the Recordset object in a multiuser environment. The page containing the record that is being edited is locked as soon as you use the Edit method.
dbOptimisticValue | Uses optimistic concurrency based on record values. The cursor compares data values in old and new records to determine if changes have been made since the record was last accessed.
dbOptimisticBatch | Uses batch optimistic updating. For more information, see “Using Batch Optimistic Updating” earlier in this section.
dbReadOnly | Default for ODBCDirect workspaces. Prevents users from making changes to the data in the Recordset object.

When values or record versions are compared to test concurrency, the original record as it was prior to using the Edit or AddNew methods is compared with the record as it exists in the temporary buffer prior to being committed by the Update method. If the record data changed in this time frame, a trappable error results. At this point, your temporary record buffer is lost. To carry out the changes, you must refresh the current record by using Move 0, then use the Edit or AddNew method, fill in the new values, and try the Update method again.

Some combinations of cursors and lock types will not work together. For example, with Microsoft SQL Server version 6.0 cursors, if you specify the dbOpenSnapshot constant for the type argument of the OpenRecordset method, you must specify the dbReadOnly constant for the lockedits argument. Static cursors do not support the other types of record locking. Which combinations work together depends on the cursor driver. For specific information about compatible lock types, refer to your cursor driver documentation.

Your cursor driver can handle different combinations of cursor types and lock types in different ways. In some cases, it may return an error if it does not handle a specific combination. In other cases, it may switch to the nearest possible combination that it supports. If an error occurs, DAO places the error information in the Errors collection.


Cursor Limitations

In an ODBCDirect workspace, the default Recordset object is a read-only, forward-only-type Recordset object. Therefore, if you create the default Recordset object by opening it without specifying a value for the type argument, you won’t be able to edit data on the server. If you want to edit data on the server, you need to explicitly specify a lock type other than dbReadOnly for the lockedits argument of the OpenRecordset method.

When using an updatable ODBCDirect Recordset object, you can’t read or set the Value property of a field with a Memo (SQL_LONGVARCHAR) or OLE Object (SQL_LONGVARBINARY) data type more than once without refreshing the current record. If you try to do so, DAO will return error 3032. To work around this, you can use Move 0 to refresh the current record after a reading or setting the Value property of the field once. For example, to read and then set the Value property of the field, first read the property, use Move 0 to refresh the current record, then write the new value.

When performing operations on an ODBCDirect Recordset object within a transaction, the cursor is closed when you commit the transaction. If you try to use a method that requires the cursor immediately after committing the transaction, such as the RecordCount method, an error will occur. To avoid this, use the Requery method on the Recordset object after committing the transaction to reopen the cursor. The cursor is closed after you commit or roll back a transaction because this is the default behavior of the Microsoft SQL Server ODBC Driver. It is possible to change this default to keep the cursor open, however to do so, you must use the ODBC API to call the SQLSetConnectOption function to change the SQL_PRESERVE_CURSORS option.

See Also For more information, see Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference.

Because you can’t open a table-type Recordset object in an ODBCDirect workspace, you can’t use the Index property or the Seek method to retrieve data. Also, Recordset objects opened against ODBC data sources do not support any of the Find methods: FindFirst, FindNext, FindPrevious, and FindLast. In a client/server environment, it’s more efficient to retrieve only the data that you need, rather than retrieving more records than you need and then searching through those records for the data that you want. Therefore, design your queries to return only the records that you need.


Retrieving Multiple Result Sets

Any SQL statement can include multiple SELECT statements or stored procedures that invoke one or more SELECT statements. Each SELECT statement generates a result set that must be processed by your code or discarded before the resources are released and the next result set is made available. Because you don’t necessarily know how many result sets will be generated by a stored procedure, your code must be prepared to process an unknown number of result sets. Note that when a stored procedure returns multiple result sets, none of the result sets can be updated.

You can use either client-side cursors or server-side cursors to retrieve multiple result sets. If you use client-side cursors, multiple result sets are returned no matter what type of Recordset object you open. If you use server-side cursors to retrieve multiple result sets, you must open a forward-only-type Recordset object.

Û To retrieve multiple result sets

  1. Set the workspace’s DefaultCursorDriver property to dbUseServerCursor to specify server-side cursors.

  2. Create a QueryDef object and set its SQL property to a valid SQL string that returns multiple Recordset objects.

  3. Set the CacheSize property of the QueryDef object to 1 to request that the server sends you one record at a time. When you retrieve records in this way, you don’t actually use the cursor.

  4. Open a Recordset object on the QueryDef object you just created. Specify dbOpenForwardOnly for the type argument of the OpenRecordset method.

  5. Use the NextRecordset method to access the next Recordset object in the group of Recordset objects returned by the server. This discards the current Recordset object and replaces it with the next Recordset object specified in your query’s SQL statement. If there are no more Recordset objects in the group of Recordset objects, then the return value of the NextRecordset method will be False and the current Recordset object will be empty.

The following example prints the values of each field for each record in each result set.

Function GetMultipleResults()
    Dim wrk As Workspace, rst As Recordset, cnn As Connection, qdf As QueryDef
    Dim fld As Field, strSQL As String, strConnect As String, fDone As Boolean

    ' Create ODBCDirect workspace.
    Set wrk = DBEngine.CreateWorkspace("ODBCDirect", "Admin", "", dbUseODBC)
    ' Create connection string.
    strConnect = "ODBC;DSN=Pubs;DATABASE=Pubs;UID=sa;PWD=;"
    ' Open connection.
    Set cnn = wrk.OpenConnection("", dbDriverNoPrompt, False, strConnect)
    ' Create SQL statement.
    strSQL = "SELECT au_lname, au_fname FROM Authors; SELECT title FROM Titles;"
    ' Set default cursor driver.
    wrk.DefaultCursorDriver = dbUseServerCursor

    ' Create temporary QueryDef object.
    Set qdf = cnn.CreateQueryDef("", strSQL)
    qdf.CacheSize = 1
    ' Open recordset on QueryDef.
    Set rst = qdf.OpenRecordset(dbOpenForwardOnly)

    Do Until fDone = True
        ' Print values for each field in each record of recordset.
        While Not rst.EOF
            For Each fld In rst.Fields
                Debug.Print fld.Value
            Next fld
            rst.MoveNext
        Wend
        fDone = Not rst.NextRecordset()
    Loop
    rst.Close
    cnn.Close
    wrk.Close
End Function


Working with Stored Procedures

You can use ODBCDirect QueryDef objects to run stored procedures. ODBCDirect QueryDef objects support stored procedures that have both input parameters and return values. Input parameters are the parameter values supplied to the procedure at run time. The procedure’s return value is the value that it returns when it has finished running. For example, a stored procedure may return the number of records that have been affected.

The following example creates a stored procedure named GetEmps on the server, where strSQL is a string variable and cnn is a Connection object:

strSQL = "CREATE PROCEDURE GetEmps AS " _
    & "SELECT * FROM EMPLOYEE;"
cnn.Execute strSQL

If there is already a stored procedure named GetEmps on the server, you can use the DROP statement to delete it before creating a new one, as shown in the following example.

strSQL = "DROP PROCEDURE GetEmps;"
cnn.Execute strSQL

You can run the stored procedure by using the Execute method of a Connection object. To retrieve the return value, create a QueryDef object and open a recordset on it. In this example, qdf is a QueryDef object and rst is a Recordset object:

Set qdf = cnn.CreateQueryDef("qry", "{ call GetEmps() }")
Set rst = qdf.OpenRecordset

Use the Parameter object to work with parameters. The Direction property of a Parameter object tells DAO how the parameter will function. The ODBC driver tries to determine the parameter direction, but the Direction property is read/write, so you can set it if you need to. The following example creates a simple stored procedure with an input parameter and a return value. It then runs the procedure and retrieves the return value into a variable of type Variant, varX.

' Create stored procedure on the server.
strSQL = "CREATE PROCEDURE UpdateEmps (@invar int) AS RETURN @invar;"
cnn.Execute strSQL

' Create QueryDef object to run stored procedure.
Set qdf = cnn.CreateQueryDef("qry", "{ ? = call UpdateEmps(?) }")

' Handle parameters.
qdf.Parameters(0).Direction = dbParamReturnValue
qdf.Parameters(1) = 10
qdf.Execute

' Get return value.
varX = qdf.Parameters(0).Value


Managing Connection Resources

Microsoft Jet connections to remote data sources require memory resources on both client and server systems. The process of connecting to a server when you need to retrieve or change data can also take time on both machines. You can minimize the effects of connection delays and conserve system resources by following the guidelines in this section.

Note The information in this section applies to both Microsoft Jet and ODBCDirect Workspace objects.


Preconnecting

One of the things that Microsoft Jet hides from your program is the concept of connections. Every operation on an ODBC database requires a mechanism for conversing with the database server , and connections provide this mechanism. The first time you issue a command that requires access to the server, for instance, when you open a server table, Microsoft Jet opens a connection to the server. When you finish the operation, Microsoft Jet keeps the connection open in anticipation of the next operation. Similarly, if you have a connection open for a long time but aren’t doing anything with it, Microsoft Jet will silently close it and then reopen it when you resume working.

In some situations, you may want the connection to be opened at a time you control rather than when the first operation occurs. For example, if your program has a startup or initialization routine already in place, you may want to establish connections in that routine. This front-loads the execution time of your application, making it easier to avoid delays later in your application. Also, connections to your particular database might take a long time to establish if, for example, the database server is across the country or you’re connected by a modem.

One way to minimize delays in opening forms or populating tables is to connect to the database server when your application starts. This technique, called preconnecting, allows you to set up the connection to the database before the user requests data. Preconnecting effectively speeds up the first routine data retrieval operation in your application while causing only a slight delay during application startup.

There are two simple techniques for establishing a connection at application startup. The first technique applies to applications that can use different ODBC data sources; the second technique is used when the ODBC data source name is known and can be hard-coded into the application. Although each technique uses the database’s Close method, Microsoft Jet does not close the connection to the ODBC data source immediately. It caches the connection for the period of time specified by the ConnectionTimeout setting in the Windows Registry.

For example, the BookSales sample application opens a connection early in the program with the following code:

' Note that the sample uses the conConnect constant to provide the
' connection string. The constant has been replaced with
' full connection information in this code example.
Set dbs = OpenDatabase("", False, False, _
    "ODBC;DSN=Pubs;UID=sa;PWD=;DATABASE=Pubs")
dbs.Close

Although it appears that this operation won’t do anything, Microsoft Jet caches the connection for a time controlled by the ConnectionTimeout setting in the Windows Registry.

See Also For more information on Registry settings, see Appendix C, “Registry Settings.”


Preconnecting to an Arbitrary ODBC Data Source

When you design an application that uses an ODBC data source, you may not have all the information to establish a connection and authenticate a user. One way to get a user connected to an ODBC data source is to display a dialog box listing all registered ODBC data sources on the user’s system. You can prompt the user to choose an ODBC data source by supplying only "ODBC; " for the connect argument of the OpenDatabase method.

The user can then choose the appropriate data source name from the dialog box and supply user name and password information, if applicable. Your application can store the user’s connection information in a string by assigning the database’s Connect property to a string variable. The following code illustrates this technique:

Dim dbs As Database, strConnect As String

' When this line runs, the user is prompted to choose
' an ODBC data source.
Set dbs = OpenDatabase("", False, False, "ODBC;")
' The user's choice is stored in a string variable.
strConnect = dbs.Connect


Preconnecting to a Known ODBC Data Source

When you know information about the ODBC data source and can hard-code it into the application, you can preconnect to the data source. To do this, include the ODBC DSN and database information as part of your application’s code and require the user to supply his or her authentication information at run time. Construct a form to gather the user name and password and build a connection string containing these values, as in the following example:

' Create connection string.
strConnect = "ODBC;DATABASE=pubs;DSN=Pubs;" _
    & "UID=" & strUserName & ";" & "PWD=" & _
    strPassword & ";"
' Open ODBC data source.
Set dbs = OpenDatabase("", False, False, strConnect)

Note By default, if there is no explicit user name and password specified at connect time, Microsoft Jet will attempt to connect to an ODBC data source with the same user name and password supplied by the user to initialize the Microsoft Jet engine. To change this default and avoid the additional overhead of a failed logon attempt against ODBC data sources that use different user names or passwords, your application should set the Microsoft Jet TryJetAuth setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\ODBC key of the Windows Registry to 0. This disables the automatic authentication attempt.


Reducing Connection Use

Microsoft Jet can share multiple connections to an ODBC data source when the data source name and database are identical or when the data source names of the connections are identical and no connection uses a dedicated database (that is, none of the connections specifies a value for the DATABASE parameter in the connection string). For example, if you connect two applications to different tables in the same database, Microsoft Jet provides a single, dedicated connection to manage the bookmarks for each of the tables. A third, shared connection can then be used to retrieve records from, insert records into, and update records on either of the tables.

Microsoft Jet lets you execute multiple queries and allocates and terminates connections automatically. Depending on the ODBC database server you use, however, you may need to watch the number of queries that remain partially completed at a given time.

For example, an Oracle server can handle multiple, simultaneous partially completed queries. It does so by allocating additional resources at the database server layer. Microsoft SQL Server, however, uses a streamlined connection model that requires fewer resources on the database server. Consequently, each connection to Microsoft SQL Server can service only one query at a time.

If the database server used by your application supports multiple, simultaneous, partially completed queries on a single connection, your application effectively uses only one connection. If your application connects to a database server that services one query at a time, you should try to reduce the number of connections your application requires.


Reducing Connections by Limiting the Size of Recordsets

If your database server services one query at a time, you can reduce connection usage by limiting the size of dynaset-type Recordset objects to 100 records. A dynaset-type Recordset object with 100 or fewer records can be managed through a single connection. If the dynaset-type Recordset object contains more than 100 records, however, Microsoft Jet must use two connections to manage the recordset: one connection to maintain the key values and a second connection to retrieve the data associated with those keys for all records visible on the user’s screen. Although it’s possible for other dynaset-type Recordset objects to share the second (data values) connection, they cannot share the first (bookmark) connection, because it’s possible that not all key values have been retrieved from the server. You should note however, that as soon as all bookmarks have been retrieved, that connection is available for reuse.


Active Statements and Connections

When the results of a query on a remote data source haven’t been completely retrieved, the connection to the data source has an active statement on the connection. Depending on the type of data source or the behavior of the driver, you may not be able to execute another SQL statement on a connection until the active statement has completed. Furthermore, this type of connection is held open until the last record of the result set has been retrieved. In this case, Microsoft Jet may need to allocate additional connections to support other queries or operations (rather than discarding query results that have not been retrieved or forcing the application to complete the retrieval operation to close the statement).

To determine whether your ODBC data source or driver allows multiple, active statements on a single connection, Microsoft Jet requests the SQL_ACTIVE_STATEMENTS value from the ODBC driver with the SQLGetInfo API call. The value returned from the driver can represent a limitation in either the driver or the data source.

If the ODBC driver or data source is further limited by supporting only one statement per connection and only one connection per session (that is, a driver that doesn’t allow Microsoft Jet to open a second connection), Microsoft Jet forces linked tables to behave as snapshot-type Recordset objects by ignoring any unique indexes on the remote table. This allows Microsoft Jet to provide read-only data access to the remote data source, rather than denying access to the data source due to insufficient connection resources.


Completing Active Statements

If you need to close connections to conserve connection resources, you can complete the active statement on a connection. Active statements can usually be completed or cleared by retrieving the remaining records in the Recordset object containing the query results. The easiest way to complete a statement that has a pending retrieval operation is to use the MoveLast method of the Recordset object. This closes the active statement and releases the connection associated with the statement.

Note This technique is not recommended for very large Recordset objects.


Setting Connection Timeout

One way to prevent idle connections from being cached is to set the ODBC connection timeout value to a very low number. By default, the ODBC timeout value is set to 60 seconds (or one minute), but you can change this value in one of two ways. If you want to set the value for every instance of the Microsoft Jet engine to another value, simply change the ConnectionTimeout setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\ODBC key in the Windows Registry to the new idle time (in seconds).

You can also set the value of the ConnectionTimeout setting by using the SetOption method in code. For more information, see “Using the SetOption Method to Control Registry Settings” in Chapter 13, “Optimizing Performance.”

Microsoft Jet closes the connection after the specified number of seconds of idle time unless a condition exists that prevents the connection from closing. This means that Microsoft Jet closes connections even if forms displaying remote data are still open. Microsoft Jet automatically reconnects when the application requires a connection again.

See Also For information on the conditions that can prevent connections from closing, see the following section, “Connections That Microsoft Jet Cannot Close.”

Note A ConnectionTimeout setting of 0 causes the connection to remain open indefinitely (no timeout).


Connections That Microsoft Jet Cannot Close

There are two reasons that Microsoft Jet refuses a request to close a connection even when the idle time has expired. The first is pending query results, as discussed earlier in this chapter. If your application hasn’t finished retrieving the results of a query against a remote data source, Microsoft Jet will be unable to close the connection until the active statement on the connection has completed. To learn how to complete a statement, see “Completing Active Statements” earlier in this chapter.

The second reason that Microsoft Jet might refuse to close a connection is that the connection has a pending transaction. Your application must commit or roll back the transaction before Microsoft Jet can release the connection. Because the decision to commit or roll back is usually based on logic in your application, you must ensure that all branches in your decision tree result in one of these conclusions. If the application does not determine the outcome of the transaction, Microsoft Jet will automatically roll back the transaction when you close the Workspace object that initiated the transaction.


Caching Authentication Information

As discussed earlier in this chapter, Microsoft Jet tries to use the authentication information supplied to the local Microsoft Jet engine against the remote data source, unless you specify otherwise. You can use this behavior to allow the user to provide one user name and password for all data (local or remote) accessed by your application. In environments where this is not practical , you may want to set the TryJetAuth setting in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\ 3.5\Engines\ODBC key in the Windows Registry to 0. This also means that each time you attempt to use a linked table that doesn’t contain a stored user name and password, you must supply a user name and password to access the remote data. In this case, Microsoft Jet prompts the user for a user name and password through an ODBC driver logon dialog box.

Once the user has logged on to the remote data source, Microsoft Jet stores the authentication information until the application closes. This allows reconnections to the data source to occur behind the scenes and without interruption. Cached authentication information can also be applied to other tables on the same data source, provided they are in the same database. If your application connects the user to another database server or database, Microsoft Jet again prompts the user for a user name and password through the ODBC driver logon dialog box — if a user name and password are required.

Once the user establishes a connection to a remote database using a specific user name and password, that authentication information does not change throughout the user’s session (until the application closes). This behavior, which is an effect of the Microsoft Jet connection sharing and caching model, prevents the loss of the user’s authentication information even if the user opens remote linked tables that have different user names and passwords stored within. If your application design requires different levels of security on a number of remote tables, you should use the data source’s security features to ensure that each user has the necessary permissions.

See Also For more information on security, see Chapter 10, “Managing Security.”


Chapter 10 Managing Security

Establishing security for your application protects sensitive data and intellectual property such as your application’s structure and programming code. It also prevents your application’s users from inadvertently changing object designs or code in a way that would cause your application to stop working. This chapter is designed to explain the Microsoft Jet security model and show you how to properly implement it in your application.

The Microsoft Jet Security Model

Securing Your Database with Microsoft Access

Saving Your Database as an MDE File

Using DAO to Program Security

Owner Access Queries

Security and Database Replication

Database Encryption

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


The Microsoft Jet Security Model

Microsoft Jet provides a robust and powerful security model that gives you a great deal of control over users’ access to your application. Because of the flexibility, the model is somewhat more complicated than those provided by other desktop databases. Indeed, security is one of the more commonly misunderstood aspects of Microsoft Jet and is usually incorrectly implemented by developers who lack a cohesive understanding of its workings. The following section contains detailed information, structured in a way that helps you understand the model before you implement security.


User-Level and Share-Level Security

Microsoft Jet provides both share-level and user-level security. In share-level security systems, passwords are associated with specific objects, not with users. Any user who knows the password for an object can access that object. Microsoft Jet provides a simple form of share-level security: the ability to password-protect opening a database. Add a password to your database when all you want to do is prevent unauthorized users from opening your application, and you’re not concerned about what authorized users do once they’re inside.

If you need more control, however, perhaps because you want to prevent users from modifying the design of your tables and queries, Microsoft Jet also provides user-level security. With user-level security, users are authenticated when they open a Microsoft Jet Workspace object. A user is authenticated by logging on to the system with a name and password, which are compared to a database of user account information that is called a workgroup information file. After the user is authenticated, the system determines the user’s level of access to an object by comparing the user’s identification to a set of object permissions that have been defined for that object by the application’s administrator. Different users can have different levels of permissions for the same objects.

Microsoft Jet user-level security functions as follows: Administrators assign specific permissions for Database objects to users and groups. When a user starts up a session of the Microsoft Jet database engine in a secure environment, the user logs on, entering a user name and password. The password’s function is to authenticate the user, not to give access to any particular objects. Microsoft Jet then reads the current workgroup information file to determine all the groups the user belongs to, and stores that information internally. Every time the user tries to perform an action such as opening a database, browsing a table, or modifying a query, Microsoft Jet first checks to see if the user, or any of the groups to which the user belongs, has the necessary permissions to perform this action. If the user or groups the user belongs to have sufficient permissions, Microsoft Jet performs the action. If not, Microsoft Jet returns an error indicating that permission is denied, and the operation fails.


Advantages of User-Level Security

To see the advantages of user-level security, consider this example: Suppose you have a Salary table and two groups of people who use that table — managers who can update the table and workers who can view but not update the table. In a user-level system such as one created with Microsoft Jet, you can assign update permissions for the Salary table to the managers group and read permissions to the workers group. Then you add each user in your system into one of these two groups. Once this is done, users log on using their user account name and password. They need to perform no other action to use the application. They are granted the appropriate level of access to objects based on the permissions settings stored with each object and their group membership as established when logging on to the system. The users manage their own passwords, and their passwords are used to verify their identity rather than to identify a permission for an object.

In a share-level system, two passwords would be used to implement this scheme: an update password and a read password, both applied directly to the Salary table. In this scenario, you would have to provide the passwords directly to the appropriate people. This is even more complicated if you have to change a password; you must have administrative mechanisms in place to make sure that all users get their appropriate updated passwords. Also, any time a user is moved from the managers group to the workers group, the password for the managers group must be changed to maintain security.

You can see that even in this simple scenario, the administrative overhead associated with share-level security can be substantial. When you imagine a real application with many tables, users, and groups, you can quickly see that share-level security is a cumbersome solution.


Elements of the User-Level Security Model

To establish user-level security, Microsoft Jet uses a set of elements that, taken as a whole, describe the model. The four elements that make up the model are:

  • The user of your application. This may be a real person or a process running on a computer.

  • The group. A group is a logical collection of users who require the same level of access to a set of objects. Users can belong to more than one group.

  • The SystemDB or workgroup information file. This is a Microsoft Jet database that physically stores the definitions of users, groups, and passwords.

  • Your database. The database contains your application’s objects, along with the permission settings for each object for each user and group.

Figure 10.1 illustrates how these elements relate.

Figure 10.1 The Microsoft Jet security model


Users and Groups

Microsoft Jet defines users of your application in two ways: as individual users or as groups of users.

Note Users and groups share the same namespace in Microsoft Jet: You can’t have a user and group with the same name.

If you’ve never built or administered a user-level security system before, it’s important to understand that there is no such thing as a permission on an object that exists by itself. Permissions on objects are always granted to specific users or predefined groups of users. That is, a permission exists only in the dual context of a user or group and the object it applies to.

The Security Identifier

After a user logs on, Microsoft Jet uses a special number to identify that user. This number, called a security identifier (SID) is a machine-readable value that varies in length and will be 128 bytes at most. Microsoft Jet creates a SID when a user or group is created. When a user requests access to objects, Microsoft Jet uses the user’s or group’s SID for identification. The user’s name and password are not used. The name and password are used only for authentication — the process of verifying a person’s identity. From then on, Microsoft Jet uses the SIDs to determine a user’s access to objects.

The Personal Identifier

To generate a SID, Microsoft Jet uses a personal identifier (PID). This value is a variable-length string that you specify when you create a user or group. The user or group name and PID are fed to the encryption program that generates the SID for that account. If you feed the same user name and PID back into the encryption program, you get the same SID. This gives you the ability to re-create user accounts if your workgroup information file (system database) becomes corrupted or is lost.

Figure 10.2 Creating security identifiers

The Default User Account

Microsoft Jet defines a default user account named “Admin.” Before user-level security is established, all users are implicitly logged on as the Admin user. Unfortunately, the Admin user is not very well named, as this user has no particular administrative characteristics. Because most users are logged on as Admin without ever knowing it, and because by default all users have permissions for the objects they create, any objects owned by the Admin user or for which the Admin user has explicit permissions are unsecured. Think of the Admin user as the default User account or, for those familiar with Windows NT security, the Everybody account.

Understanding how the SIDs of these default accounts are generated helps you understand how the security model works. The Admin user’s SID is identical across all installations of the Microsoft Jet database engine. Even if no one in your workgroup is using the Admin account (because you have password-protected it), all objects owned by Admin, or for which Admin has explicit permissions, are still open to anyone using Microsoft Jet. This is one of the most common misunderstandings regarding Microsoft Jet security. Fortunately, once you understand the problem, it’s easily prevented: Make sure that Admin has no explicit permissions and owns no objects. The User-Level Security Wizard makes this easy.

Microsoft Jet also creates two accounts, Creator and Engine, that are used internally. These accounts are not accessible by users.

Default Groups

Microsoft Jet defines two default groups for you: the Admins group and the Users group.

The Admins Group

The Admins group is designed to hold user accounts for people who are the true administrators of the workgroup. They manage user and group membership and have the power to clear users’ passwords. Members of the Admins group that was in use when you created the database always have permission to grant permissions on any object in that database. For example, if user Bob is a member of the Admins group, he may not have permissions to open a particular table, but he will be able to grant himself permission to open that table, even if he doesn’t own the table. The Admins group of other workgroup information files don’t have this privilege. The Admins group should have at least one member at all times.

Caution Although Microsoft Jet allows you to delete the last member of the Admins group, this is not advised, because only the object’s owner has irrevocable permissions for that object. If that owner’s account ever becomes corrupted or deleted, and cannot be re-created because you don’t have that user’s name and PID, there is no way to recover the permissions for that object. In other words, there is no member of the Admins group to fall back on.

The SID of the Admins group is unique for each workgroup information file and is generated when the file is created. When you use the Workgroup Administrator to create the workgroup information file, this SID is generated by encrypting three strings that you enter: user name, organization name, and workgroup ID (WID). The Microsoft Access Setup program also creates a workgroup information file, the default System.mdw. However, Setup uses only the user’s name and the company name (provided during Setup) to generate the SID for the Admins group of the default workgroup information file. Therefore, you shouldn’t use this default workgroup information file to secure your database, because these two values are available from the Microsoft Access Help menu. If you use the default workgroup information file, unauthorized users trying to breach security could re-create an identical Admins account by using the Workgroup Administrator to create a new file with these values, and then adding themselves to the Admins group.

Before establishing user-level security, use the Workgroup Administrator to create a new workgroup information file, making sure to enter a workgroup ID value that is known only to you. This ensures that the new workgroup information file contains an Admins group with a unique, secure SID. For information about using the Workgroup Administrator, see “Creating the Workgroup Information File” later in this chapter.

The Users Group

The Users Group is the default group for new users. By definition, all users of your application are members of the Users group. Any permissions assigned explicitly to the Users group are available to all users in all installations of Microsoft Jet. By default, the Users group has full permissions for all newly created objects. This is the main mechanism Microsoft Jet uses to “hide” security for the majority of applications that don’t need it. Even though Microsoft Jet security is always “on,” if you don’t need its functionality and don’t want your users to have to log on and worry about permissions, Microsoft Jet ensures that you don’t have to worry about security by making all users members of the Users group and granting that group full permissions.

Ownership

Understanding the concept of ownership is crucial to understanding the Microsoft Jet security model. The user who creates an object owns that object. This ownership grants that user special privileges for that object; thus, he or she can always assign or revoke permissions for that object. This privilege cannot be revoked by any other user, including members of the Admins group.

Before you establish user-level security, the default Admin user is the owner of the database and all the objects in it. To effectively manage security, you need to change the ownership of the database and all the objects in it. You can do this for all objects except the database itself directly through Data Access Objects (DAO) by using the Owner property, or in Microsoft Access on the Change Owner tab in the User And Group Permissions dialog box (Tools menu, Security submenu). However, you can’t change the owner of a database on the Change Owner tab in the User And Group Permissions dialog box. The only way to transfer the ownership of all of the objects and the database itself is to log on as the user you want to own the database, create a new blank database, and then import all of the objects. When you run the User-Level Security Wizard to secure your database, it uses this method to transfer ownership of the database and all of its objects.

Note These procedures don’t change the ownership of queries whose RunPermissions property is set to Owner’s. You can change ownership of a query only if you own the query, or if its RunPermissions property is set to User’s. For more information about the RunPermissions property, see “Using the Microsoft Access RunPermissions Property with User-Level Security” later in this chapter.

The owner of the database itself is the user account in use when the database is created. The database owner can always open a database and create new objects in it. The database owner can also grant or revoke the permission to open a database. A database owner’s rights can’t be revoked by using the User And Group Permissions dialog box.

Important If you don’t transfer database ownership to a secure user account, any users of Microsoft Access or Visual Basic can open your database and grant themselves full permissions.


The Workgroup Information File

As mentioned earlier, User and Group objects and their passwords are not stored in your database. They are stored in a Microsoft Jet database known as the workgroup information file, which is also referred to as the system database. The default name of this database is System.mdw, and it’s a standard database in that Microsoft Jet can open it. While it’s structurally no different from the databases you create, it does contain several system tables that the engine uses to store security information. You can’t make changes to the contents of those tables by opening a workgroup information file directly; the information stored in them is encrypted and protected. To define user accounts and security groups, you must use the User and Group objects in DAO code, or use the Microsoft Access security user interface.

The workgroup information file stores information about each user and group in a workgroup. This information includes which users belong to which groups, encrypted passwords, and the SID for each user and group. When you use Microsoft Jet commands that affect user or group objects or their passwords, the engine reads and writes to the workgroup information file.

Figure 10.3 The workgroup information file

You can use the same workgroup information file for more than database. As long as the user, group, and password information remains the same across databases, you can point Microsoft Jet to the same workgroup information file. When a user logs on to Microsoft Jet, the engine looks in the workgroup information file for the user’s name and password to authenticate the user. From then on, Microsoft Jet uses only the SID that it found for that user.

Keep in mind that when you specify (“join”) a workgroup information file, the user and group accounts it contains remain in effect until you specify another workgroup information file. If you perform operations on other databases while that workgroup information file is current, you may see unexpected results. For this reason, you may want to create a separate workgroup information file for each database you secure.

See Also For more information, see Appendix C, “Registry Settings .”

In Microsoft Jet versions prior to 3.0, the initialization file, usually MSAccess.ini or MSAcc20.ini, was used to store the path and name of the workgroup information file. In versions 3.0 and 3.5, the path and name of the workgroup information file are stored in the Windows Registry. In all versions, the keyword or registry value used to specify the path to the workgroup information file is SystemDB. You can set or return the path for the current location of the workgroup information file in Visual Basic by using the SystemDB property of the DBEngine object.

Microsoft Access Startup Command-Line Options

You can start Microsoft Access with the following command-line options, which configure the security environment:

  • /user username. Starts Microsoft Jet using the specified user name.

  • /pwd password. Starts Microsoft Jet using the specified password.

  • /profile user profile. Starts Microsoft Jet using the options in the specified user profile instead of the standard Windows Registry settings created when you installed Microsoft Jet. This replaces /ini, used in previous versions of Microsoft Jet to specify an initialization file.

  • /wrkgrp path of workgroup information file. Starts Microsoft Jet using the specified workgroup information file.

These startup command-line options are particularly useful when creating a Windows shortcut to open a secured database. For example, instead of using the Workgroup Administrator to specify a workgroup information file before opening a secured database, you can create a shortcut that uses the /wrkgrp option to specify the file when the database is opened.

Note that you must include the full path to the Microsoft Access executable file, as well as to the database. For example, the following command line starts Microsoft Access and opens the Northwind sample database using the MySystem.mdw workgroup information file:

“C:\Program Files\Microsoft Office\Office\MSAccess.exe” “C:\Program Files\Microsoft Office\Office\Samples\Northwind.mdb” /wrkgrp “C:\Windows\System\MySystem.mdw”


The Secured Database

The final element of Microsoft Jet security is your database. This is where the objects that have to be secure are stored. The actual permissions related to specific objects for specific users and groups are stored in system tables in your database. However, the definitions of user or group accounts themselves is not stored here. Such information is stored in the workgroup information file.

For example, when a user requests access to an object, Microsoft Jet has already retrieved the SIDs for the user and any groups to which he or she belongs from the workgroup information file when the user logged on. These SIDs are then compared to the SIDs in your database’s system tables to verify that the user has sufficient permissions to access the object.

Permissions

Microsoft Jet defines a set of permissions that give you fine control over a user’s access to an object. For example, you can allow a user to read an object’s contents, but not change them.

Microsoft Jet supports both explicit and implicit permissions. Explicit permissions are those you explicitly grant to an individual user. When an explicit permission is granted to a user, no other users are affected. Implicit permissions, on the other hand, are granted to a group. Because a group contains users, all users in a group receive the permissions granted to the group. If a user is added to a group, that user automatically gets all the implicit permissions of that group. Microsoft Jet uses the least restrictive set of permissions among the permissions granted to a user and all groups to which that user belongs. For example, if a user has explicit read permissions for a table but no write permission, and that same user also belongs to a group that has write permission for the table, the user will be able to write to the table because of the implicit write permission.

Microsoft Jet defines permission levels as numeric constants. For convenience (and more readable code), you can use built-in constants when specifying permissions. These built-in constants are available in Microsoft Access, Visual Basic, Microsoft Excel, Microsoft Word, Microsoft PowerPoint, and Visual C++. For other programming environments, you may want to create the constant definitions yourself.

Note You should generally use predefined constants instead of hard-coded values. These values may change in future releases of Microsoft Jet, but the constants should always be valid. The description of the following constants apply to both Container and Document objects.

The following set of tables describes the possible permission settings. Later in this chapter you’ll learn how to apply permissions to objects by using these settings.

The following table shows permissions available for all Microsoft Jet objects.

Constant | Description
dbSecNoAccess | No access to the object
dbSecFullAccess | Full access to the object
dbSecDelete | Can delete the object
dbSecReadSec | Can read the object’s security-related information
dbSecWriteSec | Can alter access permissions
dbSecWriteOwner | Can change the Owner property

The following table shows permissions available for the Tables Container object or any Document object in a Documents collection.

Constant | Description
dbSecCreate | Can create new documents (valid only with a Container object)
dbSecReadDef | Can read the object definition, including field and index information
dbSecWriteDef | Can modify or delete the object definition, including field and index information
dbSecRetrieveData | Can retrieve data from the Document object
dbSecInsertData | Can add records
dbSecReplaceData | Can modify records
dbSecDeleteData | Can delete records

The following table shows permissions available for the Databases Container object or any Document object in a Documents collection.

Constant | Description
dbSecDBCreate | Can create new databases (valid only on the Databases Container object in the workgroup information file [System.mdw])
dbSecDBExclusive | Exclusive access
dbSecDBOpen | Can open the database
dbSecDBAdmin | Defines the right of a user to change the database password


Securing Your Database with Microsoft Access

It’s important to realize that Microsoft Jet security is always enabled — every time a user performs any action, Microsoft Jet first checks to make sure the user has sufficient permissions to perform that action. However, most Microsoft Jet users never realize that they are logging on and never see a security-related message. How does this happen? This is because the Users group (the default group the contains the default Admin user account) has full permissions for all objects in a database.

This arrangement works well because there are no “back doors” that allow a user to open a Microsoft Jet database without security being in effect. Applications that don’t need the engine’s security services still use it, even though it’s not apparent.

If you’re using Microsoft Jet with Visual Basic, you can run your application without a workgroup information file. In this case, all users are logged on as the default Admin user. In an unsecured environment, this account has full permissions. In a fully secured environment, the default Admin user doesn’t have any permissions and is not able to use the Microsoft Jet database. To use security from a Visual Basic application, first set up security using Microsoft Access, and then create a Registry entry for your application that points to your workgroup information file.

To establish security successfully, you need to do some advance planning. Make a list of all of the potential users of your applications, logical groupings of those users, and the operations that each group should be able to perform. Then map this information to the Microsoft Jet security model of users, groups, and object permissions.

Securing your database is a multiple-step process that replaces the default users and groups with your own secure set of users and groups. This section outlines each of these steps.


Using Microsoft Access as a Security Tool

If you’re building Microsoft Jet applications using a program other than Microsoft Access (such as Visual Basic or Microsoft Excel), you can secure your applications using DAO. This interface gives you much of the same functionality that exists in Microsoft Access. However, to create a secure environment, you must build workgroup information files with the Workgroup Administrator program (Wrkgadm.exe, installed by default in the \Windows\System folder). This utility ships only with Microsoft Access. Because of this, you must use Microsoft Access to set up the initial elements of a secured system.

Because Microsoft Access includes Workgroup Administrator, and because Microsoft Access has a full user interface for working with security, many developers using Visual Basic (or other programming environments that support Microsoft Jet) use Microsoft Access to establish and maintain security. Although you have full programmatic access to existing security through DAO, it’s often easier to use Microsoft Access to develop and maintain security in your application.

Additionally, the User-Level Security Wizard described in this chapter is included with Microsoft Access. If you want to use its functionality, you must use Microsoft Access.


Creating the Workgroup Information File

Because the Microsoft Jet security model relies on the existence of a workgroup information file to store user, group, and password information, your first step in securing your database is to create a workgroup information file unique to your application. This process can be accomplished only with Workgroup Administrator, which ships with Microsoft Access. The key point of this process is that it creates a unique SID for the Admins group. Using Workgroup Administrator (Wrkgadm.exe), follow these steps:

Û To create a workgroup information file unique to your application

  1. Exit Microsoft Access.

  2. Start the Workgroup Administrator program (Wrkgadm.exe). By default, Wrkgadm.exe is installed in the \Windows\System folder when you install Microsoft Access.

  3. Click Create to create a new workgroup information file.

  4. Specify a user name, an organization, and a unique workgroup ID, and then click OK.

    The workgroup ID can contain letters and numbers, and must be between 4 and 20 characters long. This value is used by the encryption program, along with the user name and organization name, to create an encrypted value unique to your workgroup.

  5. Specify the name and path for the new workgroup information file, and then click OK.

    Workgroup information files by default have an extension of .mdw, but this is not required. You can assign any extension.

    Important Don’t overwrite your default System.mdw file (for Windows 95 the default file is located in the System folder; for Windows NT Workstation it is located in the System32 folder). If you do so, you must log on by entering “Admin” for the user name and providing the appropriate password when opening all databases.

  6. A confirmation form displays the information you’ve entered so far. If the information is correct, click OK to create the workgroup information file. The new file automatically becomes the current workgroup information file for your installation of Microsoft Access. There is no need to “join” the new file.

    Important Be sure to write this information down and store it in a safe place. If your new workgroup information file ever becomes corrupted or is lost, you will need this information to re-create it.


Creating a New Administrator Account

As mentioned earlier, the Admin user is a default user used by Microsoft Jet for all installations of the Microsoft Jet engine. Because the Admin user is the same across every installation of Microsoft Jet, you want to make sure your secure objects are owned by a new account you create, not by the Admin user.

The easiest way to do this is to use Microsoft Access. You could also write DAO code to programmatically create the user.

Û To create a new administrator account with Microsoft Access

  1. On the Tools menu, point to Security, and then click User And Group Accounts.

  2. On the Users tab, click New.

  3. In the New User/Group dialog box, type a name and personal ID (PID) for the new administrator account, and then click OK.

  4. In the Name box, select the new administrator account’s name, and then under Available Groups, click Admins. Click Add. This adds your new administrator account to the Admins group.

  5. In the Name box, select Admin, and then, under Member Of, click Admins. Click Remove. This removes the default Admin user from the Admins group.

Caution Be sure to write down the name and PID for this new administrator account, and store it in a safe place. You will need it if you ever have to re-create the account. The name and PID are case-sensitive.

Before you can log on as your new administrator account, you must set a password for the default Admin user account so that the Logon dialog box will display the next time you start Microsoft Access. When you log on as your new administrator account, you can create a password for that account.

Û To set a password for the default Admin user account

  1. On the Tools menu, point to Security, and then click User And Group Accounts.

  2. On the Change Logon Password tab, enter a new password for the default Admin user account. Don’t type anything in the Old Password box.

    Caution Passwords are case-sensitive. Be sure to write down this password and store it in a safe place.

  3. Exit Microsoft Access.


Running the Microsoft Access User-Level Security Wizard

The next step does the actual work of securing your application.

The User-Level Security Wizard creates a new database, exports copies of all of the objects from the original database, secures the object types you selected by revoking all permissions of the Users group for those objects in the new database, and then encrypts the new database. The original database isn’t changed in any way.

If you choose to secure all object types in the database, the User-Level Security Wizard removes the Users group’s Open/Run permission for the database itself. This means that only members of the Admins group of the workgroup information file in use when you run the User-Level Security Wizard can open the new secured database. No other users of Microsoft Access or Visual Basic can open the database or access the secured objects within it.

If you choose to secure only some object types in the database, the User-Level Security Wizard doesn’t remove the Users group’s Open/Run permission for the database itself. In this case, all users of Microsoft Access and Visual Basic can open the new secured database and access unsecured objects, but they can’t access the secured objects within it.

Note that the User-Level Security Wizard does not change the original database in any way. The new database it creates becomes your secured database. At that point, the only users that can access the objects you select when running the wizard are your administrator account and any other users in the Admins group of the workgroup information file in use when you run the wizard. To provide access to other users, create users and groups and assign them the appropriate permissions.

Important Make sure you’re using the workgroup information file that you created in procedure described in the “Creating the Workgroup Information File” section above. You can confirm this by running Workgroup Administrator (Wrkgadm.exe). If your workgroup information file isn’t displayed in the first Workgroup Administrator dialog box, click Join and select your workgroup information file. Do not use the default System.mdw workgroup information file that ships with Microsoft Access.

Û To secure your application with the Microsoft Access User-Level Security Wizard

  1. Start Microsoft Access and enter the name for the new administrator account in the Logon dialog box. The account has no password the first time you use it, so don’t enter a password.

    This step ensures that the User-Level Security Wizard treats your new administrator account as the owner of all objects in the database.

    Tip After you log on, you can define a password using the Change Logon Password tab on the User and Group Accounts dialog box (Tools menu, Security submenu).

  2. Open the database you want to secure.

  3. On the Tools menu, point to Security, and then click User-Level Security Wizard.

  4. Select the check boxes for the object types you want to secure, and then click OK.


Creating Your Own Users and Groups

Now that you have a database containing your secured objects, create the users and groups that your application needs. The easiest way to do so is through the Microsoft Access User And Group Accounts dialog box, available from the Security submenu on the Tools menu.

Note As mentioned earlier, it’s best to create groups and then assign permissions to the groups rather than to individual users. This way, administering your workgroup becomes simply a matter of assigning users to the appropriate groups.


Assigning Permissions to Users for the Objects

The final step is to assign the actual permissions for objects to users and groups. This step tells Microsoft Jet which users can access which objects. Again, Microsoft Access offers the quickest way to accomplish this; use the User And Group Permissions dialog box (Tools menu, Security submenu).

Your new database is now secure. The only people who can get into the objects in the database are those to whom you assigned permissions, and anyone who is a member of the Admins group of the workgroup information file in place when you ran the User-Level Security Wizard.


Securing a Front-End/Back-End Application

There are additional factors to take into account when establishing user-level security for an application that has been split into a back-end database containing only tables, and a front-end database containing the remaining objects and links to the tables in the back-end database. You may not know the name of the specific network location for the back-end database in advance, or you may want to allow a database administrator to move the back-end database. In either situation, users must be able to relink to the tables in the back-end database.

Û To establish user-level security for a front-end/back-end application

  1. For both databases, follow the procedures described earlier in this section for establishing security with the User-Level Security Wizard.

  2. Assign permissions to the appropriate groups to read, update, insert, or delete data in the back-end database. To do so, either assign permissions directly for the tables in the back-end database, or remove all permissions for those tables and create queries in the front-end database that have the RunPermissions property set to Owner’s, and that use the tables in the back-end database.

    See Also For more information about the RunPermissions property, see “Using the Microsoft Access RunPermissions Property with User-Level Security” later in this chapter.

  3. Assign Open/Run permission for the Database object in the back-end database to all groups you worked with in Step 2. This is required even if you are using queries with the RunPermissions property set to Owner’s.

  4. In the front-end database, grant users Modify Design permission for the linked tables. Granting users Modify Design permission on linked tables in the front-end database doesn’t grant them the same rights to the tables in the back-end database.

    By default, after you run the User-Level Security Wizard, your database users have permission to create new tables and queries in the front-end database. If Database Access Objects (DAO) code has been used to remove this permission, you must use DAO code and the dbSecCreate constant to reassign this permission.

    See Also For more information about using DAO code to assign permissions, see “Securing Objects” later in this chapter.

  5. When the users first install your application, have them run the Linked Table Manager (Tools menu, Add-Ins submenu) from the front-end database to refresh the links to the tables in the back-end database in its new location.

    Tip You can also write code that prompts users during startup to refresh table links. For sample code that does this, see the RelinkTables and RefreshLinks functions in the RefreshTableLinks module of the Orders sample application, which is included with Microsoft Access.

Because users have Modify Design permission for the linked tables in the front-end database, they can reset the links to the back-end tables if the location of the back-end database changes. They can’t make any modifications to the design of the actual tables in the back-end database.


Removing Security from a Database

Securing a database is usually a one-way process. However, certain development issues might require you to work with an unsecured copy. You can follow these steps to reverse the process of securing your database.

Û To remove security from a database

  1. Make a backup copy of the database you want to remove security from.

  2. Start Microsoft Access and log on as a member of the Admins group. This can be the administrator account you created when you secured the database, or any member of the Admins group. Be sure you’re using your own secure workgroup information file when starting Microsoft Access.

  3. Open the database.

  4. On the Tools menu, point to Security, and then click User And Group Permissions.

  5. In the User And Group Permissions dialog box, assign full permissions to the Users group for the database and all the objects in the database.

    Because all users are automatically part of the Users group in Microsoft Jet, this step has the effect of rendering security “invisible” again.

  6. On the Tools menu, point to Security, and then click User And Group Accounts. On the Users tab, click Admin in the Name box, and then click Clear Password.

    Clearing the password for the Admin user disables the Logon dialog box that displays when you start Microsoft Access. You and your users will be automatically logged on as the Admin user the next time you or they start Microsoft Access.

    Important This step disables the Logon dialog box for all databases using the same workgroup information file.

  7. Restart Microsoft Access.

  8. Create a new database and import all objects from the secured database. You can accomplish this easily by using the Import command (File menu, Get External Data submenu).

Note In versions prior to Microsoft Access for Windows 95, database relationships cannot be imported, either through Microsoft Access or with the Microsoft Access Import Database add-in. You can write DAO code to programmatically re-create them, or re-create them manually.

The new database you created in step 8 is now an unsecured version of the original. As always, Microsoft Jet security is still active, but once again, it’s transparent.


Securing Your Database Without Asking Users to Log On

If you want to secure some objects in a database but you don’t need to grant various permissions to different groups of users, you might want to consider securing your application without asking users to log on. If you have secured your application properly, the Admin user should no longer be a member of the Admins group.

Û To secure your application without asking users to log on

  1. Follow the procedures to secure a database with the User-Level Security Wizard in the “Securing Your Database with Microsoft Access” section earlier in this chapter.

  2. While logged on as a member of the Admins group, assign the permissions to the Admin user for objects you want to be available to all users. Typical permissions may include Read Data and Update Data permissions for tables and queries, and Open/Run permission for forms and reports. Don’t give the default Admin user the permission to modify the design of tables and queries, or Administer permission for the database.

  3. Clear the default Admin user’s password.

    Important This step disables the Logon dialog box for all databases using the same workgroup information file.

Users can now open your database without logging on — that is, they are automatically logged on as the Admin user. They will be able to perform all actions you gave the Admin user permissions to perform in step 2. This works for any workgroup because the Admin user account is the same in every workgroup information file. Only members of the Admins group of the workgroup information file that was in use when you ran the User-Level Security Wizard in step 1 have full permissions on the objects in your database.

Important Do not distribute copies of the workgroup information file in use when you secured the database in this procedure. If you need to allow a user to administer your database, give the copy of the workgroup information file to that user only.

To perform administrative functions, you must use the workgroup information file that was in use when you secured the database. There are two ways you can log on as a member of the Admins group of that workgroup information file:

  • You can temporarily define a password for the Admin user to reactivate the logon procedure, and then log on as a member of the Admins group.

  • You can use the /user and /pwd command-line options to specify your user name and password when starting Microsoft Access. If you define a shortcut that uses the /user and /pwd command-line options to do this, either delete the shortcut when you’re done, or physically secure the computer that the shortcut is located on, to prevent unauthorized users from accessing this information. For more information about command-line options, see “Microsoft Access Startup Command-Line Options,” earlier in this chapter.


Using the Microsoft Access RunPermissions Property with User-Level Security

For Microsoft Access to display a table or query, it needs to read the design of that table or query. For example, it needs to read field names and other field properties, such as the Format and InputMask properties. As a result, for a user to read and display the data in a table or query, that user must also have permissions to read the design of the table or query. (This is why selecting the Read Data permission check box in the User And Group Permissions dialog box automatically selects the Read Design check box as well.) If you don’t want your users to see the design of your table or query, you can create a query and set its RunPermissions property to restrict their access to this information.

The RunPermissions property determines whether Microsoft Access uses the query user’s permissions or the query owner’s permissions when checking the user-level security permissions for each of the underlying tables in a query. If the RunPermissions property is set to User’s, then the users of a query have only their own permissions to view data in underlying tables. However, if the owner of a query sets the RunPermissions property to Owner’s, anyone who uses that query will have the same level of permissions to view data in the underlying tables as the query’s owner.

Using the RunPermissions property, you can create queries to display data to users who don’t have permission to access the underlying tables. You can build different views of your data, which will provide record-level and field-level security for a table. For example, suppose you have a secure database with an Employees table and a Salary table. Using the RunPermissions property, you can build several views of the two tables: one that allows a user or group to view but not update the Salary field; a second that allows a different user or group to view and update the Salary field; and a third that allows another user or group to view the Salary field only for a certain category of employees.

Û To prevent users from viewing the design of underlying tables or queries

  1. For the users or groups whose access you want to restrict, remove all permissions for the tables or queries whose design you want to secure.

  2. Build a new query that includes all the fields you want to include from those tables or queries. You can exclude access to a field by omitting that field. You can also limit access to a certain range of values by defining criteria for your query.

  3. In the query’s property sheet, set the RunPermissions property of the new query to Owner’s.

    Note You can also set the RunPermissions property for a query in SQL view by using the WITH OWNERACCESS OPTION declaration in the SQL statement.

  4. Grant appropriate data permissions for the new query to the users and groups that you want to be able to update data, but not view the design of the underlying table or query. This would typically be Read Design, Read Data, Update Data, Delete Data, and Insert Data permissions.

Tip You can base forms and reports on the new query.

Users can update data in the underlying tables or queries by using the new query or forms based on it. However, they won’t be able to view the design of the underlying tables or queries. If they try to view the design of the new query, they receive a message that they don’t have permissions to view the source tables or queries.

Important By default, the user who creates a query is its owner, and only the owner of a query can save changes to it if the RunPermissions property is set to Owner’s. Even members of the Admins group or users with Administer permission are prevented from saving changes to a query created by another user if the RunPermissions property is set to Owner’s. However, anyone with Modify Design permission for the query can set the RunPermissions property to User’s and then successfully save changes to the query.

Because the creator of a query owns it by default, having the RunPermissions property set to Owner’s can create problems if you need to allow more than one user to work with the design of a query. To correct this, the ownership of the query can be transferred to a group. To do this, create a group, change the owner of the query to this group on the Change Owner tab of the User And Group Permissions dialog box, and then add the users who need to modify the query to the new group. Any member of the new group will be able to edit the query and save changes.

Similarly, if a user is otherwise prevented from creating or adding to a table, you can create a make-table or append query and set its RunPermissions property to Owner’s.

See Also For information about using the WITH OWNERACCESS OPTION declaration in an SQL statement, see “Owner Access Queries” later in this chapter.


Saving Your Database as an MDE File

If you are using Microsoft Access 97 and your primary security concern is protecting the design of your forms, reports, and modules, you can save your database as an MDE file instead of using user-level security. You can also save a database as an MDE file in conjunction with using other forms of security.

Saving your database as an MDE file creates a separate copy of your database that contains no Visual Basic source code and is smaller than the original database. Your code is compiled and continues to run, but it can’t be viewed or edited. Additionally, users can’t view or modify the design of forms, reports, and modules in an MDE database. However, users can view and modify the relationships and the design of tables, queries, and macros in an MDE database.

Saving your database as an MDE file also prevents the following actions:

  • Creating forms, reports, or modules.

  • Adding or deleting references to object libraries or databases by using the References command on the Tools menu.

  • Using the Object Browser.

  • Changing code by using the properties or methods of the Microsoft Access or Visual Basic object models (because an MDE file contains no source code).

  • Importing or exporting forms, reports, or modules. However, tables, queries, and macros can be imported from or exported to non-MDE databases. Any tables, queries, or macros in an MDE database can be imported into another database, but no forms, reports, or modules can be imported into another database.

If you need to modify the design of forms, reports, or modules in a database saved as an MDE file, you must open the original non-MDE database, modify it, and then save it as an MDE file again. Saving a database that contains tables as an MDE file creates complications reconciling different versions of the data if you need to modify the design of the application later. For this reason, saving a database as an MDE file is most appropriate for the front end of an application that has been split into a front-end/back-end database, in which the back end contains only tables and the front end contains the remaining objects.

See Also For more information about splitting databases, see “The Two-Database Approach” in Chapter 6, “Creating Multiuser Applications.”

Note The process of saving a database as an MDE file compiles all modules and compacts the destination database, so there is no need to perform these steps before saving a database as an MDE file.

Û To save a database as an MDE file

  1. Close the database. If the database is being shared on a network, make sure no other users have it open.

  2. On the Tools menu, point to Database Utilities, and then click Make MDE File.

  3. In the Database To Save As MDE dialog box, select the database you want to save as an MDE file, and then click Make MDE.

  4. In the Save MDE As dialog box, specify a file name for the database and the location where you want to save it, and then click Save.

    Your original database is unchanged and a new copy is saved as an MDE file using the file name and location you specified in step 4.

Note Saving a database as an MDE file doesn’t create a run-time version of the database. To use an MDE database, users must have Microsoft Access 97 installed. Alternatively, if you have Microsoft Office 97, Developer Edition, you can save a database as an MDE file and then use the Setup Wizard to create a distributable run-time version of it.

Caution Be sure to save a backup copy of your original database in a safe place. If you need to modify the design of forms, reports, or modules in your MDE file, you must open the original non-MDE database, modify the objects, and then save the database again as an MDE file. Also, databases saved as MDE files in Microsoft Access 97 cannot be opened or converted in later versions of Microsoft Access. To convert or open it in later versions of Microsoft Access, you must use the original database.


Using Other Forms of Security with a Database That Will Be Saved as an MDE File

Saving a database as an MDE file is a good way to protect the code and the design of forms and reports in your application, and it doesn’t require users to log on or require you to create and manage the user accounts and permissions required by user-level security. However, an MDE file doesn’t control how users access your application’s tables, queries, and macros. If you want more control over these database objects, you should establish user-level security before you save your database as an MDE file. User-level security will be preserved in the new MDE file.

However, saving your database as an MDE file prevents all users (including database administrators) from modifying the design of forms, reports, and modules. If this is too restrictive and you require additional control and flexibility in these areas, you shouldn’t save your database as an MDE file — you should establish user-level security instead.

Once you have secured your database with user-level security, if you want to save it as an MDE file, you must meet the following requirements:

  • Before starting Microsoft Access, you must use the Workgroup Administrator or the /wrkgrp command-line option to join the workgroup information file that was in use when the database was created.

  • Your user account must have Open/Run and Open Exclusive permissions for the database object.

  • Your user account must have Modify Design or Administer permission for any tables in the database, or you must be the owner of any tables in the database.

  • Your user account must have Read Design permission for all objects in the database.

You can also use a database password to control who can open an MDE database, but you must set the password in the original database before you save it as an MDE file. The database password will be preserved in the new MDE database.

See Also For more information about database passwords, see “Database Passwords” later in this chapter.


Saving a Replicated Database as an MDE File

A replicated database (either a replica or Design Master) cannot be saved as an MDE file; however, once a database is saved as an MDE file, it can be replicated. To save a replicated database as an MDE file, you must first remove replication system fields, tables, and properties.

See Also For more information about removing replication fields, tables, and properties, search the Microsoft Access Help index for “replicated databases, making regular.” For more information about database replication, see Chapter 7, “Database Replication.”


Saving as an MDE File a Database That References Another Database

If you try to save as an MDE file a database (.mdb) or an add-in database (.mda) that references another database or add-in, Microsoft Access displays an error message and doesn’t let you complete the operation. To save as an MDE file a database that references another database, you must save all databases in the chain of references as MDE files, starting from the first database referenced. After saving the first database as an MDE file, you must then update the reference in the next database to point to the new MDE file before saving it as an MDE file, and so on.

For example, if Database1.mdb references Database2.mdb, which references Database3.mda, you would proceed as follows:

  1. Save Database3.mda as Database3.mde.

  2. Open Database 2.mdb and change its reference to point to the new Database3.mde.

  3. Save Database2.mdb as Database2.mde.

  4. Open Database1.mdb and change its reference to point to the new Database2.mde.

  5. Save Database1.mdb as Database1.mde.

See Also For information about referencing another database, see Building Applications with Microsoft Access 97, Chapter 12, “Using Library Databases and Dynamic-Link Libraries.”


Using DAO to Program Security

As you learned in Chapter 2, the DAO interface gives you programmatic access to Microsoft Jet. This same interface enables you to program much of the security implementation of your application. The only way to programmatically manipulate security in Microsoft Jet is by using DAO. Unlike other elements of the database, such as data definition language (DDL) operations on objects, you cannot use Microsoft Jet SQL to view or alter security settings.

Caution Although Microsoft Access may warn you if you are changing security in a way that might lock you out of your database, Microsoft Jet does not. To avoid causing irreparable damage to your database, use caution when making programmatic changes to security. Be especially careful when making changes to permissions for the Admins group or when removing user accounts from the Admins group. Make a backup copy of the database first.


Workspace Interaction with Security

When performing programmatic security operations, you must be logged on as a user with sufficient permissions to do that work. The usual way to do this is to open a Workspace object with the user name and password of an administrator user account, that is, a member of the Admins group. This is accomplished using the CreateWorkspace method of the DBEngine object.

For example, assume your workgroup has an administrator user account named “TopDog” with the password “AllPowerful.” You would use the following code to open a Workspace object from which to perform the remainder of your security operations:

Dim wrk As Workspace
Set wrk = DBEngine.CreateWorkspace("", "TopDog", "AllPowerful")

Workspace objects are often thought of as mere transaction spaces — a way to create multiple independent transactions. However, an equally important use is the ability they provide to programmatically log on to a workgroup as another user. An obvious disadvantage of this approach is that the user name and password of a powerful user are hard-coded into the program code. One way to avoid this is to programmatically prompt the person running the code for a user name and password, as shown in the following code fragment:

Dim wrk As Workspace
Dim strUser As String, strPwd As String

strUser = InputBox("Enter user account.")
strPwd = InputBox("Enter password.")
Set wrk = DBEngine.CreateWorkspace("", strUser, strPwd)

If you want to hard-code security information, you can also save your database as an MDE file (Microsoft Access 97) or as an executable file (Visual Basic 4.0, 5.0), so that no one can view or modify a module.

Note When you open a secure Workspace object in Microsoft Access, you can use that Workspace object only to work with DAO objects. You cannot work directly with Microsoft Access forms, reports, macros, or modules, since they’re not DAO objects.


Securing Objects

All DAO access to users, groups, and permissions occurs through the Container and Document objects. DAO uses the Containers and Documents collections to expose the objects in a database to the database’s host, whether that host is an application using DAO code or an application such as Microsoft Access. The Containers and Documents collections differ from other collections and objects in that they allow Microsoft Jet to store and manage objects that are created by the database’s host application. For example, in a Microsoft Jet database created by Microsoft Access, there are Document objects to store Microsoft Access forms and reports. Even though Microsoft Access forms and reports aren’t native Microsoft Jet objects, the Containers and Documents collections allow Microsoft Jet to store these objects and maintain security on them. By locating programmatic security access at the Containers and Documents level, Microsoft Jet extends its security services to any application that requires it.

See Also You may find it useful to review the overview of the DAO object hierarchy in Chapter 2, “Introducing Data Access Objects.”

All Microsoft Jet databases contain at least three containers:

  • Tables contain a Document object for each table and query in the database.

  • Relationships contain a Document object for each relationship in the database.

  • Databases contain a Document object for the database itself.

Microsoft Jet defines a set of permissions for the three containers it supports directly. Permissions for other containers must be defined by the application that defines the permissions. Only then can Microsoft Jet maintain security on these foreign objects. For example, Microsoft Access defines a set of permissions for Form objects that Microsoft Access stores in the Forms container it adds to the database. You can use the Object Browser in Microsoft Access to see these permissions constants, or you can read about them in Microsoft Access Help. The security constants for Microsoft Access objects begin with the prefix “acSec.”


Common Permissions

Permissions for objects are handled by working with the Permissions property of a Document object. This property is a Long value (also known as a Long Integer). Different permissions are assigned by setting or resetting specific bits or groups of bits of that long value. Therefore, the value of the Permissions property is typically set by combining predeclared constants using the Visual Basic bitwise operators And and Or. The following table describes the security constants and values that are predefined for all Microsoft Jet objects.

Constant | Value | Description
dbSecNoAccess | &H0 | To remove all permissions for an object from a user or group, set its Permissions property to dbSecNoAccess.
dbSecFullAccess | &HFFFFF | To grant all permissions to a given user or group for an object, set its Permissions property to dbSecFullAccess.
dbSecDelete | &H10000 | The permission to delete an object. For example, if a user has the dbSecDelete bit set on a Table object, the user can delete that table. Note that although you can set this permission on a Container object, it has no effect. Only Microsoft Jet can create and delete containers, and this is done on an internal level.
dbSecReadSec | &H200000 | The permission to read an object’s permissions settings. An owner of an object implicitly has this permission. If you do not have dbSecReadSec permission for an object and try to use DAO to do anything to the object’s Permission property, you receive an error.
dbSecWriteSec | &H40000 | The permission to alter an object’s permission settings. As with dbSecReadSec, the owner of the object implicitly has this permission, so you can’t orphan an object by revoking it.
dbSecWriteOwner | &H80000 | The permission to change the owner of an object. You do this by setting the Owner property of the appropriate Container or Document object.


Permissions for Tables and Queries

Microsoft Jet stores both tables and queries in the Tables container. This section uses “tables” to refer to both tables and queries. The following permission constants are defined.

Constant | Value | Description
dbSecReadDef | &H4 | The permission to read the definition of a table.
dbSecWriteDef | &H8 Or dbSecReadDef Or dbSecDelete | The permission to alter the definition of a table. Note that this permission is a combination of the &H8 value and the read definitions and delete permissions. You have to be able to read the definition of an object in order to change that definition.
dbSecRetrieveData | &H10 Or dbSecReadDef | The permission to read data stored in the table. This permission is combined with the read definitions permission. Although you could separate the two permissions, most applications that use Microsoft Jet, such as Microsoft Access, require the ability to read the table’s definition in order to be able to read the table’s data.
dbSecInsertData | &H20 | The permission to insert new records into the table.
dbSecDeleteData | &H80 | The permission to delete records from a table. Note the subtle distinction between deleting data and replacing all fields in the record with null data, which is controlled by the dbSecReplaceData permission constant.
dbSecReplaceData | &H40 | The permission to modify data that exists in the table.

These permissions can be combined using the Or keyword. For example, to grant a user modify data and insert data permissions for a table, you use the following:

docCurrent.Permissions = dbSecInsertData Or dbSecReplaceData Or _
    dbSecRetrieveData

Note that the dbSecRetrieveData permission constant is included: Most programs require the ability to read data before it can be updated.

Microsoft Access Permissions

Microsoft Access defines a number of permission settings for use with its own objects. These permissions are described in the following table.

Constant | Value | Description
acSecFrmRptReadDef | &H4 | The permission to read the definition of a form or report.
acSecFrmRptWriteDef | &H8 Or acSecFormRptReadDef Or dbSecDelete | The permission to alter the definition of a form or report.
acSecFrmRptExecute | &H100 | The permission to execute the form or report.
acSecMacReadDef | &HA | The permission to read the definition of a macro.
acSecMacWriteDef | &H6 Or acSecMacReadDef | The permission to alter the definition of a macro.
acSecMacExecute | &H8 | The permission to execute the macro.
acSecModReadDef | &H2 | The permission to read the definition of a module.
acSecModWriteDef | &H4 Or acSecModReadDef Or dbSecDelete | The permission to alter the definition of a module.

Execute permission defines the ability to “run” the object. For example, execute permission on a form gives you the ability to open the form and browse or print it. For macros, execute permission defines the ability to open the macro and run its contents. You can see that there is no execute permission for modules. Obviously, if Microsoft Access had to check the permission for every function for a given user, performance would be severely degraded.

Permissions with the ReadDef suffix define the ability to open the object in Design mode. Unlike tables and queries, ReadDef permissions are not required to read the data displayed by Microsoft Access forms and reports.

Permissions with the WriteDef suffix define the ability to open an object in Design mode and to save any changes made to the object’s definition.

The following table shows how Microsoft Jet permissions constants map to the permissions check boxes in the Microsoft Access User And Group Permissions dialog box. It is important to note that some of these constants represent bitwise combinations of other constants as described in the other tables in this chapter.

Object | Microsoft Access permission | Microsoft Jet permission constant
Database | Open/Run | dbSecDBOpen
 | Open Exclusive | dbSecDBOpenExclusive
 | Administer | dbSecDBAdmin
Tables and queries | Read Design | dbSecReadDef
 | Modify Design | dbSecWriteDef
 | Administer | dbSecFullAccess
 | Read Data | dbSecRetrieveData
 | Update Data | dbSecReplaceData
 | Insert Data | dbSecInsertData
 | Delete Data | dbSecDeleteData
Forms and reports | Open/Run | acSecFrmRptExecute
 | Read Design | acSecFrmRptReadDef
 | Modify Design | acSecFrmRptWriteDef
 | Administer | dbSecFullAccess
Macros | Open/Run | acSecMacExecute
 | Read Design | acSecMacReadDef
 | Modify Design | acSecMacWriteDef
 | Administer | dbSecFullAccess
Modules | Read Design | acSecModReadDef
 | Modify Design | acSecModWriteDef
 | Administer | dbSecFullAccess


Permissions for Databases

As with other objects, the database itself has its own unique permissions. Permissions for the database itself are set by modifying the Document object in the Container object named “Databases.” While this may seem to be counterintuitive at first, it does make sense when you remember that all permission settings work at the Containers and Documents level. To work the database itself into this scheme, Microsoft Jet defines a database Document object called MSysDb. By setting the Permissions property of this document, you are in effect setting the permissions for the database itself. The following permissions are defined for this document.

Constant | Value | Description
dbSecDBOpen | &H2 | Defines the permission to open the database. If a user does not have this permission, the user cannot open the database. This is the most restrictive form of permissions.
dbSecDBOpenExclusive | &H4 | Defines the permission to open the database exclusively. This permission is useful in multiuser environments where you want to deny users permission to open a shared database exclusively. If a user could open a shared database exclusively, your multiuser system would quickly become a single-user system.
dbSecDBAdmin | &H8 | Defines users’ permission to change a database password and make a database replicable. In Microsoft Access, this also defines users’ permissions to change settings in the Startup dialog box (Tools menu, Startup).

As an example, assume that you don’t want user Hannah to have permission to open a database, but you want to allow user Jayne to open it exclusively. The following code accomplishes this. Note that user accounts named Hannah and Jayne must exist in order for this code to run. In this example, strDbPath is the path to the database, and strWorkgroup is the path to the workgroup information file:

Dim doc As Document
Dim dbs As Database
Dim strUser As String

DBEngine.SystemDb = strWorkgroup
Set dbs = OpenDatabase(strDbPath)
Set doc = dbs.Containers!Databases!MSysDB

With doc
    ' Remove Hannah's permission to open the database.
    strUser = "Hannah"
    .UserName = strUser
    .Permissions = .Permissions And _
        Not dbSecDBOpen

    ' Give Jayne the ability to open the database exclusively.
    strUser = "Jayne"
    .UserName = strUser
    .Permissions = .Permissions Or _
        dbSecDBExclusive Or dbSecDBOpen
End With
Preventing Users from Creating Databases

You can also use Microsoft Jet to prevent users from creating databases. Because this permission is applied to users and not to a specific database, the permission is stored in the Container object named “Databases” in the workgroup information file. By default, anyone in the Admins group or the Users group has this permission, which is represented by the constant dbSecDBCreate.

It may not be necessary to set this permission in most circumstances. However, removing this permission is the only way you can prevent users from owning a database. For example, you can remove this permission if you’re concerned that users might create a new database and import the objects in your database into the new database, which they own. Since you can’t revoke ownership of a database, you can remove database creation permissions to prevent users from creating a new database in the first place.

To reference the path to a workgroup information file in your code, you use the SystemDB property. The following code denies a user or group the permission to create databases:

Sub RemoveCreatePerms(strWorkgroup As String, strUser As String)
    Dim dbs As Database
    Dim ctr As Container

    ' Set SystemDB property to name of workgroup information file.
    DBEngine.SystemDB = strWorkgroup
    ' Open workgroup information file.
    Set dbs = OpenDatabase(strWorkgroup)
    Set ctr = dbs.Containers!Databases
    With ctr
        ' Revoke database creation permissions for user or group
        ' in workgroup information file.
        .UserName = strUser
        .Permissions = .Permissions And Not dbSecDBCreate
    End With
End Sub
Database Passwords

In some cases, the only security a developer needs is the ability to prevent unauthorized users from opening the database; after the user has the database open, no other security is needed. To meet this need, Microsoft Jet 3.0 and 3.5 enable you to password-protect the database itself.

The database password is stored in the database header pages of the database itself. Setting a database password has no effect on the workgroup information file.

Caution If you lose the password to your database, you can never open the database again! Make sure you have adequate safeguards: Always have hard copies of your database passwords stored in a secure place.

Setting the Database Password

The database password can be set either through the Microsoft Access user interface using the Set Database Password command (Tools menu, Security submenu), or through DAO with the NewPassword method. When using either method, the database must be opened exclusively.

The following code opens the Orders database and assigns the password “topsecret” to it. In this example, strDbPath is the path to the database:

Dim dbs As Database

' Open database in exclusive mode.
Set dbs = OpenDatabase(strDbPath, True)
' Assign new password.
dbs.NewPassword "", "topsecret"
dbs.Close

Opening a Password-Protected Database

After a database password has been set, all attempts to open that database fail unless the correct password is given. To open a password-protected database using DAO, you specify the password as the connect argument of the OpenDatabase method. For example, the following code opens the password-protected Orders database. In this example, strDbPath is the path to the database:

Dim dbs As Database

' Open database with password parameter.
Set dbs = OpenDatabase(strDbPath, _
    False, False, ";pwd=topsecret")

Changing the Database Password

To change the database password, you must supply both the existing and new passwords to the NewPassword method. The following example changes the password of the Orders database from “topsecret” to “ultrasecret”:

Dim dbs As Database
Dim strDbPath As String

Set dbs = OpenDatabase(strDbPath, True, _
    False, ";pwd=topsecret")
dbs.NewPassword "topsecret", "ultrasecret"

You can remove the database password by using the NewPassword method to set the password to a zero-length string (""), or through the Microsoft Access user interface by using the Unset Database Password command (Tools menu, Security submenu).

Note This new capability increases the chance that generic OpenDatabase method code will stop working. Always check to see if your attempt to open a database has succeeded. Given that your code may encounter a password-protected database, it should have the flexibility to handle such cases.

Preventing Users from Setting a Database Password

In an unsecured environment, any user can set a database password. This can create problems for your application because you and other users won’t be able to open it. You can prevent users from doing this by removing Administer permissions from the Database object.

If you want to make it impossible for users to set a database password, follow the steps to secure a database using the User-Level Security Wizard in the “Securing Your Database with Microsoft Access” section earlier in this chapter. This will prevent all users who aren’t members of the Admins group from setting the database password.

If, however, you just want to make it difficult for users to set a database password, you don’t need to fully implement security. Simply remove Administer permissions from the Database object for the Admin user, the Users group, and the Admins group. All users who try to set the database password will fail and receive a permission denied error: Before they could set the password, they would first have to explicitly grant themselves permission to do so. For many applications, that is security enough.


Permissions for Containers

You can directly set permissions for Container objects to control a user’s ability to create new objects of a given type. The dbSecCreate permission constant gives the user the ability to create an object in the container it’s associated with. Note that this permission is useful only in the context of Container objects. The following code shows you how to check to see if a user has permission to create an object in a given container. In this example, strWorkGroup is the path to the workgroup information file, strDbPath is the path to the database, strUser is the name of the user or group, and strContainer is the name of the container for which permissions are being checked:

Dim dbs As Database
Dim ctr As Container

' Specify the workgroup information file to use.
DBEngine.SystemDB = strWorkgroup

Set dbs = OpenDatabase(strDbPath)
Set ctr = dbs.Containers(strContainer)

' Specify the user account to check.
ctr.UserName = strUser
' Check whether the dbSecCreate permission is set.
If ctr.Permissions And dbSecCreate Then
    Debug.Print strUser & " can create " & strContainer & "."
Else
    Debug.Print strUser & " can't create " & strContainer & "."
End If
dbs.Close

Note that you can use the Inherit property in conjunction with the Permissions property to define what permissions new documents will have automatically when they’re created. If you set the Inherit property to True, and then set a permission on a container, then whenever a new document is created in that container, that permission will be set for the new document. This is a convenient way of presetting permissions on an object.

Setting the Inherit property will not affect existing documents in the container — you can’t modify all the permissions on all existing documents in a container by setting the Inherit property and a new permission. It will affect only new documents that are created after the Inherit property is set.


Securing Users and Groups

The Users collection contains all users defined in the current workgroup information file. Similarly, the Groups collection contains all groups. By manipulating these collections, you define and control the security accounts that Microsoft Jet uses. The Groups and Users collections are interesting in that they are self-referencing. You can see from the DAO hierarchy that the Users collection contains a Groups collection, and the Groups collection contains a Users collection. Using this structure, you can easily find which users belong to which groups, and which groups contain which users.

There are a few things to keep in mind when you’re working with users and groups:

  • When you add a new user or group, you create the user or group by using the CreateUser or CreateGroup method of a Workspace object. After you’ve appended the User or Group object to its respective collection under the Workspace object, you must also create and append the object to define the new user’s membership in groups, or to specify which users are in a new group. If you’ve created a new user, you add the new user to a group by using the CreateUser method of the group you want the user to belong to, then appending the User object to that group. Alternately, if you’ve created a new group, you can add users to that group by using the CreateGroup method of the User object and appending the Group object to the Groups collection for that user.

  • If you add a user programmatically in Microsoft Access, you must add the user to the Users group. That is, you must append the new User object to the Users collection of the Users group, or append the Users group to the Groups collection of the new User object.

  • You may need to refresh the User and Group collections after you’ve updated them.

  • If you’re using DAO to manipulate users and groups in an application other than Microsoft Access, you must set the SystemDB property to the path and file name of the workgroup information file. Otherwise, Microsoft Jet will not know which workgroup information file you want to update. In Microsoft Access, you can set this property, but it will be ignored, as Microsoft Access uses whichever workgroup information file is currently active.

The following example illustrates these points. The procedure creates a new user, appends it to the Users group, then appends it to the Admins group.

Sub AddUserToAdmins(strWorkgroup As String, strUser As String, _
        strPID As String, strPwd As String)
    Dim wrk As Workspace
    Dim usr As User, grp As Group

    ' Specify which workgroup information file to use.
    DBEngine.SystemDB = strWorkgroup

    Set wrk = DBEngine(0)
    ' Create new user in Users collection of default Workspace object.
    Set usr = wrk.CreateUser(strUser, strPID, strPwd)
    wrk.Users.Append usr

    ' Create user in Users collection of Users group.
    Set grp = wrk.Groups("Users")
    Set usr = wrk.CreateUser(strUser)
    grp.Users.Append usr

    ' Create user in Users collection of Admins group.
    Set grp = wrk.Groups("Admins")
    Set usr = grp.CreateUser(strUser)
    grp.Users.Append usr
    grp.Users.Refresh
End Sub

Keep the following additional points in mind when working with User and Group objects:

  • If a user or group is deleted from the workgroup information file, any object that has specific permissions for that user or group retains those permissions. If that user or group is re-created using the same user name and PID, those permissions will become active again. This behavior is evidenced by the fact that the Owner property of objects from which the owner has been deleted returns “<Unknown>.”

  • To determine all the users and groups that have permissions on an object, you should iterate through each user and group name, set the UserName property, and check the Permissions property. You can also use the AllPermissions property. Unlike the Permissions property, which returns only the permissions for the given user or group, the AllPermissions property returns all the permissions a user has for an object, including permissions implied through group membership.

  • Not everyone has permissions to view user and group information. Generally, it’s best to be logged on as an administrator account — that is, a user who is a member of the Admins group — before attempting to retrieve this information. This is also true if you want to modify security settings: You must be logged on as user with sufficient permissions.

  • If you’re using a workgroup information file that was created with Microsoft Access 1.x using Microsoft Jet 1.x, you cannot use the Groups collection of a User object. Instead, write a short program that looks at each of the groups and determines if the user is in that group. Of course, the best way to avoid this problem is to upgrade your workgroup information file to Microsoft Jet 2.0 format.


Granting Read Permission to a User for a Table

The following example grants a user or group permissions on a single table or query in a database. It sets the UserName property of the document to the user or group for whom you want to change permissions. Then the code sets the permissions by modifying the Permissions property of the Document object. Note that instead of explicitly setting the Permissions property to dbSecRetrieveData, the Or operator is used to add the permission to any existing permissions. This ensures that this code does not remove other permissions that the user has already been assigned. In this example, strDbPath is the path to the database, strUser is the name of a user or group, and strDocument is the name of the Document object.

Sub GrantReadPerms(strDbPath As String, strUser As String, strDocument As String)
    Dim dbs As Database
    Dim doc As Document

    Set dbs = OpenDatabase(strDbPath)
    Set doc = dbs.Containers("Tables").Documents(strDocument)
    With doc
        ' Specify user or group name.
        .UserName = strUser
        ' Add read permissions to existing permissions.
        .Permissions = doc.Permissions Or dbSecRetrieveData
    End With
End Sub


Determining If a User Has Read Permission for a Table

In general, you shouldn’t check for a specific permission with the equal sign (=) as in the following line:

If (doc.Permissions = dbSecRetrieveData) Then

Rarely does a user have only one permission set. If the user had any other permission set (besides the dbSecRetrieveData permission constant), this example would fail. To check for a specific permission, use And with that permission’s value and the object’s Permission property, and check for a resulting value greater than 0. If it is, the user has the specified permission. The following example shows how to do this. In this example, strDbPath is the path to the database, strUser is the name of a user or group, and strDocument is the name of the Document object.

Sub CheckReadPerms(strDbPath As String, strUser As String, _
    strDocument As String)

    Dim dbs As Database
    Dim doc As Document

    Set dbs = OpenDatabase(strDbPath)
    Set doc = dbs.Containers("Tables").Documents(strDocument)
    ' Specify user or group name.
    doc.UserName = strUser
    ' Use And operator to compare permissions.
    If (doc.Permissions And dbSecRetrieveData) > 0 Then
        Debug.Print strUser & " has read permissions for " _
            & strDocument & "."
    Else
        Debug.Print strUser & " does not have read permissions " _
            & "for " & strDocument & "."
    End If
End Sub

You should also note that this example checks only for explicit permissions: those assigned to that user for that object. It does not take into account any implicit permissions that exist through group membership. The following example is more thorough in that it uses the AllPermissions property to take into account the user’s group permissions. In this example, strDbPath is the path to the database, strUser is the name of a user or group, and strDocument is the name of the Document object:

Sub CheckAllReadPerms(strDbPath As String, strUser As String, _
        strDocument As String)
    Dim dbs As Database, doc As Document

    Set dbs = OpenDatabase(strDbPath)
    Set doc = dbs.Containers("Tables").Documents(strDocument)
    ' Specify name of user or group.
    doc.UserName = strUser
    ' Check all permissions for user on document.
    If (doc.AllPermissions And dbSecRetrieveData) > 0 Then
        Debug.Print strUser & " has implicit or explicit read " _
            & "permissions for " & strDocument & "."
    Else
        Debug.Print strUser & " has no permissions."
    End If
End Sub


Removing Write Permission from a User for a Table

This example shows how you can remove all write data permissions for a user or group. It works by first creating a Long Integer value that combines the insert data, replace data, and delete data permissions by using an Or operation. It then uses the And Not operator to reset the appropriate bits in the document’s Permissions property. This has the effect of removing those permissions. Remember that you’re affecting only the user’s explicit permissions with this code. The user may have other permissions due to membership in various groups.

Sub RemoveWritePerms(strDbPath As String, strUser As String, _
        strDocument As String)
    Dim dbs As Database, doc As Document
    Dim lngNoWrite As Long

    Set dbs = OpenDatabase(strDbPath)
    Set doc = dbs.Containers("Tables").Documents(strDocument)
    lngNoWrite = dbSecInsertData Or dbSecReplaceData Or dbSecDeleteData
    With doc
        .UserName = strUser
        ' Specify permissions user should not have.
        .Permissions = .Permissions And Not lngNoWrite
    End With
End Sub


Granting Modify Design Permission to a Group for a Table

This example sets permissions for a group. It does this by setting the UserName property of the document to the name of a group. Because users and groups share the same namespace and as such cannot have identical names, Microsoft Jet knows that you’re referring to a group and not a user.

Sub GrantModifyDesign(strDbPath As String, strGroup As String, _
        strDocument As String)
    Dim dbs As Database, doc As Document

    Set dbs = OpenDatabase(strDbPath)
    Set doc = dbs.Containers("Tables").Documents(strDocument)
    With doc
        ' Specify group account.
        .UserName = strGroup
        ' Add modify design permissions.
        .Permissions = .Permissions Or dbSecWriteDef
    End With
End Sub


Showing All Permissions for All Objects for All Users and Groups

This example iterates through each Users and Groups collection. Within those collections it iterates through all the Containers and Documents collections in the

current database and displays the permissions. This approach is useful if you want to see permissions for all the objects in the database, not just the default table and query objects that Microsoft Jet recognizes. For example, this code could be used on a database created with Microsoft Access to show permissions on not only the Microsoft Jet objects, but also the application-specific Microsoft Access objects such as forms, reports, macros, and modules.

In this example, strDbPath is the path to the database. Note that if there are many objects in the database, this procedure may take a while to run.

Sub ShowPermissions(strDbPath As String)
    Dim wrk As Workspace, dbs As Database
    Dim usr As User, grp As Group
    Dim ctr As Container, doc As Document

    Set wrk = DBEngine.Workspaces(0)
    Set dbs = OpenDatabase(strDbPath)

    For Each usr In wrk.Users
        Debug.Print "User: " & usr.Name
        Debug.Print "----------------------"
        For Each ctr In dbs.Containers
            Debug.Print "  container: " & ctr.Name
            For Each doc In ctr.Documents
                doc.UserName = usr.Name
                Debug.Print "    Document: " & doc.Name & _
                    " Permissions: " & doc.Permissions
            Next doc
        Next ctr
    Next usr

    For Each grp In wrk.Groups
        Debug.Print "Group: " & grp.Name
        Debug.Print "----------------------"
        For Each ctr In dbs.Containers
            Debug.Print "  container: " & ctr.Name
            For Each doc In ctr.Documents
                doc.UserName = grp.Name
                Debug.Print "    Document: " & doc.Name & _
                    " Permissions: " & doc.Permissions
            Next doc
        Next ctr
    Next grp
End Sub
A Better Solution

The main problem with the ShowPermissions code is that it returns only a numeric permission value for each object. The following three procedures produce a much more readable output.

First, the ShowPermissions procedure is modified to call a function to decode the value of an object’s Permissions property:

Sub ShowPermissionsAsText(strDbPath As String)
    Dim wrk As Workspace, dbs As Database
    Dim usr As User, grp As Group
    Dim ctr As Container, doc As Document
    Dim intType As Integer

    Set wrk = DBEngine.Workspaces(0)
    Set dbs = OpenDatabase(strDbPath)

    For Each usr In wrk.Users
        Debug.Print "User: " & usr.Name
        Debug.Print "----------------------"
        For Each ctr In dbs.Containers
            Debug.Print "  Container: " & ctr.Name
            Select Case ctr.Name
                Case "Tables": intType = 1
                Case "Databases": intType = 2
                Case Else: intType = -1
            End Select
            For Each doc In ctr.Documents
                doc.UserName = usr.Name
                Debug.Print "    Document: " & doc.Name
                Debug.Print "      Permissions: " & _
                    DecodePerms(intType, doc.Permissions)
            Next doc
        Next ctr
    Next usr

    For Each grp In wrk.Groups
        Debug.Print "Group: " & grp.Name
        Debug.Print "----------------------"
        For Each ctr In dbs.Containers
            Debug.Print "  Container: " & ctr.Name
            Select Case ctr.Name
                Case "Tables": intType = 1
                Case "Databases": intType = 2
                Case Else: intType = -1
            End Select
            For Each doc In ctr.Documents
                doc.UserName = grp.Name
                Debug.Print "    Document: " & doc.Name
                Debug.Print "      Permissions: " & _
                    DecodePerms(intType, doc.Permissions)
            Next doc
        Next ctr
    Next grp
End Sub

Next, two new procedures are added to perform the actual conversion:

Function DecodePerms(intType As Integer, lngPerms As Long) As String
    Dim strPerms As String

    ' Decode the common permissions.
    If (lngPerms And dbSecNoAccess) = dbSecNoAccess Then _
        strPerms = AddString(strPerms, "dbSecNoAccess")

    If (lngPerms And dbSecFullAccess) = dbSecFullAccess Then _
        strPerms = AddString(strPerms, "dbSecFullAccess")

    If (lngPerms And dbSecDelete) = dbSecDelete Then _
        strPerms = AddString(strPerms, "dbSecDelete")

    If (lngPerms And dbSecReadSec) = dbSecReadSec Then _
        strPerms = AddString(strPerms, "dbSecReadSec")

    If (lngPerms And dbSecWriteSec) = dbSecWriteSec Then _
        strPerms = AddString(strPerms, "dbSecWriteSec")

    If (lngPerms And dbSecWriteOwner) = dbSecWriteOwner Then _
        strPerms = AddString(strPerms, "dbSecWriteOwner")

    ' Decode specific permissions.
    Select Case intType
        Case 1
            ' Object is table or query.
            If (lngPerms And dbSecReadDef) = dbSecReadDef Then _
                strPerms = AddString(strPerms, "dbSecReadDef")

            If (lngPerms And dbSecWriteDef) = dbSecWriteDef Then _
                strPerms = AddString(strPerms, "dbSecWriteDef")

            If (lngPerms And dbSecRetrieveData) = dbSecRetrieveData Then _
                strPerms = AddString(strPerms, "dbSecRetrieveData")

            If (lngPerms And dbSecInsertData) = dbSecInsertData Then _
                strPerms = AddString(strPerms, "dbSecInsertData")

            If (lngPerms And dbSecReplaceData) = dbSecReplaceData Then _
                strPerms = AddString(strPerms, "dbSecReplaceData")

            If (lngPerms And dbSecDeleteData) = dbSecDeleteData Then _
                strPerms = AddString(strPerms, "dbSecDeleteData")

        Case 2
            ' Object is database.
            If (lngPerms And dbSecDBOpen) = dbSecDBOpen Then _
                strPerms = AddString(strPerms, "dbSecDBOpen")

            If (lngPerms And dbSecDBExclusive) = dbSecDBExclusive Then _
                strPerms = AddString(strPerms, "dbSecDBExclusive")

            If (lngPerms And dbSecDBAdmin) = dbSecDBAdmin Then _
                strPerms = AddString(strPerms, "dbSecDBAdmin")

        Case 3
            ' Object is container.
            If (lngPerms And dbSecWriteOwner) = dbSecWriteOwner Then _
                strPerms = AddString(strPerms, "dbSecWriteOwner")

            If (lngPerms And dbSecCreate) = dbSecCreate Then _
                strPerms = AddString(strPerms, "dbSecCreate")

        Case Else
            ' Unknown object, pass the permissions back.
            strPerms = CStr(lngPerms)
    End Select

    DecodePerms = strPerms
End Function

Function AddString(strCurrent As String, strIn As String) As String
    Dim strPerms As String

    If strCurrent = "" Then
            strPerms = strIn
    Else
            strPerms = strCurrent & "," & strIn
    End If

    AddString = strPerms
End Function

This works as follows:

  1. The ShowPermissionsAsText procedure iterates through each object and retrieves the value of the Permissions property.

  2. This value is sent to the DecodePerms function, which compares it to the permission constants for that particular object type.

  3. The DecodePerms function calls the AddString function to add the text representation of the permission name to a string, and returns it to the DecodePerms function.

  4. The DecodePerms function takes the final concatenated string and passes it back to the ShowPermissionsAsText procedure for display.

Note The DecodePerms function decodes permissions only for Microsoft Jet-specific objects. You can modify it to include other application-defined permissions, such as those defined by Microsoft Access for Microsoft Access-specific objects.


Evaluating Users and Groups

The following code shows all the groups a given user belongs to:

Sub ShowUserGroups(strWorkgroup As String, strUser As String)
    Dim usr As User
    Dim grp As Group

    ' Specify a workgroup information file.
    DBEngine.SystemDB = strWorkgroup

    ' Return a reference to the specified user.
    Set usr = DBEngine.Workspaces(0).Users(strUser)

    ' Enumerate the Groups collection of the User object.
    For Each grp In usr.Groups
        Debug.Print grp.Name
    Next grp
End Sub

The following code reverses this logic to show all the users in a given group:

Sub ShowGroupUsers(strWorkgroup As String, strGroup As String)
    Dim usr As User
    Dim grp As Group

    ' Specify a workgroup information file.
    DBEngine.SystemDB = strWorkgroup

    ' Return a reference to the specified group.
    Set grp = DBEngine.Workspaces(0).Groups(strGroup)

    ' Enumerate the Users collection of the Group object.
    For Each usr In grp.Users
        Debug.Print usr.Name
    Next usr
End Sub

Or you can show all the users and all the groups they belong to with the following code:

Sub ShowUsersGroups(strWorkgroup As String)
    Dim wrk As Workspace
    Dim usr As User
    Dim grp As Group

    ' Specify a workgroup information file.
    DBEngine.SystemDB = strWorkgroup
    ' Return a reference to the default Workspace object.
    Set wrk = DBEngine.Workspaces(0)

    For Each usr In wrk.Users
        Debug.Print "User: " & usr.Name
        For Each grp In usr.Groups
            Debug.Print "  Group: " & grp.Name
        Next grp
    Next usr
End Sub

And, as with other collections, you use the Count property to determine the number of members. To find the number of user accounts in the current workgroup information file, you use:

Debug.Print DBEngine.Workspaces(0).Users.Count

Similarly, to find the number of groups in the current workgroup information file, use:

Debug.Print DBEngine.Workspaces(0).Groups.Count

To determine the number of groups a specific user belongs to, use the Count property of that user’s User object’s Groups collection:

Debug.Print DBEngine.Workspaces(0).Users!Hannah.Groups.Count


User and Group Examples

Now that you understand the conceptual points of working with users and groups, here are some specific examples.

Adding a New User to an Existing Group

This example shows how to create a new user. The important step to note is adding the new user to the default Users group. Unlike creating a new user with the Microsoft Access user interface, this is not done automatically by Microsoft Jet: Your code must handle adding new users to the default Users group, along with any other group. The code works by first creating a new user with the CreateUser method of the Workspace object. It then appends this new user to the Users collection to make it a permanent part of security. Finally, it adds the default Users group to the collection of groups associated with this user.

Function AddUser(strWorkgroup As String, strUser As String, _
    strPID As String, strPwd As String) As Boolean

    Dim wrk As Workspace
    Dim usr As User, grp As Group

    On Error GoTo Err_AddUser
    DBEngine.SystemDB = strWorkgroup
    Set wrk = DBEngine(0)
    ' Create user and append to default Workspace object.
    Set usr = wrk.CreateUser(strUser, strPID, strPwd)
    wrk.Users.Append usr

    ' Add user to Users group.
    Set grp = usr.CreateGroup("Users")
    usr.Groups.Append grp
    usr.Groups.Refresh
    AddUser = True

Exit_AddUser:
    Exit Function

Err_AddUser:
    MsgBox "Error: " & Err.Number & vbCrLf & Err.Description
    AddUser = False
    Resume Exit_AddUser
End Function
Removing a User from a Group

The following example removes a user from a specified group. It works by first checking to see if the specified user is actually a member of the specified group. It does this by disabling error handling and assigning a temporary string to the group name you want to check. If the string is assigned, the user is a member of the group. In this case, the Delete method is used on the user’s Groups collection. If the string is not assigned, this indicates that the user is not a member of the specified group, and the code displays an error message.

Sub RemoveUserFromGroup(strWorkgroup As String, strUser As String, _
        strGroup As String)
    Dim usr As User
    Dim strTemp As String

    DBEngine.SystemDB = strWorkgroup
    Set usr = DBEngine.Workspaces(0).Users(strUser)

    On Error Resume Next
    ' If user does not belong to specified group, then
    ' strTemp will be an empty string.
    strTemp = usr.Groups(strGroup).Name
    If strTemp = strGroup Then
        usr.Groups.Delete strGroup
    Else
        Debug.Print "User " & strUser & _
            " is not a member of group " & strGroup
    End If
End Sub

You can remove a user account altogether by removing the user from the Users collection of the default Workspace object.

Microsoft Jet maintains information about user and group accounts in the workgroup information file, but a user’s or group’s permissions for an object are stored in the database that contains that object. When you remove a user account by using DAO, it’s important to keep in mind that permissions for that user remain on the objects in the database. If someone knew the PID for the user account that you removed, he or she could re-create the user account and access the objects in your database with the same permissions that that user account had previously. To ensure that the database remains secure when you remove a user account, remove the user from any groups the user belongs to, revoke the user’s permissions for all objects in the database, and then remove the user account.

Determining If a User Has a Password

You can determine if a user has a password by attempting to log on to a new Workspace object as that user and supplying a blank password. If the user has no password, this will succeed. If the user does have a password, this will return a trappable error.

Function UserHasPassword(strWorkgroup As String, strUser As String) As Boolean
    Dim wrk As Workspace
    Const errBadPassword As Integer = 3029

    On Error GoTo Err_UserHasPassword
    DBEngine.SystemDB = strWorkgroup

    ' Attempt to log on to a new Workspace object with a blank password.
    ' If an error occurs error handler will set return value to True.
    Set wrk = DBEngine.CreateWorkspace("NewWorkspace", strUser, "")
    UserHasPassword = False

UserHasPasswordExit:
    Exit Function

Err_UserHasPassword:
    Select Case Err
        Case errBadPassword
            UserHasPassword = True
        Case Else
            ' Unexpected error.
            MsgBox Err & ": " & Err.Description
    End Select
    Resume UserHasPasswordExit
End Function
Changing a User’s Password

To change another user’s password, you must be logged on as a member of the Admins group. To change a password, use the NewPassword method on the user’s User object. If the current password is unknown, you can use a zero-length string ("") for the current password. This enables you to easily manage situations where users have forgotten their passwords.

Sub ChangePwd(strWorkgroup As String, strUser As String, _
        strOld As String, strNew As String)
    Dim usr As User

    DBEngine.SystemDB = strWorkgroup
    Set usr = DBEngine.Workspaces(0).Users(strUser)
    usr.NewPassword strOld, strNew
End Sub


Owner Access Queries

Microsoft Jet gives you a powerful method to leverage security when using queries. You can use the optional WITH OWNERACCESS OPTION clause in your queries to give the person running the query the permission levels assigned to the query’s owner.

For example, suppose that a user doesn’t have permission to view the payroll information in your application. You can create a query that uses the WITH OWNERACCESS OPTION to allow the user to view this information:

SELECT LastName,
FirstName, Salary
FROM Employees
ORDER BY LastName
WITH OWNERACCESS OPTION;

Additionally, if a user is otherwise prevented from creating or adding to a table, you can use the WITH OWNERACCESS OPTION to enable the user to run a make-table or append query.

Owner access queries have two restrictions. First, you can’t change the owner of an owner access query. If you need to do this, you must remove the WITH OWNERACCESS clause, change the owner, and then change it back to an owner access query. Second, only the owner of an owner access query can save design changes to that query. If you have several developers who need to modify an owner access query, assign those developers to a group, and then make the group the owner of the query, using the steps previously described.


Security and Database Replication

Database replicas use the same security model as all Microsoft Jet databases: Users’ permissions on database objects are determined at the time they start Microsoft Jet and log on. As a developer, it’s up to you to make sure the same security information is available at each location where a replica is used.

You can do this by making the identical workgroup information file available to users at each location where a replica is used. Although the workgroup information file cannot be replicated, it can be manually copied to each location.

Another way to make the same workgroup information file available to all your users is to re-create the entries for users and groups at each location in the local workgroup information file by entering the same user and group names with their associated PIDs at each location. Modifications to permissions are considered design changes and can be made only in the Design Master replica.

Some security permissions control certain aspects of database replication. For example, a user must have Administer permission on the database to:

  • Convert a database into a replicable database.

  • Change the setting of the ReplicableBool property, make a local object replicable, or make a replicable object local. You can perform these actions only in the Design Master replica.

  • Transfer Design Master status to another replica You can perform this action in any replica in the set; however, you should never have more than one Design Master replica in a set at a time.

By default, Administer permission is granted to the Users group, the Admins group, and the creator of the database. If security is to be maintained, you must restrict this permission to selected users.

See Also For more information about replication, see Chapter 7, “Database Replication.”


Database Encryption

Database encryption is separate from Microsoft Jet security in that it’s not part of the security model. Rather, it’s a function provided by the engine, which you can use to encrypt the database file at the operating-system level.

If a database isn’t encrypted, it’s possible for a person to use a disk or file editor to view the contents of the file. Even though the representation of your data and object definitions may be hard to read, the data is there. Also, a knowledgeable person could use a disk or file editor to read the SID of the database owner or the SID of the Admins group in use when the database was created. Given that SID, a person could assume ownership of the database and do anything he or she wanted. For these reasons, it is recommended that you encrypt your database if you want to protect against this potential problem.

If you look at pages in an unencrypted database, you would see something similar to this:

Figure 10.4 An unencrypted database

You can see that some of the database’s contents are visible. However, after you encrypt the database, the same page would look like this:

Figure 10.5 An encrypted database

This version is obviously unreadable: Microsoft Jet has encrypted every byte in the database file.

Encryption works on the entire database file, not just table data. Microsoft Jet reads and writes data one “page” at a time. These pages, each consisting of 2,048 bytes of data, are encrypted as units.

Only the owner of the database, or a member of the Admins group of the workgroup information file in use when the database was created, can encrypt or decrypt that database. Due to the overhead of encrypting and decrypting, there is a performance degradation of approximately 10% to 15% in encrypted databases. Encrypted files are also essentially uncompressible using compression software such as that included with MS-DOS. This is because compression relies upon repeated patterns in the data, and encryption effectively removes any patterns.

Note For database encryption, Microsoft Jet uses the RSA Data Security Incorporated RC4 algorithm with a 32-bit key per 2K page.

See Also For more information about encrypting and decrypting database, see Chapter 13, “Optimizing Performance.”


Chapter 11 Programming with DAO in C++ and Java

This chapter describes how to use Data Access Objects (DAO) in languages other than Basic to access Microsoft Jet programmatically. It begins with a description of the three methods of programming with DAO: the DAO Automation interfaces, the dbDAO classes, and the Microsoft Foundation Classes (MFC) DAO classes. The chapter then describes how to use the dbDAO classes in C++. This section includes a simple dbDAO application that shows how to program with dbDAO. The final section of this chapter illustrates how to use the DAO Automation interfaces in Visual J++.

Programming with DAO

Using Microsoft Visual C++ and the dbDAO Classes

Using Microsoft Visual J++ and the DAO Automation Interfaces


Programming with DAO

DAO, the programming interface to Microsoft Jet, was originally designed for use with Visual Basic and Access Basic. With the development of Automation, it is now possible to write C++ and Java code that uses DAO.

The DAO dynamic-link library (DLL) is an Automation in-process server containing an embedded type library. With the type library, client programs can query the server for descriptions of exposed objects through the IDispatch interface. Because it is time-consuming to use DAO, the dbDAO classes and the MFC DAO classes were created to make coding easier in C++.


DAO Automation Interfaces

DAO provides a vtable-based implementation of Automation. The DAO Software Development Kit (SDK) provides header files for use with C++; they describe the vtable interfaces for DAO, eliminating the need to use IDispatch. After the initial creation of an instance of the DBEngine object, instances of objects are created through their parent objects, using the same object hierarchy as described in Chapter 2, “Introducing Data Access Objects.” Because these objects are typical Component Object Model (COM) objects, you are responsible for explicitly releasing the objects when they are no longer needed.

If you use C++ and are familiar with COM programming, the DAO interfaces may be all you need. Even so, programming with these interfaces requires the declaration of many temporary variables in order to work down the DAO hierarchy to the object you want to work with.

See Also For information about using Automation, see the OLE Automation Programmer’s Reference published by Microsoft Press.


dbDAO Classes

The dbDAO classes expose the same DAO functionality as Visual Basic and use similar syntax. By handling such tasks as managing object instance lifetimes, these classes make programming in C++ as convenient as programming in Visual Basic. The dbDAO classes are available in the DAO SDK.

For the DAO programmer familiar with Visual Basic, programming with the DAO interfaces requires acquiring additional knowledge about COM. Using the dbDAO classes has these advantages:

  • Requires limited COM knowledge. The dbDAO classes handle reference counting (using the AddRef and Release methods), support dynamic allocation and deallocation of objects, and provide full collection support. They also use native C++ data types where possible, or standard OLE variants.

  • Requires limited C++ knowledge. The syntax of the C++ calls is very similar to Visual Basic.

  • Supports bulk fetches directly into an application-defined data structure.

With the addition of a 190-K DLL, the dbDAO classes enable you to write more concise code without needing to track object lifetimes. Conveniences, such as classes that handle variants (often the required type used by DAO properties and methods), make programming much easier. Rather than filling out the VARIANT structure, you can simply cast the variable to the appropriate class.

Like the DAO interfaces, the dbDAO classes expose the full functionality of DAO properties and methods. The dbDAO classes were designed to be nearly as efficient as programming directly to the Automation object. They are part of the DAO SDK.


MFC DAO Classes

DAO functionality is accessible through the MFC. Based on the CRecordset classes that were written for Open Database Connectivity (ODBC), the CDAORecordset classes provide similar functionality using a similar design paradigm.

The MFC DAO classes are fully integrated with the MFC wizards and the general MFC object structure. If you are already familiar with MFC, or if you’re writing simple database applications that you’d like to easily integrate into your existing MFC-based code, use the MFC DAO classes. If you’re more experienced with programming DAO using Visual Basic, you’ll probably find it easier to use the dbDAO classes.

See Also For more information about using the MFC DAO classes, see the Visual C++ MFC documentation.


Using Microsoft Visual C++ and the dbDAO Classes

The purpose of the dbDAO classes is to make the resulting DAO C++ code as similar to equivalent Visual Basic code as possible. Fundamental DAO concepts such as properties, methods, and collections are supported by the dbDAO classes.

This section introduces a simple dbDAO application that illustrates how to program with dbDAO. Then it provides information about specific differences between dbDAO and Visual Basic DAO.


The Employee Application

The Employee application is a simple dbDAO application for displaying and updating table entries. It is located in the \DAOSDK\Samples\Employee folder of the DAO SDK.

The application displays a form, shown in Figure 11.1, containing various database fields. You can use toolbar buttons or commands on the Edit menu to add records, delete records, move forward, and move backward.

Figure 11.1 The Employee application

The Employee application uses the Employee.mdb database located in the \DAOSDK\Samples folder. This database contains a single table called Employees, which includes the following fields.

Field | Data type
Employee ID | AutoNumber
Last Name | Text
First Name | Text
Hire Date | Date/Time
Home Phone | Text
Notes | Memo

The following sections describe the Employee application in greater detail.


Setting Up the Employee Application

To install the DAO SDK, run the Setup program. For more information about installing the DAO SDK, see “DAO SDK Files” later in this chapter. Using Visual C++, open the Employee.dsw project workspace file, which is located in the \Samples\Employee folder. This file contains all the settings needed for the Employee application. To create the application, Employee.exe, click the Build command on the Build menu.

See Also For more information, see “Installing DAO on Another Machine” in the DAO SDK Help.

Note To run this application, the DAO run-time libraries must be properly installed. By default, the DAO SDK Setup program installs those libraries. Setup can also be modified to install them without including the other SDK contents (such as header files and import libraries, which only the application developer would require).

The DAO SDK Setup program adds header, library, and source file directories to your Visual C++ environment. The Employee.dsw project workspace file contains the specific settings for building the Employee application.

In addition to the corresponding header files for each source file, the Setup program installs the following files:

  • Dbdao.h Contains the dbDAO class definitions.

  • Stdafx.h Includes other necessary header files for using Win32®, OLE, and MFC.

Because the dbDAO classes use MFC classes (such as CObject and COleVariant), your application must include the appropriate files for them. Likewise, because the dbDAO classes are built on top of the DAO Automation interfaces, the various OLE header files must be included. Stdafx.h includes all the necessary header files for building your dbDAO application.

The Employee application uses the MFC document/view architecture. The document portion manipulates the data, so most of the dbDAO code resides there. The view portion contains display code. The Employee application is form-based and derives from the MFC CFormView class. The focus of this discussion is the dbDAO code, so details about using MFC are not provided.

See Also For more information about the MFC document/view architecture, see the MFC documentation that comes with Visual C++.

The document (Daoemdoc.cpp) holds the database connection and defines the following member variables:

public:
    CdbRecordset		m_cEmpRecordSet;
    BOOL				m_bConnected;
    BOOL				m_bEmptyTable;
protected:
    CdbDBEngine		m_cDBEngine;
    CdbDatabase		m_cEmpDatabase;
    CdbBookmark		m_cLastGoodRecord;

These declarations are similar to Visual Basic declarations using the Dim statement. Note that dbDAO classes in the preceding declarations are prefaced by the Cdb prefix.

The document contains member variables for the engine, the database connection, and the recordset that is displayed by the form — that is, m_cDBEngine, m_cEmpDatabase, and m_cEmpRecordSet, respectively. The m_bConnected member variable is a Boolean value that is TRUE if the connection to Employee.mdb is open; otherwise, it is FALSE. The m_bEmptyTable member variable is a Boolean value that is TRUE if the Employees table does not contain any records; otherwise, it is FALSE.

A bookmark, the m_cLastGoodRecord member variable, is defined for use by the various positioning functions to keep track of the last record visited. Note that dbDAO defines the CdbBookmark class for bookmarks, whereas bookmarks are typed as Variant in Visual Basic.

Because various code modules use the recordset, connected, and empty table flags, they are given public access. The engine, database, and bookmark member variables are given protected access, so that other code modules cannot modify their values.

The member variables are initialized upon creation of the document. The OnNewDocument member function is overridden in order to perform the initialization. This function also initializes the DBEngine object. For more information about other ways of initializing the engine, see “Initializing CdbDBEngine” later in this chapter. The OnNewDocument function calls the ConnectToDatabase function, which is described in the following section.


Connecting to the Employee Database

The Employee application displays a dialog box that prompts for the location of Employee.mdb. The application uses this information to connect to the database and open a table-type Recordset object on the Employees table. The application is positioned on the first record in the table. Note that this application is designed to work only with Employee.mdb; specifying another database causes an error (unless that database happens to have a table named Employees). The following code contains the ConnectToDatabase function:

BOOL CDAOEMPDoc::ConnectToDatabase()
{
    CFileDialog		cOpenFile(	TRUE,
                                _T("MDB"),
                                _T("employee.mdb"),
                                OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
                                (_T("Access Files (*.mdb) | *.mdb ||")));

    //Get the location of database (assume it's the Employee example).
    cOpenFile.DoModal();

    //Open the database and the recordset.
    try
        {
        //NOTE: Using the default collection rather than Workspaces.Items.
        m_cEmpDatabase = m_cDBEngine.OpenDatabase(cOpenFile.m_ofn.lpstrFile);
        m_cEmpRecordSet = m_cEmpDatabase.OpenRecordset(_T("Employees"));
        m_cEmpRecordSet.MoveFirst();
        }

    catch (CdbException dbExcept)
        {
        CdbLastOLEError exError;
        TCHAR szBuf[256];

        //If error is "No Current Record," assume it's an empty table.
        if(dbExcept.m_hr == E_DAO_NoCurrentRecord)
            {
            m_bEmptyTable = TRUE;
            }
        else
            {
            wsprintf(szBuf, _T("Error %d : %s\n"), DBERR(dbExcept.m_hr),
                 (LPCTSTR) exError.GetDescription());
            AfxMessageBox(szBuf);
            return (FALSE);
            }
        }

    return TRUE;
}

The C++ calls to the OpenDatabase and OpenRecordset methods are similar to equivalent methods called in Visual Basic. The dbDAO variables behave like objects with associated properties and methods. Collections are also supported; for example, the call to the OpenDatabase method could be rewritten using the Workspaces collection as follows:

m_cEmpDatabase = m_cDBEngine.Workspaces[0L].OpenDatabase(cOpenFile.m_ofn.lpstrFile);

Note that the index is cast as LONG; otherwise, the compiler interprets the “0” as NULL.

See Also For more information about dbDAO properties, methods, and collections, see “dbDAO Syntax” later in this chapter.

To handle exceptions raised by errors, the dbDAO code is nested in a try-catch block. For example, if the Employees table is not found in the database, the call to the OpenRecordset method throws an exception. Execution immediately switches to the catch block, which in this case displays a dialog box containing information about the error. The DBERR macro is used to convert the error from an HRESULT to a Visual Basic error number. For more information about errors, see “Handling dbDAO Errors” later in this chapter.

Strings in the ConnectToDatabase function are wrapped in the _T macro. Using this macro allows the code to compile for both ANSI and Unicode. The dbDAO class run-time libraries are available in both ANSI and Unicode versions. For more information, see “ANSI and Unicode Strings” later in this chapter.


Reading Records from the Employee Database

After the connection is made, the first record from the Employees table is displayed on the Employee form. Reading data and displaying the results on the form occur in the view portion of the code. The application updates the view whenever the position in the recordset is changed in response to one of the four commands available from the toolbar or Edit menu. Each command invokes a related function within the document. (Because the recordset contains the data, it is associated with the document and not the view.) For example, clicking the Next command calls the OnEditNext function:

void CDAOEMPDoc::OnEditNext()
{
    if(!OKToMove())
        return;

    m_cEmpRecordSet.MoveNext();

    //Watch for end of recordset.
    if(m_cEmpRecordSet.GetEOF())
        {
        MessageBeep(0);
        m_cEmpRecordSet.MovePrevious();
        }
    else
        {
        UpdateAllViews(NULL);
        }

}

The OnEditNext function first calls the OKToMove function, which checks to see if the recordset is empty or if the record has been updated. (Record updates are discussed in “Updating Records from the Employee Database” later in this chapter.) The MoveNext method then positions the recordset to the next record. The end-of-file condition is checked. If it occurs, the application moves back to the previous record; otherwise, the view is updated. If the end of the recordset is reached, there is no need to update the view, and the application moves back to the last record displayed.

In terms of actually reading data, the application calls the OnUpdate member function to update the form view’s appearance:

void CDAOEMPView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
    CDAOEMPDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    if (pDoc->m_bEmptyTable)
        return;

    try
        {
        m_nEmpNum = VTOLONG(pDoc->m_cEmpRecordSet.Fields[EMP_EMPLOYEE_ID].GetValue());

        //Convert the variant strings into Cstrings.
        VarToCStr(&m_strFirstName,
                    &pDoc->m_cEmpRecordSet.Fields[EMP_FIRST_NAME].GetValue());
        VarToCStr(&m_strHomePhone,
                    &pDoc->m_cEmpRecordSet.Fields[EMP_HOME_PHONE].GetValue());
        VarToCStr(&m_strLastName,
                    &pDoc->m_cEmpRecordSet.Fields[EMP_LAST_NAME].GetValue());
        VarToCStr(&m_strNotes,
                    &pDoc->m_cEmpRecordSet.Fields[EMP_NOTES].GetValue());

        //ColeDateTime:
        m_HireDate = pDoc->m_cEmpRecordSet.Fields[EMP_HIRE_DATE].GetValue();
        }
    catch (CdbException dbExcept)
        {
        CdbLastOLEError exError;
        TCHAR szBuf[256];

        //If error is "No Current Record," assume empty table and clear members.
        if(dbExcept.m_hr == E_DAO_NoCurrentRecord)
            {
            pDoc->m_bEmptyTable = TRUE;

            m_nEmpNum		= 0;
            m_strFirstName	= "";
            m_strHomePhone	= "";
            m_strLastName	= "";
            m_strNotes		= "";
            m_HireDate		= COleDateTime(1990,1,1,0,0,0);
            }
        else
            {
            wsprintf(szBuf, _T("Error %d : %s\n"), DBERR(dbExcept.m_hr), (LPCTSTR) exError.GetDescription());
            AfxMessageBox(szBuf);
            return;
            }
        }

    UpdateData(FALSE); //Invoke Data Exchange (copy member data to form controls).

    Invalidate(); //Repaint.
}

Data is returned as variants, so you must convert returned values to member variables’ data types. Daoemp.h defines several useful macros for handling data conversion, such as VTOLONG, which converts a variant to a LONG data type:

#define VTOLONG(v)   ((v).vt==VT_I4 ? (LONG)(v).iVal:0L)

Note that this conversion converts a null value represented by VT_NULL into the long value 0L, which may not always be what you want.

Although not shown in the Employee application, the dbDAO GetRowsEx method provides an alternate method of reading data directly as native C data types. Although it involves writing more code, the GetRowsEx method improves performance because it bypasses this extra data conversion.

See Also For more information about the GetRowsEx method, see “Performing Bulk Fetches with GetRows and GetRowsEx” later in this chapter.

The final step is to update the form controls themselves by calling the UpdateData member function with an argument of FALSE. The UpdateData function uses DDX to maintain mappings between data of the CDAOEMPView class and the controls in the dialog box template resource for this form view.


Adding Records to the Employee Database

The OnEditAdd member function adds records to the Employee database:

void CDAOEMPDoc::OnEditAdd()
{
    //If the table is empty, add a blank record.
    if(m_bEmptyTable)
        {
        m_cEmpRecordSet.AddNew();
        m_cEmpRecordSet.Update();
        m_cEmpRecordSet.MoveFirst();
        m_bEmptyTable = FALSE;
        return;
        }
    else
        {
        if(!OKToMove())
            return;

        //Remember the location before adding in case the user
        //cancels and wants to return there.
        m_cLastGoodRecord = m_cEmpRecordSet.GetBookmark();
        m_cEmpRecordSet.AddNew();
        m_cEmpRecordSet.Fields[EMP_HIRE_DATE].SetValue(COleVariant(32874L)); // Default to 1/1/90
        }

    UpdateAllViews(NULL);
}

As with any attempt to reposition within the recordset, the OKToMove function is called first to check whether or not the current record has been edited by the user. Before adding the new record, the current position is saved by using a bookmark. This is done with the GetBookmark method as in Visual Basic. The bookmark itself is typed as CdbBookmark in dbDAO rather than as a Variant as in Visual Basic. The fact that the type is different is not important because the value of the bookmark itself is meaningless. The bookmark is merely used for returning to a record. If the user deletes the new record, the bookmark is used to move back to the previous position.


Deleting Records from the Employee Database

The OnEditDelete member function deletes records from the Employee database:

void CDAOEMPDoc::OnEditDelete()
{
    //If table is empty, there is nothing to delete.
    if(m_bEmptyTable)
        return;

    //Delete method depends on the current mode.
    short nEditMode = m_cEmpRecordSet.GetEditMode();

    try
        {
        switch (nEditMode)
            {
            case dbEditNone: // Just delete it.
                {
                m_cEmpRecordSet.Delete();
                m_cEmpRecordSet.MoveFirst();
                break;
                }

            case dbEditInProgress: //Forget changes.
                {
                m_cEmpRecordSet.CancelUpdate();
                m_cEmpRecordSet.Delete();
                m_cEmpRecordSet.MoveFirst();
                break;
                }

            case dbEditAdd: //If new record, go back to last known one.
                {
                m_cEmpRecordSet.CancelUpdate();
                m_cEmpRecordSet.SetBookmark(m_cLastGoodRecord);
                }
            }

        UpdateAllViews(NULL);
        }

    catch (CdbException e)
        {
        CdbLastOLEError exError;
        TCHAR szBuf[256];

        wsprintf(szBuf, _T("Error 0x%lx : %s\n"), e.m_hr, (LPCTSTR) exError.GetDescription());
        AfxMessageBox(szBuf);
        }
}

The behavior of this member function depends on whether the current record is new or has been edited. The OnEditDelete function uses case statement logic based on the value returned by the GetEditMode method. Note that dbDAO conveniently defines the same data access constants as Visual Basic DAO, such as dbEditNone. Depending on this value, the OnEditDelete function behaves as follows:

  • If the record has not been edited (dbEditNone), delete it and move to the first record in the table.

  • If the record has been edited (dbEditInProgress), first cancel the update, then delete the record and move to the first record in the table.

  • If the record is a newly added record (dbEditAdd), first cancel the update, and then move to the previous record using the saved bookmark. There is no need to call the Delete method, because newly added records are not part of the recordset until the Update method is called.

In all three cases, it is necessary to update the view by calling the UpdateAllViews function.


Updating Records from the Employee Database

The UpdateEmpRec member function updates records from the Employee database:

void CDAOEMPDoc::UpdateEmpRec(long m_nEmpNum, LPCTSTR lpszFirstName,
            LPCTSTR lpszHomePhone, LPCTSTR lpszLastName,
            LPCTSTR lpszNotes, DATE HireDate)
{
    //Convert the date to a dbVariant.
    COleVariant cdbHireDate;
    cdbHireDate.date = HireDate;
    cdbHireDate.vt = VT_DATE;

    try
        {
        //The recordset must be in edit mode.
        if(m_cEmpRecordSet.GetEditMode() == dbEditNone)
            m_cEmpRecordSet.Edit();

        m_cEmpRecordSet.SetField(EMP_FIRST_NAME, COleVariant(lpszFirstName, VT_BSTRT));
        m_cEmpRecordSet.SetField(EMP_HOME_PHONE, COleVariant(lpszHomePhone, VT_BSTRT));
        m_cEmpRecordSet.SetField(EMP_LAST_NAME, COleVariant(lpszLastName, VT_BSTRT));
        m_cEmpRecordSet.SetField(EMP_NOTES, COleVariant(lpszNotes, VT_BSTRT));
        m_cEmpRecordSet.SetField(EMP_HIRE_DATE, cdbHireDate);

        //Commit the changes.
        m_cEmpRecordSet.Update();

        m_bEmptyTable = FALSE;

        //Return to the edited record.
        CdbBookmark cBookmark = m_cEmpRecordSet.GetLastModified();
        m_cEmpRecordSet.SetBookmark(cBookmark);
        }

    catch (CdbException e)
        {
        CdbLastOLEError exError;
        TCHAR szBuf[256];

        wsprintf(szBuf, _T("Error 0x%lx : %s\n"), e.m_hr, (LPCTSTR) exError.GetDescription());
        AfxMessageBox(szBuf);
        }
}

Because recordset manipulation functions live in the document, the application must pass the record’s field values from the view to the UpdateEmpRec member function. To update the values, this member function uses the dbDAO SetField method, which does not exist in Visual Basic DAO. You use the SetField method to bypass the Fields collection, resulting in more readable code and a performance gain.

In terms of data conversion, the process of updating values is the reverse of reading values: Member variables must be converted to variants. The MFC COleVariant class handles most common data type conversions. However, there is no conversion for DATE types, so the UpdateEmpRec member function uses a temporary variable (cdbHireDate) to store the resulting variant.

Note You must make sure that the recordset is in Edit mode before attempting an update. This happens by default for an added record, but Edit mode must be turned on explicitly for an existing record. This is a result of the document/view split; actions in the view must be mapped to corresponding behavior in the document. Any edits entered into the form are not updated until the user issues a menu or toolbar command, resulting in a position change in the recordset.


dbDAO Syntax

The following dbDAO code (with lengthy argument names and macro declarations removed) is syntactically similar to corresponding Visual Basic code.

dbDAO Visual Basic
CdbDBEngine		e;
CdbDatabase		d;
CdbRecordset		r;

d = e.OpenDatabase("Northwind.mdb");
r = d.OpenRecordset("Employees");

Dim dbs As Database
Dim rst As Recordset

Set dbs = OpenDatabase("Northwind.mdb")
Set rst = dbs.OpenRecordset("Employees")

There is one notable difference between dbDAO and Visual Basic. Unlike Visual Basic, dbDAO requires declaring a variable of class CdbDBEngine to be the first DAO call. This declaration calls the constructor function that creates the CdbDBEngine object. For more information, see “Initializing CdbDBEngine” later in this chapter.

See Also For more information about language differences, see the DAO SDK Help topics for C++, and the corresponding Visual Basic examples.

Besides language differences (for example, how variables are typed), there are some other differences in the way DAO properties, methods, and collections appear in code in dbDAO and Visual Basic. They are described in more detail in the following sections.


Properties

Properties in the dbDAO classes are defined differently from the way they are defined in Visual Basic. The PropName type library property, for example, is exposed as two methods: GetPropName and SetPropName. If the property is read-only, only the Get method is exposed. If the property is write-only, only the Set method is exposed. The Get method takes no arguments and returns the value of the property, using the appropriate C data type where possible. The Set method takes a single argument — the new value to which you want to set the property — again using the appropriate C data type where possible.

One example of a read/write property is the Name property. It is mapped to the GetName and SetName methods. Therefore, its C prototypes are as follows:

CString		GetName (VOID);
VOID		SetName (LPCTSTR pstr);

Some properties return data of various types. The most obvious of these are the Value properties of Field objects. These have to return variants because the data type of the return value isn’t known until run time.

Visual Basic has default properties. For example, the following code:

a = rst!FirstName

is equivalent to:

a = rst!FirstName.Value

Here, Value is the default property of the Field object. Because C++ has no equivalent syntax, you have to ask for the default property by name:

a = rst.Fields["FirstName"].GetValue();

Note For this particular call, the performance of this code could be improved by using the dbDAO GetField method, as follows:

a = rst.GetField("FirstName");


Methods

Visual Basic methods correspond directly to methods in the dbDAO classes. Visual Basic methods that take optional arguments may also omit any or all optional arguments in these classes. If you omit an optional argument, a default value is supplied for the argument.

Methods take C types as arguments rather than the more common variant data type used in the OLE classes and VBA. For more information, see the individual method prototypes in Dbdao.h.


Collections

The dbDAO classes emulate Visual Basic collections by providing member variables that point to collections. As with default values, there is no automatic lookup syntax, which means you have to use an explicit Item method. This method is overloaded to allow you to specify either an ordinal or string index in the collection.

Automatic construction and destruction of these references means that statements of the following kind are valid:

d.m_Tabledefs.Item(i).Name

They result in the correct allocation of the referenced Item method (a TableDef object in this case), and its deallocation when the reference is complete. Of course, for repeated access, it is better to declare an explicit reference, but for one-time use, this is a convenient way to retrieve collection values.

A convenient shorthand for specifying an item in a collection is to use the [ ] operator. The same statement would then be:

d.m_Tabledefs[i].Name

Note If you reference fields by ordinal position cast the index as LONG to prevent the compiler from interpreting “0” as NULL.


Objects

The dbDAO classes have the same object names as in Visual Basic, except that all objects have a Cdb prefix. For example, Recordset becomes CdbRecordset, DBEngine becomes CdbDBEngine, and so on. New objects defined by the dbDAO classes that don’t exist in Visual Basic, such as CdbBookmark, CdbLastOleError, and CdbException, are described later in this chapter.

Other objects used internally in the dbDAO classes are CdbObject, CdbOleObject, and CdbGetRowsEx. Because they are used internally, they exist in the dbDAO header file.


Initializing CdbDBEngine

In dbDAO, the CdbDBEngine class initializes the DAO DBEngine object. (As noted earlier in this chapter, declaring a variable of class CdbDBEngine is not done in Visual Basic). CdbDBEngine has two constructors:

void CdbDBEngine::CdbDBEngine(BOOL bPrivate=FALSE, BOOL bStart=TRUE,
        LPCTSTR pstrIniPath=NULL, LPCTSTR pstrDefUser=NULL,
        LPCTSTR pstrDefPW=NULL, LONG lType=dbUseJet)

void CdbDBEngine::CdbDBEngine(DAODBEngine *peng, BOOL bAddRef=FALSE)

The first constructor allows you to specify the whether the DBEngine object is private or shared, whether the engine should start on construction, the IniPath, the default user, the default password, and the default type for workspaces. Once the engine is started, these properties cannot be modified. The second constructor allows you to add an additional reference to an existing instance of the DBEngine object. Both constructors call CoCreateInstance to create an instance of the DBEngine object and obtain a DAODBEngine interface pointer (the underlying Automation interface as defined in Dbdaoint.h).

Each private engine is a new instance of DAO. A shared engine is common across all instances, enabling them to share open workspaces, databases, recordsets, and so on.

If you don’t start the engine on construction, you can use a Start method to start it. Before invoking the Start method, you can override the default registry key by setting the IniPath property and override the default workgroup information file by setting the SystemDB property.


Handling dbDAO Errors

The dbDAO classes use exception handling to let you know when an error has occurred. The CdbException class provides minimal exception handling (it only has an HRESULT member). The CdbLastOLEError and CdbError classes offer more detailed error handling.

To catch an exception for dbDAO, use the CdbException class in your catch statement:

try
    {
    // Make some dbDAO function calls.
    }
catch (CdbException e)
    {
    // Handle the exception.
    }

In many cases, you’ll just want your exception handling code to examine the HRESULT returned. In that case, the only thing you need is the CdbException object. The only member of CdbException is m_hr, the HRESULT. Error constants that can be returned by dbDAO member functions are defined in Dbdaoerr.h. To determine exactly what the error was, you can compare the HRESULT in CdbException with values defined in Dbdaoerr.h:

#include <dbdao.h>
#include <dbdaoerr.h>

void OpenADatabase(CString stDBName)
    {
    CdbDBEngine dben;
    CdbDatabase db;

    try
        {
        db = dben.OpenDatabase(stDBName);
        }
    catch (CdbException e)
        {
        assert (e.m_hr == E_DAO_FileNotFound);
        // Inform the user that the file specified doesn't exist.
        // E_DAO_FileNotFound is a constant defined in dbdaoerr.h.
        }
    }

DAO returns numeric error values in two forms. One is in the form of an HRESULT. This is the form familiar to OLE programmers. The other form is a long integer. This error number is the same number as is returned by the Err.Number property in Visual Basic. To read the HRESULT, examine the m_hr member of the CdbException object. To get the equivalent Visual Basic error number wrap the m_hr member of CdbException in a DBERR macro (defined in Dbdao.h).

For more involved error handling, you may want to use the CdbError class. This class is associated with the CdbDBEngine object’s CdbErrors collection. It holds the last DAO errors that have occurred. The CdbErrors collection may hold more than one error if there is more information that can be passed. For example, an ODBC error usually results in more than one error in this collection. The members of this class contain the following elements:

  • The number value associated with the error.

  • The DAO description for that error.

  • The object on which the error occurred (in the form DAO.object — for example, DAO.Database).

  • The DAO Help file and Help context for obtaining more information about the error.

The number value stored in the CdbError object is in the form of an IDA rather than an HRESULT, as in the CdbException object:

void OpenADatabase(CString stDBName)
    {
    CdbDBEngine dben;
    CdbDatabase db;

    try
        {
        db = dben.OpenDatabase(stDBName);
        }
    catch (CdbException e)
        {
        int iError, cError;
        CString stError = _T("");

        cError = dben.Errors.GetCount();
        for (iError = 0; iError < cError; iError++)
            {
            stError += dben.Errors[iError].GetDescription();
            stError += dben.Errors[iError].GetNumber();
            stError += _T("\n");
            }
        //Inform the user of the error with stError.
        }
    }

The CdbLastOLEError class can be used to get extended error information, including the error description, on the last OLE error to occur. This class can be used regardless of whether the last error to occur was a DAO or an OLE error.

void OpenADatabase(CString stDBName)
    {
    CdbDBEngine dben;
    CdbDatabase db;

    try
        {
        db = dben.OpenDatabase(stDBName);
        }
    catch (CdbException e)
        {
        CdbLastOLEError ex;
        CString stError;

        stError.Format("0x%lx : %s", e.m_hr,
                (LPCTSTR)ex.GetDescription());
        //Inform the user of the error with stError.
        }
    }


Performing Bulk Fetches with GetRows and GetRowsEx

The dbDAO classes support the GetRows method for retrieving multiple records in one call. Like the Visual Basic version, the GetRows method returns the data as variants.

The GetRowsEx method is a performance improvement by dbDAO and MFC DAO to avoid the necessity of retrieving all data as variants and then converting to native types. The GetRowsEx method does not exist in Visual Basic DAO.

Important The underlying ICDAORecordset::GetRows method might not be supported in future versions of DAO; however, the MFC and dbDAO classes that use this method will continue to be supported.


Using the dbDAO GetRows Method

The GetRows method takes one parameter specifying the number of records to fetch, and returns the data as variants. Unlike Visual Basic, you cannot simply use subscripts to indicate the array field in a variant. Rather, the data is returned as an OLE SafeArray. The parray member of the variant points to an array descriptor. Individual fields are accessed by calling the SafeArrayGetElement function.

See Also For more information about using the SafeArray data type, see the OLE Automation Programmer’s Reference published by Microsoft Press.

The following code from the GetRows sample (found in the \Samples\GetRows folder of the DAOSDK installation) shows how to use the GetRows method. Note that the table schema is known a priori, allowing you to hard-code the display functions.

void CGetRowsDlg::DoGetRows()
{
    COleVariant		cRows;
    COleVariant		varField;
    CString			strLBRow;
    TCHAR			szId[32];
    LONG			lNumRecords;
    LONG			lIndex[2];
    HRESULT			hResult;
    CListBox		*pListBox = (CListBox *)GetDlgItem(IDD_GETROWSLIST);

    //Perform GetRows on Employee table.
    //This GetRows uses VARIANTS.
    cRows = m_cEmpRecordSet.GetRows(MAX_EMP_REC); //Arbitrarily get MAX_EMP_REC rows.

    //Find out how many records were actually retrieved.
    //(SafeArrays are 1-based.)
    SafeArrayGetUBound(cRows.parray, 2, &lNumRecords);

    //Clear the list box.
    pListBox->ResetContent();

    for (lIndex[1] = 0; lIndex[1] <= lNumRecords; lIndex[1]++)
        {
        strLBRow.Empty();//Clear the string.

        lIndex[0] = EMP_ID;

        //Use OLE safe array function to access fields.
        hResult = SafeArrayGetElement(cRows.parray, &lIndex[0], &varField);

        //Watch out for bad variants.
        if(FAILED(hResult))
            break;

        if(varField.vt == VT_I4) //Must be a long.
            {
            wsprintf(szId, _T("%d,  "), varField.iVal);
            }
        else
            {
            lstrcpy(szId, _T("Unexpected Data Type"));
            }

        strLBRow += (LPCTSTR)szId;

        //Get last name.
        lIndex[0] = EMP_LNAME;
        SafeArrayGetElement(cRows.parray, &lIndex[0], &varField);
        strLBRow += (LPCTSTR)varField.bstrVal;

        //Get first name.
        strLBRow += _T(", ");
        lIndex[0] = EMP_FNAME;
        SafeArrayGetElement(cRows.parray, &lIndex[0], &varField);
        strLBRow += (LPCTSTR)varField.bstrVal;

        pListBox->AddString(strLBRow);
        }
}


Using the dbDAO GetRowsEx Method

The GetRowsEx method retrieves multiple records and enables you to specify the data types of the fields stored in your application variables. It has the following parameters.

Parameter | Data type | Description
pvBuffer | LPVOID | Pointer to the buffer in which to store returned records.
CbRow | LONG | Length of the record, in bytes.
Prb | LPDAORSETBINDING | Pointer to the binding structure (see the following description).
Cbinding | LONG | Number of bindings.
PvVarBuffer | LPVOID | Pointer to the buffer in which to store variable-length data.
CbVarBuffer | LONG | Length, in bytes, of pvVarBuffer.
Lrows | LONG | Number of records requested.

The DAORSETBINDING structure specifies how data is to be copied from the records to the memory buffer. A separate binding structure must be filled in for each field retrieved. DAORSETBINDING is defined as follows:

typedef struct
    {
    DWORD   dwBindIndexType;
    union
        {
        LONG    i;
        LPCTSTR pstr;
        };

    DWORD   dwType;
    DWORD   dwOffset;
    DWORD   cb;

    } DAORSETBINDING, *LPDAORSETBINDING;

The DAORSETBINDING structure has the following members.

Member | Description
dwBindIndexType | Value specifying whether a field is indicated by an index number or by name. This member is one of the following values: dbBindIndexINT (index) dbBindIndexSTR (name)
dwType | Data type. This member is one of the following values: dbBindI2 (long) dbBindI4 (short) dbBindR4 (float) dbBindR8 (double) dbBindCY (currency) dbBindDATE (DATE) dbBindBOOL (VARIANT_BOOL) dbBindUI1 (unsigned char) dbBindVARIANT (VARIANT) dbBindWCHAR (wchar_t[]) dbBindSTRING (same as dbBindWCHAR for Unicode, dbBindUI1 for ANSI) dbBindLPSTR (char_pointer) dbBindLPTSTR (wchar_pointer) dbBindLPSTRING (dbBindLPTSTR for Unicode, dbBindLPSTR for ANSI) dbBindBookmark (CdbBookmark) dbBindBlob (unsigned char pointer)
dwOffset | Offset, in bytes, in the record buffer where the data is copied.
Cb | Field length, in bytes.

The following code from the GetRows example shows how to use the GetRowsEx method. Note that the table schema is known a priori, allowing you to predefine a structure for the data.

Two different binding schemes for retrieving string data are shown. If the size of the string is fixed, you can retrieve the strings directly into the structure. If you are using variable-length strings, they will be placed sequentially in a single large buffer, and a pointer into that buffer will be stored in the structure.

// Structure for GetRowsEx:
typedef struct
    {
    LONG	lEmpId;
    TCHAR	*strLastName;
    TCHAR	strFirstName[20];
    } EMP, *LPEMP ;

// Employee table binding:
DAORSETBINDING	Bindings[] =
{
//Index Type		Column		Type			Offset						Size
{dbBindIndexINT,	EMP_ID,		dbBindI4,		offsetof(EMP,lEmpId),		sizeof(LONG)},
{dbBindIndexINT,	EMP_LNAME,	dbBindLPSTRING,	offsetof(EMP,strLastName),	sizeof(TCHAR *)},
{dbBindIndexINT,	EMP_FNAME,	dbBindSTRING,	offsetof(EMP,strFirstName),	sizeof(TCHAR) * 20}
};

// Perform C++ GetRowsEx against the Employee table.
void CGetRowsDlg::DoGetRowsEx()
{
    LPEMP			pEmpRows = new EMP[MAX_EMP_REC];
    CListBox		*pListBox = (CListBox *)GetDlgItem(IDD_GETROWSLISTEX);
    CString			strLBRow;
    TCHAR			szId[16];
    LONG			lNumRecords;
    LONG			lCount;
    TCHAR			pBuf[MAX_EMP_REC * 15];		// Allow average of 15 chars/name.

    //Perform GetRows on Employee table.
    //This GetRows uses a specific C++ structure.
    try
        {
        lNumRecords = m_cEmpRecordSet.GetRowsEx(pEmpRows, sizeof(EMP),
                                    &Bindings[0], sizeof(Bindings) / sizeof(DAORSETBINDING),
                                    pBuf, sizeof(pBuf),
                                    MAX_EMP_REC); //arbitrarily get MAX_EMP_REC rows
        }
    catch (CdbException e)
        {
        //Differentiate between GetRowsEx Errors and other CdbExceptions.
        //See defines in DAOGETRW.H.
        if(	e.m_hr == E_ROWTOOSHORT ||
            e.m_hr == E_BADBINDINFO ||
            e.m_hr == E_COLUMNUNAVAILABLE )
            {
            AfxMessageBox(_T("Error in GetRowsEx call."));
            }
        else
            {
            AfxMessageBox(_T("General CdbException"));
            }
        delete [] pEmpRows;
        return;
        }

    //Step through the returned rows.
    for (lCount = 0; lCount < lNumRecords; lCount++)
        {
        strLBRow.Empty();
        wsprintf(szId, _T("%d,  "), pEmpRows[lCount].lEmpId);
        strLBRow += szId;
        strLBRow += pEmpRows[lCount].strLastName;
        strLBRow += _T(", ");
        strLBRow += (LPCTSTR) pEmpRows[lCount].strFirstName;
        pListBox->AddString(strLBRow);
        }

    delete [] pEmpRows;
}


ANSI and Unicode Strings

All 16-bit applications use ANSI strings. 32-bit applications can use either ANSI or Unicode strings. ANSI applications store strings as unsigned char, each byte containing a different character. Unicode applications store strings as unsigned short, each short integer value containing a different character.

To specify ANSI, declare the following statement in your project settings:

#define OLE2ANSI

To specify Unicode, declare the following statement in your project settings:

#define _UNICODE

The advantage of using Unicode in your applications is that it enables you to easily localize your application into languages such as Chinese, where two bytes are required to store characters. ANSI applications require special double-byte character set (DBCS) functions to be localized into such languages.

The advantage of using ANSI strings is that the Windows system calls take ANSI strings in Windows 95. In contrast, if you use Unicode strings, you must convert them to ANSI strings before calling these functions.

You can compile your application in ANSI or Unicode by using the _T macro. The _T macro is defined by Visual C++ and is used to wrap all string literals. In ANSI, this macro does nothing. In Unicode, it appends the L keyword to inform the compiler that the literal is to be read in as Unicode.

void OpenADatabase()
    {
    CdbDBEngine dben;
    CdbDatabase db;

    db = dben.OpenDatabase(_T("MyDB.MDB"));
    }

The preceding code will compile for both ANSI and Unicode applications.


Using dbDAO in a DLL

Although many people use DAO from an executable file, you may want to create dynamic-link libraries (DLLs) for distribution so that others can use them in their applications. If you use a DLL, you must start DAO from inside the DLL. Various restrictions on DLLs prevent this from being done automatically while the DLL loads, and the dbDAO classes will not handle it for you. Instead, you must explicitly force the load.

For example, the following code works fine inside an executable, but the same code does not work as a DLL:

class CMyObj
    {
public:
    CMyObj();
     ~CMyObj();
private:
    CdbDBEngine m_cDBEng;
    };

CMyObj::CMyObj()
    {
    }

CMyObj::~CMyObj()
    {
    delete m_cDBEng;
    }

// Globally instantiated object - the cause of the DLL problem.
CMyObj myObj;

#ifdef MAKE_AS_DLL
// Exported function that is called by some console application.
// This case hangs.
__declspec(dllexport) void hello()
    {
    }

#else
// When this is built as a console application, it works fine.
void main()
    {
    return;
    }
#endif // MAKE_AS_DLL

The line causing the problem is the declaration:

CMyObj myObj;

When the DLL is loaded and the class is instantiated, an attempt is made to start DAO. To work around this problem, you must create an entry point in your routine that explicitly creates DAO, and have your DLL users call that entry point. For example, you could do this as part of another initialization — for example, declaring the global variable as a pointer to CdbDBEngine and then constructing the class in your initialization function.


Other dbDAO DLL Versions

Other versions of the dbDAO DLL can be built that vary with respect to the following capabilities:

  • Use of ANSI or Unicode strings

  • Built-in debugging functionality

  • Use of shared or static MFC DLLs

The MakeFile that is installed with the DAO SDK (Dbdao.mak) provides you with four prebuilt dbDAO DLLs and subprojects to build them, plus four other subprojects to build additional DLLs. There are the following four prebuilt DLLs:

  • Release\Ddao35.dll (dbDAO-Win32 Release)

    This is the ANSI version of the dbDAO classes. It dynamically links with the MFC DLL, making Ddao35.dll smaller and allowing it to share the MFC DLLs on a user’s system with other applications that also use MFC version 4.2 or later. If you write applications with this DLL, you must ensure that your Setup program distributes the MFC DLLs. The DAO SDK Setup program installs this file into your system directory.

  • Releaseu\Ddao35u.dll (dbDAO-Win32 Release Unicode)

    This is the Unicode version of the dbDAO classes. It is otherwise identical to Ddao35.dll. The DAO SDK Setup program installs this file into your system directory.

  • Debug\Ddao35d.dll (dbDAO-Win32 Debug)

    This is the debug ANSI version of Ddao35.dll. It is intended for developers who have to debug into the dbDAO source code. You should not ship this version with your application, as it is slower and has debug functionality that users will not want.

  • Debugu\Ddao35ud.dll (dbDAO-Win32 Debug Unicode)

    This is the debug Unicode version of Ddao35u.dll. It is intended for developers who have to debug into the dbDAO source code. You should not ship this version with your application, as it is slower and has debug functionality that users will not want.

These are the other four subprojects defined in the dbDAO MakeFile:

  • Releases\Ddao35s.dll (dbDAO-Win32 Release Static MFC)

    This file is for ANSI applications that are statically linked to the MFC library. It has the MFC class library built into it.

  • Releaseus\Ddao35us.dll (dbDAO-Win32 Release Unicode Static MFC)

    This file is for Unicode applications that are statically linked to the MFC library. It has the MFC library built into it.

  • Debugs\Ddao35sd.dll (dbDAO-Win32 Debug Static MFC)

    This is the debug version for ANSI applications that are statically linked to the MFC library. You should not ship this version with your application, as it is slower and has debug functionality that users will not want.

  • Debugus\Ddao35usd.dll (dbDAO-Win32 Debug Unicode Static MFC)

    This is the debug version for Unicode applications that are statically linked to the MFC library. You should not ship this version with your application, as it is slower and has debug functionality that users will not want.


DAO SDK Files

The DAO 3.5 SDK is included in Visual C++ versions 4.2 and 5.0. Attempting to build new SDK libraries with older versions of MFC will result in a compile error.

The DAO SDK Setup program does everything required to start programming with dbDAO, including copying all necessary DLLs, updating the Registry, and setting up a directory structure for the header files, libraries, and sample programs.

The following tables describe the files that are installed by the DAO SDK Setup program.


Include Files

File | Description
Dbdao.h | Provides all dbDAO class definitions and helper macros, and includes other necessary header files. Dbdao.h must be included by all applications that use dbDAO.
Dbdaoerr.h | Defines all the errors that can be returned by DAO. For more information about how to handle errors in dbDAO, see “Handling dbDAO Errors” earlier in this chapter.
Daogetrw.h | Required by applications that use the GetRowsEx method. It defines HRESULT values not included in Dbdaoerr.h, binding constants for the function, and the DAOCOLUMNBINDING structure. Daogetrw.h does not have to be included in programs that use dbDAO; it is provided so you can rebuild Dbdao.cpp. For more information about using the GetRowsEx method, see “Performing Bulk Fetches with GetRows and GetRowsEx” earlier in this chapter.
_Dbdao.h | Used in the compilation of Dbdao.cpp. _Dbdao.h contains needed definitions and helper macros for the dbDAO source files. _Dbdao.h does not have to be included in programs that use dbDAO; it is provided so you can rebuild Dbdao.cpp.
Dbdaoint.h | Defines the underlying DAO Automation interfaces, their methods, parameters, and so on. Use this file to program directly to the DAO Automation interfaces. It is included by Dbdao.h.
Dbdaoid.h | Defines the interface identifiers for the underlying DAO Automation objects. It is included by Dbdao.h.


.Src Files

File | Description
Dbdao.cpp | Source code for the dbDAO class libraries. This code creates a set of C++ classes that “cover” the Automation interfaces that DAO exposes. A compiled version of this code is provided in Ddao35.dll, which is installed in your system directory. The source code is provided for ease of debugging and building other versions of the dbDAO DLL.
Dbdao.dsp | Project file for dbDAO.
Dbdao.dsw | Project workspace for dbDAO.
Dbdao.mak | MakeFile for dbDAO class libraries. For information about the other versions you can build, see “Other dbDAO DLL Versions” earlier in this chapter.
Dbdao.rc | Generated resource file.
Dbdaouid.cpp | Source code for Dbdaouid.lib, which is useful for programming directly to the DAO Automation interface.
Resource.h | Generated resource header file.
Stdafx.h | Standard MFC header file.
Version.rc | Version resource file.
Verstamp.h | Version stamp header file.


.Lib Files

File | Description
Ddao35.lib | Import library for Ddao35.dll.
Ddao35d.lib | Import library for Ddao35d.dll (debug version of Ddao35.dll).
Ddao35u.lib | Import library for Ddao35u.dll (Unicode version of Ddao35.dll).
Ddao35ud.lib | Import library for Ddao35d.dll (debug version of Ddao35u.dll).


Samples Files

Folder | Description
ADDRBOOK | Demonstrates how to use DAO to create an address book using data stored on an Internet server.
EMPLOYEE | Based on Employees table in Employee.mdb. This sample allows viewing of data and simple edits.
DAOREAD | Demonstrates different methods for reading data and displays fetch time for each method.
GETROWS | Demonstrates how to use the GetRows and GetRowsEx methods.
MULTTHRD | Demonstrates how to write a multithreaded DAO application.
SECURITY | Displays the user and group information from a SystemDB workgroup information file.
STORES | Maintains a database of branch stores.


Using Microsoft Visual J++ and the DAO Automation Interfaces

This section introduces the DAOSample application, a simple Java application that illustrates how to program with the DAO Automation interfaces in Visual J++. The DAOSample application includes the simple DAO applet that displays table entries. The application is written to run either as a Web page or as a stand-alone application.

See Also For information about how to install the DAOSample application, search the Visual J++ Help index for “DAOSample” and follow the instructions in the Help topic.


Setting up the DAOSample Application

From Visual J++, open the Daosample.dsw project workspace file, which is located in the \Samples\Microsoft\Daosample folder.

You will need to create a Java class that wraps the DAO version 3.5 Automation interfaces. Visual J++ includes the Java Type Library Wizard, which you can use to create Java wrappers for COM interfaces.

For each type library imported, the Java Type Library Wizard creates a directory below the trusted library directory having the same name as the type library. For DAO 3.5, the Java Type Library Wizard creates the directory \Windows\System\Java\TrustLib\dao350. The Java Type Library Wizard fills that directory with .class files, one for each COM class and/or interface described in the type library. All the generated classes and interfaces are part of a Java package having the same name as the type library file.

Each .class file generated by the Java Type Library Wizard contains a special attribute identifying it as a wrapper for a COM class. When the Java Support in Internet Explorer sees this attribute on a class, it translates all Java method invocations on the class into COM function invocations on the COM class.

Û To create the Java wrappers for DAO version 3.5

  1. On the Tools menu, click Java Type Library Wizard.

  2. Select the Microsoft DAO 3.5 Object Library check box.

  3. Click OK.

After you have created the Java description of the COM component, the output window shows two useful types of information: the import statement and the Summary.txt file notice. It also specifies the Typelib conversion utility version.

Microsoft (R) Visual J++ Java Typelib Conversion Utility Version 1.01.7022
Copyright (C) Microsoft Corp 1996. All rights reserved.
import dao350.*;
C:\WINNT\java\trustlib\dao350\summary.txt(1): Class summary information created

Tool returned code: 0

The import statement is typically copied and inserted into the beginning of any Java source file that uses the COM component. The summary file (Summary.txt) lists the Java signatures of all the methods for the COM interfaces and classes described by the type library. You can open this file in Microsoft Developer Studio by double-clicking the file name in the output window.

To build the DAO Sample application, click Build on the Build menu. In the dialog box, enter simpledao as the class name.


Simpledao Applet

The simpledao.java applet includes several standard J++ packages as well as classes specific to the DAOSample application. The following table describes the classes imported into simpledao.java.

Import statement | Description
java.applet.* | Classes needed to create World Wide Web applets.
java.awt.* | Abstract Window Toolkit (AWT). A machine-independent windowing toolkit.
Simpledaoframe | Frame that is used when the applet is run as a stand-alone application.
dao_dbengine | The Java class that creates the _DBEngine object.
dao350.* | The Java DAO version 3.5 wrapper classes that were created by running the Java Type Library Wizard.
com.ms.com.variant | The com.ms.com package contains classes that are necessary when using COM services from Java. For this applet, only the variant class is needed.

The dao_dbengine class is a wrapper for the DAO _DBEngine object. To create a licensed COM component (such as DAO) from Java, you must use the IlicenseMgr interface. The license key for DAO can be found in the _dbdao.h file included in the DAO 3.5 SDK. The following code demonstrates how to create the DAO database engine using ILicenseMgr.

public class dao_dbengine
{
    // The static public method creates the DBEngine object.
    static public _DBEngine create()
    {
        // The return value:
        _DBEngine result;

        // Create the License Manager object.
        ILicenseMgr mgr = new LicenseMgr();

        // Use the License Manager to create the DBEngine.
        result = (_DBEngine) mgr.createWithLic(

            // The license key for the DAO DBEngine:
            "mbmabptebkjcdlgtjmskjwtsdhjbmkmwtrak",

            // The CLSID for the DAO DBEngine:
            "{00000010-0000-0010-8000-00AA006D2EA4}",

            // The aggregation IUnknown* punkOuter:
            null,

            // The ctxFlag to create in the in-process server:
            ComContext.INPROC_SERVER
            );

        return result;
    }
}


Connecting to the Database

The init method is the entry point for the simpledao.java applet. It instantiates the dao_dbengine class, and then opens the database and the recordset using the related parameter values.

public void init()
{
    if (!m_fStandAlone)
        GetParameters(null);
   // If you use a ResourceWizard-generated "control creator" class to
   // arrange controls in your applet, you may want to call its
   // CreateControls method from within this method. Remove the following
   // call to resize before adding the call to CreateControls;
   // CreateControls does its own resizing.
   //----------------------------------------------------------------------
    resize(320, 200);
    // Set the name of the database (parameter).
    if (m_fStandAlone)
        // Use the value on the command line for the MDB filename.
        filename = m_Database;
    else
    try {
        // Otherwise, generate it relative to the applet.
        java.net.URL fn;
        fn = new java.net.URL(getDocumentBase(), m_Database);
        // Strip away the "file:/" from the URL.
        filename = fn.getFile().substring(1);
    } catch(Exception e) {
        showStatus("Error: " + e.getMessage());
    }
    _DBEngine idbengine = (_DBEngine) dao_dbengine.create();
    // Open the database.
   Variant vExclusive = new Variant();
   Variant vReadOnly = new Variant();
   Variant vConnect = new Variant();
   vExclusive.putBoolean(false);
   vReadOnly.putBoolean(readOnly);
   vConnect.putString("");
   database = idbengine.OpenDatabase(filename, vExclusive, vReadOnly, vConnect);
    // Create a new recordset.
   Variant vOpenType = new Variant();
   Variant vOptions = new Variant();
   vOpenType.putShort((short)RecordsetTypeEnum.dbOpenDynaset);
   vOptions.putShort((short)(readOnly ? RecordsetOptionEnum.dbReadOnly : 0));
   recordset = database._30_OpenRecordset(m_Recordset, vOpenType, vOptions);
}

For each variant parameter for the OpenDatabase and OpenRecordset methods, there is a corresponding variant variable. The variant data type maps to the Java com.ms.com.variant class. Use the methods of the class to set the contents of the variable.

In Java, optional parameters are exposed as ordinary parameters. Therefore, a variant object must be specified even if the parameter is declared as optional in the component’s type library. It is possible to omit a specific value for a parameter by using the noParam method of the Java variant class. The noParam method makes the variant object act as a placeholder for a missing optional parameter. For example, the following code demonstrates how to call the OpenDatabase method without specifying values for the optional parameters vExclusive, vReadOnly, and vConnect.

Variant vOptParam = new Variant();
vOptParam.noParam();
database = idbengine.OpenDatabase(filename, vOptParam, vOptParam, vOptParam);


Reading Records from the Database

When the page containing the simpledao.java applet first appears on the screen, the applet’s start method is called. In this sample, this method creates the form, moves to the first field in the recordset, and then displays the field values.

public void start()
{
    // Create the form, passing it the recordset.
    m_form = new SimpleForm(recordset);

    // Add it to the applet.
    add("Center", m_form);

    // Get the first record in the recordset.
    recordset.MoveFirst();

    // Display that record.
    m_form.showData();

}

Reading data and displaying the results is done in the SimpleForm class. The fields on the form are created in the SimpleForm constructor. The showData method displays the value for each field in the recordset in the corresponding field on the form.

// Show the data in the form.
void showData()
{
    // Get the fields in the recordset.
    Fields fields = m_recordset.getFields();
    _Field f;

    // Create a Variant to get the value of each field.
    Variant value;

    // Get the first field.
    f = fields.getItem(name1);
    // Get its value.
    value = f.getValue();
    // Set the field in the form.
    field1.setText(value.toString());

    // Repeat for the other fields.
    f = fields.getItem(name2);
    value = f.getValue();
    field2.setText(value.toString());

    f = fields.getItem(name3);
    value = f.getValue();
    field3.setText(value.toString());

    f = fields.getItem(name4);
    value = f.getValue();
    field4.setText(value.toString());

    f = fields.getItem(name5);
    value = f.getValue();
    field5.setText(value.toString());

    f = fields.getItem(name6);
    value = f.getValue();
    field6.setText(value.toString());

    f = fields.getItem(name7);
    value = f.getValue();
    field7.setState(value.getBoolean());
}

The getItem method is used to retrieve a field from the fields collection. The Java Type Library Wizard generated the getItem method from the declaration of the Item method. All methods declared with the propget or propput attributes are exposed with the get or put prefixes attached to their names. The following Object Description Language (ODL) for the Item method shows the Item method declared with propget.

[id(00000000), propget, hidden]
HRESULT _stdcall Item(
                [in] VARIANT Index,
                [out, retval] Field** ppfld);

Note To browse the DAO version 3.5 object library, use the OLE\COM Object Viewer. In Microsoft Developer Studio, click OLE\COM Object Viewer on the Tools menu. In the OLE\COM Object Viewer, click View TypeLib on the File menu. In the File Open dialog box, select \Program Files\Common Files\Microsoft Shared\DAO\dao350.dll.

If you are familiar with using DAO from within Visual Basic, you will not recognize Item as a property of the Fields collection. The Java expression:

Fields.getItem(name5)

is equivalent to the Visual Basic expression:

Fields(name5).

Because the Item method is marked as hidden, Visual Basic will not display it as a property of the Fields collection. Also, because Item is the default property, as identified by id(00000000), it is not necessary to explicitly reference it in Visual Basic.

The Item method takes a variant parameter that can be either the name or the ordinal position of the Field object to return. In this sample, the variant variables that contain the names of the fields (name1..name5) are initialized in the constructor for SimpleForm.

The variables fields and f are not explicitly released at the end of the method. The function relies on Java’s garbage collection to clean things up. In this example, it is fine to let Java clean things up. However, there may be instances when you want to explicitly release an object. The following code uses the ComLib object found in com.ms.com to release the fields object.

ComLib.release(fields);


Chapter 12 ODBC Desktop Database Drivers

ODBC Desktop Database Drivers

The Microsoft Open Database Connectivity (ODBC) Desktop Database Drivers use the Microsoft Jet database engine to provide ODBC-enabled applications with access to six different database formats. This chapter discusses the architecture of the drivers, Microsoft Jet features that the drivers expose, implementation details, and enhancements that improve driver performance.

This chapter assumes you’re familiar with the ODBC Application Programming Interface (API). For information about ODBC, refer to the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference published by Microsoft Press, or to the release notes in the Microsoft ODBC 3.0 Software Development Kit (SDK).

Introduction

History

Architecture

Files

Implementation Details

Microsoft Jet Feature Support

Performance Considerations


Introduction

ODBC is an API that uses Structured Query Language (SQL) as the database access language. You can access a wide variety of database management systems (DBMSs) with the same ODBC source code that is directly incorporated into an application’s source code. With the Microsoft ODBC Desktop Database Drivers, a user of an ODBC-enabled application can open, query, and update a desktop database through the ODBC interface.

ODBC architecture consists of a Driver Manager and a set of ODBC drivers. ODBC drivers are database-specific modules that accept function calls from the Driver Manager and translate them into function calls that can operate directly upon databases or database files. These drivers isolate applications from database-specific calls, much the same way printer drivers enable an application to communicate with a variety of printers. Drivers are loaded at run time, so that a user only has to add a new driver to access a new DBMS.

The Microsoft ODBC Desktop Database Drivers are a Microsoft Jet-based set of ODBC drivers. Whereas Microsoft ODBC Desktop Database Drivers 2.0 includes both 16- and 32-bit drivers, versions 3.0 and later include only 32-bit drivers that work on Microsoft Windows 95 or later, or Microsoft Windows NT Workstation or Server version 3.51 or later. These drivers provide access to the following types of data sources:

  • Microsoft Access

  • dBASE

  • Microsoft Excel

  • Microsoft FoxPro

  • Paradox

  • Text

See Also Access to Lotus 1-2-3, Microsoft Exchange, and HTML is enabled by installable ISAM (IISAM) drivers. For more information about these drivers, see Chapter 8, “Accessing External Data.”

The Desktop Database Drivers were created to provide access to the desktop data sources listed (which are among the most widely used in the industry), to satisfy the need of Microsoft applications for this data, and to provide ODBC drivers whose architecture is compatible with Microsoft products. Using the Microsoft Jet database engine to access data has helped the Desktop Database Drivers achieve these goals. Because a primary design goal of the drivers has been to expose as much Microsoft Jet functionality as possible in an ODBC context, the drivers provide users with a powerful extension of the Microsoft Jet engine.


History

The following sections briefly describe the versions of the Desktop Database Drivers.

Version 1.0

Version 2.0

Version 3.0

Version 3.5


Version 1.0

ODBC Desktop Database Drivers 1.0 was released in August 1993. These drivers used the SIMBA query processor produced by PageAhead Software. SIMBA received ODBC calls and SQL statements, processed them into Microsoft Jet installable ISAM calls, then called the Microsoft Jet ISAM dispatch layer to load and call the appropriate installable ISAM driver.


Version 2.0

ODBC Desktop Database Drivers 2.0 was released in December 1994. These drivers were released for use with ODBC 2.0, which significantly expanded their ODBC functionality. The major change in version 2.0 was that the Microsoft Jet database engine replaced the SIMBA query processor. There were many advantages to this:

  • With the Microsoft Jet database engine, the Desktop Database Drivers integrated much more tightly with the Microsoft Jet installable ISAM drivers and Microsoft Access technology.

  • The Microsoft Jet cursor model offered significant improvements over the SIMBA model, which used the ODBC Cursor Library to provide scrollable cursors. Microsoft Jet provided native support for scrollable cursors.

  • Microsoft Jet provided native support for outer joins, updatable and heterogeneous joins, and transactions.

  • The Microsoft Jet query optimizer, with enhancements such as Rushmore technology, dramatically increased driver performance.

  • Microsoft Jet provided 32-bit versions of the drivers for Microsoft Windows NT.


Version 3.0

ODBC Desktop Database Drivers 3.0 was released in October 1995. The primary motivation for this release was to provide support for Microsoft Windows 95 and Microsoft Windows NT Workstation or Server 3.51. Only 32-bit drivers were included in this release; the 16-bit drivers for Microsoft Windows version 3.1 were removed.


Version 3.5

ODBC Desktop Database Drivers 3.5 was released in October 1996. These drivers were double-byte character set (DBCS)-enabled, were better suited for use with Internet applications than previous versions, and accommodated the use of File data source names (DSNs). The Microsoft Access driver was released in a RISC version for use on Alpha platforms.

Note The version 3.5 drivers were designed to work with ODBC 2.x. Although they also work with ODBC 3.0, they do not support all ODBC 3.0 features. For more information about how these drivers work with ODBC 3.0, see “Backward Compatibility and Standards Compliance” in Chapter 17, “Programming Considerations,” in the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference.


Architecture

The Desktop Database Drivers are two-tier drivers. In a two-tier configuration, the driver does not perform the process of parsing, validating, optimizing, and executing the query. Instead, Microsoft Jet performs these tasks. It processes ODBC API calls and acts as an SQL engine. Microsoft Jet has become an integral, inseparable part of the drivers: It is shipped with the drivers and resides with the drivers, even if no other application on the machine uses it.

The Desktop Database Drivers consist of six different drivers — or, more precisely, one driver file (ODBCJT32.DLL) that the ODBC Driver Manager uses in six different ways. The DRIVERID flag in the registry entry for a data source determines which driver in ODBCJT32.DLL the Driver Manager uses. An application passes this flag in the connection string included in a call to SQLDriverConnect. By default, the flag is the ID of the Microsoft Access driver.

The driver setup file changes the DRIVERID flag at setup time. All drivers except the Microsoft Access driver have an associated setup dynamic-link library (DLL). When you click the Setup button in the ODBC Administrator for a data source, the ODBC installer DLL (ODBCINST.DLL) loads the setup DLL. The setup DLL exports the ODBC installer function SQLConfigDataSource. If a window handle is passed to SQLConfigDataSource, this function displays a setup window and changes the DRIVERID flag according to the driver selected from the user interface.

When a file is created programmatically, a NULL window handle is passed to SQLConfigDataSource, and the function creates a data source dynamically, changing the DRIVERID flag according to the lpszDriver argument in the function call. For more information, see the Creating .MDB Files Programmatically section later in this chapter.

ODBCJT32.DLL implements ODBC functions on top of the Microsoft Jet API. There is no direct mapping between ODBC and Microsoft Jet functions, however. Many factors, such as the cursor models and SQL mapping, prevent a direct correlation of the functions.

The ODBC driver resides between the Microsoft Jet engine and the ODBC Driver Manager. Some ODBC functions called by an application are handled by the Driver Manager and not passed to the driver. For these functions, Microsoft Jet never sees the function call because it does not have a direct connection to the Driver Manager.

The following figure illustrates the Desktop Database Drivers architecture.

Figure 12.1 Desktop Database Drivers architecture


Driver Files

The following driver files are used by each Desktop Database Driver.

File | Description
ODBCJI32.DLL | Separate library containing localized strings for the Desktop Database Drivers.
ODBCJT32.DLL | The Desktop Database Drivers file. For more information, see the “Architecture ” section earlier in this chapter.
ODBCTL32.DLL | File containing generic conversion functions for parsing SQL strings passed in ODBC escape sequences. For more information, see the “Data Conversion ” section later in this chapter.


Microsoft Jet Files

The following Microsoft Jet files, discussed in Appendix E, “Microsoft Jet Components,” are used by each Desktop Database Driver.

File | Description
MSJINT35.DLL | Contains all localized Microsoft Jet strings.
MSJET35.DLL | Contains the main engine functionality.
MSJTER35.DLL | Contains the Microsoft Jet error service.
VBAJET32.DLL | Initializes the Visual Basic for Applications (VBA) expression service for Microsoft Jet.
VBAR332.DLL | A run-time version of the VBA expression service.


Installable ISAM Driver Files

Each of the Desktop Database Drivers has its own IISAM driver, except for the Microsoft Access 2.0, 7.0, and 8.0 driver. This is because the ISAM driver for the native Microsoft Access format is built directly into Microsoft Jet. (Note that the MSRD2X32.DLL IISAM driver is required for access to Microsoft Access versions 1.x and 2.0 data.) All other database formats require an IISAM driver that provides data access between Microsoft Jet and the other DBMS.

IISAM driver and setup files for each of the Desktop Database Drivers are listed in the following table.

Driver | IISAM file | Setup file
Microsoft Access (versions 2.0, 7.0, and 8.0) | None (included in MSJET35.DLL) | None
Microsoft Access (versions 1.x) | MSRD2X35.DLL | None
dBASE | MSXBSE35.DLL | ODDBSE32.DLL
Microsoft Excel | MSEXCL35.DLL | ODEXL32.DLL
Microsoft FoxPro | MSXBSE35.DLL | ODFOX32.DLL
Paradox | MSPDOX35.DLL | ODPDX32.DLL
Text | MSTEXT35.DLL | ODTEXT32.DLL

Note The ODBC Desktop Database Drivers version 3.5 does not include a driver for Microsoft Visual FoxPro 3.0. A new driver for Visual FoxPro 3.0 is available from Microsoft.

MSRD2X32.DLL provides backward compatibility for Microsoft Access 1.x and 2.0 files. No IISAM driver is needed for Microsoft Access 2.0, 7.0, and 8.0 because the IISAM driver for that data is part of the stand-alone Microsoft Jet engine.

The same IISAM files are used for the dBASE and FoxPro drivers, but the setup files are different. The FoxPro driver uses a generic dBASE engine that ensures compatibility with dBASE data sources.

See Also For more information about how the installable ISAM drivers work, see Chapter 8, “Accessing External Data.”


ODBC Files

The Desktop Database Drivers include the ODBC Core components.

File | Description
ODBC32.DLL | ODBC Driver Manager
ODBCAD32.EXE | ODBC Administrator
ODBCCP32.DLL ODBCCP32.CPL | ODBC Installer/ODBC Administrator Control Panel
ODBCCR32.DLL | ODBC Cursor Library. Because the Desktop Database Drivers use the native scrollable cursors provided by Microsoft Jet, the Cursor Library is usually not needed. An application can, however, force use of the Cursor Library.
ODBCINT.DLL | ODBC Core localized strings
ODBC16GT.DLL ODBC32GT.DLL | Thunking files for the Driver Manager
ODBC.INF ODBCKEY.INF ODBCSTF.DLL | ODBC setup files


Implementation Details

The following sections describe how the Desktop Database Drivers implement certain ODBC and SQL features.


ODBC API Conformance Level

The Desktop Database Drivers are Level 1 conformant (as defined in ODBC 2.x), and support the following Level 2 ODBC functions:

  • SQLDrivers

  • SQLExtendedFetch

  • SQLMoreResults

  • SQLNativeSQL

  • SQLNumParams

  • SQLProcedureColumns

  • SQLSetPos


SQL Conformance Level

The Desktop Database Drivers conform to the Minimum SQL Grammar level (as defined in ODBC 2.x), with the following Core and Extended enhancements. For more information about the SQL support provided by the Desktop Database Drivers, see the SQL Support section later in this chapter.

  • Approximate numeric literals (Core)

  • ALTER TABLE statement (Core)

  • Binary literals (Extended)

  • Between…And operator (BETWEEN predicate) (Core)

  • Correlation names (Core)

  • EXISTS predicate (Core)

  • IN (valuelist) (Core)

  • Procedure Invocation (Extended)

  • SELECT statement including GROUP BY, HAVING, and UNION clauses (Extended)

  • Subqueries (sub-SELECTs) (Core)


Integrity Enhancement Facility Conformance

The Desktop Database Drivers support the same optional Integrity Enhancement Facility (IEF) features (as contained in the X/Open and SAG SQL CAE specifications) that Microsoft Jet exposes. A call to SQLGetInfo with an fInfoType argument of SQL_INTEGRITY (SQL_ODBC_SQL_OPT_IEF in ODBC 2.x) returns an “N,” indicating that the drivers do not support the full IEF syntax contained in the X/Open and SAG SQL CAE specifications. However, the Microsoft Access driver supports the IEF features supported by Microsoft Jet by using Microsoft Jet syntax. For instance, the Microsoft Access driver supports the same syntax for creating a primary key that Microsoft Jet does.


Scrollable Cursor Support

Desktop Database Drivers 1.0 supported scrollable cursors through the ODBC Cursor Library. The Cursor Library implements the ODBC function SQLExtendedFetch by repeatedly calling SQLFetch to populate the Cursor Library cache one record (row) at a time. Scrolling is emulated when the application calls SQLExtendedFetch to reposition the cursor on data in the cache. Data is re-fetched from the driver only when necessary. Because the Cursor Library supports only static cursors, it cannot detect changes in the underlying data source.

With version 2.0 and later drivers, the Cursor Library is no longer necessary. The Desktop Database Drivers directly support SQLExtendedFetch and SQLSetPos through native Microsoft Jet cursors, which are much more efficient than the Cursor Library. While the Cursor Library can still be used, its use is discouraged. The Cursor Library incurs a lot of overhead in parsing the SELECT statement it uses to generate the data in its cache, performing a catalog lookup to update data, and creating a searched UPDATE statement with a WHERE clause to update the base table based on data in the temporary file. Microsoft Jet cursors perform direct updates, operating on the base table, that are much faster. For more information, see the “Insertion, Deletion, and Update Performance ” section later in this chapter.


Supported Cursor Types

The Desktop Database Drivers support forward-only and keyset-driven cursors. The drivers support updates on keyset-driven cursors only. The drivers do not support mixed and dynamic cursors; the driver creates a keyset-driven cursor if a dynamic cursor is requested. The drivers support static cursors only if an application uses the Cursor Library in conjunction with the driver.


Cursor Positioning and Positioned Operations

In ODBC, updates and deletes that require cursor positioning can normally be performed by executing a positioned UPDATE or DELETE SQL statement (with the WHERE CURRENT OF clause), or by calling the SQLSetPos function with an fOption argument of SQL_POS_UPDATE or SQL_POS_DELETE. The Desktop Database Drivers do not support positioned UPDATE or DELETE SQL statements. They do, however, support the SQLSetPos function.

In addition to SQL_POS_UPDATE and SQL_POS_DELETE, SQLSetPos can also be called with the SQL_POS_ADD, SQL_POS_POSITION, and SQL_POS_REFRESH fOption arguments. The Microsoft Excel and Text drivers support only SQL_POS_POSITION and SQL_POS_REFRESH, because the files are read-only.


Cursor Concurrency and Locking

When an application calls the SQLSetPos function to add, update, or delete a record, the Desktop Database Drivers can make sure that a record will stay in the same locked or unlocked state as it was in before SQLSetPos was called. To do this, SQLSetPos is called with an fLock argument of SQL_LOCK_NO_CHANGE. This uses the locking enforced by Microsoft Jet. To guarantee locking by Microsoft Jet, an application must create a keyset-driven cursor and specify SQL_CONCUR_LOCK for the SQL_CONCURRENCY fOption argument in SQLSetStmtOption.

Microsoft Jet does not support exclusive or explicit record-level locking. Therefore, the Desktop Database Drivers cannot lock the record specifically by using the SQL_LOCK_EXCLUSIVE fLock argument in SQLSetPos and then later unlock the record by using the SQL_LOCK_UNLOCK fLock argument. If a transaction is used (auto-commit mode off), setting the cursor-concurrency statement fOption argument to SQL_CONCUR_LOCK guarantees that the page-level locking supported by Microsoft Jet will be enforced for the duration of the transaction.

In addition to using Microsoft Jet locking (SQL_CONCURRENCY set to SQL_CONCUR_LOCK), you can also set the concurrency of a keyset-driven cursor to read-only (SQL_CONCUR_READ_ONLY), or optimistic concurrency in which record values are compared (SQL_CONCUR_VALUES). The Desktop Database Drivers do not support optimistic concurrency in which record versions are compared (SQL_CONCUR_ROWVER).

See Also For more information about page-level locking, see Chapter 10, “Managing Security.”


Support for Transactions

Only the Microsoft Access driver supports the use of transactions. The default transaction mechanism is auto-commit, in which each statement that can be contained in a transaction is, in effect, in its own transaction. A transaction is implicitly started when such an SQL statement is executed; upon completion of the statement, it’s committed automatically.

When a transaction is explicitly started (by calling SQLSetConnectOption with the SQL_AUTOCOMMIT fOption argument set to SQL_AUTOCOMMIT_OFF), the SQLTransact function is called to commit or roll back the transaction. The Microsoft Access driver supports multiple active statement handles on a single connection, so when SQLTransact is called, all statements on the connection are either committed or rolled back.

All open cursors on all hstmt arguments associated with the hdbc argument are closed when the transaction is committed or rolled back. SQLTransact leaves any hstmt argument present in a prepared state if the statement was prepared, or in an allocated state if it was executed directly. Closing all cursors can have unforeseen consequences. For example, suppose an application has two active statements within an explicit transaction: one statement in which an UPDATE statement was executed, and another statement in which a SELECT statement was executed and then SQLExtendedFetch called to return a recordset. If SQLTransact is called to commit the update, all the operations performed by the UPDATE statement on the first statement handle are committed (as expected), but in addition, the recordset generated by SQLExtendedFetch is deleted, because the cursor on the second statement handle is closed.

The Microsoft Access driver supports multiple active transactions. Because transactions are associated with a connection in ODBC, each transaction must be on a different connection (hdbc argument). Because nested transactions, which are supported natively by the Microsoft Jet engine, are not supported in ODBC, they are not available through the Desktop Database Drivers.

The driver supports only the read-committed transaction isolation level. Transactions can contain data definition language (DDL) statements and data manipulation language (DML) statements in any order.


Connection Pooling Support

The Desktop Database Drivers support connection pooling, but it should not be used in multithread environments. Problems can result if a connection is disconnected on a different thread than it was allocated and connected on. This situation can occur either when an application calls SQLDisconnect, or when a disconnect timeout occurs (as set by a call to SQLConfigDriver with the fRequest argument set to ODBC_CONFIG_DRIVER and the lpszArgs argument set to CPTimeout). An application can use the Desktop Database Drivers in a multithread environment, but connections should always be allocated, connected, and disconnected on the same thread.


Error Handling

The Desktop Database Drivers can return both ODBC and Microsoft Jet errors. A call to SQLError returns an ODBC SQLSTATE value, a Microsoft Jet error code (if applicable), and an error message. The error message can include an ODBC component identifier, a data-source identifier from Microsoft Jet, a text message from ODBC, and data-source-supplied text from Microsoft Jet.

The native Microsoft Jet error codes can be an important indicator of the nature of a problem. This is especially true when SQLSTATE S1000 (General error) is returned, because the ODBC error text for this SQLSTATE value does not convey any information about the problem. An application must rely on the Microsoft Jet error code in this case, and possibly the error message. Some Microsoft Jet error codes are returned for more than one type of error, in which case the error message must be used to determine the error that occurred.

The Desktop Database Drivers handle most ODBC errors, but the Driver Manager handles state transition errors, invalid arguments, connection option errors, and errors in calls to the driver. The ODBC component identifier in the error message text indicates the component that returns an error.

See Also For a list of Microsoft Jet error codes, see Appendix D, “Error Reference.”


Desktop Driver ODBC SQL vs. Microsoft Jet SQL

The ODBC SQL supported by the Desktop Database Drivers is very nearly the same as the SQL supported by Microsoft Jet, with only two major differences:

  • ODBCTL32.DLL translates strings passed by an application in ODBC escape syntax, such as date literals, procedure calls, scalar functions, and outer join calls, into strings that are compatible with Microsoft Jet. The Microsoft Access driver supports only ANSI string constants (with single quotation marks).

  • ODBCJT32.DLL translates pattern-matching characters from those that ODBC uses to those compatible with the Desktop Database Drivers and Microsoft Jet. The search character for a single character is an underscore ( _ ) in ODBC and a question mark (?) in Microsoft Jet; the search character for a sequence of zero or more characters is a percent sign (%) in ODBC and an asterisk (*) in Microsoft Jet. Microsoft Jet also translates wildcards embedded in dynamic parameter values in parameter queries.


SQL-92 Compliance

The Desktop Database Drivers and the underlying Microsoft Jet engine are not SQL-92 compliant. They do support many features that have been defined in SQL-92, but some features supported in the drivers are not supported in SQL-92. The following are the major differences between the two:

  • The Desktop Database Drivers use SQL that supports more powerful expressions than those specified by SQL-92.

  • Different rules apply to the Between…And operator (BETWEEN predicate).

  • The Desktop Database Drivers use SQL that supports different keywords than those specified by SQL-92.

The following SQL-92 features are not supported by Microsoft Jet SQL:

  • Security statements, such as GRANT and LOCK

  • DISTINCT with aggregate function references

The following features are enhancements in the SQL used by the Desktop Database Drivers, but are not specified by SQL-92:

  • The TRANSFORM statement, which provides support for crosstab queries

  • Additional aggregate functions (StDev and VarP)


SQL Implementation Specifics

This section describes how the Desktop Database Drivers implement SQL statements and clauses through Microsoft Jet, particularly when they are not SQL-92 compliant or there are limitations to their use.

Aggregate Functions

An aggregate function and a non-aggregate field (column) reference cannot both be used in the clauses of a single SQL statement.

Between…And Operator (BETWEEN Predicate)

ODBC SQL supports the same Between…And Operator syntax as ANSI, as follows:

expression1 Between expression2 And expression3

The semantics of this syntax are different for the Desktop Database Drivers and Microsoft Jet SQL. When the Desktop Database Drivers are used, this statement returns TRUE only if expression1 is greater than or equal to expression2, and expression1 is less than or equal to expression3. This is different from Microsoft Jet SQL, in which expression2 can be greater than expression3, so that the statement would return TRUE only if expression1 is greater than or equal to expression3, and expression1 is less than or equal to expression2.

Data Conversion

Each driver converts the C data types used by ODBC applications to the SQL data types used in Microsoft Jet, and vice versa. The drivers convert the data types in two steps.

  1. A generic conversion function in ODBCTL32.DLL converts the C data types to an SQL data type that is common to all drivers.

  2. Each driver’s code within ODBCJT32.DLL converts the common SQL data types to the driver-specific SQL data type used by Microsoft Jet.

ODBCTL32.DLL also contains a generic conversion function for parsing SQL strings that are passed in ODBC escape sequences, such as date literals, procedure calls, scalar functions, and outer join calls. Like data conversion, this is a two-step process, in which the generic conversion process performs parsing that is common to all drivers, and each driver performs its own additional parsing as required.

Datetime Values

For maximum interoperability, applications should pass datetime literals using the ODBC escape-clause syntax. Pass date literals using {d ‘value’}, where value is of the form “yyyy-mm-dd.” Pass time literals using {t ‘ value ‘}, where value is of the form “hh-mm-ss,” and timestamp literals using {ts ‘ value ‘}, where value is of the form “yyyy-mm-dd hh-mm-ss[.f…].” Datetime literals can also be delimited by the Microsoft Jet-specific datetime delimiter (“#”), which is not portable. If a datetime literal is not passed using the ODBC escape clause or the datetime delimiter, the literal will be treated as an arithmetic expression without raising a warning or error. For example, an application should represent the date “February 7, 1997” as {d ‘1997-02-07’} or #02-07-1997#. If the application submits only 02-07-1997, it will be evaluated as 2 minus 7 minus 1997.

Neither the SQL_DATE nor the SQL_TIMESTAMP data type can be converted to another data type (or itself) by the CONVERT function in escape-clause syntax.

The Desktop Database Drivers support adding or subtracting an integer from an SQL_DATE field. The integer specifies the number of days to add or subtract. Date arithmetic is not supported for adding (or subtracting) an SQL_DATE data type to (or from) another SQL_DATE data type.

DISTINCT Predicate

The Desktop Database Drivers do not support the DISTINCT predicate for binary data, Memo (Long Text) fields in Microsoft Access, or Memo fields in dBASE or FoxPro. The drivers also do not support the DISTINCT predicate in the set functions (Avg, Max, Min, and Sum).

Identifier Quote Character

The identifier quote character allows strings or characters that would normally be invalid to be used in identifiers. For the Desktop Database Drivers, a valid identifier is a string of no more than 64 characters. The first character of a valid identifier cannot be a space. Valid identifiers cannot include control characters or special characters (`|#*?[].!$\), or the reserved words listed in Appendix C of the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference. If an identifier (field or table name) violates any of these rules, the identifier must be enclosed in the identifier quote character to be used. For the Desktop Database Drivers, this character is a back quote (`). Note that for many other SQL processors, the identifier quote character is a forward quote character.

Like Operator (LIKE Predicate)

The Desktop Database Drivers support the Like operator, but do not support the use of escape clauses with a Like operator. A Like operator used in a procedure is supported only with literals. The drivers support ANSI Like pattern matching. If data in a Long Text (Memo) field is longer than 255 characters, the Like comparison is based only on the first 255 characters.

Multiple SQL Statements

Microsoft Jet can perform neither ad-hoc nor stored queries consisting of multiple SQL statements. Only single SQL statements are supported.

NULL/NOT NULL

The Desktop Database Drivers do not support the NOT NULL constraint in a CREATE TABLE statement. A Microsoft Access application can create a field that does not allow Null values by creating an index on a field with the DISALLOW NULL option. However, an application using the Desktop Database Drivers is unable to detect whether a field can contain Null values, because SQLColumns returns SQL_NULLABLE in the NULLABLE field of its result set for all fields (even if Null values are disallowed by virtue of an index), or for a table that has a field for which the Required property has been set to Yes.

ORDER BY Clause

The fields in the ORDER BY clause do not have to be in the SELECT statement’s field list. Expressions can be used in the ORDER BY clause. If a SELECT statement contains a GROUP BY clause and an ORDER BY clause, the ORDER BY clause can contain only a field in the result set or an expression that also appears in the GROUP BY clause.

OUTER JOIN Clauses

The Desktop Database Drivers support left and right outer joins, as well as inner joins. You can use the right table in a left outer join, or the left table in a right outer join, in an inner join. The Desktop Database Drivers do not support full or nested outer joins.

A SELECT statement can contain a list of OUTER JOIN clauses. The field names in the ON clause of the outer join do not have to be in the same order as their respective table names in the OUTER JOIN clause. The comparison operator in the ON clause can be any of the ODBC comparison operators.

To create an outer join, execute an ODBC OUTER JOIN statement in the ODBC escape clause, or use Microsoft Jet syntax. The ODBC OUTER JOIN syntax is as follows:

{oj tablename1 [LEFT or RIGHT] OUTER JOIN tablename2 ON
ú tablename1.field=tablename2.field}

See Also Appendix B, “SQL Reference,” shows the syntax of join statements in Microsoft Jet.

Procedure Invocation

To invoke procedures by using the Microsoft Access driver, call the SQLExecDirect or SQLPrepare function with the following escape-clause syntax

{CALL procedure-name [(parameter[,parameter]...)]}

where procedure-name specifies the name of a procedure stored on the data source and parameter specifies a procedure parameter. This escape clause acts as an indicator to the driver that it must translate the statement within it from ODBC standard syntax to DBMS-specific syntax.

To use a parameter query with the same syntax, pass markers (“?”) for parameters in the CALL statement and use SQLBindParameter to bind the parameter markers. The Desktop Database Drivers do not support expressions and constants as parameters to a called procedure. An input parameter must be a parameter marker; the drivers support only bound parameters. If a procedure name includes a hyphen, it must be delimited by the identifier quote character, a back quote (`).

ODBC Scalar Functions

This section describes the ODBC string functions, numeric functions, time and date functions, and data type conversion functions supported by the Desktop Database Drivers. System scalar functions are not supported. The Desktop Database Drivers do not support user-defined functions.

The Desktop Database Drivers support the following ODBC string functions:

ASCII | LENGTH | RTRIM
CHAR | LOCATE | SPACE
CONCAT | LTRIM | SUBSTRING
LCASE | RIGHT | UCASE
LEFT |  |

The Desktop Database Drivers support the following ODBC numeric functions:

ABS | FLOOR | SIGN
ATAN | LOG | SIN
CEILING | MOD | SQRT
COS | POWER | TAN
EXP | RAND |

The Desktop Database Drivers support the following ODBC time and date functions:

DAYOFMONTH | HOUR | SECOND
DAYOFWEEK | MINUTE | WEEK
DAYOFYEAR | MONTH | YEAR

The Microsoft Access, Microsoft Excel, and Text drivers support the following additional ODBC time and date functions:

CURDATE | NOW
CURTIME |

Explicit data-type conversions, using the CONVERT function in an ODBC escape sequence, can be performed on the following ODBC data types:

SQL_BINARY | SQL_NUMERIC
SQL_CHAR | SQL_REAL
SQL_DATE | SQL_SMALLINT
SQL_DOUBLE | SQL_TIME
SQL_FLOAT | SQL_TIMESTAMP
SQL_INTEGER | SQL_TINYINT
SQL_LONGVARBINARY | SQL_VARBINARY
SQL_LONGVARCHAR | SQL_VARCHAR

Explicit data-type conversions cannot be performed on the following ODBC data types:

SQL_BIGINT

SQL_BIT

SQL_DECIMAL

See Also For more information about the ODBC scalar functions that Microsoft Jet supports, see Appendix E, “Scalar Functions,” of the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference.

Search Conditions

The maximum number of fields in a WHERE clause is 40. The maximum number of search conditions in a HAVING clause is 40.


Microsoft Jet Feature Support

The following paragraphs describe Microsoft Jet features that are exposed through the Desktop Database Drivers.


Heterogeneous Joins and Linked Tables Support

You can link a table in a different data source to a Microsoft Access database. The table cannot be linked programmatically through the Microsoft Access Desktop Database Driver, however. It must be linked from within Microsoft Access or through Data Access Objects (DAO).

After a table has been linked, you can access its data through the Microsoft Access driver. You can use the linked table in a join with other tables, including other linked tables, in the Microsoft Access database. So it’s possible, for example, to join a table in a Microsoft Access database with another table in a FoxPro database. Such a join between tables created in different DBMSs is called a heterogeneous join. It’s also possible to create a heterogeneous join in a stored query by using the Microsoft Access user interface. Such a stored query can be accessed as a view in a SELECT statement. Microsoft Jet provides transparent access to the linked table, regardless of the data’s location or format. As a result, a linked table, or a heterogeneous join, looks just like any other Microsoft Access table.


Updatable Query Support

You can call queries as views by using a FROM clause in a SELECT statement. The restrictions on updating views depend on the query on which the view is based. If a query is updatable by using SQLSetPos, the view that is based on the query is also updatable. Updatability is not affected by how the SQL query is executed. For example, it does not matter whether the SQL query is executed by including an SQL string in a call to the SQLExecDirect function, or by executing a stored query.

Queries based on a single table and queries based on tables with a one-to-one relationship are updatable. Queries based on tables with a one-to-many relationship are usually updatable. The following queries are not updatable:

  • Aggregation queries (for example, Sum or Count)

  • UNION queries

  • Queries whose UniqueValues property is set to Yes

  • Queries that include an attached ODBC table with no unique index or a Paradox table without a primary key

  • Queries (or underlying tables) for which update or delete permission isn’t granted

  • Queries that include more than one table or query, where the tables or queries are not joined by a join line in Microsoft Access query Design view

  • Queries that include a calculated field

  • Queries that include a read-only field

  • Queries that include a field in a record that has been deleted or locked by another user

See Also For more information about queries, see Chapter 4, “Queries.”


Updatable Join Support

The Desktop Database Drivers support updatable joins, meaning that updates made to the table created by the join are reflected in the base table and updates made to the base table are also reflected in the table created by the join. However, if you open a cursor on a query that is based on a one-to-many relationship, you may not be able to update its underlying tables by calling SQLSetPos. The following types of fields may not be modifiable:

  • New records, if the “many” side’s join field does not appear in the SELECT list. To add new records, add the join field from the “many” side to your query.

  • The join field from the “many” side, after data on the “one” side has been updated. Once you save the record, you can make changes to the “many” side’s join field.

  • A blank field in the many-only table of an OUTER JOIN if the joined “one” side is also empty.

  • A blank field on the “one” side of an OUTER JOIN. This can be done only on the many-only table.

  • New records, if the entire unique key of an attached ODBC table is not output. To allow insertions, select all primary key fields of the tables.


Update Delays

You can set up a data source with a page timeout that retains a page in its buffer for a period before it is removed. Specify a page timeout either with the Page Timeout option in the Driver Options section of the Data Source Setup dialog box, or through the PageTimeout keyword in a call to SQLConfigDataSource (see the next section, “Creating .Mdb Files Programmatically”). A page timeout can cause undesirable delays in updates. To specify that there be no delay in updating when you are using the Microsoft Access Desktop Database Driver, set the SafeTransactions data source setup option to 1, and set PageTimeout to 0.


Creating .Mdb Files Programmatically

Use the SQLConfigDataSource function to add, modify, or delete a data source dynamically. If you call SQLConfigDataSource with a window handle, data source setup is performed interactively through the Setup dialog box. If you call the function with a NULL window handle, the data source is set up by using keywords that are contained in the lpszAttributes argument.

SQLConfigDataSource takes as arguments the type of request (add, configure, or remove), the name of the driver, and a list of keyword-value pairs. The following keyword-value pairs are used to add, modify, or delete a file.

Keyword | Description
DBQ | The name of the database file
DEFAULTDIR | The path to the database file for Microsoft Access, or the directory for the other drivers
DESCRIPTION | A description of the data in the data source
DRIVER | The path specification for the driver
DRIVERID | An integer ID for the driver (“25” for Microsoft Access)
FIL | The file type (Microsoft Access)
JETINIPATH | The path to the Microsoft Jet initialization file
PWD | A password
READONLY | Indicates whether the file is read-only (TRUE) or not (FALSE)
SYSTEMDB | The path to the system database file
UID | The user ID name used for logging on

See Also For a complete specification of the keywords, see the Microsoft ODBC Desktop Database Drivers Help file (ODBCJET.HLP).

To perform the following operations on a Microsoft Access database file, or to set the following settings, include the appropriate keyword in the call to SQLConfigDataSource.

Keyword | Description
COMPACT_DB | Compacts data. See the “Database Compaction ” section later in this chapter.
CREATE_DB | Creates a Microsoft Access database file. (If the path contains a space, the entire path must be enclosed in double quotation marks.)
CREATE_SYSDB | Creates a system database. This keyword takes the database name and the desired sort order as arguments.
CREATE_V2DB | Creates a database that is compatible with Microsoft Access 2.0. This keyword takes the database name and the desired sort order as arguments.
EXCLUSIVE | Opens the database so that it can be accessed by only one user at a time (EXCLUSIVE=TRUE), or by more than one user at a time (EXCLUSIVE=FALSE).
IMPLICITCOMMIT SYNC | Indicates whether implicit commits are performed asynchronously. A value of YES (the default) means that the driver will wait for commits in an implicit transaction to be completed.
MAXBUFFERSIZE | Indicates the size of the internal buffer, in kilobytes, that is used to transfer data to and from the disk. Any integer value divisible by 256 can be used. The default is 512 KB.
MAXSCANROWS | Indicates the number of records (rows) to be scanned when a field’s data type is set based upon existing data. A number from 1 to 16 can be entered; a value of 0 means all records will be scanned.
PAGETIMEOUT | Indicates the period of time, in tenths of a second, that a page (if not used) remains in the buffer before being removed.
REPAIR_DB | Repairs a file damaged during the commit process. See the “Database Repair ” section later in this chapter.
SAFETRANSACTIONS | If this is 1, it indicates that transactions will be committed immediately, without the delay specified by the PAGETIMEOUT value. If this is 0, it indicates that transactions will be truly committed when the operating system flushes the data to disk.
THREADS | The number of background threads for the engine to use.
USERCOMMITSYNC | Indicates whether user-defined transactions are performed asynchronously. A value of YES (the default) means that the driver will wait for commits in a user-defined transaction to be completed.

All of the previously listed values are used for Microsoft Jet files, but this is not a complete list of the keywords that can be used in a call to SQLConfigDataSource. Other keywords can be used for other drivers.


Database Compaction

To programmatically perform data compaction on a database file, include COMPACT_DB=source db; dest db; sort order in the list of attributes (lpszAttributes argument) of the SQLConfigDataSource function. In this function, source db is the full path to the Microsoft Access database to be compacted and dest db is the full path for the compacted Microsoft Access database. When compacting a password-protected file, you must enter a password and user ID in the list of attributes for SQLConfigDataSource.

This operation runs the standard Microsoft Access compaction process on the database. To perform the same process through the Setup dialog box for a Microsoft Access data source, double-click the ODBC icon in the Control Panel to access the ODBC Data Source Administrator.


Database Repair

When you are using the Microsoft Access driver, if a client failure occurs during the commit process (after SQLTransact has been issued, but before the function returns), you may need to repair the affected database. You can repair a database either through the Setup dialog box for a Microsoft Access data source, or programmatically by using the SQLConfigDataSource function. When repairing a password-protected file, you must enter a password and a user ID in the Set Advanced Options dialog box displayed from the ODBC Setup dialog box (or in the list of attributes for SQLConfigDataSource, if you are repairing the database programmatically).

These operations run the standard Microsoft Jet repair process on the database. Data in Microsoft Access databases could be affected by this operation.

To repair the database programmatically, include REPAIR_DB=pathname in the list of attributes (lpszAttributes argument) of the SQLConfigDataSource function. In this function, pathname is the full path to the Microsoft Access database.


Unsupported Microsoft Jet Features

The Desktop Database Drivers expose as much Microsoft Jet functionality as possible, but they cannot expose some Microsoft Access features and Microsoft Jet functionality. As a desktop database, Microsoft Access has many features, particularly user-interface features, that cannot be exposed through the ODBC interface. The following are not supported:

  • Microsoft Access forms and reports

  • VBA or Access Basic or DAO functions. The drivers have no way to call these functions. Queries cannot contain VBA or Access Basic functions.

  • Microsoft Access macros

  • Creation of referential integrity. The Desktop Database Drivers do not support the Integrity Enhancement Facility (IEF) syntax necessary for referential integrity.

  • Creation of secure Microsoft Access databases with the GRANT statement. The Desktop Database Drivers can use the security if it’s set up, but security cannot be set up through the drivers.

  • Administration of replication.

See Also For more information about SQL grammar, see Appendix C of the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference.


Performance Considerations

This section discusses ways to enhance the performance of the Desktop Database Drivers. It discusses both ODBC API issues and Microsoft Foundation Classes (MFC) Database programming considerations for improving the speed of an application that uses one of the Desktop Database Drivers.

The Desktop Database Drivers are feature-rich, high-performance ODBC drivers. They offer multiple ways to perform insertions, deletions, and updates. Carefully designing applications to use the optimal methods whenever possible will help maximize the performance of an ODBC application.


Insertion, Deletion, and Update Performance

Several options are available for performing insertions, deletions, and updates. To perform these operations, you can execute an SQL statement or call SQLSetPos. If you perform them with an SQL statement, you can call SQLExecDirect, or prepare the statement and call SQLExecute. You can include multiple statements in a transaction. This section describes the relative performance of these methods.


Prepared Execution

You can execute queries in one of two ways:

  • Call SQLExecDirect with the SQL statement.

  • Call SQLPrepare to prepare the query, and then call SQLExecute to execute the prepared query.

SQLExecDirect is a better choice if you execute the query only once. If you execute the query multiple times, however, (or if you run a parameter query multiple times), preparing it with SQLPrepare, and then executing it with SQLExecute, is considerably faster. Every time you execute a query with SQLExecDirect, Microsoft Jet parses the query, generates an execution plan, and then executes the query. When you use SQLPrepare, the query is parsed just once. Over the course of multiple queries, this can significantly enhance performance.


SQLSetPos

The SQLSetPos function is much faster for performing insertion, deletion, or update operations than completing the same operation with an SQL statement. One reason is that there is no SQL statement to parse. Another reason is that the catalog need be checked just once for multiple calls to SQLSetPos, whereas the catalog is checked each time an SQL statement is executed.

The Cursor Library can also provide static updatable cursors. Desktop Database Drivers 1.0 required the Cursor Library for scrollable cursor support, but later drivers use the native scrollable cursors in Microsoft Jet. Because the Cursor Library incurs a lot of overhead, it is much slower to do a positioned update than to use SQLSetPos with an fOption argument of SQL_UPDATE. The Cursor Library must parse the SELECT statement it uses to generate the data in its cache, whereas Microsoft Jet stores parsed strings. To update the data, Microsoft Jet must perform a catalog lookup to open the table that is being updated, and the Cursor Library must create a searched UPDATE statement with a WHERE clause that may have to search through all the data in the temporary file. Even then, the appropriate data may not be found, because the buffered data represents a recordset produced by a fetch, which may not accurately represent the base table. Microsoft Jet cursors can directly update the base table, which is much faster.


Transactions

Use transactions when executing multiple insertion, deletion, or update operations to enhance performance. Call SQLSetConnectOption on the connection handle (hdbc) with SQL_AUTOCOMMIT set to SQL_AUTOCOMMIT_OFF to turn auto-commit mode off. When the driver uses auto-commit mode and SQL statements, it must make sure that each individual INSERT, DELETE, or UPDATE statement is flushed to the safe store (usually to disk). If the statements are grouped in a transaction, the driver can delay the writes to disk, and then perform only one write during commit time. Because disk I/O takes a relatively long time, turning auto-commit mode off enhances performance.


Query Performance

The following table shows the relative performance of the different query methods. To make the comparison more concrete, as a test, each of these methods performed 100 insertions into a Microsoft Access 2.0 table that had five fields of text data. Note that these are not exhaustive benchmarking results. They are provided to illustrate the relative merits of these methods. The methods are listed in order of increasing performance.

Method | Time
100 SQLExecDirect insertions in auto-commit mode | 5457 ms
100 SQLExecDirect insertions in a single transaction | 4756 ms
100 insertions performed by calling SQLPrepare/SQLExecute in auto-commit mode | 3515 ms
100 insertions performed by calling SQLPrepare/SQLExecute in a single transaction | 2994 ms
100 SQLSetPos/SQL_ADD insertions in auto-commit mode | 831 ms
100 SQLSetPos/SQL_ADD insertions in a single transaction | 721 ms

Adding data by calling SQLSetPos in a transaction results in 139 insertions per second. Calling SQLExecDirect in auto-commit mode to execute an INSERT SQL statement results in 18 insertions per second. Although the results specifically apply to insertions, the concepts apply equally well to deletions and updates.


Using Stored Queries

The Microsoft Access Desktop Database Driver supports using stored queries in a Microsoft Access database. Stored queries cannot be created by using the Microsoft Access driver; they must be created by using Microsoft Access or Visual Basic. Use stored queries as follows with the Desktop Database Drivers:

  • You can treat stored queries as views, so that you can perform a SELECT statement on the query just as you would from a table. This method is easy-to-use and intuitive, but it is not, however, the best method as far as performance is concerned, because the SELECT statement that invokes the query incurs the overhead of syntax checking and parsing.

  • You can treat stored queries as stored procedures. Use the {CALL query-name...} syntax to call the stored query (see the “Procedure Invocation” section earlier in this chapter). This is faster than treating the stored query as a view because syntax checking for the SELECT statement is bypassed and the stored query is directly invoked. Use parameter markers with this method to execute parameter queries. You cannot use parameter markers when a stored query is treated as a view.


Using Microsoft Foundation Classes

The CRecordset C++ class provided with Microsoft Foundation Classes (MFC) uses the SQLPrepare function to prepare queries before execution. You can use the CRecordset::Requery function to re-execute the query for the CRecordset class without requiring the ODBC driver to re-parse the SQL statement, because the statement has already been prepared.

By default, MFC library classes load the Cursor Library. The Cursor Library permits updatable snapshots. To get updatable recordsets, but not use the Cursor Library, use the dynaset Crecordset class. To specify it, pass CRecordset::dynaset for the first argument of CRecordset::Open. With the 32-bit MFC library database classes, it’s not enough to pass CRecordset::dynaset to CRecordset::Open. You must pass FALSE for the last argument of CDatabase::Open. This prevents the Cursor Library from loading. The code looks like this:

CDatabase db;
db.Open("DataSourceName",FALSE,FALSE,"ODBC;",FALSE);
CYourRecordset rs(&db);
rs.Open(CRecordset::dynaset);

To use dynasets, the DYNSET.EXE code, or 32-bit classes use the SQLSetPos functionality of the Microsoft Access Desktop Database Driver when you are performing updates, deletions, or insertions. As mentioned previously, this greatly increases the speed of an application.

The Microsoft Access Desktop Database Driver does not support MFC transactions. The MFC classes require ODBC drivers to support Recordset object cursor preservation across rollbacks and commits of transactions.

The Microsoft Access Desktop Database Driver does not guarantee this cursor preservation; however, you can use transactions if you requery after any transaction so that the cursor is restored to the first record in the Recordset object. Force the m_bTransactions member variable of the Database object to True before using the BeginTrans method. The code could look like this:

class CTransactDatabase: public CDatabase
{
    public:
    void SetTransactions(){ m_bTransactions=TRUE;}
};
.
.
.
CTransactDatabase db;
db.Open("SomeDataSourceName",FALSE,FALSE,"ODBC;",FALSE);
db.SetTransactions();
db.BeginTrans();
CRecordSet rs(&db);
rs.Open(CRecordset::dynaset);
.
// Manipulate data.
.
db.CommitTrans(); // or db.Rollback()
::SQLFreeStmt(rs.m_hstmt, SQL_CLOSE);
db.BeginTrans();
rs.Requery();
.
// Manipulate data.
.
db.CommitTrans() // or db.Rollback()
rs.Close();
db.Close();
.

See Also For more information, see the documentation for SQLGetInfo and the SQL_CURSOR_COMMIT_BEHAVIOR and SQL_CURSOR_ROLLBACK_
BEHAVIOR parameters in the Microsoft ODBC 3.0 Software Development Kit and Programmer’s Reference.


Chapter 13 Optimizing Performance

The Microsoft Jet database engine provides a variety of administrative services designed keep your database running smoothly. This chapter explains Microsoft Jet configuration settings, and how to repair and compact databases. It also covers performance enhancements in Microsoft Jet 3.5 and unsupported tuning functions.

Configuration Using the Registry

Repairing Databases

Compacting Databases

Performance Enhancements in Microsoft Jet 3.5

Unsupported Tuning Functions

Using the Code Examples in This Chapter

You can use the code examples in this chapter to help you understand the concepts discussed, or you can modify them and use them in your own applications.

The code examples are located in the JetBook\Samples subfolder on the companion CD-ROM. The code examples for Microsoft Access 97 are in JetSamples.mdb, and the corresponding code examples for Microsoft Visual Basic version 5.0 and other applications that support Visual Basic for Applications are referenced in JetSamples.vbp. Both JetSamples.mdb and JetSamples.vbp use tables and queries in NorthwindTables.mdb, also located in the JetBook\Samples subfolder.

To use the code examples, copy the sample files to your hard disk. Be sure to copy NorthwindTables.mdb as well so that you can use the code examples to work with data.

See Also For more information about copying and using the code examples from the companion CD-ROM, see “Using the Companion CD-ROM” in the Preface.


Configuration Using the Registry

The registry is an operating system database that stores configuration information used by Windows and the applications it runs. There are a variety of settings stored in the Windows Registry that control the behavior of the Microsoft Jet database engine.

Configuration in Earlier Versions of Microsoft Jet

Prior to version 3.0, Microsoft Jet used the Windows 3.1 convention of text initialization (.ini) files. Unlike other applications that often have their own .ini files, Microsoft Jet relied on its host application to make sure that the .ini file was created and maintained. For example, when used by Microsoft Access, Microsoft Jet would use the settings in the Microsoft Access .ini file (Msaccess.ini for Microsoft Access 1.0, and Msacc20.ini for Microsoft Access version 2.0). For Visual Basic 3.0, application developers were expected to manually create an .ini file for their applications.

You can use the Registry Editor (Regedit.exe for Microsoft Windows 95 and Microsoft Windows NT Workstation 4.0, and Regedt32.exe for Microsoft Windows NT Workstation 3.51) to view and change registry settings.

Note You should change registry settings only if you have an advanced understanding of them. What appears to be a simple change may have unwanted effects in your application. Also, it’s always a good idea to make a backup copy of your registry contents before making modifications. You can do this with the Export Registry File command (Registry menu) in the Registry Editor for Windows 95 and Windows NT Workstation 4.0, or the Unload Hive (Registry menu) command in the Registry Editor for Windows NT Workstation 3.51.

When Microsoft Jet is installed on your system, it’s typically done through a host’s installation program, such as the Microsoft Access Setup program. Microsoft Jet automatically adds default settings to the registry when requested to do so by its host installation program. These default settings are used whenever the program calling Microsoft Jet doesn’t supply explicit settings.

Note Unlike Microsoft Jet 3.0, which does not add engine-specific default settings and values to the Engines key of the Windows Registry, Microsoft Jet 3.5 does add settings and values.

Unlike Microsoft Jet 2.x and earlier versions, Microsoft Jet 3.5 can apply different settings for each instance of the engine.

Microsoft Jet 3.5 stores its settings in the \HKEY_LOCAL_MACHINE\SOFTWARE
\Microsoft\Jet\3.5 key of the Windows Registry. Figure 13.1 shows the Microsoft Jet 3.5 settings in the Registry Editor.

Note In this manual, the folders in the left pane of the Registry Editor are called registry keys, the entries in the Name column are called settings, and the entries in the Data column are called values.

Figure 13.1 The Microsoft Jet registry keys with sample settings and values

The Engines key contains settings that control how Microsoft Jet 3.5 runs. The ISAM Formats key contains settings that allow programs using Microsoft Jet to determine which data formats are available. The settings in the Engines key are discussed later in this chapter.

See Also For more information about the settings that Microsoft Jet 3.5 installs into the Windows Registry, see Appendix C, “Registry Settings.”


Repairing Databases

Accidents happen. Users reboot workstations, power goes out, and disk drives go bad. If any of these occur while Microsoft Jet is writing data, then Microsoft Jet may mark the database as potentially corrupt.

When Microsoft Jet begins a write operation, it sets a flag, and resets the flag when the operation is complete. If a write operation is interrupted, the flag remains set. When you try to open that database again, Microsoft Jet determines that the flag is set and reports that the database is corrupt. In most cases, the data in the database is not actually corrupt, but the set flag alerts Microsoft Jet that corruption may have occurred.

You can use the RepairDatabase method to check for corruption. This method takes an inventory of the objects in the database, checking each one for integrity. If no corruption has occurred, the RepairDatabase method resets the write flag. If corruption has occurred, then Microsoft Jet attempts to repair the database.

Important Before using the RepairDatabase method, take the following items into consideration:

  • Always make a back-up copy of the database.

  • Do not use the RepairDatabase method on a replicated database. If you do, the database is no longer replicable and you will not be able to synchronize it with other replicas in the replica set. If you suspect a replica is corrupted, delete the replica and create a new replica from another replica in the set. The RepairDatabase method modifies certain system tables in the .mdb file. If the database were to remain a member of the replica set after this modification of the system tables, the other replicas in the set would become corrupt. The RepairDatabase method prevents this possibility by stopping the database from being replicable, and thus preventing synchronizations with other replicas.

  • In Microsoft Jet 2.x, it was recommended that you repair a database before compacting it. This is because the RepairDatabase method and the corresponding menu command in that version can recover truncated records of data. However, this is not true for Microsoft Jet 3.0 and 3.5 file formats. For this reason, you should only repair a Microsoft Jet 3.0 or 3.5 database if an error message indicates that this is necessary.

The syntax for the RepairDatabase method is:

DBEngine.RepairDatabase databasename

In this syntax, databasename is the path and file name of the database to be repaired.

The following code attempts to open a database. If the database is marked as corrupt, the procedure calls the RepairDatabase method. In this example, strDbPath is the path to the database.

Function RepairAndOpen(strDbPath As String) As Boolean
    Dim dbs As Database
    Const conDatabaseCorrupt As Integer = 3049

    On Error Resume Next
    Set dbs = OpenDatabase(strDbPath)
    If Err.Number <> 0 Then
        ' If it is a corruption error, then attempt to repair.
        If Err = conDatabaseCorrupt Then
            MsgBox "Database is corrupt. Attempting repair..."
            DBEngine.RepairDatabase strDbPath
            Err.Clear
            ' Attempt to open database again.
            Set dbs = OpenDatabase(strDbPath)
            ' Check whether another error occurred.
            If Err.Number <> 0 Then
                MsgBox "Database cannot be opened." & vbCrLf _
                    & Err.Number & ": " & Err.Description
                RepairAndOpen = False
                Exit Function
            End If
        Else
            MsgBox "Database cannot be opened." & vbCrLf _
                & Err.Number & ": " & Err.Description
            RepairAndOpen = False
            Exit Function
        End If
    End If

    dbs.Close
    Set dbs = Nothing
    RepairAndOpen = True	Err
End Function

Note In Microsoft Jet 3.0, using the RepairDatabase method or command before compacting the database could result in a database that could no longer be opened. This problem, which happens only rarely due to duplicate indexes on a database’s system tables, has been resolved in Microsoft Jet 3.5. An updated release of Microsoft Jet 3.0 that corrects this problem is available on http://www.microsoft.com/kb/articles/q151/1/86.htm for users of Microsoft Access 95 and other programs that use Microsoft Jet 3.0.


Compacting Databases

With use, the internal structure of a database can become fragmented, leading to wasted disk space — and to poor performance if the fragmentation is excessive.

Compacting a database:

  • Reorganizes a table’s pages so they reside in adjacent database pages. This improves performance because the table is no longer fragmented across the database.

  • Reclaims unused space created by object and record deletions. When objects or records are deleted from the database, the space they occupied is marked as available for new additions to the database. However, the size of the database never shrinks unless the database is compacted. For databases in which objects and records are frequently added, deleted, and updated, you should compact frequently.

  • Resets incrementing AutoNumber fields so the next value allocated will be one more than the highest value in the remaining records. For example, if all records in the database have been deleted, after compacting the database the value in the AutoNumber field will be 1 when the next record is added. If the highest remaining AutoNumber value in the database is 50, after compacting the value will be 51 when the next record is added. Note that this is true even if records containing values higher than 50 were added previously but were deleted prior to compacting.

  • Regenerates the table statistics used in the query optimization process. These statistics can become out-of-date over time, typically if transactions were rolled back, or if the database was not properly closed due to power loss or failure to completely exit the program using Microsoft Jet before turning the computer off.

  • Flags all queries so that they will be recompiled the next time the query is executed. This is important because database statistics can change and a previously compiled query may have an inaccurate query plan.

Before you can compact a database, the following conditions must be met:

  • The user compacting the database must be logged on using an account that has Modify Design or Administer security permissions for all tables in the database. For more information on Microsoft Jet security, see Chapter 10, “Managing Security.”

  • Sufficient disk space must exist for both the original and compacted versions of the database, even if the database is being compacted through the Microsoft Access user interface using the same file name. The compacted database is renamed as the original database only when compacting is successful.

  • Other users must not have the database open. When a database is compacted, it has to be open exclusively by Microsoft Jet to prevent any users from accessing and modifying the database.

Note that you cannot compact the database that is currently open in Microsoft Access. You must close the database and compact it either from the Microsoft Access user interface or from another Visual Basic project.

Determining whether the current database is open exclusively is not simply a matter of trying to open the database. If you have the database open exclusively and try to open it again, you won’t receive a run-time error as you may expect. Actually, you can open a database as many times as you want to, exclusively or not, with the same DBEngine object.

To determine whether the current database is open exclusively, you can try to open the database using the PrivDBEngine object. For more information, see “Determining Whether the Current Database Is Open Exclusively” in Chapter 2, “Introducing Data Access Objects.”

You can compact a Microsoft Jet database using the CompactDatabase method of the DBEngine object. Although the primary use of this method is to compact a database, it supports a variety of other operations with the options argument. The syntax of the CompactDatabase method is:

DBEngine.CompactDatabase olddb, newdb [,locale [,options[,password]]]

In this syntax:

  • olddb is the name of the database you want to compact.

  • newdb is the name of the compacted database you want to create.

  • locale is a string expression used to specify collating order (the language-specific sort order) for newdb.

  • options is a constant or combination of constants that specify one or more options.

  • password is a string expression that contains a password if the database is password-protected.

You can’t use the newdb argument to specify the same database file as the olddb argument.


Setting the Collating Order

A collating order, specified by the locale argument of the CompactDatabase method, determines how Microsoft Jet sorts the data in your database. This is important because the character sets in different languages may require a language-specific sort order. If the wrong collating order is specified, data may not sort properly. You can specify a collating order when you create the database, or when you compact it. If you do not want to change the database’s collating order during the compacting process, leave the locale argument blank. The following table lists the constants you can use in the locale argument to specify various collating orders.

Constant | Collating order
dbLangGeneral | English, German, French, Portuguese, Italian, and Modern Spanish
dbLangArabic | Arabic
dbLangChineseSimplified | Simplified Chinese
dbLangChineseTraditional | Traditional Chinese
dbLangCyrillic | Russian
dbLangCzech | Czech
dbLangDutch | Dutch
dbLangGreek | Greek
dbLangHebrew | Hebrew
dbLangHungarian | Hungarian
dbLangIcelandic | Icelandic
dbLangJapanese | Japanese
dbLangKorean | Korean
dbLangNordic | Nordic languages (Microsoft Jet 1.0 only)
dbLangNorwDan | Norwegian and Danish
dbLangPolish | Polish
dbLangSlovenian | Slovenian
dbLangSpanish | Traditional Spanish
dbLangSwedFin | Swedish and Finnish
dbLangThai | Thai
dbLangTurkish | Turkish


Setting Options

You use the options argument of the CompactDatabase method to specify various operations for Microsoft Jet to perform as part of the compact process. Using the options argument, you can encrypt and decrypt database files, and specify the version of the data format for the compacted database. You cannot use the CompactDatabase method to convert a database to a previous version.

Caution Because the CompactDatabase method doesn’t convert Microsoft Access-specific objects such as forms, reports, macros, and modules, you shouldn’t use the CompactDatabase method to convert a database containing such objects. To convert a database containing Microsoft Access objects, open the database in Microsoft Access and use the Convert Database command (Tools menu, Database Utilities submenu).

The following table lists the constants you can use in the options argument.

Constant | Description
dbEncrypt | Encrypts the database while compacting.
dbDecrypt | Decrypts the database while compacting.
dbVersion10 | Creates a database that uses the Microsoft Jet 1.0 file format while compacting.
dbVersion11 | Creates a database that uses the Microsoft Jet 1.1 file format while compacting.
dbVersion20 | Creates a database that uses the Microsoft Jet 2.0 file format while compacting.
dbVersion30 | Creates a database that uses the Microsoft Jet 3.0 file format while compacting. This option also applies to databases created with Microsoft Jet 3.5.

If you omit an encryption constant or include both dbDecrypt and dbEncrypt, the new database will have the same encryption as the old database.

See Also For more information on database encryption and Microsoft Jet security features, see Chapter 10, “Managing Security.”

The following code example uses the CompactDatabase method to compact a database to a temporary file and then renames that temporary file to the original name if the compacting process is successful.

Note that this code calls the CanOpenDbExclusively function defined in Chapter 2, “Introducing Data Access Objects.” This function determines whether the database can be open exclusively for compacting.

Function CompactDb(strDbPath As String) As Boolean
    Dim dbs As Database
    Dim intLength As Integer
    Dim varPosition As Variant
    Dim strDbTemp As String, strDbCompacted As String
    Dim strDbBackup As String
    Dim strMsg As String
    Const conPermissionDenied As Integer = 70

    On Error GoTo Err_CompactDb
    ' Initialize string for message.
    strMsg = "Database " & strDbPath & " cannot be opened exclusively. " _
        & "The database may have already been opened by you or another user."

    ' Compact the database to a temporary file.
    intLength = Len(strDbPath)
    varPosition = InStr(strDbPath, ".mdb")
    If varPosition > 0 Then
        strDbTemp = Left(strDbPath, varPosition - 1)

        ' Create backup file before compacting.
        strDbBackup = strDbTemp & ".bak"
        FileCopy strDbPath, strDbBackup

        ' Check whether database can be opened exclusively.
        ' This line calls a function defined in Chapter 2.
        If Not CanOpenDbExclusively(strDbPath) Then
            MsgBox strMsg
            GoTo Exit_CompactDb
        End If

        ' Compact to new file.
        strDbCompacted = strDbTemp & "Compacted.mdb"
        DBEngine.CompactDatabase strDbPath, strDbCompacted

        ' Delete uncompacted database.
        Kill strDbPath

        ' Rename compacted database to original name.
        Name strDbCompacted As strDbPath
    End If
    CompactDb = True

Exit_CompactDb:
    On Error Resume Next
    dbs.Close
    Set dbs = Nothing
    Exit Function

Err_CompactDb:
    If Err = conPermissionDenied Then
        MsgBox strMsg
    Else
        MsgBox "Error " & Err & ": " & vbCrLf & Err.Description
    End If
    CompactDb = False
    Resume Exit_CompactDb
End Function

Tip If your hard disk is badly fragmented, it’s a good idea to defragment it using a program such as Microsoft Disk Defragmenter before compacting the database. This allows for the data to be written not only sequentially in the database, but also sequentially on disk should space allow.


Important Considerations When Compacting Replicated Databases

Replicated databases make extensive use of temporary space, especially when many design changes are made to an application. Just as with non-replicated databases, compacting regularly reduces file size and improves performance.

For optimal results, compact a replicated database twice. Compacting a replica the first time reclaims some space and flags other space as available for reclaiming later. Compacting a second time reclaims all available space. Compacting more than twice doesn’t provide additional benefits.

When Microsoft Jet opens a replica, it stores the name and location of the file in the database. The next time Microsoft Jet opens the replica, it checks to see if the file has the same name and location. If so, then the file opens normally. If not, Microsoft Jet gives the replica a new ReplicaID property value and it will be treated as an additional replica in the replica set. Additionally, if the file was the Design Master, Microsoft Jet removes the Design Master flag. This prevents two replicas from having the same ReplicaID property value and keeps a replica set from having two Design Masters.

To prevent this, when using the CompactDatabase method with a replicated database, make sure your code renames the new compacted .mdb file to the original file name before opening it.


Performance Enhancements in Microsoft Jet 3.5

Microsoft Jet 3.5 includes features that significantly improve performance. The following sections discuss the performance enhancements of Microsoft Jet 3.5, as outlined below:

New configurable performance settings and changes to existing settings:

  • Multithreading

  • Dynamic memory usage

  • Asynchronous writes

  • Cache refreshing

  • Controlling lock retries

  • Setting the maximum number of locks

  • Controlling long value page recycling

  • Using the SetOption method to control Microsoft Jet registry settings

  • Programmatically refreshing the Microsoft Jet cache

Key internal performance enhancements:

  • Reduced locking

  • Improved Rushmore algorithms

  • Removal of implicit transactions for SQL data manipulation language (DML) statements

  • Reduced index navigation

  • Improved cache reuse

  • Persistent connections to security system tables

  • Reduced flushing of the cache

  • Larger allocation by extents

  • Improved performance with DELETE statements

  • Faster generation of query plans

  • Explicit registry settings and values


Configurable Performance Settings

Settings in the Engines subkey of the Windows Registry control how Microsoft Jet uses memory and performs other aspects of its operations. Microsoft Jet automatically provides default settings that usually give the best performance for most common database operations. However, depending on what kind of operations your application performs and how much memory is available at any given time, you may be able to improve performance by adjusting Microsoft Jet registry settings.

Keep in mind that the optimum value for a setting can change from computer to computer, and can change depending on how much memory is available, the kind of operation the user is performing, or the level of activity in the database at any given time. Using a system and working environment that’s as much like your typical user’s system as possible, experiment with these settings to find out what works best.

If you decide to change any of the Microsoft Jet registry settings in the Windows Registry, you have four options. You can:

  • Use the SetOption method in code to temporarily modify Microsoft Jet registry settings at run time. This is the recommended method for adjusting the Microsoft Jet registry settings in the Engines subkey. For more information on using the SetOption method, see “Using the SetOption Method to Control Registry Settings” later in this chapter.

  • Create a custom set of registry keys, called a user profile, that contains settings to override default registry settings.

    To have these settings affect only a particular Microsoft Access database application or session of Microsoft Access, use the /profile command-line option when starting Microsoft Access to specify the user profile you want to use. If you are creating a Microsoft Access run-time application using the Setup Wizard available in Microsoft Office 97, Developer Edition, you can use the wizard to define custom registry settings that are created when your application is installed.

    To have these settings affect any Visual Basic or Visual Basic for Applications application, specify the location of the user profile by using the IniPath property in your application’s code. Your application must set the IniPath property before executing any other DAO code. The scope of this setting is limited to your application and can’t be changed without restarting your application.

  • Change the default values for Microsoft Jet settings in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Jet 3.5 key.

    These defaults are written when Microsoft Jet is installed. Changes made to settings in this key can affect all applications that use Microsoft Jet 3.5, such as Microsoft Access 97, Visual Basic version 5.0, or Microsoft Excel 97. Make changes to values in this key only if you want to affect all the applications that use Microsoft Jet 3.5 on a user’s computer.

  • Create a Jet 3.5 subkey in the \HKEY_LOCAL_MACHINE\SOFTWARE
    \Microsoft\Office\8.0\Access\Jet\3.5\Engines key, and then add values for any default setting you want to override.

    These changes affect only Microsoft Access 97.

See Also For information on user profiles or changing Microsoft Access registry settings, search the Microsoft Access Help index for “user profiles” or “Registry .” For information on the IniPath property or SetOption method, search the DAO Help index for “IniPath property” or “SetOption method.”


Multithreading

A thread is a software process running independently on a multitasking operating system such as Windows 95 or Windows NT Workstation. For example, the ability to use multiple threads allows you to run a communications software program to download a file at the same time as you use your word processor to edit a document. Microsoft Jet 3.5 uses threads internally to enhance performance and provide background services such as:

  • Read-ahead caching.

  • Write-behind caching (transaction commit).

  • Cache maintenance.

  • Detecting if changes have been made to a shared database.

By default, Microsoft Jet uses a maximum of three threads. You can increase this number by modifying the value of the Threads setting in the registry. You may want to try increasing the number of threads if there is a large amount of activity in your database, or if it contains a large number of linked tables.

Note It is not possible to use multiple threads to call an instance of Microsoft Jet from Visual C++ or any other programming language. All multithreading support for Microsoft Jet is internal to the engine itself.


Dynamic Memory Usage

Microsoft Jet reads and writes data in 2K pages, placing the data in a temporary holding area called the cache as required by its operations. By default, when performing operations that add, delete, or update records and aren’t part of an explicit transaction, Microsoft Jet automatically performs internal transactions that group changes to records and temporarily saves them in the cache. After a specified amount of time, or when the size specified by the MaxBufferSize setting is exceeded, it then writes the data as a chunk to the database. This is analogous to the way an operating system’s lazy-write cache works. This minimizes the time spent reading and writing data to the database. Additionally, Microsoft Jet can minimize the time spent reading data for tables, queries, forms, or reports by reading available data from its cache.

See Also For information on other settings that control how transactions are written to disk, see “Asynchronous Writes” later in this section.

The MaxBufferSize setting specifies a “high water mark” for the size of the cache that Microsoft Jet uses to work with records in memory, measured in kilobytes. Microsoft Jet can temporarily exceed the MaxBufferSize setting. As soon as it does, however, it starts a background thread to write data to the database to bring the cache’s size down to the specified “high water mark.”

By default, Microsoft Jet allocates memory for its cache on an as-needed basis up to the size specified by the MaxBufferSize setting. The default value written in the Windows Registry for the MaxBufferSize setting when Microsoft Access is installed is 0, which indicates that Microsoft Jet calculates the setting based on the following formula:

((Total RAM in K - 12,288)/4 + 512K)

For example, on a system with 16 MB of RAM (16,384K), Microsoft Jet uses a MaxBufferSize setting of 1,536. Because computers with 64 MB or more of RAM are becoming more common, Microsoft Jet 3.5 sets a maximum of 13,824K on the result of the formula to prevent Microsoft Jet from claiming an overly large amount of RAM for the cache.

You can override the default calculated setting by specifying a different MaxBufferSize value. This sets a new “high water mark.” The minimum value that Microsoft Jet uses by default is 512K, but you can specify a value as low as 128K. However, setting the MaxBufferSize setting to a value less than 512K is not recommended for Microsoft Access applications because it can seriously degrade performance.

For computers with 16 MB or less of installed RAM, there is generally no need to override the calculated setting. For computers with 32 MB of RAM or more, you may see some performance improvement when specifying a larger MaxBufferSize if no other applications are running at the same time as Microsoft Access. For example, tests have shown performance improvements in applications that perform large transactions when a MaxBufferSize as large as 8 MB is specified.

Specifying a MaxBufferSize larger than 8 MB hasn’t been found to increase performance. Setting a value too high can degrade performance due to the added CPU overhead needed to manage the cache, and due to the fact that the operating system may start swapping the Microsoft Jet cache to disk as virtual memory.

Important If you have specified a MaxBufferSize setting that is larger than the default and have disabled the FlushTransactionTimeout setting, you should also increase the SharedAsyncDelay setting to increase the time that data is held in the Microsoft Jet cache. If you don’t increase the SharedAsyncDelay setting, Microsoft Jet writes the contents of its cache to the database before it has had time to utilize the additional memory you specified. For more information on the SharedAsyncDelay setting, see the following section “Asynchronous Writes.”


Asynchronous Writes

Microsoft Jet 3.5 can write changes to the database in either synchronous mode or asynchronous mode.

In synchronous mode, Microsoft Jet doesn’t return control to the application code until the changes are written to the database. If transactions are used, the changes are written when the CommitTrans method is used; if transactions are not used, the changes are written every time the Update method is used. Synchronous mode is the only mode available in Microsoft Jet 2.x.

In asynchronous mode, Microsoft Jet stores the changes in its cache, returns control to the application code immediately, and then writes the changes to the database in a background thread. Microsoft Jet begins writing the changes either after a specified period of time (determined by the FlushTransactionTimeout setting, or by the SharedAsyncDelay or ExclusiveAsyncDelay settings described later in this section) or when the MaxBufferSize is exceeded.

Asynchronous (or background) writes can improve performance in several ways:

  • Effective overlap of disk I/O and CPU use is enabled.

  • Control of the CPU is returned to the program executing the code much sooner.

  • Disk I/O is reduced. Multiple changes can be merged into a single change. For instance, updating every record on a page may require only a single write of the page when done.

  • Locking is reduced. A request to lock a page may find that the page is queued for writing and is already locked.

The following sections describe the registry settings that are used to control how Microsoft Jet performs disk writes.

UserCommitSync

The UserCommitSync setting determines whether changes made as part of an explicit transaction (a change made to data by using the BeginTrans, CommitTrans, and Rollback methods) are written to the database in synchronous mode or asynchronous mode. The default value for the UserCommitSync setting is Yes, which specifies synchronous mode. It is not recommended that you change this value because in asynchronous mode, there is no guarantee that information has been written to disk before your code proceeds to the next command.

ImplicitCommitSync

By default, when performing operations that add, delete, or update records outside of explicit transactions, Microsoft Jet automatically performs internal transactions called implicit transactions that temporarily save data in its memory cache, and then later write the data as a chunk to the disk. The ImplicitCommitSync setting determines whether changes made by using implicit transactions are written to the database in synchronous mode or asynchronous mode. The default value of the ImplicitCommitSync setting is No, which specifies that these changes are written to the database in asynchronous mode; this provides the best performance. If you want implicit transactions to be written to the database in synchronous mode, change the value of the ImplicitCommitSync setting to Yes. If you change the value of the setting to Yes, you get behavior similar to Microsoft Jet versions 2.x and earlier when you weren’t using explicit transactions. However, doing so can also impair performance considerably, so it is not recommended that you change the value of this setting.

Note There is no longer a need to use explicit transactions to improve the performance of Microsoft Jet. A database application using Microsoft Jet 3.5 should use explicit transactions only in situations where there may be a need to roll back changes. Microsoft Jet can now automatically perform implicit transactions to improve performance whenever it adds, deletes, or changes records. However, implicit transactions for SQL DML statements were removed in Microsoft Jet 3.5. For more information, see “Removal of Implicit Transactions for SQL DML Statements” later in this chapter.

See Also For more information on transactions, see “Microsoft Jet Transactions” in Chapter 5, “Working with Records and Fields.”

FlushTransactionTimeout

The FlushTransactionTimeout setting determines how Microsoft Jet performs asynchronous writes to a database file. The FlushTransactionTimeout setting is the number of milliseconds of inactivity after which Microsoft Jet starts writing database changes to disk from its cache. Changes are written to disk after the specified amount of time has expired and if no new pages have been added to the cache during that interval. The only exception is if the size of the cache exceeds the MaxBufferSize setting, at which point the cache starts asynchronous writes regardless of whether the time has expired. The default setting is 500 milliseconds. The only reason to increase the value of the FlushTransactionTimeout setting is if the database is being updated over a slow wide area network (WAN) or local area network (LAN) connection. Increasing this value for databases being updated over fast WAN and typical LAN connections does not improve performance.

The FlushTransactionTimeout setting overrides both the ExclusiveAsyncDelay and SharedAsyncDelay registry settings that are used to control asynchronous writes in Microsoft Jet 3.0. The FlushTransactionTimeout setting is the preferred method of determining how Microsoft Jet 3.5 performs asynchronous writes to a database file. Although it is not recommended that you do so, you can enable the ExclusiveAsyncDelay and SharedAsyncDelay settings by setting the value of the FlushTransactionTimeout setting to 0.

ExclusiveAsyncDelay

The ExclusiveAsyncDelay setting is the maximum time that can pass before asynchronous mode changes start to be written to a database that’s open exclusively. The default setting is 2,000 milliseconds. Decrease this setting if you want to be sure that changes are written to your database more frequently, but note that this decreases overall performance. Because the default setting is already quite long, increasing this setting doesn’t improve performance in most cases. However, if your system has 32 MB of RAM or more and you have specified a large value for the MaxBufferSize setting, you may see some performance improvement if you increase the value of this setting. By default, this setting is disabled by the FlushTransactionTimeout setting.

SharedAsyncDelay

The SharedAsyncDelay setting is the maximum time that can pass before asynchronous mode changes start to be written to a database that’s open in shared mode. The default setting is 50 milliseconds. Note that this produces a small delay before changes are made available to other users. Increasing this value enhances performance in shared databases because there are fewer disk writes. However, it may reduce the overall concurrency because pages are locked while they are in the Microsoft Jet cache waiting to be written to the database. If your application performs operations that affect many records, increase the value of this setting to give Microsoft Jet additional time to temporarily save additions, deletions, and updates in its cache before it writes them to the database. This applies whether your application uses action queries, DAO code, or SQL statements to add, delete, or update records. By default, this setting is disabled by the FlushTransactionTimeout setting.


Cache Refreshing

The PageTimeout setting determines how long Microsoft Jet waits before checking to see if other users have made changes to the database. If changes have been made, Microsoft Jet refreshes the data in its cache.

Note Refreshing data in the cache is equivalent to pressing F9 while viewing a table or query in Microsoft Access, or using the Refresh method in DAO code.

The default PageTimeout setting is 5,000 milliseconds. Decreasing this setting increases the amount of time spent reading from the disk, thus impairing performance, but can make the data available to the user more current. In Microsoft Access 97, the setting in the Refresh Interval box on the Advanced tab of the Options dialog box (Tools menu) overrides the PageTimeout setting.

Tip You can override the PageTimeout setting and refresh the Microsoft Jet cache by using the dbRefreshCache argument of the Idle method in DAO code. This allows users to see other users’ changes immediately. For more information on the Idle method, see “Programmatically Refreshing the Microsoft Jet Cache” later in this chapter.


Controlling Lock Retries

If Microsoft Jet tries to place a lock on a page in a shared database and a message that locking has failed is returned, the LockDelay setting determines how long it waits before it retries. If the time it takes to return the message exceeds the LockDelay setting, there is no delay. The default setting is 100 milliseconds. On systems that don’t manage lock retries themselves, such as Windows 95 peer-to-peer networking, the LockDelay setting prevents Microsoft Jet from performing repeated retries over a short period of time. If you are using such a system, the default setting is usually sufficient to reduce the number of lock requests sent across the network, which frees up network bandwidth for other purposes. If your application or its users frequently lock a large number of records, you may want to try increasing this setting to further reduce the number of lock requests being sent across the network. If you are using a server-based networking system that manages lock retries itself, such as Windows NT Server or Novell NetWare, there is no need to change this setting.


Setting the Maximum Number of Locks

Microsoft Jet places locks on the locking information file (.ldb file) while modifying data. Many locks can accumulate when data is being manipulated inside a transaction. The accumulation of locks can cause problems with NetWare servers because they can handle only up to 10,000 locks per connection. This limit ensures good performance on a NetWare server, but can prevent Microsoft Jet from completing large transactions. When users encounter this, they experience long delays before Microsoft Jet returns an error message stating that the transaction needs to be rolled back. Prior to Microsoft Jet 3.5, the only workaround was to break DAO transactions that affected many records into smaller transactions, and to replace SQL DML statements with the equivalent DAO looping routines that perform smaller transactions.

The MaxLocksPerFile setting determines the maximum number of locks that Microsoft Jet places against a file. The default setting is 9,500 locks. If the number of locks required to perform a transaction exceeds the MaxLocksPerFile setting, the transaction commits (writes) the data that has locks associated with it, frees the locks, and then continues processing the transaction. If the maximum number of locks a server can handle is less than the MaxLocksPerFile setting, the server returns an error message or appears to hang when performing a large transaction. If this occurs, you should decrease the MaxLocksPerFile setting. A Novell server can be configured to perform a maximum of 10,000 locks per connection. However, you should note that a Novell server connection can include more than one database, so it’s possible to exceed the maximum number of available locks if you are using more than one database at a time.


Controlling Long Value Page Recycling

Long value (LV) pages store data in fields with Memo, Long Binary (OLE Object), and Hyperlink data types, as well as the data that defines forms, reports, and modules. When a database is open in shared mode, the size of the database increases when data, forms, reports, and modules are deleted or changed in such a way that the current LV page must be discarded and replaced with a new LV page. Sometimes a new LV page is needed to prevent errors for other users who may still be using the object or data in its original form.

The RecycleLVs setting determines when discarded LV pages become available for reuse. The default RecycleLVs setting is 0, which specifies that discarded LV pages continue to occupy space in the database and only become available for reuse after the last user closes the database. You can change the RecycleLVs setting to 1 so that discarded LV pages become available for reuse after Microsoft Jet determines that there’s only one user in the database in shared mode and that new LV data has been added to the database. Note that setting RecycleLVs to 1 slows down performance somewhat. Therefore, this is primarily useful when you are creating or modifying forms, reports, and modules because it minimizes the need to compact the database.

When a database is open in exclusive mode, the RecycleLVs setting has no effect: discarded LV pages become available for reuse immediately. In both exclusive mode and shared mode, discarded LV pages are reused only when new LV data needs to be written to the database. To remove discarded LV pages before that time, you must compact the database.

Microsoft Access Users Microsoft Access 97 turns this setting on and off automatically when modifying forms, reports, and modules to prevent the accumulation of discarded LV pages.


Using the SetOption Method to Control Registry Settings

Although creating a user profile (a custom set of Microsoft Jet registry keys and settings specifically for your application) is more flexible than overwriting the Microsoft Jet default values, it still requires that you maintain those values in the registry. If you choose to change the default values of registry settings, you will have to edit the registry every time changes are required. In previous versions of Microsoft Jet, these were the only two strategies available.

Microsoft Jet 3.5 provides a new way to modify registry settings at run time with the SetOption method. This is the recommended way to fine-tune registry settings for your application. This method provides the most control over how the registry is changed. The following table lists the Microsoft Jet registry settings and the corresponding DAO constants that can be manipulated at run time.

Registry setting | DAO constant
PageTimeout | dbPageTimeout
SharedAsyncDelay | dbSharedAsyncDelay
ExclusiveAsyncDelay | dbExclusiveAsyncDelay
LockRetry | dbLockRetry
UserCommitSync | dbUserCommitSync
ImplicitCommitSync | dbImplicitCommitSync
MaxBufferSize | dbMaxBufferSize
MaxLocksPerFile | dbMaxLocksPerFile
LockDelay | dbLockDelay
RecycleLVs | dbRecycleLVs
FlushTransactionTimeout | dbFlushTransactionTimeout

The SetOption method only affects the run-time values of the registry and does not physically change the values in the registry. Thus, once Microsoft Jet is restarted or is run by any other application, it will read the original values in the registry. This means that in order to control the Microsoft Jet registry settings, you must use the SetOption method in code that runs every time an application starts. In Visual Basic, you could call the SetOption method in a Sub Main() procedure. In Microsoft Access, you could call the SetOption method in the Load event of a form that’s automatically loaded when the database opens, or you could call a function in a macro named AutoExec, which automatically runs when the database opens.

The following code samples illustrate how you might use the SetOption method to compare values for the MaxBufferSize setting. Note that these code examples use the ISAMStats function described in “Unsupported Tuning Functions” later in this chapter. The ISAMStats function returns information about the number of disk reads and writes that occur for the different registry values.

The first procedure sets the MaxBufferSize setting to 128K, then opens a recordset. It calls other procedures to reset the ISAMStats values, update the recordset, begin and commit a null transaction to prevent other asynchronous activity, and print the values returned by the ISAMStats function. The procedure then sets the MaxBufferSize setting to 2048K and repeats the process.

Sub CompareBufferSizes()
    Dim dbs As Database, rst As Recordset, wrk As Workspace
    Dim strSQL As String
    Dim strDbPath As String

    strDbPath = "C:\JetBook\Samples\NorthwindTables.mdb"
    Set dbs = OpenDatabase(strDbPath)
    strSQL = "SELECT * FROM Customers;"

    ' Use SetOption to set MaxBufferSize to 128K.
    DBEngine.SetOption dbMaxBufferSize, 128
    ' Open recordset.
    Set rst = dbs.OpenRecordset(strSQL, dbOpenDynaset)
    ' Return default workspace.
    Set wrk = Workspaces(0)
    ' Set ISAMStats counters to zero.
    ResetISAMStats
    ' Run update on recordset.
    UpdateRecordset rst
    ' Commit null transaction.
    NullTransaction wrk
    ' Print ISAMStats values.
    PrintISAMStats strSQL

    ' Set MaxBufferSize to 2048K and repeat all.
    DBEngine.SetOption dbMaxBufferSize, 2048
    Set rst = dbs.OpenRecordset(strSQL, dbOpenDynaset)
    Set wrk = Workspaces(0)
    ResetISAMStats
    UpdateRecordset rst
    NullTransaction wrk
    PrintISAMStats strSQL
End Sub

' This procedure resets the values of ISAMStats.
Sub ResetISAMStats()
    Dim intI As Integer
    ' Reset each value.
    For intI = 0 To 5
        DBEngine.ISAMStats intI, True
    Next
End Sub

' This procedure runs an update on the recordset.
Sub UpdateRecordset(rst As Recordset)
    Dim strCompanyName As String, strContactName As String

    Do Until rst.EOF
        rst.Edit
        strCompanyName = rst!CompanyName
        strContactName = rst!ContactName
        rst!CompanyName = strCompanyName
        rst!ContactName = strContactName
        rst.Update
        rst.MoveNext
    Loop
End Sub

' This procedure begins and commits a null transaction.
Sub NullTransaction(wrk As Workspace)
    ' The null transaction ensures there will be
    ' no more asynchronous activity that could
    ' yield inaccurate statistics.
    wrk.BeginTrans
    wrk.CommitTrans
End Sub

' This procedure prints the values of ISAMStats
' to the Debug window.
Sub PrintISAMStats(Optional strSQL As String)
    Debug.Print "Query: ", strSQL
    Debug.Print "Number of disk reads: ", DBEngine.ISAMStats(0)
    Debug.Print "Number of disk writes: ", DBEngine.ISAMStats(1)
    Debug.Print "Number of reads from cache: ", DBEngine.ISAMStats(2)
    Debug.Print "Number of reads from read-ahead cache: ", DBEngine.ISAMStats(3)
    Debug.Print "Number of locks placed: ", DBEngine.ISAMStats(4)
    Debug.Print "Number of release lock calls: ", DBEngine.ISAMStats(5)
    Debug.Print
End Sub

Note When the dbMaxBufferSize constant is modified from 128 to 2048, the number of writes will decrease significantly. The low value used in this example is exaggerated to illustrate the point while using the NorthwindTables database. It is not recommended that you use a value this low for a real-world scenario.


Programmatically Refreshing the Microsoft Jet Cache

To improve performance in Microsoft Jet 3.0, the behavior of the PageTimeout setting was altered. In Microsoft Jet 2.x, if you set the PageTimeout setting to 2 seconds, a shared cache is kept at least 2 seconds, and probably longer before Microsoft Jet refreshes data in its cache. In Microsoft Jet 3.0, the cache is kept no more than 2 seconds, providing more precise control over the currency of the data returned. The default value for the PageTimeout setting in both Microsoft Jet 3.0 and 3.5 is 5000 milliseconds. In addition, Microsoft Jet 3.0 and 3.5 recognize when shared databases are not being updated, and suppress cache refreshing. In this way, performance of shared databases that are infrequently changed may approach the performance of databases that are open exclusively.

Although these changes prove optimal for performance, a drawback is that almost ten seconds may elapse before a user in a shared database can see changes made by other users. One way to work around this problem is to set the PageTimeout registry setting to a lower value. However, doing this degrades the performance of Microsoft Jet by forcing it to read the database header more frequently, which results in increased I/O.

Microsoft Jet 3.5 addresses this problem by providing a way to force its cache to be refreshed regardless of the PageTimeout setting. The Idle method of the DBEngine object has a new argument, dbRefreshCache, that you can use to force Microsoft Jet to immediately read the database header to see if any changes have occurred. If they have, Microsoft Jet refreshes its cache and the user of the application that issued the command sees any changes made by other users. This change allows you to leave the PageTimeout setting at its default to provide optimal performance, but gives you the ability to force Microsoft Jet to check for other users’ changes. However, keep in mind that just as setting PageTimeout to a low value can degrade performance, using the dbRefreshCache argument of the Idle method repeatedly will increase disk and network I/O and degrade performance.


Key Internal Performance Enhancements

Microsoft Jet 3.5 includes the following internal enhancements to improve performance.


Reduced Locking

To improve multiuser performance, the locking algorithms in Microsoft Jet 3.5 were modified in two areas:

Write locks on index pages When a Microsoft Jet 3.0 database uses enforced referential integrity, Microsoft Jet always places a write lock on any index page that is being read. This prevents other users from changing data in the index that might be needed to enforce referential integrity. The disadvantage of this is that write locks on a series of index pages prevent other users from using the index to read data.

In Microsoft Jet 3.5, this is resolved by replacing the write locks on index pages that are not being modified with read locks. This allows Microsoft Jet to continue to enforce referential integrity while still allowing other users to read the index pages.

Read locks on long value (LV) data pages LV pages typically contain fields with the Memo, Long Binary (OLE Object), and Hyperlink and data types. In Microsoft Jet 3.0, a read lock is placed on every LV page that is being read. The primary reason for using read locks is to ensure that the user sees unchanged data when reading large LV pages.

Because many users use a field with the Memo data type that has only a minimal amount of data stored in it and because LV data that does not span more than one page does not require read locks, Microsoft Jet 3.5 eliminates read locks on those LV pages. This substantially reduces concurrency conflicts and improves performance.


Improved Rushmore Algorithms

Microsoft Jet 3.5 was improved to use Rushmore technology on more SQL statements. In particular, SQL statements that use the FALSE, <, and > operators now provide substantial performance improvements.


Removal of Implicit Transactions for SQL DML Statements

Even with all the work in Microsoft Jet 3.0 to eliminate transactions in order to obtain better performance, SQL DML statements were still placed in an implicit transaction. In Microsoft Jet 3.5, SQL DML statements are not placed in an implicit transaction. This substantially improves performance when running SQL DML statements that affect many records of data.

Although this change provides a substantial performance improvement, it also introduces a change to the behavior of SQL DML statements. When using Microsoft Jet 3.0 and previous versions that use implicit transactions for SQL DML statements, an SQL DML statement rolls back if any part of the statement is not completed. When using Microsoft Jet 3.5, it is possible to have some of the records committed by an SQL DML statement while others are not. An example of this would be when the Microsoft Jet cache is exceeded. The data in the cache is written to disk and the next set of records is modified and placed in the cache. Therefore, if the connection is terminated, it is possible that some of the records were saved to disk, but others were not. This is the same behavior as using DAO looping routines to update data without an explicit transaction in Microsoft Jet 3.0. If you want to avoid this behavior, you need to add explicit transactions around the SQL DML statement to define a set of work and you must sacrifice the performance gains.


Reduced Index Navigation

Microsoft Jet 3.5 has been enhanced to not re-seek a page within an index if the index is a primary key index or a unique index. This reduces the amount of disk I/O that occurs because Microsoft Jet does not need as many disk reads.


Improved Cache Reuse

Microsoft Jet 3.0 always refreshes its cache on a page when a read lock is promoted to a write lock. In Microsoft Jet 3.5, it is not necessary to refresh the cache when this operation occurs, which results in reduced disk I/O.


Persistent Connections to Security System Tables

Microsoft Jet 3.5 keeps the MSysObjects and MSysACEs system tables open for the duration of a Microsoft Jet instance in order to improve performance through the Microsoft Access user interface and other areas where security is implemented and objects are being referenced through DAO. This reduces the overhead of frequently opening and closing those tables when security is implemented, and reduces disk I/O.


Reduced Flushing of the Cache

When adding data to a table, Microsoft Jet 3.0 always flushes (writes) its cache to disk every two seconds after adding each extent (up to 16K). Microsoft Jet 3.5 flushes its cache only after all new extent data has been added to the database. This results in less disk I/O.


Larger Allocation by Extents

Microsoft Jet 3.0 allocates up to 8 pages (16K) at a time when adding data to a table. This allows data to be contiguous within a table, thus optimizing sequential reads and the Microsoft Jet read-ahead cache. Microsoft Jet 3.5 allocates up to 32 pages (64K) at a time when adding data. Increasing this maximum allocation results in improved performance. Like Microsoft Jet 3.0, Microsoft Jet 3.5 only adds large extents to tables when large numbers of records exist in a table.


Improved Performance with DELETE Statements

Microsoft Jet 3.5 has optimized how it removes pages in the database by reading and processing fewer directory pages. Therefore, removing a table or deleting all the records in a table is even faster than in Microsoft Jet 3.0.


Faster Generation of Query Plans

In Microsoft Jet 3.5, the query plan generation process has been optimized to improve performance of temporary queries (for example, dbs.Execute SQL statement) and frequent modifications of stored queries in code.


Explicit Registry Settings and Values

With Microsoft Jet 3.0, it was difficult for users to manipulate registry values to control Microsoft Jet because the settings and values were not created in the registry. Microsoft Jet 3.5 remedies that by explicitly loading the settings and default values when Microsoft Jet 3.5 is registered. Explicit registry settings and values are also loaded for the Microsoft Jet 2.x installable ISAM.


Other Optimization Tips and Techniques

This section describes other tips and tricks that you can use to optimize the performance of your application.


Using DBEngine(0)(0) Instead of CurrentDb

If you’re working in Microsoft Access 97, you can use either the CurrentDb function or the DBEngine(0)(0) syntax to return a reference to the database that is currently open in the Microsoft Access user interface. The CurrentDb function differs from the DBEngine(0)(0) syntax in that it creates another instance of the current database and returns a reference to that instance, and it refreshes all the collections in the current database. On the other hand, the DBEngine(0)(0) syntax doesn’t refresh any collections, so it may perform faster.

If you switch from using the CurrentDb function to the DBEngine(0)(0) syntax, make sure that your code doesn’t depend on any collections being refreshed. If it does, you can refresh these individual collections separately.

See Also For more information on using the CurrentDb function and DBEngine(0)(0) syntax, see Chapter 2, “Introducing Data Access Objects jb2Introducing_Data_Access_Objects.”


Prevent Unnecessary Query Recompilation

An area that can cause performance degradation is the constant recompiling of stored queries. This can occur when DAO objects are not explicitly closed. The reason for this is that stored queries reference a particular memory address in order to use the Visual Basic for Applications expression service. If a DAO object is opened, and then a stored query is executed, re-executing that query may cause Microsoft Jet to recompile the query because the original memory address space is occupied. In certain circumstances, this also causes the database to increase in size. Many of these issues were resolved by enhancements to Microsoft Jet 3.5, but some of the issues can be resolved only by using the Close method to explicitly close DAO objects when they are no longer being used. You should not rely on Visual Basic for Applications to implicitly close objects in your code when a Sub or Function procedure is exited.

If you cannot explicitly close a DAO object because it is nested in an explicit transaction or some other scenario, another possible workaround is to use the dbOpenSnapShot or the dbForwardOnly constant for the type argument of the OpenRecordset method when you open a recordset. Using the dbOpenSnapShot or the dbForwardOnly constant prevents Microsoft Jet from recompiling the query. This is most useful when using the data grid that comes with Microsoft Visual Basic 5.0, because you can’t explicitly close the DAO objects being used.


Avoid Embedding Expressions in Queries

Using an expression in a query prevents the Microsoft Jet query optimizer from using the field for index or Rushmore optimization. In addition, expressions are evaluated for every record, not just the records that are returned. You can significantly improve performance by removing embedded expressions from queries. The best place to put expressions is in forms or reports because then expressions are only evaluated for the records returned.


Use Indexes Cautiously

Although having indexes can reduce the time it takes to retrieve data, they always carry a cost in maintenance and concurrency issues. When should a field be indexed? There is no strict answer for this because it depends on the type of application.

The first guideline is that fields containing highly duplicated data should not be indexed (for example, fields with the Yes/No data type, and fields that represent gender, state abbreviations, or country codes).

The second guideline is that fields should not be indexed simply to force Rushmore to use more than one index. An example of this would be indexing a field called City and a field called PostalCode in a customer table when the application is always going to be using both fields for retrieval purposes. In this instance, the PostalCode field is going to be the most unique index and would return a result set faster if the City field was not indexed. Because Rushmore doesn’t need to use the index on the City field, omitting the index on the City field will reduce overall disk I/O. Of course, if both values were not always being entered and they were used alternatively and equally, then having an index on both fields would probably be advantageous. Rushmore is best utilized on combined indexes when they are required to generate a unique result set.

It is also important to remember that indexes create concurrency issues. One index page represents many data pages. Therefore, modifying an index page can cause users with data on an entirely different data page to be locked out when trying to update the indexed field.

To see this behavior, open the Northwind database in Microsoft Access 97 and turn on pessimistic locking by clicking Edited Record on the Advanced tab of the Options dialog box (Tools menu). Update a value in one indexed field in the Customers table, but don’t move to the next record. On another workstation, open the Customers table and try to edit another value in the same indexed field that the other workstation is editing. Next, try updating a value in a non-indexed field in the Customers table. What will become evident is that substantially more records of data are locked when you try to update a value in an indexed field than when you try to update a value in a non-indexed field.


Use SQL DDL Statements Instead of DAO Looping Routines

In most cases when using Microsoft Jet 3.5, it is better to use a single SQL data definition language (DDL) statement than a DAO looping routine. The following example runs an update first by using a recordset, then by executing an SQL DDL statement. Both updates are wrapped in transactions and rolled back after they have been timed.

In this example, updating with the SQL DDL statement is only slightly faster than the recordset update, but the difference becomes significant for large data sets. In this example, strDbPath is the path to the NorthwindTables database:

Dim wrk As Workspace
Dim dbs As Database
Dim rst As Recordset
Dim lngTimer As Long
Dim strDbPath As String, strSQL As String

' Return reference to default workspace.
Set wrk = DBEngine(0)
Set dbs = OpenDatabase(strDbPath)
strSQL = "UPDATE [Order Details] SET UnitPrice " _
    & "= UnitPrice*1.1, Discount = Discount+0.1;"

' Begin transaction.
wrk.BeginTrans
' Begin timing.
lngTimer = Timer
Set rst = dbs.OpenRecordset("Order Details")
' Update each record in the recordset.
Do Until rst.EOF
    rst.Edit
    rst!UnitPrice = rst!UnitPrice * 1.1
    rst!Discount = rst!Discount + 0.1
    rst.Update
    rst.MoveNext
Loop
Debug.Print "Elapsed time for recordset update = " & Timer - lngTimer
' Roll back changes.
wrk.Rollback

' Perform same operation with SQL DDL query.
wrk.BeginTrans
lngTimer = Timer
dbs.Execute strSQL, dbFailOnError
Debug.Print "Elapsed time for DDL update = " & Timer - lngTimer
wrk.Rollback

There are two reasons that the SQL DDL update is faster than the recordset update:

  • Microsoft Jet 3.5 removed implicit transactions for SQL DDL statements.

  • In this example, string values are being manipulated. Visual Basic uses the Unicode format and DAO inherently uses Visual Basic; however, Microsoft Jet only stores string data in ANSI format for U.S. code pages. This requires Visual Basic to translate from ANSI to Unicode and then back to ANSI whenever string data passes through DAO. This translation, especially for long string types, can slow down performance. Because the query optimizer in Microsoft Jet does not process string data through Visual Basic (unless expressions are being used), performing bulk type operations from SQL can be significantly faster when manipulating string data.

See Also For more information on SQL DDL statements, see “Removal of Implicit Transactions for SQL DDL Statements” earlier in this chapter.


Implement Persistent Connections with Linked Tables

Microsoft Jet uses the locking information (.ldb) file to track which users have the database open and to track the locking of pages in the .mdb file. In Microsoft Jet 3.0 and 3.5, the .ldb file is deleted when the last user closes the database. This is done to prevent accumulation of .ldb files when replication is being used. However, in situations where only one user is accessing a linked table, particularly in a looping routine, this causes a significant degradation in performance. The reason for this is that linked tables do not keep persistent connections to the database where their data resides. This causes additional disk I/O to delete, re-create, and establish locks on the locking information file.

To prevent this, design your application so that it maintains a persistent connection to any linked Microsoft Jet tables. To do this, declare public recordset variables for each linked table your application uses, use the OpenRecordset method to open those tables at the beginning of your application, and only close these recordset variables when your application itself closes. Maintaining persistent connections to linked tables can improve performance significantly because it prevents Microsoft Jet from constantly deleting, creating, and obtaining locking information from the other database’s locking information file.


Use Explicit Transactions When Implementing OLTP

Due to the changes in Microsoft Jet 3.5 regarding the FlushTransactionTimeout registry setting, it is possible for throughput to decrease significantly due to concurrency issues when online-transaction processing (OLTP) is being implemented from many workstations. The reason for this is that Microsoft Jet 3.5 saves modified pages in its cache until no activity is encountered or the cache is full. Thus, the fast throughput of OLTP statements causes Microsoft Jet not to flush its cache and release the locks on index and data pages. This may cause many pages to be locked during this time period, which can cause concurrency issues when many users are trying to update one record at a time in the same table. To avoid this problem, use explicit transactions for these scenarios to force Microsoft Jet to flush its cache when the transaction is committed.


Check Parameter Queries for Optimal Performance

Parameter queries can only be implemented by using a stored query. Because stored queries have a precompiled query plan, parameter queries that contain parameters on indexed fields may not execute efficiently. Because the query engine does not know the values to be passed in a parameter in advance, it can only guess as to the most efficient query plan. In some instances, you can substantially improve performance by replacing a stored parameter query with a temporary query. To do this, create the SQL string in code and pass it to the Database object’s OpenRecordset method or Execute method.


Unsupported Tuning Functions

You can use the built-in ISAMStats and ShowPlan functions to measure and improve Microsoft Jet performance. These functions apply only to native Microsoft Jet database files. Similar functions do not exist for the other database formats.

Important The ISAMStats and ShowPlan functions are not supported as an official part of DAO. They have not been through the same quality testing as the supported components of Microsoft Jet. You use them entirely at your own risk. The results returned by the ISAMStats and ShowPlan functions may vary. The availability of these features in future releases of Microsoft Jet is not guaranteed.


The ISAMStats Function

You can use the ISAMStats function, which returns information about the raw disk reads, writes, locks, and caching, to analyze and tune the performance of your application. If you want to get every last bit of performance out of your database, you may find the ISAMStats function useful when trying to substantiate timing results based on various ways of manipulating data in the database. Some typical scenarios in which you might use the ISAMStats function include:

  • When you’re trying to tune a query and you’re not sure how to reduce network traffic.

  • When you’re looking at the effect of placing explicit transactions versus letting Microsoft Jet use its internal transactions.

  • When you have adjusted the registry settings and you want to see how they affect the number of reads and writes to the database.

  • When you want to validate timings, which may vary due to external circumstances that Microsoft Jet does not control, such as varying traffic on the LAN, different processes running on the machine, and disk fragmentation. Although elapsed time for a computer may vary, the number of reads or writes generally stays the same.

The syntax for the ISAMStats function is:

ISAMStats (StatNum As Long [, Reset As Boolean]) As Long

This function returns the value of a given engine statistic as defined by StatNum. The following table lists and describes the values you can use for StatNum.

StatNum | Description
0 | Number of disk reads
1 | Number of disk writes
2 | Number of reads from cache
3 | Number of reads from read-ahead cache
4 | Number of locks placed
5 | Number of release lock calls

If the optional Reset argument is supplied, then the statistic defined by StatNum is reset and no value is returned. A Reset value of False is equivalent to not supplying the argument. The statistics returned apply to the whole engine, regardless of how many databases or sessions are active, including temporary databases.

The following may help you interpret the statistics returned by the ISAMStats function:

  • The number of disk reads and writes includes all reads and writes in all circumstances, including background read-ahead cache reads in separate threads. One read or write doesn’t necessarily equal one page: One read or write could represent many pages that were read or written simultaneously. An example of this is commands wrapped in a transaction.

  • There are two types of cached reads returned. The regular cache is a set of pages. For example, after executing an SQL DML statement, it’s likely that the pages read would remain in the cache. The read-ahead cache statistic shows reads that occurred so that they would be available if needed. For example, in certain scenarios, Microsoft Jet 3.5 issues a read request for more pages than it actually needs. In this way, pages can be used without actually going to disk again. The number of cache reads doesn’t include read-ahead cache reads.

  • The number of locks placed and released may not balance. A single call to release a lock may result in the release of many locks.

The following procedure uses the ISAMStats function to return statistics on a query. The first procedure calls two procedures shown earlier in this chapter, ResetISAMStats and PrintISAMStats:

Sub ShowStats(strDbPath As String)
    Dim dbs As Database
    Dim strSQL As String
    Dim lngVersion As Long

    ' If you are using Visual Basic, make sure that the database
    ' is open or that the engine is initialized before issuing
    ' any ISAMStats calls. This example uses DBEngine.Version to
    ' initialize the database.
    lngVersion = DBEngine.Version

    ' Explicitly set the counters to zero.
    ResetISAMStats
    ' Open the database.
    Set dbs = OpenDatabase(strDbPath), _
        False, False)
    ' Prompt user to enter SQL statement.
    strSQL = InputBox("Enter a SQL string", "SQL Box", _
        "UPDATE Customers SET ContactName = ContactName")
    ' Execute query.
    dbs.Execute strSQL, dbFailOnError
    ' Print the values to the Debug window.
    PrintISAMStats strSQL
    ' Reset each value.
    ResetISAMStats
End Sub


The ShowPlan Function

The Microsoft Jet query engine implements a cost-based query optimizer. When a query is compiled, the query engine creates a plan of execution. This plan is used internally to find the quickest way to execute a query. This information is not exposed to the user of Microsoft Jet. However, you can add and use the JETSHOWPLAN setting in the registry to cause the engine to create a text file that contains the query execution plans.


Activating the ShowPlan Function

To activate the ShowPlan function, add the following key to the registry: \\HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\Jet\3.5\Engines\Debug. Under this key, add a setting called JETSHOWPLAN that has the String data type, making sure you use all capital letters in the name of the registry setting. To activate the ShowPlan function, set the value of the new registry setting to ON. To turn it off, set the value to OFF. Note that after you create this registry setting, you must re-initialize Microsoft Jet.


Using the ShowPlan Function

When the ShowPlan function is activated, Microsoft Jet appends text to a file called Showplan.out when a query is compiled. This text contains the plan that the query optimizer has generated for that query.

Note If you have the ShowPlan function activated, Microsoft Jet will append query plans to the Showplan.out text file every time a query is compiled. As such, the file can grow quite large. You should only turn this feature on during your development and debugging processes. Never leave it on for production databases.

The following text shows the query plan for the Invoices query stored in the Northwind.mdb sample database included with Microsoft Access 97:

---------------------------------------------
DATE: 01/19/97
VER:  3.50.3428

NOTE: Currently does not handle subqueries, vt [virtual table] parameters, and subqueries
NOTE: You may see ERROR messages in these cases

--- Invoices ---

- Inputs to Query -
Table 'Orders'
Table 'Order Details'
    Using index 'OrdersOrder Details'
    Having Indexes:
    OrdersOrder Details 2155 entries, 8 pages, 830 values
      which has 1 column, fixed
    OrderID 2155 entries, 8 pages, 830 values
      which has 1 column, fixed
Table 'Customers'
    Using index 'PrimaryKey'
    Having Indexes:
    PrimaryKey 91 entries, 1 page, 91 values
      which has 1 column, fixed, unique, primary-key, no-nulls
    PostalCode 91 entries, 1 page, 87 values
      which has 1 column, fixed
    CompanyName 91 entries, 3 pages, 91 values
      which has 1 column, fixed
    City 91 entries, 1 page, 69 values
      which has 1 column, fixed
Table 'Employees'
Table 'Products'
Table 'Shippers'
- End inputs to Query -

01) Sort table 'Orders'
02) Inner Join table 'Shippers' to result of '01)'
      using temporary index
      join expression "Shippers.ShipperID=Orders.ShipVia"
03) Sort table 'Employees'
04) Inner Join result of '02)' to result of '03)'
      using temporary index
      join expression "Orders.EmployeeID=Employees.EmployeeID"
05) Inner Join result of '04)' to table 'Customers'
      using index 'Customers!PrimaryKey'
      join expression "Orders.CustomerID=Customers.CustomerID"
06) Inner Join result of '05)' to table 'Order Details'
      using index 'Order Details!OrdersOrder Details'
      join expression "Orders.OrderID=[Order Details].OrderID"
07) Sort table 'Products'
08) Inner Join result of '06)' to result of '07)'
      using temporary index
      join expression "[Order Details].ProductID=Products.ProductID"

--- temp query ---

- Inputs to Query -
Table 'Customers'
    Using index 'CompanyName'
    Having Indexes:
    CompanyName 91 entries, 3 pages, 91 values
      which has 1 column, fixed
    City 91 entries, 1 page, 69 values
      which has 1 column, fixed
- End inputs to Query -

01) Scan table 'Customers'
    Using index 'CompanyName'

--- temp query ---

- Inputs to Query -
Table 'Products'
    Using index 'ProductName'
    Having Indexes:
    ProductName 77 entries, 1 page, 77 values
      which has 1 column, fixed
    PrimaryKey 77 entries, 1 page, 77 values
      which has 1 column, fixed, unique, clustered and/or counter, primary-key, no-nulls
    CategoryID 77 entries, 1 page, 8 values
      which has 1 column, fixed
    CategoriesProducts 77 entries, 1 page, 8 values
      which has 1 column, fixed
- End inputs to Query -

01) Scan table 'Products'
    Using index 'ProductName'


Appendix A Specifications

This appendix lists specifications for Microsoft Jet databases, tables, and queries. It also gives a comparison of data types, and shows how to estimate the size of Microsoft Jet tables and databases.

Database Specifications

Table Specifications

Query Specifications

Data Types

Estimating the Size of a Table or Database


Database Specifications

The following table gives the maximum size for various attributes of a database.

Attribute | Maximum
Database (.mdb) file size | 1 gigabyte. Because your database can include linked tables in other files, its total size is limited only by available storage capacity.
Number of characters in an object name | 64
Number of characters in a user password | 14
Number of characters in a database password | 20
Number of characters in a user name or group name | 20
Number of concurrent users | 255


Table Specifications

The following table gives the maximum size for various attributes of a table.

Attribute | Maximum
Number of characters in a table name | 64
Number of characters in a field name | 64
Number of fields in a table | 255
Table size | 1 gigabyte
Number of characters in a Text field | 255
Number of characters in a Text field for double-byte character sets (DBCS), which require 2 bytes to store a single character | 127
Number of characters in a Memo field | 1 gigabyte
Size of a Long Binary (OLE Object) field | 1 gigabyte
Number of indexes in a table | 32
Number of fields in an index | 10
Number of characters in an index | 255
Number of characters in a validation message | 255
Number of characters in a validation rule | 2048
Number of characters in a table or field description | 255
Number of characters in a record (excluding Memo and OLE Object fields) | 2000
Number of characters in a field property setting | 255


Query Specifications

The following table gives the maximum size for various attributes of a query.

Attribute | Maximum
Number of tables in a query | 32
Number of fields in a recordset | 255
Number of fields in an ORDER BY clause | 10
Number of levels of nested queries | 50
Number of characters in a parameter name | 255
Number of ANDs in an expression | 40
Number of characters in an SQL statement | Approximately 64,000


Data Types

Microsoft Jet stores data using the set of data types shown in the following table. However, keep in mind that each tool you use to view or manipulate data stored in Microsoft Jet might call the data type by a different name. For example, a Boolean field in Microsoft Jet is called a Yes/No field in table Design view in Microsoft Access. In SQL, the same field can be referred to several different ways: BIT, LOGICAL, LOGICAL1, YESNO, or BOOLEAN.

Data type | Field size | Range of values
Boolean | 1 byte | 0 or – 1
Byte | 1 byte | 0 through 255
Integer | 2 bytes | – 32,768 through 32,767
Long Integer | 4 bytes | – 2,147,483,648 through 2,147,483,647
Currency | 8 bytes | – 922,337,203,685,477.5808 through 922,337,203,685,477.5807
Single | 4 bytes | – 3.4E-38 through 3.4E+38
Double | 8 bytes | – 1.8E-308 through 1.8E+308
DateTime | 8 bytes | Date and time values between the years 100 and 9999. Stored as a floating-point value. The integer portion represents the number of days since December 30, 1899. The fractional portion represents the number of seconds since midnight.
Text | n bytes | 1 through 255 characters, stored variable length
Memo | 12+ bytes | 12 bytes in the record, plus the actual data length (14 bytes in versions prior to 3.0)
Long Binary (OLE Object) | 12+ bytes | 12 bytes in the record, plus the actual data length (14 bytes in versions prior to 3.0)
Binary | n bytes | 1 through 255 characters of binary data
GUID | 16 bytes | System-generated number that is guaranteed to be unique. Users should never write to this field.

Microsoft Jet recognizes several overlapping sets of data types. In Microsoft Access, there are four different contexts in which you may need to specify a data type — in table Design view, in the Query Parameters dialog box, in SQL view in a query, and in Visual Basic.

See Also For more information on these contexts and their functionality, see Microsoft Access Help.

The following table compares the sets of data types that correspond to each context. The first column lists the Microsoft Jet data type. The second column lists the Type property settings available in table Design view and the five FieldSize property settings for the Number data type. The third column lists the corresponding query parameter data types available for designing parameter queries in the Query Parameters dialog box. The fourth column lists the corresponding SQL data types defined by Microsoft Jet along with their valid synonyms. The fifth column lists the corresponding Visual Basic data types.

Microsoft Jet | Table fields | Query parameters | Microsoft Jet SQL and synonyms | Visual Basic
Boolean | Yes/No | Yes/No | BOOLEAN (Synonyms: BIT, LOGICAL, LOGICAL1, YESNO) | Boolean
Byte | Number (FieldSize =Byte) | Byte | BYTE (Synonym:INTEGER1) | Byte
Integer | Number (FieldSize =Integer) | Integer | SHORT (Synonyms: INTEGER2, SMALLINT) | Integer
Long Integer | Number (FieldSize =Long Integer) and AutoNumber (FieldSize =Long Integer) | Long Integer | COUNTER, INTEGER, INT, AUTOINCREMENT | Long
Currency | Currency | Currency | CURRENCY (Synonym: MONEY) | Currency
Single | Number (FieldSize =Single) | Single | SINGLE (Synonyms:FLOAT4, IEEESINGLE, REAL) | Single
Double | Number (FieldSize =Double) | Double | DOUBLE (Synonyms: FLOAT, NUMBER) | Double
DateTime | Date/Time | Date/Time | DATETIME (Synonyms: DATE, TIME, TIMESTAMP) | Date
Text | Text | Text | TEXT (Synonyms: ALPHANUMERIC, CHAR, CHARACTER, STRING, VARCHAR) | String
Long Text | Memo | Memo | LONGTEXT (Synonyms: MEMO, LONGCHAR, NOTE) | String
Long Binary | OLE Object | Binary, OLE Object | LONGBINARY (Synonyms: OLEOBJECT, GENERAL) | String
Binary | Not supported | Binary | BINARY (Synonym: VARBINARY) | String
GUID | Number or AutoNumber (FieldSize =Replication ID) | Replication ID | GUID | Not supported

Keep in mind the following points about data types:

  • The only difference between Number and AutoNumber is whether Microsoft Access automatically generates a value for you or whether you need to set the value manually.

  • Microsoft Access itself doesn’t use the Binary data type. It’s recognized only for use in queries on linked tables from other database products that support the Binary data type.

  • The INTEGER data type in Microsoft Jet SQL doesn’t correspond to the Integer data type for table fields, query parameters, or Visual Basic. Instead, in Microsoft Jet SQL, the INTEGER data type corresponds to a Long Integer data type for table fields and query parameters and to a Long data type in Visual Basic.

  • The VALUE reserved word doesn’t represent a data type defined by Microsoft Jet. However, in Microsoft Access or SQL queries, the VALUE reserved word can be considered a valid synonym for the Visual Basic Variant data type.

  • If you are setting the data type for a DAO object in Visual Basic code, you must set the object’s Type property.


Estimating the Size of a Table or Database

You can estimate the size of a Microsoft Jet table or database. You cannot determine the exact size of a table or database for the following reasons:

  • The size of system tables can vary greatly.

  • The estimate doesn’t account for the space taken up by indexes in your database.

  • For simplicity, the estimate assumes contiguous record storage. Microsoft Jet stores records on 2K pages and doesn’t split records across pages. Therefore, you might have unused space on individual data pages, generally less than the size of an average record.

  • The length of some fields can vary depending on the data they contain. When you estimate the size of a table that contains a variable-length field, the estimate is based on the maximum size of the field, not on the actual length of the data it contains. For example, suppose you define a variable-length field with a size of 25 characters, and you store the character Y in this field. The actual storage consumed will be 2 bytes: 1 for the character and 1 for the overhead. This is 23 bytes fewer than you might use to estimate the storage space for a table.

Consider the following when you estimate the size of a Microsoft Jet table or database:

  • Records in Microsoft Jet versions 1.0 through 2.5 databases have an overhead of 7 bytes per record, and records in version 3.0 and 3.5 databases have an overhead of 6 bytes per record.

  • Fields have an overhead of 1 byte, plus 1 byte for every 256 bytes of record storage.

  • Fixed-length fields for Yes/No, Byte, Integer, Long Integer, Single, Double, and Date/Time data types have an overhead of 1 byte per field, rounded to the next higher byte.

  • Zero-length text strings occupy 1 byte in the record (plus the overhead described in the preceding bullet).

  • For Microsoft Jet versions 1.0 through 2.5, the minimum size of a database is 65,536 bytes. These databases always grow in 32K chunks (32,768 bytes). Versions 3.0 and 3.5 databases have a minimum size of 40,960 bytes and grow in multiples of 2K.

  • A new database created in Microsoft Access 97 is 60K (61,440 bytes) because of additional system tables added by Microsoft Access.

The following table estimates the size of a table in a Microsoft Jet 3.5 database. The table has 10,000 records.

Field | Data type/Overhead description | Size in bytes | Overhead bytes
1 | Text | 25 | 1
2 | Text | 25 | 1
3 | Number/Double | 8 | 1
4 | Date/Time | 8 | 1
 | Record Overhead | N/A | 6
Totals |  | 66 | 10

The total size of the table is 66 + 10, or 76, bytes per record. Because the table has 10,000 records, the total size of the table is 760,000 bytes (76 x 10,000). Because this example takes into account the maximum size of the text fields, this is only an estimate of the table size. The actual size of the values in the text fields determine the actual size of the table.

The following table estimates the size of the Customers table in the Microsoft Jet 3.5 Northwind.mdb sample database. The Customers table has 91 records.

Field name/Overhead description | Data type | Size in bytes | Overhead bytes
CustomerID | Text | 5 | 1
CompanyName | Text | 40 | 1
ContactName | Text | 30 | 1
ContactTitle | Text | 30 | 1
Address | Text | 60 | 1
City | Text | 15 | 1
Region | Text | 15 | 1
PostalCode | Text | 10 | 1
Country | Text | 15 | 1
Phone | Text | 24 | 1
Fax | Text | 24 | 1
Record Overhead | N/A | N/A | 6
Overhead for >256 bytes of text | N/A | N/A | 1
Totals |  | 268 | 18

The total size of the Customers table is 268 + 18, or 286, bytes per record. Because the table has 91 records, the size of the table is 26,026 bytes (286 x 91). Again, because the actual sizes of the text field values aren’t known, this is only an estimate.


Appendix B SQL Reference

The following SQL statements define the Microsoft Jet SQL grammar, using the Backus-Naur Form (BNF).

This BNF follows the notation standards used by the American National Standards Institute (ANSI) in defining standard X.135-1992 (ANSI-SQL 92).

Symbol | Meaning
<> | Angle brackets delimit character strings that are the names of syntactic elements.
::= | Definition operator. Used in a production rule to separate the elements defined by the rule from its definition. <defined_element> ::= <definition>
{} | Grouping delimiter. The portion of the formula within the braces is explicitly specified.
[] | Optional elements delimiter. The portion of the formula within the brackets can be explicitly specified or it can be omitted.
| | Alternative operator. The vertical bar indicates that the portion of the formula following the bar is an alternative to the portion preceding the bar. If the vertical bar appears at a position where it is not enclosed in braces or square brackets, it specifies a complete alternative for the element defined by the production rule. If the vertical bar appears in a portion of a formula enclosed in braces or square brackets, it specifies alternatives for the contents of the innermost pair of braces or brackets.
... | Repeat indicator. The ellipsis indicates that the element to which it applies in a formula can be repeated any number of times.
!! | English text. Defines a syntactic element without the benefit of BNF notation.

<SQL-language-character> ::=
      <SQL-alpha-numeric-character>
    | <SQL-special-characters>

<SQL-alpha-numeric-character> ::=
      <lower-case-letter>
    | <upper-case-letter>
    | <digit>

<lower-case-letter> ::= [a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v | w | x | y | z]

<upper-case-letter> ::= [A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z]

<digit> ::= [0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9]

<SQL-special-characters> ::=
    <space>
  | <tab>
  | <newline>
  | <carriage-return>
  | <form-feed>
  | <double-quote>
  | <percent>
  | <ampersand>
  | <quote>
  | <asterisk>
  | <plus-sign>
  | <minus-sign>
  | <back-slash>
  | <solidus>
  | <colon>
  | <semicolon>
  | <less-than-character>
  | <equals-character>
  | <greater-than-character>
  | <question-mark>
  | <underscore>
  | <number-sign>

<space> ::= !! space character

<tab> ::= !! tab character

<newline>  ::= !! newline character

<carriage-return> ::= !! carriage-return character

<form-feed> ::= !! form-feed character
<double-quote> ::= "

<back-quote> ::= `

<percent> ::= %

<ampersand> ::= &

<quote> ::= '

<left-paren> ::= (

<right-paren> ::= )

<asterisk> ::= *

<plus-sign> ::= +

<comma> ::= ,

<minus sign> ::= -

<period> ::= .

<back-slash> ::= \

<solidus> ::= /

<colon> ::= :

<semicolon> ::= ;

<less-than-character> ::= <

<equals-character> ::= =

<greater-than-character> ::= >

<question-mark> ::= ?

<underscore> ::= _

<vertical-bar> ::= |

<left-bracket> ::= [

<right-bracket> ::= ]

<number-sign> ::= #

<whitespace> ::=
    <space>
  | <tab>
  | <newline>
  | <carriage-return>
  | <form-feed>

<key-word> ::=
    ALL | ANY | AS | ASC | AUTOINCREMENT | AVG
  | BETWEEN | BINARY | BOOLEAN | BY | BYTE
  | CHAR[ACTER] | COUNT | COUNTER | CURRENCY
  | DATABASE | DATABASENAME | DATE | DATETIME | DELETE | DESC | DISTINCT | DISTINCTROW
  | DOUBLE
  | EXISTS
  | FIRST | FLOAT | FROM
  | GROUP
  | HAVING
  | IN | INNER | INSERT | INT | INTEGER | INTO
  | JOIN
  | LAST | LEFT | LEVEL | LIKE | LONG | LONGBINARY | LONGTEXT
  | MAX | MIN
  | NOT | NULL
  | OLEOBJECT | ON | OPTION | ORDER | OUTER | OWNERACCESS
  | PARAMETERS | PERCENT | PIVOT | PROCEDURE
  | REAL | RIGHT
  | SELECT | SET | SHORT | SINGLE | SMALLINT | SOME | STDEV | STDEVP | SUM
  | TABLEID | TIMESTAMP | TEXT | TOP | TRANSFORM
  | UPDATE
  | VALUES | VAR | VARBINARY | VARCHAR | VARP
  | WHERE | WITH
  | YESNO

<token> ::=
    <literal>
  | <comparison-operator>
  | <special-symbol>
  | <identifier>
  | <key-word>

<special-symbol> ::=
    <left-paren>
  | <right-paren>
  | <comma>
  | <period>
  | <colon>
  | <semicolon>
  | <equals-operator>
  | <asterisk>
  | <left-bracket>
  | <right-bracket>
<identifier> ::=
  <SQL-alpha-numeric-character> [<SQL-language-character>...]
  !! Note: Can include up to 64 characters.

<identifier-delimiter> ::=
    <left-id-delim>
  | <right-id-delim>

<left-id-delim> ::= !! Left identifier delimiter
    <back-quote>
  | <left-bracket>

<right-id-delim> ::= !! Right identifier delimiter
    <back-quote>
  | <right-bracket>

<scalar-expression> ::=
    <numeric-scalar-expression>
  | <character-string-literal>

<numeric-scalar-expression> ::=
    <term>
  | <numeric-scalar-expression> [<plus-operator> | <minus-operator>] <term>

<term> ::=
    <factor>
  | <term> [<multiplication-operator> | <divides-operator>] <factor>

<factor> ::= [<plus-operator> | <minus-operator>]<primary>

<primary> ::=
    <atom>
  | <column-reference>
  | <function-reference>
  | <left-paren><scalar-expression><right-paren>

<atom> ::=
    <literal>
  | <parameter-name>

<literal> ::=
    <character-string-literal>
  | <numeric-literal>
  | <boolean-literal>
  | <datetime-literal>
  | NULL

<character-string-literal> ::= {<single-quote> | <double-quote>} <char-list> {<single-quote> | <double-quote>}  !! Note that the leading and trailing delimiter must be the same.  Embedded delimiters must be doubled as in Visual Basic.
<numeric-literal> ::=
    <exact-numeric-literal>
  | <approximate-numeric-literal>

<exact-numeric-literal> ::=
    <signed-integer>[<period><unsigned-integer>]
  |<period><unsigned-integer>

<approximate-numeric-literal> ::=
  <exact-numeric-literal>{E|e}<signed-integer>

<signed-integer> ::=
  [<plus-sign> | <minus-sign>]<unsigned-integer>

<unsigned-integer> ::= <digit> ...

<boolean-literal> ::= {TRUE | FALSE | YES | NO | ON | OFF}

<datetime-literal> ::= <number-sign><date-time-text><number-sign>

<date-time-text> ::= !! any literal that can be parsed as a valid date/time by the
expression service

<table-name> ::=
    <base-table-name>
  | <query-name>
  | <link-name>

<base-table-name> ::= <identifier>

<query-name> ::= <identifier>

<link-name> ::= <identifier>

<qualified-table-name> ::= [[ <connect-string> <period> ] <database-name> <period>]
  <table-name>

<connect-string> ::= <identifier> !!<connect-string> specifies the data source to look for the given <database-name> in.  If the connect string starts with the name of a Microsoft Jet data provider followed by a semicolon, then the given database is opened using that data provider.  If the data provider is missing, or if it is "ODBC", then the rest of the connect string is interpreted as an ODBC connect string.  In this case, a DSN field is added to the connect string containing the <database-name>.  If a DSN field had previously existed, it is replaced with this new DSN field.

<column-reference> ::= <identifier>

<column-reference-list> ::= <column-reference> [<comma> <column-reference> ...]

<constraint-name> ::= <identifier>
<index-name> ::= <identifier>

<database-name> ::= <identifier>

<manipulation-statement> ::=  [<parameter-declaration> <semicolon>] <data-manipulation-language-statement> <semicolon>
!! Note: Microsoft Jet SQL data manipulation statements consist of an optional <parameter-declaration> followed by a single <data-manipulation-language-statement>.  Manipulation statements define stored or temporary query objects in Microsoft Jet.  Note that subqueries are not supported and similar functionality may be used by using one query as the input to another.

<parameter-declaration> ::= PARAMETERS <parameter-definition-list>

<parameter-definition-list> ::= <parameter-definition> [{<comma> <parameter-definition>} ...]

<parameter-definition> ::= <parameter-name> <parameter-datatype>

<parameter-name> ::= <identifier>

<parameter-datatype> ::=
    <Jet-parameter-datatype-name>
  | <ANSI-SQL-parameter-datatype-name>

<Jet-parameter-datatype-name> ::=
    BOOLEAN
  | BYTE <left-paren> <length> <right-paren>
  | SHORT
  | LONG
  | CURRENCY
  | SINGLE
  | DOUBLE
  | DATETIME
  | TEXT <left-paren> <length> <right-paren>
  | BINARY
  | LONGTEXT <left-paren> <length> <right-paren>
  | LONGBINARY
  | DATABASE
  | TABLEID
  | OLEOBJECT
  | YESNO

<ANSI-SQL-parameter-datatype-name> ::=
    SMALLINT
  | INT[EGER]
  | REAL
  | FLOAT
  | CHAR[ACTER] <left-paren> <length> <right-paren>
  | VARCHAR <left-paren> <length> <right-paren>
<data-definition-language-statement> ::=
    <table-definition>
  | <alter-table-statement>
  | <drop-table-statement>
  | <index-definition>
  | <drop-index-statement>

<alter-table-statement>  ::=
  ALTER TABLE <base-table-name>
  {ADD
    {COLUMN <column-reference>> <parameter-datatype>[CONSTRAINT <field-constraint>]
  | CONSTRAINT <table-constraint> }
  | DROP {COLUMN <column-reference> | CONSTRAINT <constraint-name> }}

<index-definition>  ::=
  CREATE  [ UNIQUE ] INDEX <index-name>
  ON <base-table-name> <left-paren> <column-reference>
  [ASC|DESC][{<comma> <column-reference> [ASC|DESC]} ...] <right-paren>
  [WITH { PRIMARY | DISALLOW NULL | IGNORE NULL }]

<table-definition>  :==
  CREATE TABLE <base-table-name>
  <left-paren> {<column-reference> <parameter-datatype>
   [CONSTRAINT <field-constraint>]}
  [{<comma> <column-reference> <parameter-datatype>
  [CONSTRAINT <field-constraint>]}...]]]<right-paren>

<drop-table-statement>  ::=
  DROP TABLE <base-table-name>

<drop-index-statement>  ::=
  DROP  INDEX <index-name> ON <base-table-name>

<column-constraint-definition> ::= <constraint-name-definition>
  <column-constraint>

<column-constraint> ::= <unique-specification> |
  <references-specification>

<constraint-name-definition> ::= CONSTRAINT <constraint-name>

<table-constraint-definition> ::= <constraint-name-definition>
  <table-constraint>

<table-constraint> ::=
    <unique-constraint-definition>
  | <referential-constraint-definition>

<unique-constraint-definition> ::=
  <unique-specification> <left-paren> <column-reference-list> <right-paren>
<unique-specification> ::= UNIQUE | PRIMARY KEY

<referential-constraint-definition> ::=
  FOREIGN KEY <left-paren> <referencing-columns> <right-paren>
  <references-specification>

<references-specification> ::=
  REFERENCES <referenced-table-and-columns>

<referencing-columns> ::= <column-reference-list>

<referenced-table-and-columns> ::= <table-name>
  <left-paren> <reference-column-list> <right-paren>

<reference-column-list> ::= <column-reference-list>

<data-manipulation-language-statement>  :==
  <delete-statement>
  | <insert-statement>
  | <select-statement>
  | <select-into-statement>
  | <transform-statement>
  | <update-statement>

<delete-statement> ::= DELETE <from-clause> [where-clause]

<insert-statement> ::=
  INSERT INTO <table-name> [IN external-database] <insert-columns-and-source>

<insert-columns-and-source> ::=
  {<left-paren> <column-reference-list> <right-paren>}  {SELECT <column-reference-list>
  FROM <table-expression>
  | VALUES <left-paren><scalar-expression-list><right-paren>}

<select-statement> ::=
  SELECT <set-quantifier> <select list> <table expression>

<table-expression> ::= 	<from-clause>
  [<where-clause>]
  [<group-by-clause>]
  [<having-clause>]
  [<order-by-clause>]
  [<with-owneraccess-clause]

<select-list> ::=
    <asterisk>
  | <select-sublist> [{<comma> <select-sublist>}...]

<select-sublist> ::=
    <column-qualifier><period><asterisk>
  | <scalar-expression> [AS <column-reference>]
<from-clause> ::= FROM <table-reference-list>

<table-reference-list> ::=
  <table-reference> [{<comma> <table-reference>}...] [<in-clause>]

<table-reference> ::=
  {<from-table-name> [AS <correlation-name>]} | <joined-table>

<from-table-name> ::=
    <table-name>
  | <tableid-parameter>

<tableid-parameter> ::= <parameter> <identifier> of type TABLEID

<in-clause> ::=
  IN
    <filename-path>		!! For local, Jet-native databases
  | {<left-id-delim><filename-path><right-id-delim> <left-id-delim><database-name><right-id-delim>}

<filename-path> ::= !! as defined by operating system

<joined-table> ::=
  <table-reference> [ <join-type> ] JOIN <table-reference> ON <search-condition>

<join-type> ::=
    INNER
  | LEFT
  | RIGHT

<where-clause> ::= WHERE <search-condition>

<search-condition> ::=
    <boolean-term>
  | <search-condition> OR <boolean-term>

<boolean-term> ::=
    <boolean-factor>
  | <boolean-term> AND <boolean-factor>

<boolean-factor> ::= [ NOT ] <boolean-primary>

<boolean-primary> ::=
    <predicate>
  | <left-paren><search-condition><right-paren>

<predicate> ::=
    <comparison-predicate>
  | <between-predicate>
  | <in-predicate>
  | <like-predicate>
  | <null-predicate>

<comparison-predicate> ::=
  <scalar-expression><comparison-operator><scalar-expression>

<between-predicate> ::=
  <scalar-expression> [NOT] BETWEEN <scalar-expression> AND <scalar-expression>
  !! The BETWEEN predicate y BETWEEN x AND z is semantically equivalent to (x <= y AND y <= z) OR (z <= y AND y <= x).

<in-predicate> ::=
  <scalar-expression> [NOT] IN <left-paren> <scalar-expression-list> <right-paren>

<scalar-expression-list> ::= <scalar-expression> [{<comma> <scalar-expression>}...]

<like-predicate> ::= <scalar-expression> [NOT] LIKE
    <scalar-expression>
  | <pattern>

<pattern> ::=
    <left-bracket> <char-list> <right-bracket>
    !! matches any single character in <char-list>
  | <left-bracket> <bang> <char-list> <right-bracket>
    !! matches any single character not in <char-list>
  | <left-bracket> <char-range> <right-bracket>
    !! matches any single character in <char-range>
  | <left-bracket> <bang> <char-range> <right-bracket>
    !! matches any single character not in <char-range>

<pattern-matching-character> ::=
    <question-mark>  !! matches any single <SQL-alphabetic-character>
  | <pound-sign>  !! matches any single <digit>
  | <asterisk>  !! matches any sequence of zero or more <SQL-alphabetic characters>

<dash> ::= -

<escape-character> ::= ^

<bang> ::= !
<char-list> ::=
    <SQL-alpha-numeric-character>
  | {<escape-character> <special-symbol>}
    [[<SQL-alpha-numeric-character>
  | {<escape-character> <special-symbol>}] ...]

<char-range> ::=
  <SQL-alpha-numeric-character> <dash> <SQL-alpha-numeric-character>
  !! Where the first <SQL-alpha-numeric-character> has lower ordinal value than the
  !! second <SQL-alpha-numeric-character>

<null-predicate> ::= <scalar-expression> IS [NOT] NULL

<group-by-clause> ::= GROUP BY <grouping-specification-list>

<grouping-specification-list> ::= <grouping-specification>[{<comma> <grouping-specification>}...]

<grouping-specification> ::=
    <column-reference>
  | <scalar-expression>

<having-clause> ::= HAVING <search-condition>

<order-by-clause> ::= ORDER BY <sort-specification-list>

<sort-specification-list> ::=
  <sort-specification> [{<comma> <sort-specification>}...]

<sort-specification> ::=
  <scalar-expression> [ASC | DESC]

<select-into-statement>  ::=
  SELECT <set-quantifier> <reference-column-list>INTO <base-table-name> [in-clause]
   <table-expression>

<transform-statement> ::=
  TRANSFORM <aggregate-function> <select-statement> PIVOT <pivot-field>
  [IN <left-paren> <scalar-expression> [{<comma> <scalar-expression>}...]<right-paren>]

<union-clause> ::=
  <tabular-data> UNION [ALL] <tabular-data> [{UNION [ ALL ] <tabular-data>}  ...]

<update-statement> ::=
  UPDATE <table-reference-list> SET <set-clause-list> [<where-clause>]

<set-clause-list> ::= <set-clause> [{<comma> <set-clause>}...]

<set-clause> ::=
  <column-reference> <equals-operator>
    {<scalar-expression>
  | NULL}

<aggregate-reference>::=
    {Avg
  | Count
  | First
  | Last
  | Min
  | Max
  | StDev
  | StDevP
  | Sum
  | Var
  | VarP} <left-paren><scalar-expression><right-paren>

<comparison-operator> ::=
    <equals-operator>
  | <greater-than-operator>
  | <less-than-operator>
  | <greater-than-or-equals-to-operator>
  | <less-than-or-equals-to-operator>
  | <not-equals-operator>

<less-than-operator> ::= <less-than-character>

<greater-than-operator> ::= <greater-than-character>

<equals-operator> ::= <equals-character>

<greater-than-or-equals-to-operator> :== <greater-than-operator> <equals-operator>

<less-than-or-equals-to-operator> ::= <less-than-operator> <equals-operator>

<not-equals-operator> ::= <bang> <equals-operator>

<plus-operator> ::= <plus-sign>

<minus-operator> ::= <minus-sign>

<database-type> ::=
    {dBASE III
  | dBASE IV
  | Paradox 3.x
  | Paradox 4.x
  | Btrieve
  | FoxPro 2.5
  | Excel 3.0
  | Excel 4.0
  | Excel 5.0
  | ODBC
  }<semicolon>

<pivot-field> ::= <identifier>

<set-quantifier> ::=
    ALL
  | DISTINCT
  | {TOP <unsigned-integer> [PERCENT]}

<tabular-data> ::=
    <select-statement>
  | <stored-query>
  | TABLE <base_table_name>


Appendix C Registry Settings

This appendix describes the settings that Microsoft Jet 3.5 installs into the Windows registry. These settings configure Microsoft Jet and control how external data formats are handled.

ISAM Formats Settings

Engine Settings


ISAM Formats Settings

The ISAM Formats key is used by client applications to enumerate the available data formats and to find the corresponding engine that supports each format. The ISAM Formats key is located at \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet
\3.5\ISAM Formats, and contains a subkey for each data format supported. The following table describes the registry settings contained in the data format subkeys and gives example values.

Setting | Description | Example values
Engine | Provides the name of the engine. | “Paradox”, “Xbase”
ExportFilter | Provides a filter string that is passed to the common open dialog boxes used for exporting files. | “Paradox (*.db)”
ImportFilter | Provides a filter string that is passed to the common open dialog boxes used for importing files. | “Paradox (*.db)”
CanLink | True if Microsoft Jet supports linking to this format. Possible values are 01 (True) and 00 (False). | 01, 00
IsamType | Specifies the transfer macro this ISAM driver applies to (used by Microsoft Access) where 0 = database, 1 = spreadsheet, and 2 = text. | 0, 1, 2
OneTablePerFile | Specifies whether the ISAM driver has one table per file and is required by client applications that need to prompt for file names. Possible values are 01 (True) and 00 (False). | 01, 00
IndexDialog | Specifies whether the ISAM driver requires a separate dialog box for index files. If the value of this setting is True, then a value for the IndexFilter setting must also be provided. Possible values are 01 (True) and 00 (False). | 01, 00
IndexFilter | This setting has the same format as the Filter setting, but is used to prompt for index file names. | “FoxPro Index (*.idx; *.cdx)”
CreateDBOnExport | If set to True, the file name specified for export need not exist for the export to function. Possible values are 01 (True) and 00 (False). | 01, 00
ResultTextImport | The common dialog result string for importing. | “Import data and objects from the external database into the current database.”
ResultTextLink | The common dialog result string for linking. | “Create tables in the current database that are linked to tables in the external file.”
ResultTextExport | The common dialog result string for exporting. | “Export data from the current database into an Excel spreadsheet.”
SupportsLongNames | If set to 01 (True), supports the Windows 95 long file name convention. Possible values are 01 (True) and 00 (False). | 01, 00

The rest of this section gives the settings and values for each data format subkey that the ISAM Formats key contains.


Microsoft Jet 2.x ISAM Formats Settings

The following table gives the settings and values that Microsoft Jet 3.5 installs for the Microsoft Jet format. These settings are located in the \HKEY_LOCAL_MACHINE \SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\Jet 2.x key of the Windows registry.

Setting | Value
Engine | Jet 2.x
IsamType | 0
OneTablePerFile | 00
IndexDialog | 00
CreateDBOnExport | 00


Paradox ISAM Formats Settings

The following tables give the settings and values that Microsoft Jet 3.5 installs for the Paradox formats. These settings are located in the \HKEY_LOCAL_MACHINE
\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\Paradox version key of the Windows registry, where version is a Paradox version number.


Paradox 3.x

Setting | Value
Engine | Paradox
ExportFilter | Paradox 3 (*.db)
ImportFilter | Paradox (*.db)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 00
CreateDBOnExport | 00
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ResultTextExport | Export data from the current database into a Paradox 3.0 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


Paradox 4.x

Setting | Value
Engine | Paradox
ExportFilter | Paradox 4 (*.db)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 00
CreateDBOnExport | 00
ResultTextExport | Export data from the current database into a Paradox 4.0 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


Paradox 5.x

Setting | Value
Engine | Paradox
ExportFilter | Paradox 5 (*.db)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 00
CreateDBOnExport | 00
ResultTextExport | Export data from the current database into a Paradox 5.0 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


dBASE ISAM Formats Settings

The following tables give the settings and values that Microsoft Jet 3.5 installs for the dBASE formats. These settings are located in the \HKEY_LOCAL_MACHINE
\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\dBASE version key of the Windows registry, where version is III, IV, or 5.0.


dBASE III

Setting | Value
Engine | Xbase
ExportFilter | dBASE III (*.dbf)
ImportFilter | dBASE III (*.dbf)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | dBASE Index (*.ndx)
CreateDBOnExport | 00
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ResultTextExport | Export data from the current database into a dBASE III file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


dBASE IV

Setting | Value
Engine | Xbase
ExportFilter | dBASE IV (*.dbf)
ImportFilter | dBASE IV (*.dbf)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | dBASE Index (*.ndx, *.mdx)
CreateDBOnExport | 00
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ResultTextExport | Export data from the current database into a dBASE IV file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


dBASE 5.0

Setting | Value
Engine | Xbase
ExportFilter | dBASE 5 (*.dbf)
ImportFilter | dBASE 5 (*.dbf)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | dBASE Index (*.ndx, *.mdx)
CreateDBOnExport | 00
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ResultTextExport | Export data from the current database into a dBASE 5.0 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


FoxPro ISAM Formats Settings

The following tables give the settings and values that Microsoft Jet 3.5 installs for the Microsoft FoxPro formats. These settings are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\FoxPro version key of the Windows registry, where version is 2.0, 2.5, 2.6, 3.0, or DBC.


Microsoft FoxPro 2.0

Setting | Value
Engine | Xbase
ExportFilter | Microsoft FoxPro 2.0 (*.dbf)
ImportFilter | Microsoft FoxPro (*.dbf)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | FoxPro Index (*.idx; *.cdx)
CreateDBOnExport | 00


Microsoft FoxPro 2.0 (continued)

Setting | Value
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ResultTextExport | Export data from the current database into a FoxPro 2.0 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


Microsoft FoxPro 2.5

Setting | Value
Engine | Xbase
ExportFilter | Microsoft FoxPro 2.5 (*.dbf)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | FoxPro Index (*.idx; *.cdx)
CreateDBOnExport | 00
ResultTextExport | Export data from the current database into a FoxPro 2.5 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


Microsoft FoxPro 2.6

Setting | Value
Engine | Xbase
ExportFilter | Microsoft FoxPro 2.6 (*.dbf)
CanLink | 01
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | FoxPro Index (*.idx; *.cdx)
CreateDBOnExport | 00
ResultTextExport | Export data from the current database into a FoxPro 2.6 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


Microsoft FoxPro 3.0

Setting | Value
Engine | Xbase
ExportFilter | Microsoft FoxPro 3.0 (*.dbf)
CanLink | 00
OneTablePerFile | 01
IsamType | 0
IndexDialog | 01
IndexFilter | FoxPro Index (*.idx; *.cdx)
CreateDBOnExport | 00
ResultTextExport | Export data from the current database into a FoxPro 3.0 file. This process will overwrite the data if exported to an existing file.
SupportsLongNames | 00


Microsoft FoxPro DBC

Setting | Value
Engine | Xbase
ImportFilter | Microsoft FoxPro 3.0 (*.dbc)
CanLink | 00
OneTablePerFile | 00
IsamType | 0
IndexDialog | 00
CreateDBOnExport | 00
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
SupportsLongNames | 00


Text ISAM Formats Settings

The following table gives the settings and values that Microsoft Jet 3.5 installs for the text formats. These settings are located in the \HKEY_LOCAL_MACHINE
\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\Text key of the Windows registry.

Note The FormatFunction, WizardProject, ImportWizardFunction, and ExportWizardFunction settings are added by Microsoft Access.

Setting | Value
Engine | Text
ExportFilter | Text files (*.txt; *.csv; *.tab; *.asc)
ImportFilter | Text files (*.txt; *.csv; *.tab; *.asc)
CanLink | 01
OneTablePerFile | 01
IsamType | 2
IndexDialog | 00
CreateDBOnExport | 00
FormatFunction | txt,SOA_RptToAscii,1,MS-DOS Text (*.txt)
WizardProject | wzlib80.mde
ImportWizardFunction | mx_entry
ExportWizardFunction | mx_entry
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ResultTextExport | Export data from the current database into a text file. This process will overwrite data if exported to an existing file.
OutputFormat | MS-DOS Text (*.txt)
ReportFormat | MS-DOS Text
SupportsLongNames | 01


Microsoft Excel ISAM Formats Settings

The following tables give the settings and values that Microsoft Jet 3.5 installs for the Microsoft Excel formats. These settings are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\Excel version key of the Windows registry, where version is 3.0, 4.0, 5.0, or 8.0.

Note The WizardProject and ImportWizardFunction settings listed in the following tables are added by Microsoft Access.


Microsoft Excel 3.0

Setting | Value
Engine | Excel
ExportFilter | Microsoft Excel 3 (*.xls)
CanLink | 01
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
CreateDBOnExport | 01
SupportsLongNames | 01
WizardProject | Wzlib80.mde
ImportWizardFunction | mx_entry
ResultTextExport | Export data from the current database into a Microsoft Excel 3.0 file. This process will overwrite data if exported to an existing file.


Microsoft Excel 4.0

Setting | Value
Engine | Excel
ExportFilter | Microsoft Excel 4 (*.xls)
CanLink | 01
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
CreateDBOnExport | 01
SupportsLongNames | 01
WizardProject | Wzlib80.mde
ImportWizardFunction | mx_entry
ResultTextExport | Export data from the current database into a Microsoft Excel 4.0 file. This process will overwrite data if exported to an existing file.


Microsoft Excel 5.0

Note The settings and values shown for Microsoft Excel 5.0 also apply to Microsoft Excel version 7.0.

Setting | Value
Engine | Excel
ExportFilter | Microsoft Excel 5-7 (*.xls)
ImportFilter | Microsoft Excel (*.xls)
CanLink | 01


Microsoft Excel 5.0 (continued)

Setting | Value
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
CreateDBOnExport | 01
SupportsLongNames | 01
OutputFormat | Microsoft Excel (*.xls)
ReportFormat | Microsoft Excel
WizardProject | Wzlib80.mde
ImportWizardFunction | mx_entry
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file. Only one user at a time can change data in the file.
ResultTextExport | Export data from the current database into a Microsoft Excel 5.0 file.


Microsoft Excel 97

Setting | Value
Engine | Excel
ExportFilter | Microsoft Excel 97 (*.xls)
ImportFilter | Microsoft Excel (*.xls)
CanLink | 01
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
CreateDBOnExport | 01
SupportsLongNames | 01
OutputFormat | Microsoft Excel (*.xls)
ReportFormat | Microsoft Excel
WizardProject | Wzlib80.mde
ImportWizardFunction | mx_entry
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file. Only one user at a time can change data in the file.
ResultTextExport | Export data from the current database into a Microsoft Excel 5.0 file.


Lotus 1-2-3 ISAM Formats Settings

The following tables give the settings and values that Microsoft Jet 3.5 installs for the Lotus 1-2-3 formats. These settings are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\Lotus version key of the Windows registry, where version is WK1, WK3, or WK4.

Note The WizardProject and ImportWizardFunction settings listed in the following tables are added by Microsoft Access.


Lotus WK1

Setting | Value
Engine | Lotus
ExportFilter | Lotus 1-2-3 WK1 (*.wk1)
ImportFilter | Lotus 1-2-3 (*.wk*)
CanLink | 00
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
SupportsLongNames | 01
WizardProject | Wzlib80.mde
CreateDBOnExport | 01
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextExport | Export data from the current database into Lotus 1-2-3 version 2.0 file. This process will overwrite data if exported to an existing file.


Lotus WK3

Setting | Value
Engine | Lotus
ExportFilter | Lotus 1-2-3 WK3 (*.wk3)
CanLink | 00
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
CreateDBOnExport | 01
SupportsLongNames | 01


Lotus WK3 (continued)

Setting | Value
WizardProject | Wzlib80.mde
ImportWizardFunction | mx_entry
ResultTextExport | Export data from the current database into Lotus 1-2-3 version 3.0 file.


Lotus WK4

Setting | Value
Engine | Lotus
CanLink | 00
OneTablePerFile | 00
IsamType | 1
IndexDialog | 00
CreateDBOnExport | 01
SupportsLongNames | 01
WizardProject | Wzlib80.mde
ImportWizardFunction | mx_entry


HTML Export ISAM Formats Settings

The following table gives the settings and values that Microsoft Jet 3.5 installs for the HTML Export formats. These settings are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\HTML Export key of the Windows registry.

Setting | Value
Engine | Text
ExportFilter | HTML Documents (*.html, *.htm)
CanLink | 00
OneTablePerFile | 01
IsamType | 2
IndexDialog | 00
CreateDBOnExport | 00
ResultTextExport | Export data from the current database into a text file. This process will overwrite data if exported to an existing file.
OutputFormat | HTML (*.html)
ReportFormat | HTML
SupportsLongNames | 01


HTML Import ISAM Formats Settings

The following table gives the settings and values that Microsoft Jet 3.5 installs for the HTML Import formats. These settings are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\HTML Import key of the Windows registry.

Setting | Value
Engine | Text
ImportFilter | HTML Documents (*.html,*.htm)
CanLink | 01
OneTablePerFile | 00
IsamType | 2
IndexDialog | 00
CreateDBOnExport | 00
ResultTextImport | Import data from the external file into the current database. Changing data in the current database will not change data in the external file.
ResultTextLink | Create a table in the current database that is linked to the external file. Changing data in the current database will change data in the external file.
ImportWizardFunction | mx_entry
SupportsLongNames | 01


Exchange ISAM Formats Settings

The following table gives the settings and values that Microsoft Jet 3.5 installs for the Exchange formats. These settings are located in the \HKEY_LOCAL_MACHINE
\SOFTWARE\Microsoft\Jet\3.5\ISAM Formats\Exchange 4.0 key of the Windows registry.

Setting | Value
Engine | Exchange
ImportFilter | Exchange (*.*)
CanLink | 01
OneTablePerFile | 00
IsamType | 3
IndexDialog | 00
CreateDBOnExport | 00
ImportWizardFunction | mx_entry
WizardProject | Wzmapi80.mde
SupportsLongNames | 01


Engine Settings

The Engines key is located at \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft
\Jet\3.5\Engines in the Windows registry, and contains a subkey for each engine and installable ISAM driver supported.


Common Engine Settings

This section describes the settings that are common to two or more of the engines in the Engines key of the Windows registry. The following table describes the registry settings contained in the Engines key.

Note For Microsoft Jet 3.0, the two settings listed in the following table were repeated under the subkeys for each engine.

Setting | Data type | Default value | Description
CompactByPKey | Integer | 1 | Disables or enables compacting by primary keys. When the value of this setting is 1 and the user compacts a database, records will be sorted in primary key order. This creates a pseudo-clustered index effect that Microsoft Jet doesn’t maintain during normal use of the database. A value of 0 disables this feature.
SystemDB | String | “system.mdb” | The path and name of the workgroup information file (also known as the SystemDB database) that is used. If no path is specified, the current directory is used.

The following table describes a registry setting that all installable ISAM drivers must provide in their subkey, located at \HKEY_LOCAL_MACHINE\SOFTWARE\ Microsoft\Jet\3.5\Engines\enginename. This setting is required.

Setting | Data type | Default value | Description
win32 | String | varies, depending on the engine | The path and name of the DLL that supplies the engine services.

The settings described in the following table are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Jet 2.x\ISAM key of the Windows registry. They apply to Microsoft Jet version 1.x and 2.x databases. This registry key is used by Microsoft Jet when a version 1.x or 2.x database is accessed.

Unlike Microsoft Jet 3.0, when either Microsoft Jet 3.5 or the Microsoft Jet 2.x installable ISAM driver is registered, default settings and values are created.

Note In Microsoft Jet 3.5, you can change the values of these settings at run time by using the SetOption method in DAO code. For more information on the SetOption method, see Chapter 13, “Optimizing Performance.”

Setting | Data type | Default value | Description
PageTimeout | Integer | 5 | The length of time between when data that is not read-locked is placed in an internal cache and when it is invalidated (expressed in tenths of a second).
LockedPageTimeout | Integer | 5 | The length of time between when data that is read-locked is placed in an internal cache and when it is invalidated (expressed in tenths of a second).
LockRetry | Integer | 20 | The number of times to repeat attempts to access a locked page. (Note that the LockRetry setting is related to the CommitLockRetry setting.)
CommitLockRetry | Integer | 20 | The number of times Microsoft Jet attempts to get a lock on data to commit changes to that data. If it fails to get a commit lock, updates to the data will be unsuccessful. The number of attempts Microsoft Jet makes to get a commit lock is directly related the LockRetry setting. For each attempt made at getting a commit lock, Microsoft Jet will try LockRetry the same number of times to get a lock. For example, if the value of the CommitLockRetry setting is 20 and the value of the LockRetry setting is 20, Microsoft Jet will attempt to get a commit lock as many as 20 times; and for each of those times, it can try to get a lock as many as 20 times, for a total of 400 attempts at locking.
CursorTimeout | Integer | 5 | This value is applicable only to version 1.x of Microsoft Jet. The CursorTimeout setting controls the amount of time a reference to a page (a cursor) will remain on that page. If a cursor times out, the internal reference to the page it was referring to is removed. A page can be freed only if all cursors have timed out. The cursor timeout value is measured in tenths of a second and can have a value from 0 to approximately 65,000.
IdleFrequency | Integer | 10 | Number of internal operations before causing an Idle call internally.
ForceOSFlush | Integer | 0 | Any setting other than 0 means a commit or a write will force flushing the operating system cache to disk. A setting of 0 means no force flush occurs.

Setting | Data type | Default value | Description
MaxBufferSize | Integer | 512 | The size of the Microsoft Jet internal cache, measured in kilobytes (K). The value of the MaxBufferSize setting must be between 9 and 4096, inclusive.
ReadAheadPages | Integer | 8 | Number of pages, measured in kilobytes (K), that Microsoft Jet attempts to read ahead for long value objects. The default value is 8K, which corresponds to four data pages.

The registry settings described in the following table are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Jet 3.5 key of the Windows registry. They control access to Microsoft Jet data. None of these keys are automatically created.

Setting | Data type | Default value | Description
ExclusiveAsyncDelay | Integer | 2000 | Maximum time before asynchronous changes will be written when the database is opened exclusively.
FlushTransactionTimeout | Integer | 500 | The amount of time of inactivity before the asynchronous write cache is flushed to disk. This registry setting causes the ExclusiveAsyncDelay and SharedAsyncDelay registry settings to be ignored.
ImplicitCommitSync | String | No | A value of Yes indicates that Microsoft Jet will wait for commits to finish. A value other than Yes means that Microsoft Jet will perform commits asynchronously.
LockDelay | Integer | 100 | Depending on the network operating system, Microsoft Jet can cause short bursts of network traffic when attempting to retry for a lock. In order to prevent these short bursts of network traffic that could cause performance problems for the network administrator, this registry setting works in conjunction with the LockRetry registry setting and places a delay of the specified number of milliseconds between every lock retry. Besides eliminating the short burst of lock retries that could occur on certain network operating systems, this setting provides a more consistent feel to users when they encounter locking conflicts.
LockRetry | Integer | 20 | The number of times to repeat attempts to access a locked page.

Setting | Data type | Default value | Description
MaxBufferSize | Integer | calculated | The size of the Microsoft Jet internal memory cache, measured in kilobytes (K). The value of the MaxBufferSize setting can be a minimum of 512 or the maximum that is limited by available memory. The MaxBufferSize setting should never be set to a value larger than half the total memory. The default is calculated by the following formula: ((Total RAM in MB - 12)/4) + 512K. For example, on a system with 32 MB of RAM, the default buffer size is ((32 MB - 12 MB) / 4) + 512K, or 5632K. If the default value of zero is used, the maximum that Microsoft Jet will allocate is 13,824K.
MaxLocksPerFile | Integer | 9500 | Maximum number of accumulated locks allowed before an explicit transaction will automatically partially commit. While this setting is primarily for NetWare users, it is also useful to users of Windows NT Server or Workstation 3.5x because performance will start to degrade when Windows NT Server or Workstation has to manage more than 9500 locks. Windows NT Server and Workstation version 4.0 address many of those performance issues, but still runs optimally with the MaxLocksPerFile registry setting at its default value.
PageTimeout | Integer | 5000 | The length of time between when data is placed in an internal cache and when it is checked to be potentially invalidated, expressed in milliseconds. In general, you shouldn’t change the value of the PageTimeout setting. If the cache is not being updated quickly enough to see other users’ changes, you can override the PageTimeout setting and refresh the cache by using the dbRefreshCache argument of the Idle method in DAO code. This allows users to see other users’ changes immediately. For more information, search the Help Index for ”Idle method.”

Setting | Data type | Default value | Description
RecycleLVs | Integer | 0 | The RecycleLVs setting determines when discarded long value (LV) pages become available for reuse. When the setting has a value of 0, discarded LV pages continue to occupy space in the database and only become available for reuse after the last user closes the database. You can change the RecycleLVs setting to 1 so that discarded LV pages become available for reuse after Microsoft Jet determines that there’s only one user in the database in shared mode and that new LV data has been added to the database. Note Setting RecycleLVs to 1 slows down performance somewhat when manipulating long value data types. Microsoft Access will dynamically turn this feature on immediately before manipulating any of its objects that utilize LV data and will turn it off when those objects are no longer being manipulated. This provides the user with optimal performance while minimizing database bloat.
SharedAsyncDelay | Integer | 50 | Maximum time before asynchronous changes will be written when the database is opened in shared mode.
Threads | Integer | 3 | Number of background threads for the engine to use.
UserCommitSync | String | Yes | When the setting has a value of Yes, Microsoft Jet will wait for commits to finish. Any other value means that Microsoft Jet will perform commits asynchronously.

See Also For more information on how these registry settings affect transactions and performance, see Chapter 13, “Optimizing Performance.”


ODBC Engine Settings

The registry settings described in the following table are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\ODBC key of the Windows registry. They are not installed by any Microsoft ODBC client applications. As users can do with many of the Microsoft Jet registry settings, they can change the default behavior by creating the appropriate keys and settings, and then setting values accordingly.

Setting | Data type | Default value | Description
LoginTimeout | Integer | 20 | Number of seconds to wait for a login attempt to succeed.
QueryTimeout | Integer | 60 | Number of seconds to wait for a query to execute before failing.
ConnectionTimeout | Integer | 600 | Number of seconds to wait before closing an idle connection.
AsyncRetryInterval | Integer | 500 | Number of milliseconds to wait between retries on an asynchronous connection.
AttachCaseSensitive | Boolean | 0 | If False (0), will attach to the first name matching the specified string in a match that isn’t case-sensitive. If True (1), requires a case-sensitive match of the name.
AttachableObjects | String | TABLE | The list of objects that Microsoft Jet will allow attachments to (TABLE, NEW, SYSTEMTABLE, ALIAS, SYNONYM). Values are of type REG_SZ.
SnapshotOnly | Boolean | 0 | If False (0), you will get index information on attachment to allow dynasets if possible. If True (1), Microsoft Jet will ignore index information and thereby force snapshots on all attachments.
TraceSQLMode | Boolean | 0 | Initiates sending a trace of SQL statements sent to an ODBC data source to the file Sqlout.txt. Values are 0 (False) and 1 (True). Values are of type REG_DWORD. This setting is interchangeable with SQLTracemode.
TraceODBCAPI | Boolean | 0 | Initiates sending a trace of ODBC API calls to the file Odbcapi.txt. Values are 0 (False) and 1 (True). Values are of type REG_DWORD.
DisableAsync | Boolean | 0 | Forces synchronous query execution. Values are 0 (use asynchronous query execution if possible) and 1 (force synchronous query execution). Values are of type REG_DWORD.
TryJetAuth | Boolean | 1 | Uses the Microsoft Access user name and password to log on to the server before prompting. Values are 0 (False) and 1 (True). Values are of type REG_DWORD.

Setting | Data type | Default value | Description
PreparedInsert | Boolean | F | Uses a prepared INSERT statement that inserts data in all columns. Values are 0 (use a custom INSERT statement that inserts only non-Null values) and 1 (use a prepared INSERT statement). Values are of type REG_DWORD. Using prepared INSERT statements can cause Null values to overwrite server defaults and can cause triggers to execute on columns that weren’t inserted explicitly.
PreparedUpdate | Boolean | F | Uses a prepared UPDATE statement that updates data in all columns. Values are 0 (use a custom UPDATE statement that sets only columns that have changed) and 1 (use a prepared UPDATE statement). Values are of type REG_DWORD. Using prepared UPDATE statements can cause triggers to execute on columns that weren’t changed explicitly.
FastRequery | Boolean | F | Uses a prepared SELECT statement for parameterized queries. Values are 0 (False) and 1 (True). Values are of type REG_DWORD.


Xbase Engine Settings

The following table describes the registry settings that are installed for the Xbase engine. They are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft
\Jet\3.5\Engines\Xbase key of the Windows registry. None of these settings are required.

Setting | Data type | Default value | Description
NetworkAccess | Boolean | 01 | Determines file locking preference. If the value of the NetworkAccess setting is 00 (False), tables are opened without any file locks, and records are added and edited without any record locks. This is appropriate only for single-user access.
PageTimeout | Integer | 600 | The length of time between when data is placed in an internal cache and when it’s invalidated. The value is specified in 100 millisecond units.
INFPath | String | N/A | The full path to the folder that contains the .inf file. Microsoft Jet first looks for an .inf file in the folder that contains the table. If the .inf file isn’t in the database folder, it looks in the INFPath. If no .inf file is found, the ISAM driver uses the production index file (.cdx or .mdx) for the table. This setting is not written by default.

Setting | Data type | Default value | Description
CollatingSequence | String | ASCII | The collating sequence for all dBASE tables created or opened using Microsoft Access. Possible values are ASCII and International.
DataCodePage | String | OEM | The character set in which string and Memo values are stored. Possible values are OEM and ANSI. If the value of this setting is OEM, string values are translated from ANSI to OEM prior to being stored in the Xbase table. In the same manner, data retrieved from Xbase tables are translated from OEM to ANSI before being returned.
Deleted | Boolean | 01 | Determines how records marked for deletion are handled by Microsoft Jet. A value of 01 corresponds to the dBASE command SET DELETED ON and indicates never to retrieve or position on a deleted record. A value of 00 corresponds to the dBASE command SET DELETED OFF and indicates to treat a deleted record like any other record.
Century | Boolean | 00 | Used for formatting the century component of dates in cases where date-to-string functions are used in index expressions. A value of 01 corresponds to the dBASE command SET CENTURY ON, and a value of 00 corresponds to the dBASE command SET CENTURY OFF.
Date | String | MDY | The date formatting style to use in cases where date-to-string functions are used in index expressions. The possible values for this setting, which correspond to the dBASE SET DATE command, are American, ANSI, British, French, DMY, German, Italian, Japan, MDY, USA, and YMD.
Mark | Integer | 0 | The decimal value of the ASCII character used to separate date parts.
Exact | Boolean | 00 | Used for date comparisons. A value of 01 corresponds to the dBASE command SET EXACT ON. A value of 00 corresponds to the dBASE command SET EXACT OFF.
KanjiStr | Boolean | 00 | 00 means to treat strings as simple byte arrays. 01 means to treat strings as DBCS character strings. The KanjiStr setting is equivalent to the dBASE command SET KANJISTRING = ON. It affects the operation of the LEFT(), RIGHT(), SUBSTR(), AT(), REPLICATE(), and STUFF() functions inside the dBASE engine. A setting of 01 is recommended for DBCS countries.


Paradox Engine Settings

The following table describes the registry settings that are installed for the Paradox engine. They are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft
\Jet\3.5\Engines\Paradox key of the Windows registry. None of these settings are required.

Setting | Data type | Default value | Description
PageTimeout | Integer | 600 | The length of time between when data is placed in an internal cache and when it’s invalidated. The value is specified in 100 millisecond units. The default is 600 (60 seconds).
ParadoxUserName | String |  | The name to be displayed by Paradox if a table is locked by the Paradox ISAM driver, and an interactive user accessing the data from Paradox (rather than the ISAM driver) attempts to place an incompatible lock. This setting isn’t added if the computer isn’t on a network.
ParadoxNetPath | String |  | The full path to the folder containing the Paradox.net file (for Paradox 3.x) or the Pdoxusrs.net file (for Paradox 4.x). This setting isn’t added unless the computer is on a network. You need to add the initial setting. The full ParadoxNetPath (including the drive letter) must be consistent for all users sharing a particular database. If you indicate a value for the ParadoxNetPath setting, you must also specify a value for the ParadoxUserName and ParadoxNetStyle settings, or you’ll receive an error when trying to access external Paradox data.
ParadoxNetStyle | String | 4.x | The network access style to use when accessing Paradox data. Possible values are 3.x and 4.x (Note that Paradox 3.x users can’t set this to 4.x or the ISAM driver will use the wrong locking method.) This setting isn’t added unless the computer is on a network. This setting should correspond to whatever version of Paradox users in the group are using. It must be consistent for all users sharing a particular database.
CollatingSequence | String | ASCII | The collating sequence for all Paradox tables created or opened using Microsoft Jet. Possible values are ASCII, International, Norwegian-Danish, Japanese and Swedish-Finnish. Note that the value of the CollatingSequence setting must match the collating sequence used when the Paradox table was built, or the ISAM returns an error with the TableOpen API is used.

Setting | Data type | Default value | Description
DataCodePage | String | OEM | The character set in which string and Memo values are stored. Possible values are OEM and ANSI. If the value of this setting is OEM, string values are translated from ANSI to OEM prior to being stored in the Paradox table. In the same manner, data retrieved from Paradox tables are translated from OEM to ANSI before being returned.


Microsoft Excel Engine Settings

The following table describes the registry settings that are installed for the Microsoft Excel engine. They are located in the \HKEY_LOCAL_MACHINE\SOFTWARE
\Microsoft\Jet\3.5\Engines\Excel key of the Windows registry. None of these settings are required.

Setting | Data type | Default value | Description
TypeGuessRows | Integer | 8 | The number of rows in the worksheet range to scan to determine column types. If set to zero, then all rows in the range are checked.
ImportMixedTypes | String | Text | If a column contains more than one type of data (during the scan of TypeGuessRows rows), the type of the column is determined to be JET_coltypText if the value of this setting is Text. If the value of this setting is Majority Type, the most common type in the column determines the column type.
AppendBlankRows | Integer | 1 | For single worksheet formats (Microsoft Excel 3.0, Microsoft Excel 4.0, Lotus WK1), when exports are performed, the new table is added after any existing data on the sheet plus n rows, where n is given by this registry setting. For example, if the worksheet dimensions are A1..B50 prior to the export, and the value of the AppendBlankRows setting is 4, new data is stored starting at row 55. This setting can have values between 0 and 16.
FirstRowHasNames | Boolean | 01 | True (01)if the cells of the first row of the range contain the column names for the table.


Lotus Engine Settings

The registry settings that are installed for the Lotus engine are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Lotus key of the Windows registry. The setting names, data types, and default values are the same as the Microsoft Excel engine settings.


Text Engine Settings

The following table describes the registry settings that are installed for the Text engine. They are located in the \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft
\Jet\3.5\Engines\Text key of the Windows registry. None of these settings are required.

Setting | Data type | Default value | Description
MaxScanRows | Integer | 25 | The number of rows in the file to scan to determine column types. If set to zero, then all rows in the file are checked.
FirstRowHasNames | Boolean | 01 | True (01), if the first row of the file contains the column names for the table.
CharacterSet | String | OEM | The character set in which string and Memo values are stored. Possible values are OEM and ANSI. If the value of this setting is OEM, string values are translated from ANSI to OEM prior to being stored in the file. In the same manner, data retrieved from the file is translated from OEM to ANSI before being returned.
Format | String | CSV Delimited | The default format for files read or written by the text installable ISAM driver. The format can be overridden by import and export specifications for specific files (through the text wizard) but this value is the default in the absence of a specific file specification.
Extensions | String | txt, csv, tab, asc | A list of file name extensions to use when determining the list of files to return from the GetObjectInfo API (Infolevel JET_ObjInfoList). Each extension in this list must be from 1 to 3 characters or the string ”None.”
ExportCurrencySymbols | Boolean | 01 | When set to True (01), values in columns of type JET_coltypCurrency are written to the file with the currency symbol. For example, $1.12. Storing the currency symbols in the file allows the data to be re-imported as a currency type. When set to False (00), the previous value would be written as 1.12.


Appendix D Error Reference

This appendix presents the complete list of error numbers and error messages returned by the Microsoft Jet database engine. It also indicates the class to which each error belongs. The following table gives a description of each class.

Class | Description
BTRIEVE | Btrieve installable ISAM-specific errors (Microsoft Jet 2.5 and earlier)
DAO | DAO-specific errors
DBASE | dBASE installable ISAM-specific errors
DDL | Data Definition Language-specific errors
EXCEL | Microsoft Excel installable ISAM-specific errors
EXTENDED | Errors that may have extended error information
IMEX | Generic import/export errors
INST ISAM | Generic installable ISAM errors
INTERNET | Internet-specific errors
ISAM | Generic Microsoft Jet ISAM errors
JPM | Microsoft Jet errors related to property management
MISC | Microsoft Jet errors that don’t fit into another category
PARADOX | Paradox installable ISAM-specific errors
PARSE | Microsoft Jet expression parsing errors
QUERY | Microsoft Jet errors related to queries
REF INTEGRITY | Microsoft Jet errors related to referential integrity
REMOTE | Microsoft Jet errors specific to ODBC
REPLICATOR | Microsoft Jet errors related to replication
SECURITY | Microsoft Jet errors related to security
TEXT | Text installable ISAM-specific errors
TLV | Table-level validation errors
UNUSED | Microsoft Jet errors that are no longer used or that have special meaning. Errors that have special meaning are usually translations from other errors and are not generated in the Microsoft Jet code.

For additional information about these errors, see the Microsoft Jet Error Message Reference Help file (Jeterr35.hlp), which is included with Microsoft Office 97, Microsoft Access 97, Visual Basic 5.0, Visual C++ 5.0, and Visual J++ 1.1. The Microsoft Jet Error Message Reference Help file contains the Microsoft Jet error number, error message, and Help topic for each error listed in the following table.

An asterisk (*) means that there is no Jet error message text for a particular error message. An italicized item between angle brackets, such as <name>, represents a placeholder for a value that is given when the error message is displayed.

Error number | Microsoft Jet error message | Class
2420 | Syntax error in number. | PARSE
2421 | Syntax error in date. | PARSE
2422 | Syntax error in string. | PARSE
2423 | Invalid use of '.', '!', or '()'. | PARSE
2424 | Unknown name. | PARSE
2425 | Unknown function name. | PARSE
2426 | Function isn’t available in expressions. | PARSE
2427 | Object has no value. | PARSE
2428 | Invalid arguments used with domain function. | PARSE
2429 | In operator without (). | PARSE
2430 | Between operator without And. | PARSE
2431 | Syntax error (missing operator). | PARSE
2432 | Syntax error (comma). | PARSE
2433 | Syntax error. | PARSE
2434 | Syntax error (missing operator). | PARSE
2435 | Extra ). | PARSE
2436 | Missing ), ], or Item. | PARSE
2437 | Invalid use of vertical bars. | PARSE
2438 | Syntax error. | PARSE
2439 | Wrong number of arguments used with function. | PARSE
2440 | IIf function without (). | PARSE
2442 | Invalid use of parentheses. | PARSE
2443 | Invalid use of Is operator. | PARSE
2445 | Expression too complex. | PARSE
2446 | Out of memory during calculation. | PARSE
2447 | Invalid use of '.', '!', or '()'. | PARSE
2448 | Can’t set value. | PARSE
3000 | Reserved error <item>; there is no message for this error. | UNUSED
3001 | Invalid argument. | MISC
3002 | Couldn’t start session. | ISAM
3003 | Couldn’t start transaction; too many transactions already nested. | ISAM
3004 | * | UNUSED
3005 | <Database name> isn’t a valid database name. | ISAM
3006 | Database <name> is exclusively locked. | ISAM
3007 | Can’t open library database <name>. | ISAM
3008 | The table <name> is already opened exclusively by another user, or it is already open through the user interface and cannot be manipulated programmatically. | ISAM
3009 | You tried to lock table <name> while opening it, but the table can’t be locked because it is currently in use. Wait a moment, and then try the operation again. | ISAM
3010 | Table <name> already exists. | MISC
3011 | The Microsoft Jet database engine could not find the object <name>. Make sure the object exists and that you spell its name and the path name correctly. | MISC
3012 | Object <name> already exists. | ISAM
3013 | Couldn’t rename installable ISAM file. | ISAM
3014 | Can’t open any more tables. | ISAM
3015 | <Index name> isn’t an index in this table. Look in the Indexes collection of the TableDef object to determine the valid index names. | ISAM
3016 | Field won’t fit in record. | ISAM
3017 | The size of a field is too long. | MISC
3018 | Couldn’t find field <name>. | MISC
3019 | Operation invalid without a current index. | ISAM
3020 | You tried to call Update or CancelUpdate or attempted to update a Field in a recordset without first calling AddNew or Edit. | MISC
3021 | No current record. | MISC
3022 | The changes you requested to the table were not successful because they would create duplicate values in the index, primary key, or relationship. Change the data in the field or fields that contain duplicate data, remove the index, or redefine the index to permit duplicate values and try again. | ISAM
3023 | AddNew or Edit already used. | QUERY
3024 | Couldn’t find file <name>. | MISC
3025 | Can’t open any more files. | ISAM
3026 | Not enough space on disk. | ISAM
3027 | Can’t update. Database or object is read-only. | MISC
3028 | Can’t start your application. The workgroup information file is missing or opened exclusively by another user. | ISAM
3029 | Not a valid account name or password. | SECURITY
3030 | <Account name> isn’t a valid account name. | SECURITY
3031 | Not a valid password. | SECURITY
3032 | Can’t perform this operation. | SECURITY
3033 | You don’t have the necessary permissions to use the <name> object. Have your system administrator or the person who created this object establish the appropriate permissions for you. | MISC
3034 | You tried to commit or roll back a transaction without first using BeginTrans. | ISAM
3035 | * |
3036 | Database has reached maximum size. | ISAM
3037 | Can’t open any more tables or queries. | MISC
3038 | * |
3039 | Couldn’t create index; too many indexes already defined. | ISAM
3040 | Disk I/O error during read. | ISAM
3041 | Can’t open a database created with a previous version of your application. | ISAM
3042 | Out of MS-DOS file handles. | ISAM
3043 | Disk or network error. | UNUSED
3044 | <Path name> isn’t a valid path. Make sure that the path name is spelled correctly and that you are connected to the server on which the file resides. | ISAM
3045 | Couldn’t use <file name>; file already in use. | ISAM
3046 | Couldn’t save; currently locked by another user. | ISAM
3047 | Record is too large. | ISAM
3048 | Can’t open any more databases. | ISAM
3049 | Can’t open database <name>. It may not be a database that your application recognizes, or the file may be corrupt. | MISC
3050 | Couldn’t lock file. | ISAM
3051 | The Microsoft Jet database engine cannot open the file <name>. It is already opened exclusively by another user, or you need permission to view its data. | MISC
3052 | MS-DOS file sharing lock count exceeded. You need to increase the number of locks installed with Share.exe. | ISAM
3053 | Too many client tasks. | MISC
3054 | Too many Memo or OLE Object fields. | UNUSED
3055 | Not a valid file name. | MISC
3056 | Couldn’t repair this database. | MISC
3057 | Operation not supported on linked tables. | MISC
3058 | Index or primary key can’t contain a Null value. | ISAM
3059 | Operation canceled by user. | MISC
3060 | Wrong data type for parameter <parameter>. | QUERY
3061 | Too few parameters. Expected <number>. | EXTENDED
3062 | Duplicate output alias <name>. | EXTENDED
3063 | Duplicate output destination <field name>. | EXTENDED
3064 | Can’t open action query <name>. | QUERY
3065 | Can’t execute a select query. | QUERY
3066 | Query must have at least one destination field. | EXTENDED
3067 | Query input must contain at least one table or query. | EXTENDED
3068 | Not a valid alias name. | QUERY
3069 | The action query <name> cannot be used as a row source. | EXTENDED
3070 | The Microsoft Jet database engine does not recognize <name> as a valid field name or expression. | QUERY
3071 | This expression is typed incorrectly, or it is too complex to be evaluated. For example, a numeric expression may contain too many complicated elements. Try simplifying the expression by assigning parts of the expression to variables. | QUERY
3072 | <Visual Basic error string>. | EXTENDED
3073 | Operation must use an updatable query. | QUERY
3074 | Can’t repeat table name <name> in FROM clause. | EXTENDED
3075 | <Message> in query expression <expression>. | EXTENDED
3076 | <Message> in criteria expression. | EXTENDED
3077 | <Message> in expression. | EXTENDED
3078 | The Microsoft Jet database engine cannot find the input table or query <name>. Make sure it exists and that its name is spelled correctly. | EXTENDED
3079 | The specified field <field name> could refer to more than one table listed in the FROM clause of your SQL statement. | EXTENDED
3080 | Joined table <name> not listed in FROM clause. | EXTENDED
3081 | Can’t join more than one table with the same name <name>. | EXTENDED
3082 | JOIN operation <operation> refers to a field that isn’t in one of the joined tables. | EXTENDED
3083 | Can’t use internal report query. | QUERY
3084 | Can’t insert data with action query. | QUERY
3085 | Undefined function <name> in expression. | EXTENDED
3086 | Couldn’t delete from specified tables. | QUERY
3087 | Too many expressions in GROUP BY clause. | QUERY
3088 | Too many expressions in ORDER BY clause. | QUERY
3089 | Too many expressions in DISTINCT output. | QUERY
3090 | Resultant table not allowed to have more than one AutoNumber field. | ISAM
3091 | HAVING clause <clause> without grouping or aggregation. | UNUSED
3092 | Can’t use HAVING clause in TRANSFORM statement. | EXTENDED
3093 | ORDER BY clause <clause> conflicts with DISTINCT. | EXTENDED
3094 | ORDER BY clause <clause> conflicts with GROUP BY clause. | EXTENDED
3095 | Can’t have aggregate function in expression <expression>. | EXTENDED
3096 | Can’t have aggregate function in WHERE clause <clause>. | EXTENDED
3097 | Can’t have aggregate function in ORDER BY clause <clause>. | EXTENDED
3098 | Can’t have aggregate function in GROUP BY clause <clause>. | EXTENDED
3099 | Can’t have aggregate function in JOIN operation <operation>. | EXTENDED
3100 | Can’t set field <name> in join key to Null. | EXTENDED
3101 | The Microsoft Jet database engine can’t find a record in the table <table name> with key matching field(s) <field name>. | EXTENDED
3102 | Circular reference caused by <query reference>. | EXTENDED
3103 | Circular reference caused by alias <name> in query definition’s SELECT list. | EXTENDED
3104 | Can’t specify fixed column heading <value> in a crosstab query more than once. | EXTENDED
3105 | Missing destination field name in SELECT INTO statement <statement>. | EXTENDED
3106 | Missing destination field name in UPDATE statement <statement>. | EXTENDED
3107 | Record(s) can’t be added; no insert permission on <name>. | EXTENDED
3108 | Record(s) can’t be edited; no update permission on <name>. | EXTENDED
3109 | Record(s) can’t be deleted; no delete permission on <name>. | EXTENDED
3110 | Couldn’t read definitions; no read definitions permission for table or query <name>. | EXTENDED
3111 | Couldn’t create; no modify design permission for table or query <name>. | EXTENDED
3112 | Record(s) can’t be read; no read permission on <name>. | EXTENDED
3113 | Can’t update <field name>; field not updatable. | UNUSED
3114 | Can’t include Memo or OLE Object when you select unique values <statement>. | EXTENDED
3115 | Can’t have Memo or OLE Object fields in aggregate argument <statement>. | EXTENDED
3116 | Can’t have Memo or OLE Object fields in criteria <statement> for aggregate function. | EXTENDED
3117 | Can’t sort on Memo or OLE Object <clause>. | EXTENDED
3118 | Can’t join on Memo or OLE Object <name>. | EXTENDED
3119 | Can’t group on Memo or OLE Object <clause>. | EXTENDED
3120 | Can’t group on fields selected with '*' <table name>. | EXTENDED
3121 | Can’t group on fields selected with '*'. | EXTENDED
3122 | You tried to execute a query that doesn’t include the specified expression <name> as part of an aggregate function. | EXTENDED
3123 | Can’t use '*' in crosstab query. | EXTENDED
3124 | Can’t input from internal report query <name>. | QUERY
3125 | The database engine can’t find <name>. Make sure it is a valid parameter or alias name, that it doesn’t include invalid characters or punctuation, and that the name isn’t too long. | MISC
3126 | Invalid bracketing of name <name>. | EXTENDED
3127 | The INSERT INTO statement contains the following unknown field name: <field name>. Make sure you’ve typed the name correctly, and try the operation again. | EXTENDED
3128 | Specify the table containing the records you want to delete. | QUERY
3129 | Invalid SQL statement; expected 'DELETE', 'INSERT', 'PROCEDURE', 'SELECT', or 'UPDATE'. | QUERY
3130 | Syntax error in DELETE statement. | QUERY
3131 | Syntax error in FROM clause. | QUERY
3132 | Syntax error in GROUP BY clause. | QUERY
3133 | Syntax error in HAVING clause. | QUERY
3134 | Syntax error in INSERT INTO statement. | QUERY
3135 | Syntax error in JOIN operation. | QUERY
3136 | The LEVEL clause includes a reserved word or argument that is misspelled or missing, or the punctuation is incorrect. | QUERY
3137 | Missing semicolon (;) at end of SQL statement. | QUERY
3138 | Syntax error in ORDER BY clause. | QUERY
3139 | Syntax error in PARAMETER clause. | QUERY
3140 | Syntax error in PROCEDURE clause. | QUERY
3141 | The SELECT statement includes a reserved word or an argument name that is misspelled or missing, or the punctuation is incorrect. | QUERY
3142 | Characters found after end of SQL statement. | QUERY
3143 | Syntax error in TRANSFORM statement. | QUERY
3144 | Syntax error in UPDATE statement. | QUERY
3145 | Syntax error in WHERE clause. | QUERY
3146 | ODBC — call failed. | UNUSED
3147 | * | UNUSED
3148 | * | UNUSED
3149 | * | UNUSED
3150 | * | UNUSED
3151 | ODBC — connection to <name> failed. | EXTENDED
3152 | * | UNUSED
3153 | * | UNUSED
3154 | ODBC — couldn’t find DLL <name>. | REMOTE
3155 | ODBC — insert on a linked table <name> failed. | EXTENDED
3156 | ODBC — delete on a linked table <name> failed. | EXTENDED
3157 | ODBC — update on a linked table <name> failed. | EXTENDED
3158 | Couldn’t save record; currently locked by another user. | INST ISAM
3159 | Not a valid bookmark. | MISC
3160 | Table isn’t open. | INST ISAM
3161 | Couldn’t decrypt file. | INST ISAM
3162 | You tried to assign the Null value to a variable that isn’t a Variant data type. | MISC
3163 | The field is too small to accept the amount of data you attempted to add. Try inserting or pasting less data. | MISC
3164 | The field can’t be updated because another user or process has locked the corresponding record or table. | MISC
3165 | Couldn’t open .inf file. | DBASE
3166 | Cannot locate the requested Xbase memo file. | DBASE
3167 | Record is deleted. | MISC
3168 | Invalid .inf file. | DBASE
3169 | The Microsoft Jet database engine could not execute the SQL statement because it contains a field that has an invalid data type. | QUERY
3170 | Couldn’t find installable ISAM. | UNUSED
3171 | Couldn’t find network path or user name. | PARADOX
3172 | Couldn’t open Paradox.net. | UNUSED
3173 | Couldn’t open table 'MSysAccounts' in the workgroup information file. | SECURITY
3174 | Couldn’t open table 'MSysGroups' in the workgroup information file. | SECURITY
3175 | Date is out of range or is in an invalid format. | INST ISAM
3176 | Couldn’t open file <name>. | IMEX
3177 | Not a valid table name. | IMEX
3178 | * |
3179 | Encountered unexpected end of file. | IMEX
3180 | Couldn’t write to file <name>. | IMEX
3181 | Invalid range. | IMEX
3182 | Invalid file format. | IMEX
3183 | Not enough space on temporary disk. | ISAM
3184 | Couldn’t execute query; couldn’t find linked table. | EXTENDED
3185 | SELECT INTO on a remote database tried to produce too many fields. | EXTENDED
3186 | Couldn’t save; currently locked by user <name> on machine <name>. | EXTENDED
3187 | Couldn’t read; currently locked by user <name> on machine <name>. | EXTENDED
3188 | Couldn’t update; currently locked by another session on this machine. | ISAM
3189 | Table <name> is exclusively locked by user <name> on machine <name>. | UNUSED
3190 | Too many fields defined. | ISAM
3191 | Can’t define field more than once. | ISAM
3192 | Couldn’t find output table <name>. | EXTENDED
3193 | (unknown). | UNUSED
3194 | (unknown). | UNUSED
3195 | (expression). | UNUSED
3196 | The database <name> is already in use by another person or process. When the database is available, try the operation again. | ISAM
3197 | The Microsoft Jet database engine stopped the process because you and another user are attempting to change the same data at the same time. | MISC
3198 | Couldn’t start session. Too many sessions already active. | ISAM
3199 | Couldn’t find reference. | REF INTEGRITY
3200 | The record cannot be deleted or changed because table <name> includes related records. | EXTENDED
3201 | You can’t add or change a record because a related record is required in table <name>. | EXTENDED
3202 | Couldn’t save; currently locked by another user. | ISAM
3203 | Subqueries cannot be used in the expression <expression>. | EXTENDED
3204 | Database already exists. | ISAM
3205 | Too many crosstab column headers <value>. | EXTENDED
3206 | Can’t create a relationship between a field and itself. | REF INTEGRITY
3207 | Operation not supported on a Paradox table with no primary key. | PARADOX
3208 | Invalid Deleted setting in the Xbase key of the Windows Registry. | DBASE
3209 | * | UNUSED
3210 | The connection string is too long. | QUERY
3211 | The database engine couldn’t lock table <name> because it’s already in use by another person or process. | EXTENDED
3212 | Couldn’t lock table <name>; currently in use by user <name> on machine <name>. | UNUSED
3213 | Invalid Date setting in the Xbase key of the Windows Registry. | DBASE
3214 | Invalid Mark setting in the Xbase key of the Windows Registry. | DBASE
3215 | Too many Btrieve tasks. | BTRIEVE
3216 | Parameter <name> specified where a table name is required. | EXTENDED
3217 | Parameter <name> specified where a database name is required. | EXTENDED
3218 | Couldn’t update; currently locked. | ISAM
3219 | Invalid operation. | MISC
3220 | Incorrect collating sequence. | PARADOX
3221 | Invalid settings in the Btrieve key of the Windows Registry. | BTRIEVE
3222 | Query can’t contain a Database parameter. | QUERY
3223 | <Parameter name> is invalid because it is too long or contains invalid characters. | EXTENDED
3224 | Can’t read Btrieve data dictionary. | BTRIEVE
3225 | Encountered a record locking deadlock while performing a Btrieve operation. | BTRIEVE
3226 | Errors encountered while using the Btrieve DLL. | BTRIEVE
3227 | Invalid Century setting in the Xbase key of the Windows Registry. | DBASE
3228 | Invalid Collating Sequence setting in the Paradox key of the Windows Registry. | PARADOX
3229 | Btrieve — can’t change field. | BTRIEVE
3230 | Out-of-date Paradox lock file. | PARADOX
3231 | ODBC — field would be too long; data truncated. | REMOTE
3232 | ODBC — couldn’t create table. | REMOTE
3233 | * | UNUSED
3234 | ODBC — remote query timeout expired. | REMOTE
3235 | ODBC — data type not supported on server. | REMOTE
3236 | * | UNUSED
3237 | * | UNUSED
3238 | ODBC — data out of range. | REMOTE
3239 | Too many active users. | ISAM
3240 | Btrieve — missing Btrieve engine. | BTRIEVE
3241 | Btrieve — out of resources. | BTRIEVE
3242 | Invalid reference in SELECT statement. | EXTENDED
3243 | None of the import field names match fields in the appended table. | IMEX
3244 | Can’t import password-protected spreadsheet. | IMEX
3245 | Couldn’t parse field names from the first row of the import table. | IMEX
3246 | Operation not supported in transactions. | MISC
3247 | ODBC — linked table definition has changed. | REMOTE
3248 | Invalid NetworkAccess setting in the Windows Registry. | INST ISAM
3249 | Invalid PageTimeout setting in the Windows Registry. | INST ISAM
3250 | Couldn’t build key. | ISAM
3251 | Operation is not supported for this type of object. | MISC
3252 | Can’t open a form whose underlying query contains a user-defined function that attempts to set or get the form’s RecordsetClone property. | MISC
3253 | * | UNUSED
3254 | ODBC — Can’t lock all records. | REMOTE
3255 | * | UNUSED
3256 | Index file not found. | DBASE
3257 | Syntax error in WITH OWNERACCESS OPTION declaration. | QUERY
3258 | The SQL statement couldn’t be executed because it contains ambiguous outer joins. To force one of the joins to be performed first, create a separate query that performs the first join and then include that query in your SQL statement. | QUERY
3259 | Invalid field data type. | MISC
3260 | Couldn’t update; currently locked by user <name> on machine <name>. | EXTENDED
3261 | Table <name> is exclusively locked by user <name> on machine <name>. | EXTENDED
3262 | Couldn’t lock table <name>; currently in use by user <name> on machine <name>. | EXTENDED
3263 | Invalid database object. | MISC
3264 | No field defined — cannot append TableDef or Index. | DAO
3265 | Item not found in this collection. | DAO
3266 | Can’t append a Field that is already a part of a Fields collection. | DAO
3267 | Property can be set only when the Field is part of a Recordset object’s Fields collection. | DAO
3268 | Can’t set this property once the object is part of a collection. | DAO
3269 | Can’t append an Index that is already part of an Indexes collection. | DAO
3270 | Property not found. | DAO
3271 | Invalid property value. | DAO
3272 | Object isn’t a collection. | DAO
3273 | Method not applicable for this object. | DAO
3274 | External table isn’t in the expected format. | INST ISAM
3275 | Unexpected error from external database driver <error number>. | INST ISAM
3276 | Invalid database object reference. | MISC
3277 | Can’t have more than 10 fields in an index. | ISAM
3278 | The Microsoft Jet database engine has not been initialized. | MISC
3279 | The Microsoft Jet database engine has already been initialized. | MISC
3280 | Can’t delete a field that is part of an index or is needed by the system. | ISAM
3281 | Can’t delete this index or table. It is either the current index or is used in a relationship. | ISAM
3282 | Operation not supported on a table that contains data. | ISAM
3283 | Primary key already exists. | ISAM
3284 | Index already exists. | ISAM
3285 | Invalid index definition. | ISAM
3286 | Format of memo file doesn’t match specified external database format. | INST ISAM
3287 | Can’t create index on the given field. | ISAM
3288 | Paradox index is not primary. | PARADOX
3289 | Syntax error in CONSTRAINT clause. | DDL
3290 | Syntax error in CREATE TABLE statement. | DDL
3291 | Syntax error in CREATE INDEX statement. | DDL
3292 | Syntax error in field definition. | DDL
3293 | Syntax error in ALTER TABLE statement. | DDL
3294 | Syntax error in DROP INDEX statement. | DDL
3295 | Syntax error in DROP TABLE or DROP INDEX. | DDL
3296 | Join expression not supported. | MISC
3297 | Couldn’t import table or query. No records found, or all records contain errors. | IMEX
3298 | There are several tables with that name. Please specify owner in the format 'owner.table'. | REMOTE
3299 | ODBC Specification Conformance Error <message>. Report this error to the developer of your application. | UNUSED
3300 | Can’t create a relationship. | REF INTEGRITY
3301 | Can’t perform this operation; features in this version are not available in databases with older formats. | MISC
3302 | Can’t change a rule while the rules for this table are in use. | TLV
3303 | Can’t delete this field. It’s part of one or more relationships. | REF INTEGRITY
3304 | You must enter a personal identifier (PID) consisting of at least four and no more than 20 characters and digits. | SECURITY
3305 | Invalid connection string in pass-through query. | REMOTE
3306 | You’ve written a subquery that can return more than one field without using the EXISTS reserved word in the main query’s FROM clause. Revise the SELECT statement of the subquery to request only one field. | QUERY
3307 | The number of columns in the two selected tables or queries of a union query don’t match. | QUERY
3308 | Invalid TOP argument in select query. | EXTENDED
3309 | Property setting can’t be larger than 2 K. | JPM
3310 | This property isn’t supported for external data sources or for databases created with a previous version of Microsoft Jet. | JPM
3311 | Property specified already exists. | JPM
3312 | Validation rules and default values can’t be placed on system or linked tables. | TLV
3313 | Can’t place this validation expression on this field. | TLV
3314 | The field <name> can’t contain a Null value because the Required property for this field is set to True. Enter a value in this field. | EXTENDED
3315 | Field <name> can’t be a zero-length string. | EXTENDED
3316 | <Table-level validation text>. | EXTENDED
3317 | One or more values are prohibited by the validation rule <rule> set for <field name>. Enter a value that the expression for this field can accept. | UNUSED
3318 | Values specified in a TOP clause are not allowed in delete queries or reports. | EXTENDED
3319 | Syntax error in union query. | QUERY
3320 | <Error> in table-level validation expression. | EXTENDED
3321 | No database specified in connection string or IN clause. | REMOTE
3322 | Crosstab query contains one or more invalid fixed column headings. | EXTENDED
3323 | The query cannot be used as a row source. | QUERY
3324 | The query is a DDL query and cannot be used as a row source. | QUERY
3325 | Pass-through query with ReturnsRecords property set to True did not return any records. | REMOTE
3326 | This Recordset is not updatable. | EXTENDED
3327 | Field <name> is based on an expression and can’t be edited. | EXTENDED
3328 | Table <name> is read-only. | EXTENDED
3329 | Record in table <name> was deleted by another user. | EXTENDED
3330 | Record in table <name> is locked by another user. | EXTENDED
3331 | To make changes to this field, first save the record. | EXTENDED
3332 | Can’t enter value into blank field on “one” side of outer join. | EXTENDED
3333 | Records in table <name> would have no record on the 'one' side. | EXTENDED
3334 | Can be present only in version 1.0 format. | ISAM
3335 | DeleteOnly called with non-zero cbData. | JPM


Appendix E Microsoft Jet Components

Microsoft Jet uses a variety of components, implemented as dynamic-link libraries (DLLs). These DLLs constitute the Jet database engine as a whole. The Microsoft Jet DLLs can be grouped into the following component groups:

DLL group | Description
Microsoft Jet DLLs | Responsible for storing data in the database file and for providing query and security services.
Data Access Objects (DAO) DLLs | Responsible for providing the programming interface to the Jet database engine and ODBCDirect.
External ISAM DLLs | Responsible for providing access to external (non-native) data (for example, Microsoft Excel, Microsoft Exchange, HTML, Microsoft FoxPro, Borland dBASE, Corel Paradox, and Lotus 1-2-3).
Replication DLLs | Responsible for creating and synchronizing full or partial database replicas at multiple locations.
Visual Basic for Applications (VBA) DLLs | VBA is not a component of Microsoft Jet, but it provides the expression service to evaluate functions in SQL statements.
Open Database Connectivity (ODBC) DLL | In addition to the components discussed above, Microsoft Jet uses the ODBC Driver Manager DLL to provide access to ODBC data sources.


Microsoft Jet ISAM Driver Components

The Microsoft Jet ISAM driver is responsible for storing data in the database file. It is also responsible for providing query and access control (security) services. The following table describes components that provide these ISAM services.

Component | Description
MSJet35.dll | Contains the main engine functionality, including the Microsoft Jet engine that is responsible for data storage, the query engine, and security routines. It handles all data storage and retrieval from the native ISAM driver and all querying against native and non-native data.
MSJInt35.dll | Contains all localized Microsoft Jet strings. Because Microsoft Jet is translated, or localized, for many different countries, its language-specific strings are kept in a separate library.
MSJTer35.dll | Contains Microsoft Jet error services. Moving strings and error messages to a separate DLL allows easier maintenance of the various components.


External ISAM Driver Components

Microsoft Jet provides the capability to access non-native, or external, data such as HTML tables and lists, Microsoft Exchange and Microsoft Outlook folders, and Microsoft FoxPro, Lotus 1-2-3, dBASE, and Paradox databases. The process of retrieving and storing non-native data is handled by the following external ISAM driver components (also known as installable ISAM drivers).

Component | Description
MSExcl35.dll | Provides access to Microsoft Excel worksheets and workbooks created by versions 3.0, 4.0, 5.0, 7.0, and 8.0. Microsoft Jet 2.5 introduced access to Microsoft Excel data but it was not until Microsoft Access 95 and Visual Basic 4.0 that this functionality became available to Microsoft Jet users.
MSRd2x35.dll | Provides access to Microsoft Jet 1.x and 2.0 databases. The storage engine and format were completely revised with Microsoft Jet 3.0. Given the large number of structural changes, Microsoft Jet 3.5 treats version 2.0 databases as external ISAM databases.
MSLtus35.dll | Provides access to Lotus 1-2-3 worksheets created by versions 1.x, 2.x, 3.x, and 4.x.
MSPdox35.dlL | Provides access to Paradox tables from versions 3.5, 4.0, 4.5, and 5.0.
MSXbse35.dll | Provides access to XBase files including dBASE versions III, IV, and V, and FoxPro versions 2.0, 2.5, 2.6, and 3.0.
MSText35.dll | Provides the services required for accessing text files. With Microsoft Jet, you can either link (attach) or directly open text files, which can be stored in either fixed-width or delimited format. The procedures for retrieving the data in text files are similar to those used to access data in other external data sources. Generally, the ability to access text files is provided so you can import existing text files into your Microsoft Jet database, or export Microsoft Jet or other linked database tables to text files.

See Also For more information about accessing external data, see Chapter 8, “Accessing External Data.”


Replication Components

Microsoft Jet 3.0 introduced database replication as a service. With replication, it is possible to create and maintain replicas of databases at multiple sites and synchronize these databases at regular intervals. Microsoft Jet 3.5 added support for creating partial replicas and synchronizing over the Internet or an intranet. The following table describes components that provide replication services.

Component | Description
MSRepl35.dll | Provides the main replication services, including creation of replicable databases and replica reconciliation.
MSRclr35.dll | Provides reconciliation services for the Microsoft Windows 95 Briefcase component.
MSTran35.exe and MSTrani35.exe | Physically moves data between file systems or between the file system and the Internet or intranet during synchronization.
ReplMan.exe | Provides a user interface (Microsoft Replication Manager) for various replication services. This component currently ships with Microsoft Office 97, Developer Edition.
MSRpfs35.dll | Interfaces the synchronized databases to the file system.
MSRemr35.dll and MSRecr35.dll | Provides international resources and localized strings for the replication components.

See Also For more information about replication, see Chapter 7, “Database Replication.”


Visual Basic for Applications (VBA) Interface Components

Although VBA is not a component of Microsoft Jet, it works as an expression evaluator to evaluate various functions that appear in SQL statements and to do arithmetic. The Microsoft Jet expression service takes an expression and builds a tree out of it. Elements of this tree are then passed to VBA for evaluation.

Various VBA components are used to provide this functionality to Microsoft Jet. VBAJet32.dll is used to start VBA. When Microsoft Jet is running in a stand-alone environment (not being run by a host application such as Microsoft Access), the VBA run-time component, VBAR332.dll, is used. However, if a host application other than Visual Basic 5.0 (for example, Microsoft Access 97) is running, then VBA332.dll is used. If Visual Basic 5.0 is running, then VBA5.dll is used. The following table describes the VBA interface components.

Component | Description
VBAJet32.dll | Initializes the VBA expression service for Microsoft Jet.
VBAEn232.olb | Contains international resources and localized strings for the VBA components.
VBA332.dll | The full version of the VBA expression service.
VBAR332.dll | The run-time version of the VBA expression service.

Notable VBA Interactions

Prior to version 3.0, Microsoft Jet made calls to the Basic interpreter built into Microsoft Access and Visual Basic to get its expression evaluation functionality. In current versions, Microsoft Jet uses VBA.

VBAJet32.dll checks to see whether there is a version of VBA already in memory. If there isn’t one, Microsoft Jet will load one of three potential DLLs (VBAR332.dll, VBA332.dll, VBA5.dll) to get its expression evaluation functionality, as described earlier in this section.

It is also worthwhile to note the order in which you load engine hosts, such as Microsoft Access and Microsoft Excel. For example, if you first load the ODBC Desktop Database Drivers, Microsoft Jet uses the VBA run-time DLL (VBAR332.dll). If you subsequently load Microsoft Access, the instance of Microsoft Jet loaded by Microsoft Access uses the full VBA DLL (VBA332.dll). If, on the other hand, you load Microsoft Access first, and then the ODBC Desktop Database Drivers, both instances of Microsoft Jet use the VBA332.dll because the engine’s VBA initialization DLL sees that VBA332.dll is already in memory and uses it. If you want to reduce the memory footprint of Microsoft Jet, load Microsoft Access first, and then other applications or drivers that use the Jet database engine.


DAO Interface Components

As mentioned earlier, DAO is the programming interface to Microsoft Jet. The following table describes the components that provide DAO services.

Component | Description
DAO350.dll | The DAO library, containing the object layer interface to the engine. This file contains both the DAO DLL and the type library.
DAO2535.tlb | This file provides access to the obsolete DAO Snapshot, Table, and Dynaset objects and to obsolete methods. It’s used only for code in Microsoft Access 1.1 and earlier and Visual Basic 3.0 and earlier.
MSRDO20.dll | This file provides support for ODBCDirect services.

See Also For more information about DAO’s use of type libraries and its OLE capabilities, see Chapter 2, “Introducing Data Access Objects.”


Driver Management Components

Microsoft Jet can access data through the ODBC standard. While the Microsoft Jet components contain no intrinsic ODBC services, they do call the standard ODBC Driver Manager. The ODBC32.dll dispatches calls to ODBC drivers for processing. This file is not a Microsoft Jet component.

See Also For more information about the ODBC Driver Manager and the ODBC Desktop Database Drivers, see Chapter 12, “ODBC Desktop Database Drivers.”