Note:This chapter assumes you are familiar with relational databases and Structured Query Language (SQL).
Users must have Netscape Navigator (or another web client) on client machines. Refer to the Netscape Navigator Handbook for more information.
Running the video store application
LiveWire comes with a sample database application called Video, which uses a SQL database for tracking video rentals at a fictional video store. Before you can run Video you must create the video database on the database server.
Note:
For beta, refer to the release notes for instructions on creating the video database used with the video store application.
Using the Video application you can
http://server.domain/video/
Tutorial TBD.
Working with databases
Before you create a LiveWire database application, the database should already exist on the database server, and you should be familiar with its structure. If you are creating an entirely new application, including the database, you should design, create, and populate the database first (at least in prototype form), before creating the LiveWire application to access it.
LiveWire has an object named database that has methods for working with relational databases. The database object has no predefined properties. LiveWire creates a database object when an application connects to a database server. Each application can have one and only one database object. Table 6.1 describes the database object's methods.
Connecting to a database
To connect to a database, use the database object's connect method. This method has the following syntax:
database.connect("type", "server", "user", "password", "database")
Each parameter must be enclosed in double quotation marks. The parameters are
For example, the following statement connects to an Informix database named "mydb" on a server named "myserver" with user name SYSTEM, and password MANAGER:
database.connect("INFORMIX", "myserver", "SYSTEM", "MANAGER", "mydb")
The connected method indicates if the application is currently connected to a database. If currently connected, then connected returns true; otherwise it returns false. For example,
if(!database.connected())
{ // statements to execute if not connected to a database }
Approaches to connecting to a database
You can take two different approaches to establishing database connections:
In the standard approach, the application contains a single connection statement that all clients use to establish the database connection. Thus, all clients will share the same database, user name, and so on. In general, this requires a connect in the initial startup page of the application. Other pages in the application do not have to do anything special to use the shared connection. This is the approach used in the video store sample application.
The advantage of the standard approach is that LiveWire handles all the details of establishing database connections. The disadvantage is that all connections from the application share the same user name and access privileges. This approach also requires a vendor license to use multiple database connections.
Note:
The standard approach could cause the number of simultaneous database
connections to reach the maximum number of processes or threads specified in
the configuration file for your Netscape server.
In the serial approach, each page in the application that needs a database connection first locks the project object, and then connects to the database; after it is finished with its database activity, it then disconnects from the database and unlocks the project object. For example,
project.lock()
database.connect(...)
execute(SQL stmts)
database.disconnect(...)
project.unlock()
In general, when using this approach, clients can use different databases, user names, and so on. The lock method locks the database connection so that only one application can be connected to the database at any time. For more information on lock and unlock, see "Locking the project object" on page 77.
If several applications will all be connecting to the same database, you must lock the server object to use the serial approach.
Limiting number of database connections
Some database vendors charge fees based on the number of connections made to the database server. On Unix platforms, you can limit the number of simultaneous active database connections by editing the configuration file magnus.conf
. The entry for "MaxProcs" indicates the maximum number of HTTP processes allowed. If numDBApps is the number of database applications running on your server, then the maximum number of database connections is
MaxConnections = numDBApps x MaxProcs
Legally, the maximum number of database connections is determined by the license you have purchased from your database vendor, so you can calcualte the setting of MaxProcs as
MaxProcs = NumLicense / numDBApps
where NumLicense is the numer of connections for which your server is licensed. For example, if you have a 32-user database license, and you have two deployed database applications running (numDBApps = 2), then set
MaxProcs = 32 / 2 = 16
Displaying queries with SQLTable
LiveWire provides several ways for you to display the results of database queries. One of the simplest and quickest is the SQLTable method of the database object. The SQLTable method takes as its argument an SQL SELECT statement and returns an HTML table, with each row and column in the query as a row and column of the table.
Although SQLTable does not give you any explicit control over how the output is formatted, it is the easiest way to display query results. If you want to customize the appearance of the output, create your own display function using a database cursor. For more information on database cursors, see "Using cursors" on page 114.
For example, if request.sql contains a SQL query, then the following JavaScript statements will display the result of the query in a table:
write(request.sql)
database.SQLTable(request.sql)
The first line simply displays the SELECT statement, and the second line displays the results of the query. This is the first part of the HTML generated by these statements:
select * from videos
<TABLE BORDER>
<TR>
<TH>title</TH>
<TH>id</TH>
<TH>year</TH>
<TH>category</TH>
<TH>quantity</TH>
<TH>numonhand</TH>
<TH>synopsis</TH>
</TR>
<TR>
<TD>A Clockwork Orange</TD>
<TD>1</TD>
<TD>1975</TD>
<TD>Science Fiction</TD>
<TD>5</TD>
<TD>3</TD>
<TD> Little Alex, played by Malcolm Macdowell, and his droogies stop by
the Miloko bar for a refreshing libation before a wild night on the
town. </TD>
</TR>
<TR>
<TD>Sleepless In Seattle</TD>
...
As this example illustrates, SQLTable generates an HTML table, with column headings for each column in the database table and a row in the table for each row in the database table.
Disconnecting from a database
An application can close a database connection with the disconnect method as follows:
database.disconnect()
Once disconnected from a database, an application cannot create cursors or use any other database methods.
Executing passthrough SQL statemetns
The database method execute enables an application to execute any SQL statement that does not return a cursor. Using execute is referred to as performing passthrough SQL, because it passes SQL directly to the server.
You can use execute for any statement supported by the database server that does not return a cursor, including INSERT, UPDATE, and DELETE statements, CREATE, ALTER, and DROP data definition language (DDL) statements, and other control statements supported by the server. For more information, see "Using cursors" on page 114.
Note
It is preferable to use cursors to perform data modification (INSERT, UPDATE, and DELETE statements) because your application will be more database-independent. Cursors also provide support for BLOb data.
To perform passthrough SQL statements, simply provide the SQL statement as the argument to the method:
database.execute("delete from rentals r where r.customerID = " +
request.customerID + " AND r.videoID = " + request.videoID)
If you have not explicitly started a transaction, the single statement will be committed automatically. For more information on transaction control, see the next section.
Managing transactions
A transaction is a group of database actions that are performed together. Either all the actions succeed together, or all fail together. When you attempt to perform all the actions, you are said to commit a transaction. You can also roll back a transaction that you have not committed, which cancels all the actions.
Transactions are important for maintaining data integrity and consistency. Although the various database servers implement transactions slightly differently, LiveWire provides a uniform interface for transaction management.
Primarily actions that modify a database come under transaction control. These actions correspond to SQL INSERT, UPDATE, and DELETE statements performed with the execute method.
Using default transactions
If you do not control transactions explicitly, LiveWire performs each database update as a separate transaction. Essentially, LiveWire begins an implicit transaction before each statement and attempts to commit the transaction after each statement. Explicitly managing transactions overrides this default behavior.
Using explicit transaction control
Use the following methods of the database object to manage transactions:
An application must be connected to a database to use these methods. For example, to begin a transaction, write
database.beginTransaction()
Likewise, to commit a transaction, write
database.commitTransaction()
The scope of a transaction is limited to the current HTML page in an application. If the application exits the page before issuing a commitTransaction or rollbackTransaction, then the transaction is automatically committed.
If there is no current transaction (that is, if the application has not issued a beginTransaction), any commitTransaction and rollbackTransaction statements are ignored.
Nested transactions
A transaction within another transaction is a nested transaction. Essentially, a nested transaction performs two beginTransaction methods in a row, before committing or rolling back the first transaction.
LiveWire does not support nested transactions, because of the major database servers, only Sybase supports them. If you begin a transaction and then issue another beginTransaction before committing or rolling back the active transaction, the second beginTransaction is ignored. Then, the next commitTransaction or rollbackTransaction will commit or roll back all the actions since the first beginTransaction.
Transaction isolation levels
Transaction isolation level refers to the interaction among multiple simultaneous transactions. Different servers handle transaction isolation levels in different ways.
For Informix, the default isolation level is used for transactions. The default is
For more information, refer to the Informix documentation.
For Oracle, all transactions are started with the default isolation level set at the server level. In most cases it is set to "serializable = FALSE." This means Oracle maintains no read locks on rows being read, and write locks are maintained on rows being updated or inserted. For more information, refer to the Oracle documentation.
For Sybase, the default isolation level is "dirty read." For more information, refer to the Sybase documentation.
Using cursors
A database query is said to return a cursor. You can think of a cursor as a virtual table, with rows and columns specified by the query; this virtual table is sometimes referred to as an answer set. A cursor also has a notion of a current row, which is essentially a pointer to a row in the answer set. When you perform operations with a cursor, they affect the current row.
Creating a cursor
Once an application is connected to a database, you can create a cursor with the cursor method of the database object, by giving it a SELECT statement as its argument. Use the following syntax:
cursorName = database.cursor("SELECT statement", updatable)
where
Initially, the pointer, or current row, is positioned just before the first row in the answer set.
For example, the following statement creates a cursor consisting of records from the customer table containing the columns ID, NAME, and CITY, ordering the records returned by the value of the ID column:
custs = database.cursor("SELECT ID, NAME, CITY FROM CUSTOMER ORDER BY
ID")
This query might return the following rows:
1 John Smith Anytown
2 Fred Flinstone Bedrock
3 George Jetson Spacely
You can construct the SELECT statement with the string concatenation operator (+) and string variables such as client or request property values. For example,
custs = database.cursor("select * from customer where ID = " +
request.customerID)
Cursor methods and properties
The cursor object has a number of methods and properties, as summarized in Table 6.2.
Displaying record values
When you create a cursor, it acquires a property colName for each column in the answer set, as determined by the SELECT statement. In the previous example in "Creating a cursor," the cursor would have properties for the columns ID, NAME, and CITY. So, you could display the values of the current row with the following:
<SERVER>custs.next()<SERVER>
<B>Customer ID:</B> <SERVER>write(custs.ID)</SERVER> <BR>
<B>Customer Name:</B> <SERVER>write(custs.name)</SERVER> <BR>
<B>City:</B> <SERVER>write(custs.city)</SERVER>
Initially, the current row is positioned before the first row in the table. The execution of next method moves the current row to the first row, so the preceding code would display something similar to this:
Customer ID: 1
Customer Name: John Smith
City: Anytown
Note
Unlike other properties in JavaScript, cursor properties corresponding to
column names are not case-sensitive, because SQL is not case-sensitive and
some databases are not case sensitive.
You can also refer to properties of a cursor object (or any JavaScript object) as elements of an array. The zero-index array element corresponds to the first column, the one array element corresponds to the second column, and so on.
So, for example, you could use an index to display the same column values retrieved in the previous example:
<B>Customer ID:</B> <SERVER>write(custs[0])</SERVER> <BR>
<B>Customer Name:</B> <SERVER>write(custs[1])</SERVER> <BR>
<B>City:</B> <SERVER>write(custs[2])</SERVER>
This technique is particularly useful inside a loop.
Closing a cursor
Use the close method to close a cursor and free the memory consumed. For example,
custs.close()
closes the cursor named custs. LiveWire automatically closes all cursors at the end of each client request.
Determining the number of columns in a cursor
The cursor method columns returns the number of columns in a cursor. For example, you could determine the number of columns returned as
custs.columns()
This method is useful if you need to iterate over each column in a cursor.
Displaying column names
The cursor method columnName returns the name of each column in the result set. This method takes an integer as a parameter, where the integer specifies the ordinal number of the column, starting with zero. The first column in the result set is zero, the second is one, and so on.
For example, the following expression would assign the name of the first column in the custs cursor to the variable header:
header = custs.columnName(0)
Displaying expressions and aggregate functions
SELECT statements can retrieve values that are not columns in the database, such as aggregate values and SQL expressions. You can display these values by using the cursors property array index for the value. That is, you can refer to the expression as cursor[n], where n is the ordinal position (starting at zero) of the expression in the SELECT list.
For example, suppose you create the following cursor named empData:
empData = database.cursor("SELECT MIN(SALARY), AVG(SALARY), MAX(SALARY)
FROM EMPLOYEES)
Then you could display the value retrieved by the aggregate function MAX as follows:
write("Highest salary is ", empData[2])
Navigating with cursors
Initially, the pointer, or current row, for a cursor is positioned just before the first row in the answer set. Use the next method to move the pointer through the records in the cursor answer set. This method moves the pointer to the next row and returns true as long as there is another row in the answer set. When the cursor has reached the last row in the answer set, next returns false.
For example, if an answer set has columns named title, rentalDate, and dueDate, then the following code iterates through the rows with next and displays the column values in a table:
<SERVER>
while cursor.next() {
write("<TR><TD>" + cursor.title + "</TD>)
write("<TD>" + cursor.rentalDate) + "</TD>"
write("<TD>" + cursor.dueDate + "</TD></TR>")
}
Using a cursor: an example
You can easily display all the values in a query result by using the properties and methods of cursor. If you have created a cursor named custs as in the preceding examples, then you can use the following loop to display the query results in an HTML table:
// Display column names as headers
write("<TR>")
i = 0
while( i < custs.columns() ) {
write("<TH>", cursor.ColumnName(i), "</TH>")
i++
}
write("</TR>")
// Display each row in the result set
while(cursor.next()) {
write("<TR>")
i = 0
while( i < custs.columns() ) {
write("<TD>", cursor[i], "</TD>")
i++
}
write("</TR>")
}
Using updatable cursors
An updatable cursor enables you to modify a table based on the cursor's current row. To request an updatable cursor, add an additional parameter of true when creating the cursor. For example,
custs = database.cursor("SELECT ID, CUST_NAME, CITY FROM CUSTOMER",
true)
You can create an updatable cursor only for single-table queries, not for multiple-table queries or queries containing joins.
For a cursor to be updatable, the SELECT statement must be an updatable query, one that allows updating. For example, it cannot retrieve rows from more than one table, contain a GROUP BY clause, and generally must retrieve key values from a table. For more information on constructing updatable queries, consult your database vendor's documentation.
Using an updatable cursor is a two-step process:
custs.city = "New York"
custs.updateRow("CUSTOMERS")
When you use insertRow or updateRow, the values you assign to columns in the first step are used in the new or updated row. If you have previously performed next with the cursor, then the values of the current row are used for any columns without assigned values.
When inserting, if the cursor has not performed next, then any columnsthat you have not assigned values to will be null. Also, when inserting values into a table, if some columns in the table are not in the cursor, then insertRow will insert null in these columns.
You do not need to assign values when you use deleteRow, because it simply deletes an entire row.
Data-type conversion
Databases have a rich set of data types. LiveWire converts these data types to JavaScript values, which are either strings or numbers. A JavaScript number is stored with precision of a double-precision floating-point value. In general, LiveWire converts character data types to strings, numeric data types to numbers, and dates to JavaScript Date objects. It converts null values to JavaScript null.
Note:
LiveWire does not support packed decimal notation; therefore some degree of precision may be lost when reading and writing packed decimal data types. Be sure to check results before inserting values back into a database, and use appropriate mathematical functions to correct for any precision loss.
Working with dates
Date values retrieved from databases are converted to JavaScript Date objects. For more information on working with dates in JavaScript, see the JavaScript Guide.
To insert a date value in a database, use a JavaScript Date object, as follows:
cursorName.dateColumn = dateObj
where cursorName is a cursor, dateColumn is a column corresponding to a date, and dateObj is a JavaScript Date object. You create a Date object using the new operator and the Date constructor, as follows:
dateObj = new Date(dateString)
where dateString is a string representing a date. If this is the empty string, it will create a Date object for the current date. For example,
invoiceDate = new Date("Jan 27, 1997")
custs.orderDate = invoiceDate
You can combine these two statements and get the same result, as in the following example:
custs.orderDate = new Date("Jan 27, 1997")
Informix data-type conversion
Table 6.3 describes the conversion of data types from an Informix database to JavaScript values.
Oracle Data Type
|
LiveWire Data Type
|
---|---|
long
|
string
|
char or varchar2(n)
|
string
|
raw(255), long raw
|
BLOb
|
number(p,s), number(p,0)
|
number
|
float(p)
|
number
|
rowid, mislabel
|
string
|
date
|
date
|
<IMG SRC=`mycursor.imageFileName`>As the cursor navigates through the table, the name of the file in the IMG tag changes to reference the appropriate file. If you need to store binary data itself in your database (or if it is already there), LiveWire provides methods for displaying and inserting BLOb data, as described in Table 6.6.
Using blobImage
The blobImage method fetches a BLOb from the database, creates a temporary file (in memory) of the specified format, and generates an HTML image tag that refers to the temporary file. LiveWire removes the temporary file after the page is generated and sent to the client.
Because LiveWire keeps the binary data that blobImage fetches from the database in active memory while creating the page, requests that fetch a large amount of data can exceed dynamic memory on the server. It is generally good practice to limit the number of rows retrieved at one time using blobImage to stay within the server's dynamic memory limits.
Use blobImage to create an HTML image tag for a graphic image in a standard format such as GIF or JPEG. For a cursor cursorName and a column containing BLOb data colName, the syntax for this method is
cursorName.colName.blobImage(format [, altText ] [, align ]
[, widthPixels] [, heightPixels] [, borderPixels] [,ismap ])
The parameters are
All the parameters to blobImage except ismap are JavaScript string expressions.
Using blobLink
The blobLink method fetches BLOb data from the database, creates a temporary file in memory, and generates a hypertext link to the temporary file. LiveWire removes the temporary files that blobLink creates after the user clicks the link or sixty seconds after the request is processed.
Because LiveWire keeps the binary data that blobLink fetches from the database in active memory, requests that fetch a large amount of data can exceed dynamic memory on the server. It is generally good practice to limit the number of rows retrieved at one time using blobLink to stay within the server's dynamic memory limits.
Use blobLink if you do not want to display graphics (to reduce bandwidth requirements) or to provide a link to an audio clip or other multimedia content not viewable inline. The syntax of this method is
cursorName.colName.blobLink(mimeType, linkText)
where colName is the name of a column in the cursor containing BLOb data. The parameters of this method are
The following example illustrates using these methods:
cursor = database.cursor("select * from blobtest")
while(cursor.next()) {
write(cursor.id)
write(cursor.picture.blobImage("gif"))
write(cursor.picture.blobLink("image/gif", "Link" + cursor.id))
write("<BR>")
}
cursor.close()
This example will produce the following HTML:
1 <IMG SRC="LIVEWIRE_TEMP9"> <A HREF="LIVEWIRE_TEMP10">Link1 </A> <BR>
2 <IMG SRC="LIVEWIRE_TEMP11"> <A HREF="LIVEWIRE_TEMP12">Link2 </A> <BR>
3 <IMG SRC="LIVEWIRE_TEMP13"> <A HREF="LIVEWIRE_TEMP14">Link3 </A> <BR>
4 <IMG SRC="LIVEWIRE_TEMP15"> <A HREF="LIVEWIRE_TEMP16">Link4 </A> <BR>
This example illustrates the creation of temporary files with blobImage and blobLink.
Using blob
The blob function assigns BLOb data to a column in a cursor. Use this function to insert or update a row containing a BLOb using an updateable cursor. To insert or update a row using SQL and the execute method, use the syntax supported by your database vendor. The blob function is a top-level function, not a cursor method, like blobImage and blobLink.
These statements illustrate using blob to update a row in a table named EMPLOYEE with an updatable cursor:
cursor.photo = blob("myphoto.gif")
cursor.office = blob("myoffice.gif")
cursor.updateRow("employee")
These statements update BLOb data from the specified GIF files in columns PHOTO and OFFICE of the EMPLOYEE table.
Error Handling
SQL statements can fail for a variety of reasons, including referential integrity constraints, lack of user privileges, record or table locking in a multiuser database, and so on. When an action fails, the database server returns an error message indicating the reason for failure. LiveWire provides two ways of getting error information: from the status code returned by database methods and special database properties containing error messages and codes.
Database status codes
Many database and cursor methods return a status code based on the error message generated by the database server. The execute, insertRow, updateRow, deleteRow, beginTransaction, updateTransaction, and rollbackTransaction methods all return a status code.
Status codes are integers between 0 and 27, with 0 indicating a successful execution of the statement, and other numbers indicating an error, as shown in Table 6.6:
Database error methods
The database object contains four methods that return error codes and messages returned by the database server. The methods are
The results returned by these methods depends on the database server being used and the database status code.
Database error methods for Sybase with status code 7 (vendor library error) | |
---|---|
Method
|
Description
|
majorErrorMessage
|
"Vendor Library Error: string," where string is error text from DB-Library.
|
minorErrorMessage
|
Operating system error text, as reported by DB-Library.
|
majorErrorCode
|
DB-Library error number.
|
minorErrorCode
|
Severity level, as reported by DB-Library.
|