The biggest challenge in creating truly reusable administration pages
arrives when dealing with complex database tables. Imagine, for a
moment, that all our tables were simple database
tables
that don’t impose foreign key constraints
upon other tables, and have no foreign key constraints imposed upon
them (that is, a simple database table is one that has no explicit
relationships with other tables). Rarely are there many simple
database tables in a database. Since relational databases inherently
encourage the developer to create several small, related tables,
simple database tables are found only in simple data models.
Therefore, we need a system that gracefully handles complex tables as
well as simple tables.
In this section we’ll examine the theory behind creating powerful, reusable database administration pages that can be used for complex tables. We’ll begin with a discussion of what the ideal reusable administration page should contain. Since the reusable administration pages we’ll later create will make heavy use of ADO schemas, an entire section is dedicated to this topic.
Without code reuse, a developer needs to perform seven steps for each administration page. With code reuse, however, the need to repeat each of the seven steps for each administration page can be eliminated. Can code reuse help to eliminate the fact that seven steps are needed, though? The ideal situation would be to have only one step needed to create an entire set of administration pages.
Imagine that we had a page called
DatabaseAdministration.asp
that expected one
parameter through the querystring, the table name. This page would
then display the appropriate options for adding, deleting, and
updating records, gracefully handling one-to-many and one-to-one
relationships. Furthermore, the page would know what form elements
would be best suited for collecting input from the user. For example,
if a table had a date/time column, it might be wiser in some
instances to use select boxes to represent the month, day, and year;
other times, it might be wiser to use a simple text box.
This single administration page would also need to be able to
determine what columns in a table had foreign key constraints and
provide a mechanism to allow the user to only select applicable
values from the related table. For example, imagine that we had an
Employee
table with the structure shown in Figure 6.1.
Note that Title
is a lookup table, and its
Name
column contains values like
“President,” “CEO,” “Manager,”
“Secretary,” and “Grunt.” Through referential
integrity, TitleID
in Employee
can contain only integer values that are also present in
TitleID
in Title
.
How should our administration page handle such foreign keys when
adding a new record or updating an existing record? A simple text box
could be used, in which the user could enter an integer value
corresponding to a value in the Title
table. Of
course, this would require the user to know that the title
“CEO” had a TitleID
of, say, 5.
This is not an acceptable solution, since the user cannot be held
responsible for knowing what titles correspond to what
TitleID
s and what TitleID
s are
valid. Rather than seeing a text box, the user should be presented
with a list box containing the acceptable title names. To accomplish
this feat, our administration page would not only have to identify
what columns are foreign keys, but would also have to decide what
column from the lookup table to display in the list box!
Such a single, robust administration page is quite impossible to create, since it would require that the page be autonomous and know the best way to display form fields and lookup table lists. We can’t expect the script to be able to make the best decisions, but what if we could give “hints” to the script to help it decide how to handle form elements and foreign keys? In this chapter, we will strive to build a collection of ASP pages that allow the end user to update, insert, and delete database records with some input from the end developer to help the administration page decide what methods to use to collect user input.
In creating this set of reusable administration pages, some mechanism for determining a particular table’s columns and column properties needs to be used. Furthermore, since these pages strive to be as generic as possible, they shouldn’t be dependent upon the database used; that is, these administration pages should work if we use Access, Microsoft SQL Server, or some other database system. Thankfully, ADO provides a means to collect table and column information regardless of the database provider.
One of ADO’s most useful features is
schemas
. Schemas provide low-level database
information, such as table information, column information, foreign
key constraints, and primary key information, in a high-level format.
Using schemas, this system information can be collected in the same
manner for a number of different database providers.
Most database systems provide their own methods for collecting such
low-level information. For example, if you want to access table and
column information in Microsoft SQL Server without using schemas, you
would have to query system tables, like
sysobjects
or syscolumns
. Example 6.1
illustrates how to obtain all of the tables in a SQL Server database
by querying sysobjects
.
Example 6-1. List the Tables in a SQL Server Database Using the sysobjects Table
<% @LANGUAGE="VBSCRIPT" %> <% Option Explicit %> <% 'Open up a connection to the database Dim objConn Set objConn = Server.CreateObject("ADODB.Connection") objConn.ConnectionString = "DSN=MyDatabase" objConn.Open 'We want to query the sysobjects table where type = 'U' Dim strSQL strSQL = "SELECT * FROM sysobjects WHERE type='U'" 'Execute the query Dim objRS Set objRS = objConn.Execute(strSQL) 'Display the table names Do While not objRS.EOF Response.Write objRS("Name") & "<BR>" objRS.MoveNext Loop 'Clean up... objConn.Close Set objConn = Nothing %>
The sysobjects
table is fairly cryptic,
containing several columns with single character values that denote
their purpose. Rows that have a “U” in the
Type
column are tables. I think you’ll agree
that the code in Example 6.1 is fairly cryptic.
Furthermore, the code will only work for Microsoft SQL Server
databases.
Schemas make listing database information much easier and more portable. Since the code in Example 6.1 will only work for Microsoft SQL Server databases, it is anything but portable, and portability should be a key concern when developing reusable scripts.
Before you start using schemas, it is important that you have access to the enumerations defined in ADO. There are currently two ways to do this:
adovbs.inc
is a text file that contains all of
the ADO enumerations in the form of VBScript constants. To use
adovbs.inc
, copy it into the directory in which
you place your include files (I recommend creating a directory named
/inc
and turning off Read permissions.)
adovbs.inc
can be found in Program
FilesCommon FilesSystemado
.
Once adovbs.inc
is in a web-accessible
directory, you can use a server-side include to import the
contents of adovbs.inc
into any ASP page that
needs to access the ADO enumerations. Simply use the following line
of code at the top of your document:
<% @LANGUAGE="VBSCRIPT" %>
<% Option Explicit %>
<!--#include virtual="/inc/adovbs.inc"-->
Take a moment to look at the contents of
adovbs.inc
. Note that it is nothing more than a
lengthy list of VBScript constants assigned appropriate hexadecimal
values. Below is a short excerpt from the schema section of
adovbs.inc
:
'---- SchemaEnum Values ---- Const adSchemaProviderSpecific = -1 Const adSchemaAsserts = 0 Const adSchemaCatalogs = 1 Const adSchemaCharacterSets = 2 Const adSchemaCollations = 3 Const adSchemaColumns = 4
For more information on server-side includes, be sure to read Chapter 1, or check out “The low-down on #includes,” available at http://www.4guysfromrolla.com/webtech/080199-1.shtml.
The METADATA
tag imports enumerations from a type
library. For example, in each ASP page that uses a server-side
include to import the contents of adovbs.inc
, we
could replace:
<!--#include virtual="/inc/adovbs.inc"-->
with the following line of code:
<!-- METADATA TYPE="typelib" FILE="D:Program FilesCommon FilesSystemADOmsado21.tlb" -->
The METADATA
tag has the following syntax (the
line breaks are for enhanced readability and are not required):
<!-- METADATA TYPE="typelib" UUID="GUID
" FILE="FilePath
" -->
You only need to specify either the UUID
or the
FILE
when importing constants from a type library.
The UUID
expects the type library’s GUID,
while the FILE
expects the full, physical path to
the type library. The GUID for the ActiveX Data Objects type library
is: 00000201-0000-0010-8000-00AA006D2EA4
. I find
that specifying the FilePath
instead of
the GUID
makes for much easier-to-read
code. However, there is always the chance that the file may be
renamed or moved, whereas the GUID won’t change.
Now, you may be wondering why anyone would want to use the
METADATA
tag in place of
adovbs.inc
. Typing:
<!--#include virtual="/inc/adovbs.inc"-->
instead of:
<!-- METADATA TYPE="typelib" FILE="D:Program FilesCommon FilesSystemADOmsado21.tlb" -->
is quicker and less prone to mistake.
METADATA
’s advantage is that you can specify
it once in Global.asa
and have access to all of
the ADO enumerations in all of your ASP
pages! Also, according to Microsoft’s ASP Performance Tips
(http://msdn.microsoft.com/library/psdk/bdg/bdgapp03_3rhv.htm),
using a single METADATA
tag in
Global.asa
provides for better performance than
using server-side includes on each page.
If you place the METADATA
tag in
Global.asa
, remember that every ASP page will
have access to the ADO constants. If you attempt to include
adovbs.inc
in a page, you will receive a
Name redefined:
'adOpenForwardOnly'
error. This error occurs
because you are attempting to create a constant in
adovbs.inc
that already exists from the
METADATA
import.
adOpenForwardOnly
is listed because it is the
first constant defined in adovbs.inc
.
To open a schema, use the OpenSchema method of the Connection object. The OpenSchema method has the following definition:
Setrecordset
=connection
.OpenSchema(QueryType
[,Criteria
[,SchemaID
]])
QueryType
is a required parameter that specifies the type of schema to open,
and must be set to a valid SchemaEnum
type. There
are a number of possible schema types that can return a vast array of
database information. There are schemas for listing tables, columns,
column privileges, foreign keys, indexes, primary keys, referential
constraints, and other miscellaneous information. The valid
SchemaEnum
s
are listed in adovbs.inc
under the heading
“SchemaEnum Values.” Each SchemaEnum
constant defined in adovbs.inc
is prefixed with
an adSchema
; some of the more commonly used
SchemaEnum
values (including those that will be
used in code presented in this chapter) are listed in Table 6.1.
Table 6-1. Several Common SchemaEnums Are Defined in adovbs.inc
SchemaEnum |
Description |
---|---|
|
Contains detailed information about each of the columns in a database |
|
Contains detailed information about each table in a database |
|
Contains detailed information about all of the foreign key constraints in a database |
The available SchemaEnum
s that can be specified by
the QueryType
parameter differ among the
various database providers. Many database providers support a common
set of SchemaEnum
s (such as
adSchemaColumns
and
adSchemaTables
), but some database providers may
contain their own unique schemas that aren’t represented by a
value in the SchemaEnum
list. If you need to use
such a schema, set the QueryType
to
adSchemaProviderSpecific
and specify the unique
schema in the SchemaID
parameter.
The various schemas supported depend upon the database provider. For example, the OLE DB provider for AS/400 and VSAM only supports four schemas, while the OLE DB provider for SQL Server supports several more.
Example 6.2 contains code that is functionally
identical to the code presented in Example 6.1; both
scripts list the tables in a database. Example 6.2,
however, uses the adSchemaTables
QueryType
rather than
sysobjects
, which makes Example 6.2 easier to read and more portable.
Example 6-2. Listing the Tables in a Database with Schemas
<% @LANGUAGE="VBSCRIPT" %> <% Option Explicit %> <!--#include virtual="/adovbs.inc"--> <% 'Open a connection to the database Dim objConn Set objConn = Server.CreateObject("ADODB.Connection") objConn.ConnectionString = "DSN=MyDatabase" objConn.Open 'Use the OpenSchema method to grab the table schema Dim objRSSet objRS = objConn.OpenSchema(adSchemaTables)
'Loop through the contents of the schema. Do While Not objRS.EOF 'Only display TABLES - not SYSTEM TABLES, not VIEWSIf objRS("TABLE_TYPE") = "TABLE" then
Response.Write objRS("TABLE_NAME") & "<BR>"
End If
objRS.MoveNext Loop 'Clean up... objRS.Close Set objRS = Nothing objConn.Close Set objConn = Nothing %>
Since a schema is returned as a Recordset object, we can use the
Recordset object’s properties and methods to iterate through
the schema. A schema, like an ordinary database table, contains
columns, which describe the properties of an abstract object, and
rows, which serve as an instantiation of an object. The
adSchemaTables
schema we used in Example 6.2 contains several columns describing each
table, including TABLE_NAME
,
TABLE_TYPE
, DESCRIPTION
,
DATE_MODIFIED
, and
DATE_CREATED
.
The adSchemaTables
schema returns the views and
system tables along with the user tables. The
TABLE_TYPE
column specifies what type of table is
being returned, and in Example 6.2 we list only user
tables by assuring that TABLE_TYPE
equals
"TABLE
" before displaying the
TABLE_NAME
column.
Note that we explicitly included adovbs.inc
in
Example 6.2. This explicit include would not be
needed if the METADATA
tag were used in
Global.asa
. It is strongly recommended that you
use the METADATA
approach. All the examples in
this book that need to use ADO enumerations, however, will explicitly
include adovbs.inc
to assist with readability.
The OpenSchema
method has an optional
parameter named
Criteria
. This parameter can be specified
to limit the resulting schema. For example, in Example 6.2, we didn’t specify a
Criteria
, and all tables were returned,
including views and system tables. We could pass in a
Criteria
parameter, however, that would
inform ADO to return only those tables that were user tables. Using a
Criteria
to limit the contents of a schema
is similar to using a WHERE
clause to limit the
results of a SQL query.
Each SchemaEnum
has its own predefined set of
potential Criteria
that can be used to
limit the results of a schema. Some SchemaEnum
s
have no limiting Criteria
, while others
can have several columns to which Criteria
can be applied. To accommodate the differing number of
Criteria
accepted by various
SchemaEnum
s, the
Criteria
parameter should be passed in as
an array. For example, if a particular SchemaEnum
expects four Criteria
, the array should
contain four elements.
The
adSchemaTables
schema expects four Criteria
:
TABLE_CATALOG
, TABLE_SCHEMA
,
TABLE_NAME
, and TABLE_TYPE
. To
retrieve only user tables when opening the
adSchemaTables
schema, we need to supply a
Criteria
parameter that was a four-element
array, defined as:
Array(Empty, Empty, Empty, "TABLE")
Since we do not wish to filter on the first three
Criteria
—TABLE_CATALOG
,
TABLE_SCHEMA
, and TABLE_NAME
—we simply leave these blank by specifying the resulting array
element as Empty
. By using the above
Criteria
, we can alter Example 6.2 so the check for
objRS("TABLE_TYPE")
=
"TABLE"
can be avoided. Example 6.3 displays this new code using the
Criteria
parameter of the OpenSchema
method.
Example 6-3. Using the Criteria Parameter to Selectively List Tables of a Database
<% @LANGUAGE="VBSCRIPT" %>
<% Option Explicit %>
<!--#include virtual="/adovbs.inc"-->
<%
'Open a connection to the database
Dim objConn
Set objConn = Server.CreateObject("ADODB.Connection")
objConn.ConnectionString = "DSN=MyDatabase"
objConn.Open
'Use the OpenSchema method to grab the table schame
'In this example, we specify the Criteria parameter of the
'OpenSchema method
Dim objRS
Set objRS = objConn.OpenSchema(adSchemaTables, _
Array(Empty, Empty, Empty, "TABLE")
)
'Loop through the contents of the schema.
Do While Not objRS.EOF
'Since the Criteria parameter returned only user table,
'we don't need to check TABLE_TYPE here, like we did in Example 6.2
Response.Write objRS("TABLE_NAME") & "<BR>"
objRS.MoveNext
Loop
'Clean up...
objRS.Close
Set objRS = Nothing
objConn.Close
Set objConn = Nothing
%>
Keep in mind that Example 6.1, Example 6.2, and Example 6.3 all are functionally equivalent.
There are several possible SchemaEnum
values, each
with their own columns and possible criteria values — far too
many to list in a non-reference book like this one. You can obtain a
full SchemaEnum
listing at http://msdn.microsoft.com/library/psdk/dasdk/mdae0wfh.htm
or through a technical reference book, like Wrox’s
ADO 2.0 Programmer’s Reference (Wrox Press Inc.).
Schemas will greatly assist in the quest to create a generic database access script. Since schemas provide portable, low-level database information through an easy-to-use interface, they make an ideal solution for determining how to display particular database fields and database input mechanisms on a generic database page.
For more information on ADO schemas, be sure to read the following tutorials and articles:
“Listing the Tables and Columns in a Database,” found at http://www.4guysfromrolla.com/webtech/101799-1.shtml.
Technical documentation for OpenSchema
, found at
http://msdn.microsoft.com/library/psdk/dasdk/mdam2ppd.htm.
For general ADO information, check out Microsoft’s ADO site at http://www.microsoft.com/data/ado/.
3.147.238.70