JDataStore is a feature of JBuilder Professional and Enterprise.
DataStore provides embedded database functionality in your applications with a single DataStore file and the DataStore JDBC driver (and its supporting classes). No server process is needed for local connections. In addition to industry-standard JDBC support, you may take advantage of the added convenience and flexibility of accessing the DataStore directly through the DataExpress API. You may use both types of access in the same application.
JDBC access requires that the DataStore be transactional. DataExpress does not. This chapter begins with DataExpress access, then discusses transactional DataStores, and finally the local JDBC driver. The remote JDBC driver and DataStore JDBC server are discussed in "Multi-user and remote access to DataStores."
To use a DataStore as a database file, associate a component that extends from StorageDataSet, such as TableDataSet, to a stream inside a DataStore. The StorageDataSet represents a table in the embedded database and provides all the methods necessary to navigate, locate, add, edit, and delete data.
Start a new file in the dsbasic project/package and name it
DxTable.java
:
// DxTable.java package dsbasic; import com.borland.datastore.*; import com.borland.dx.dataset.*; public class DxTable { DataStoreConnection store = new DataStoreConnection(); TableDataSet table = new TableDataSet(); public void demo() { try { store.setFileName( "Basic.jds" ); table.setStoreName( "Accounts" ); table.setStore( store ); table.open(); } catch ( DataSetException dse ) { dse.printStackTrace(); } finally { try { store.close(); table.close(); } catch ( DataSetException dse ) { dse.printStackTrace(); } } } public static void main( String[] args ) { new DxTable().demo(); } }
Because the program uses DataExpress, it imports the DataExpress package in addition to the DataStore package. The class has two fields: a DataStoreConnection and a TableDataSet. The main() method instantiates a new instance of the class and executes its demo() method.
The focal point of DataExpress semantics is the class StorageDataSet. This class has three subclasses to be used for different kinds of data sources:
If you are defining a completely new table, use TableDataSet.
Each StorageDataSet has a store property that null when the object is instantiated. If it's still null when the dataset is opens, a com.borland.dx.memorystore.MemoryStore is assigned automatically, which means that the data is stored in memory. If you assign a DataStoreConnection or DataStore to the store property, the data is stored in a persistent DataStore file instead.
To connect a StorageDataSet to a DataStore, assign values to these three properties:
Peform these three steps in any order. Once you've set all three properties, you have a fully qualified connection between a StorageDataSet and a DataStore.
In DxTable.java
, the DataStore file is Basic.jds
, which you created in "Creating a DataStore file". The table stream is named "Accounts". Think of it as the name of the table. DxTable.java
assigns DataStoreConnection as the value of the TableDataSet's store property.
Then the TableDataSet opens. Opening a dataset that has a DataStore attached automatically opens that DataStore file. If the DataStore opens successfully, the program creates the named table stream if it doesn't already exist. If it does exist, that table stream reopens. This establishes an open connection between the dataset and its table stream in the DataStore. Note that if there is a file stream in the DataStore with the same name, the program throws an exception because you can't have a table stream with the same name as a file stream.
Opening a StorageDataSet connected to a DataStore results in an open table stream. For new table streams, QueryDataSet and ProcedureDataSet then fetch data from their data source and populate the table stream as explained in "Tutorial: Offline editing with DataStore". But TableDataSet has no data source. You start with an empty and undefined table stream.
Add the highlighted statements to DxTable.java
:
table.open(); if ( table.getColumns().length == 0 ) { createTable(); } } catch ( DataSetException dse ) {
To detect that a table stream is new, check the number of columns in the TableDataSet. If it's zero, you can then define the columns in the table. In this case, it's done by a method called createTable(). Add it to DxTable.java
:
public void createTable() throws DataSetException { table.addColumn( "ID" , Variant.INT ); table.addColumn( "Name" , Variant.STRING ); table.addColumn( "Update", Variant.TIMESTAMP ); table.addColumn( "Text" , Variant.INPUTSTREAM ); table.restructure(); }
In this demo program, the createTable() method uses the simplest form of the StorageDataSet.addColumn() method to add columns individually by name and type. The columns have no constraints. Character columns, defined as Variant.STRING, can contain strings of any length. You can define columns with constraints by defining Column objects, setting the appropriate properties such as precision, and then adding them with the addColumn() or setColumns() methods to the table.
After you modify the structure of the table by adding these new columns, activate the changes by calling the StorageDataSet.restructure() method. The result is an empty but structured table stream, a new table in the DataStore. (If you know the table doesn't exist, you can use addColumns to define the structure before opening the TableDataSet. Then you won't need to call restructure().)
You can store as many tables as you want in a single DataStore file. They must use different table stream names. You can use the same DataStoreConnection object in the store properties of each TableDataSet.
There are other ways to create tables in a DataStore. In particular, you can use an SQL CREATE TABLE statement through the DataStore JDBC driver.
Once the tables in the DataStore have been defined (no matter how they were created), you can use the rest of the DataExpress API through a TableDataSet object, just as you would with any dataset. You can create filters, indexes, master-detail links, and so on. In fact, such secondary indexes are also persisted and maintained in the DataStore file, making the DataStore a complete embedded database.
To complete the demonstration program, add a smattering of DataExpress functionality, with this new method:
public void appendRow( String name ) throws DataSetException { int newID; table.last(); newID = table.getInt( "ID" ) + 1; table.insertRow( false ); table.setInt( "ID", newID ); table.setString( "Name", name ); table.setTimestamp( "Update", new java.util.Date().getTime() ); table.post(); }
Add the highlighted statements to the demo() method:
if ( table.getColumns().length == 0 ) { createTable(); } table.setSort( new SortDescriptor( new String[] {"ID"} ) ); appendRow( "Rabbit season" ); appendRow( "Duck season" ); table.first(); while ( table.inBounds() ) { System.out.println( table.getInt( "ID" ) + ": " + table.getString( "Name" ) + ", " + table.getTimestamp( "Update" ) ); table.next(); } } catch ( DataSetException dse ) {
After the program opens the table, creating the table's structure if necessary, it sets a SortDescriptor on the ID field. To add some rows, it calls the appendRow() method.
The appendRow method starts by going to the last row in the table and obtaining the value of the ID field. Because of the sort order, this value should be the highest ID number used so far. (If the table is empty, the getInt() method returns zero.) The new ID value is one greater than the last. appendRow() inserts a new row and sets its attributes, including the Update field, which is set to the current date and time. Finally appendRow() saves the new row by calling the post() method.
After appending a few rows, a loop navigates through the table, displaying its contents in the console. Finally, the DataStore and TableDataSet are closed.
If you run the program a few times, you'll see that the new rows get unique ID numbers. This method of generating ID numbers works for a simple single-threaded demonstration program like this that always commits new rows after getting the old ID number. But for more realistic programs, such an approach may not be safe. To use a more robust approach, you must understand locks and transactions.
So far, changes you have made to a DataStore have been direct and immediate. If you write an object, change some bytes in a file stream, or add a new row to a table, it's done without concern for other connections that might be accessing the same stream. Such changes are immediately visible to those other connections.
While this behavior is safe for simple applications, more robust applications require some level of transaction isolation. Not only do transactions ensure that you are not reading dirty or phantom data, but you can also undo changes made during a transaction. Transaction support also enables automatic crash recovery. It's required for JDBC access.
Transaction support is provided by the com.borland.datastore. TxManager class. A DataStore can be transactional when it is first created, or you can add transaction support later. In either case, you assign a TxManager object as the value of the txManager property of the DataStore object before calling the create() or open() method. (If you attempt to assign the txManager property to an open DataStore, an exception occurs.)
The properties of the TxManager object determine various aspects of the transaction manager. When instantiated, the TxManager has usable default settings for these properties. If you want to change any of these settings, do it before creating or opening the DataStore.
The first time the now-transactional DataStore opens, it stores its transactional settings internally. The next time you open the DataStore, you don't have to assign a TxManager. Instead the DataStore automatically instantiates a TxManager with its stored settings.
To open (or create) a transactional DataStore, you must also set the DataStoreConnection.userName property. The userName property is used to identify individuals in a multi-user environment when necessary, such as during lock contention. If there is no name in particular that you find appropriate, you can set it to a dummy name.
Here's the minimum code for creating a new transactional DataStore with default settings:
DataStore store = new DataStore(); store.setFileName( "SomeFileName.jds" ); store.setUserName( "AnyNameWouldWork" ); store.setTxManager( new TxManager() ); store.create();
The two differences between this code and a non-transactional DataStore is the setting of the userName and txManager properties. (You can set them in any order.) If you don't want the default settings, the code generally looks something like this:
DataStore store = new DataStore(); TxManager txMan = new TxManager(); // Make changes to TxManager txMan.setRecordStatus( false ); store.setFileName( "SomeFileName.jds" ); store.setUserName( "AnyNameWouldWork" ); store.setTxManager( txMan ); store.create();
In this example, the recordStatus property, which controls whether status messages are written, is set to false.
DSX: See "Creating a new DataStore file".
The code for making an existing DataStore transactional is very similar. The main difference is the call to open() instead of create(). For a default TxManager, the code might look like this:
DataStore store = new DataStore(); store.setFileName( "SomeFileName.jds" ); store.setUserName( "AnyNameWouldWork" ); store.setTxManager( new TxManager() ); store.open();
Note that even though you are much more likely to use a DataStoreConnection to open an existing DataStore file, you can't use one when you are adding transaction support, because txManager is a property of DataStore, not DataStoreConnection.
DSX: See "Making the DataStore transactional".
The only difference when opening a DataStore that's transactional and one that's not is that you must specify a userName. Because it doesn't hurt to specify a userName for a non-transactional DataStore (it's simply ignored), you might want to always specify a userName when opening a DataStore. The code would look something like this:
DataStoreConnection store = new DataStoreConnection(); store.setFileName( "SomeFileName.jds" ); store.setUserName( "AnyNameWouldWork" ); store.open();
Because no TxManager was assigned, when the DataStore opens, a TxManager is automatically instantiated, with its properties set to the values that were persisted in the DataStore. The TxManager is assigned to the DataStore's txManager property. You can get the values of the persisted transaction management properties from there, but you can't change them directly.
To change a DataStore's transaction setting, assign a new TxManager object before opening. The TxManager object knows which properties have been assigned, and which ones have been left at their default value. If you assign a TxManager to a transactional DataStore, only those properties that have been assigned in the new TxManager will be changed. All other properties remain as they were; they do not revert to default values (the ones in the new TxManager).
As when adding transaction support, the TxManager with the new values must be assigned before you open the DataStore. For example, suppose you want to change the softCommit property to true, which would improve performance by not guaranteeing recently committed transactions (within approximately one second before a system failure), while still guaranteeing crash recovery:
DataStore store = new DataStore(); TxManager txMan = new TxManager(); // Make changes to TxManager txMan.setSoftCommit( true ); store.setFileName( "SomeFileName.jds" ); store.setUserName( "AnyNameWouldWork" ); store.setTxManager( txMan ); store.open();
Note that the other properties, such as recordStatus, are not set. Although the new TxManager has the default setting when it is assigned to the DataStore, the setting in the DataStore will not be affected, even if it is not the default.
DSX: See "Modifying transaction settings".
The transaction manager works by logging changes made to the DataStore, including the previous values so that the transaction can be rolled back. (The changes are not removed from the log file when the changes are committed, so if you archive the log files, it's possible to extract a complete change log, or reconstruct the contents of the DataStore.) Most of the TxManager properties control the transaction log files, including:
By default, you get one copy of the log files (simplexing instead of duplexing), in the same directory that contains the DataStore file.
Whenever a DataStore is transaction-enabled the first time, it creates its log files. The names of the log files use the name of the DataStore file, without the file extension. For example, if the DataStore file MyStore.jds
uses simplex transaction logging, the following log files are created:
MyStore_STATUS_0000000000
,
MyStore_LOGA_ANCHOR
, and
MyStore_LOGA_0000000000
.
Duplex logging adds the files MyStore_LOGB_ANCHOR
and MyStore_LOGB_0000000000
. These two sets of log files are referred to as the "A" and "B" log files. The location of these files is controlled by the ALogDir and BLogDir properties.
Once a log files reaches the size determined by the TxManager's maxLogSize property, additional status and record files are created, with the log file number incrementing by one each time. Conversely, as old log files are no longer needed for active transactions or crash recovery, they are automatically deleted. For information on archiving them, see "Saving log files".
The ALogDir and BLogDir properties include the drive and full path, which means two things:
Sometimes you will want to access a transactional DataStore, but need to bypass transaction support. Situations include:
In both cases, you can temporarily bypass transaction support by opening the DataStore in read-only mode. This must be done through a DataStore object; before opening, set its readOnly property to true, like this:
DataStore store = new DataStore(); store.setFileName( "SomeReadOnly.jds" ); store.setReadOnly( true ); store.open();
Because you are bypassing the TxManager, you do not need to set the userName. If the transaction log files are lost, use the copyStreams method (or the DataStore Explorer, see "Copying DataStore streams") to copy the streams to another file.
A DataStore is made non-transactional by assigning a new TxManager that has its enabled property set to false (by default, this property is true). The DataStore's consistent property must be true--it reflects that the DataStore is internally consistent--or the change will not be allowed to take effect. Because you are disabling the TxManager, you do not need to set the userName. For example:
DataStore store = new DataStore(); TxManager txMan = new TxManager(); // Disable TxManager txMan.setEnabled( false ); store.setFileName( "SomeFileName.jds" ); store.setTxManager( txMan ); store.open();
Disabling the TxManager does not affect any existing log files. If you make the DataStore transactional again, any existing log files will be reused if appropriate.
DSX: See "Removing transaction support".
When you delete a transactional DataStore file, be sure to delete its log files as well. If you don't, you won't be allowed to create a new DataStore file with the same name, because the log files won't match.
Once a DataStore has been made transactional, you can basically ignore the TxManager; in fact, the only time you will need to reference one is if you want to examine or change the DataStore's transaction settings. The interface for controlling transactions is on the DataStoreConnection object, primarily through the commit and rollback methods.
Each DataStoreConnection is a separate transaction context. This means that all the changes made through a particular DataStoreConnection are treated as a group and separate from changes made through all others.
(Note that as a subclass of DataStoreConnection, a DataStore object can also act as a separate transaction context. The difference is that you can only have one DataStore object accessing a particular DataStore file, while you can have many DataStoreConnection objects. When you open a DataStoreConnection, it will contain a reference to a DataStore object, as explained in "Referencing the connected DataStore". If there is no suitable DataStore object in memory, the DataStoreConnection will automatically open a DataStore to satisfy this reference. This means that if you open a DataStoreConnection first, subsequent DataStore objects accessing the same DataStore file will have their allowed functionality reduced so that they behave like a DataStoreConnection.)
A transaction's lifecycle begins with any read or write operation through a connection. The DataStore uses stream locks to control access to resources. To read a stream or make a change to any part of a stream (a byte in a file, a row in a table), you must be able to acquire a lock on that stream. Once a connection acquires a lock, it will hold on to it until the transaction is committed or rolled back.
In single-connection applications, transactions may be considered primarily as a feature that allows you to undo changes and provide crash recovery. Or you may have made a DataStore transactional so that it can be accessed via JDBC; if you want to access that DataStore via DataExpress, you must now deal with transactions. The way transactions work has deeper ramifications for multi-connection (multi-user or single-user multi-session) applications. These are discussed in "Avoiding blocks and deadlocks", along with other multi-user issues in "Multi-user and remote access to DataStores."
Controlling transactions boils down to three methods of DataStoreConnection:
When a DataStoreConnection is closed, it will attempt to commit any pending transaction. You can control this automatic behavior by listening to the DataStore's Response event for a COMMIT_ON_CLOSE, as shown in the following tutorial.
This tutorial creates a simple Swing-based application that provides a means to commit and roll back transactions. It also detects the automatic commit on close, allowing the user to decide whether to commit. In the process, it shows some important details about using a DataStore in a GUI application. The end result looks like this:
At this point, you should already have a DataStore file with some data in it: Basic.jds
. Instead of making that file transactional, make a copy of the file, and make the copy transactional. This way, you have both kinds of DataStore files, transactional and non-transactional, to play with.
Make a copy of the file, naming it Tx.jds
. This is a copy of the entire file, so you can use the Windows Explorer, or the command line copy command. Then add the following program to the project, MakeTx.java
:
// MakeTx.java package dsbasic; import com.borland.datastore.*; public class MakeTx { public static void main( String[] args ) { if ( args.length > 0 ) { DataStore store = new DataStore(); try { store.setFileName( args[0] ); store.setUserName( "MakeTx" ); store.setTxManager( new TxManager() ); store.open(); store.close(); } catch ( com.borland.dx.dataset.DataSetException dse ) { dse.printStackTrace(); } } } }
This utility program will make any DataStore file transactional, if it isn't already. For DataStores that are already transactional, nothing happens, because no properties are set on the TxManager object.
Set the runtime parameters in the Project Properties dialog box to Tx.jds
, and run the program. It will take a moment to create the three transaction log files Tx_STATUS_0000000000
, Tx_LOGA_ANCHOR
, and Tx_LOGA_0000000000
.
The next step is to create a data module with the DataStore and a TableDataSet:
dsbasic
, and set the Class name to AccountsDM
. Make sure the Invoke Data Modeler checkbox is not selected. Click OK.
AccountsDM.java
.
dataStore
(easily done by pressing F2 after adding it to the tree).
Tx.jds
you created earlier, and set the userName property to some name. (If you can't think of a name, how about Chuck
.)
Accounts
, and the store property to dataStore
.
Now, a simple table grid to display the data, with a very important detail at the end:
AccountsApp
. Click Next.
AccountsFrame
, and the Title to Accounts
. Make sure the Center frame on screen checkbox is selected; deselect the rest. Click Finish.
AccountsFrame.java
.
AccountsDM
data module. Set the Field name to dataModule
. Select the option to use a shared (static) instance. Click OK.
dataModule.TableDataSet1
(the only choice).
In this case, with only one connection, the close method would work, but because you are calling System.exit you want to make sure the DataStore is closed, no matter how many connections the application is using. You should use DataStore.shutdown in this situation, which closes the DataStore file directly. That is why this application uses a DataStore instead of a DataStoreConnection.
You could place the shutdown method call just before the System.exit, but for reasons that will become apparent shortly, you want to do this before the window physically closes. Insert the highlighted statements:
//Overriden so we can exit on System Close protected void processWindowEvent(WindowEvent e) { if (e.getID() == WindowEvent.WINDOW_CLOSING) { try { dataStore.shutdown(); } catch ( DataSetException dse ) { dse.printStackTrace(); } } super.processWindowEvent(e); if (e.getID() == WindowEvent.WINDOW_CLOSING) { System.exit(0); } }
import com.borland.datastore.*; import com.borland.dx.dataset.*;
TableScrollPane tableScrollPane1 = new TableScrollPane(); JdbTable jdbTable1 = new JdbTable(); DataStore dataStore;
private void jbInit() throws Exception { dataModule = dsbasic.AccountsDM.getDataModule(); dataStore = dataModule.getDataStore();
Run AccountsApp.java
. You can navigate through the table and add, edit, and delete rows. All the changes you make are done within the context of a single transaction--not that you can tell at the moment. When you close the window, the DataStore will be closed, and the changes you made will be committed, which you can verify by running the application again.
If you did not close the DataStore (or at least commit the current transaction) before terminating the application, you would have an uncommitted transaction in the transaction log. As a result, the changes would have been orphaned, and not written to the DataStore. No changes you made in the application would ever apply. Closing the DataStore commits those changes automatically.
This step adds direct control over the transaction by allowing the user to explicitly commit and roll back the current transaction:
AccountsFrame.java
.
GridLayout
.
dataModule.TableDataSet1
(the only choice).
GridLayout
.
commitButton
and its text property to Commit
.
rollbackButton
and its text property to Rollback
.
void commitButton_actionPerformed(ActionEvent e) { try { dataStore.commit(); } catch ( DataSetException dse ) { dse.printStackTrace(); } }
void rollbackButton_actionPerformed(ActionEvent e) { try { dataStore.rollback(); } catch ( DataSetException dse ) { dse.printStackTrace(); } }
These buttons will now call commit or rollback on the DataStore to commit or roll back any changes made during the current transaction; that is, since the last commit or rollback.
This last step enables the application to detect the auto-commit when the DataStore is closed and allows the user to decide whether to commit or rollback changes:
AccountsFrame.java
, modify the class definition so that it implements ResponseListener:
public class AccountsFrame extends JFrame implements ResponseListener {
public void response( ResponseEvent response ) { if ( response.getCode() == ResponseEvent.COMMIT_ON_CLOSE ) { if ( JOptionPane.showConfirmDialog( this, "Posted changes have not been committed. Do that now?", "Commit or rollback", JOptionPane.YES_NO_OPTION ) == JOptionPane.YES_OPTION ) { response.ok(); } else { response.cancel(); } } }
This method checks for the COMMIT_ON_CLOSE event. When that occurs, a simple yes/no dialog box is displayed, asking the user if they want to commit the changes. "Yes" will send the ok response, which signals the DataStore to commit the changes. "No" will send the cancel response, which signals the DataStore to roll back the changes.
private void jbInit() throws Exception { dataModule = dsbasic.AccountsDM.getDataModule(); dataStore = dataModule.getDataStore(); dataStore.addResponseListener( this );
With these additions, the user will get a dialog box if there are unsaved changes, asking them if they want to commit them. Remember that the DataStore is closed before the window is; otherwise the dialog box would appear after the window had already disappeared.
You can now run the completed application. In addition to using the buttons to commit and roll back changes, try making some changes and then closing the window to exercise the auto-commit handling.
DataStore tables may be accessed via the DataStore's Type 4 (direct pure Java) JDBC driver, com.borland.datastore.jdbc.DataStoreDriver.
This driver may be used for both local and remote access. Remote access requires a DataStore JDBC server, which is also the vehicle for multi-user access. For details on remote access and multi-user issues, see "Multi-user and remote access to DataStores."
jdbc:borland:dslocal:<filename>
As with any JDBC driver, you may use the JDBC API, or an added-value API like DataExpress with QueryDataSet and ProcedureDataSet to access tables.
The following program,
JdbcTable.java
, is functionally identical to its DataExpress twin, DxTable.java
. It uses the JDBC API.
// JdbcTable.java package dsbasic; import java.sql.*; public class JdbcTable { static final String DRIVER = "com.borland.datastore.jdbc.DataStoreDriver"; static final String URL = "jdbc:borland:dslocal:"; Connection con; Statement stmt; DatabaseMetaData dmd; ResultSet rs; PreparedStatement appendPStmt, getIdPStmt; public JdbcTable() { try { Class.forName( DRIVER ); con = DriverManager.getConnection( URL + "Tx.jds", "Chuck", "" ); stmt = con.createStatement(); dmd = con.getMetaData(); rs = dmd.getTables( null, null, "Accounts", null ); if ( !rs.next() ) { createTable(); } appendPStmt = con.prepareStatement("INSERT INTO \"Accounts\" VALUES" + "(?, ?, CURRENT_TIMESTAMP, NULL)" ); getIdPStmt = con.prepareStatement( "SELECT MAX(ID)FROM \"Accounts\""); } catch ( SQLException sqle ) { sqle.printStackTrace(); } catch ( ClassNotFoundException cnfe ) { cnfe.printStackTrace(); } } public void createTable() throws SQLException { stmt.executeUpdate( "CREATE TABLE \"Accounts\" (" + "ID INTEGER," + "\"Name\" VARCHAR," + "\"Update\" TIMESTAMP," + "\"Text\" BINARY)" ); } public void appendRow( String name ) throws SQLException { int newID; rs = getIdPStmt.executeQuery(); if ( rs.next() ) { newID = rs.getInt( 1 ) + 1; } else { newID = 1; } appendPStmt.setInt( 1, newID ); appendPStmt.setString( 2, name ); appendPStmt.executeUpdate(); } public void demo() { try { appendRow( "Rabbit season" ); appendRow( "Duck season" ); rs = stmt.executeQuery( "SELECT * FROM \"Accounts\"" ); while ( rs.next() ) { System.out.println( rs.getInt( "ID" ) + ": " + rs.getString( "Name" ) + ", " + rs.getTimestamp( "Update" ) ); } stmt.close(); con.close(); } catch ( SQLException sqle ) { sqle.printStackTrace(); } } public static void main( String[] args ) { new JdbcTable().demo(); } }
This JDBC application uses two prepared statements: one to append rows, and the other to get the last ID value for that append. These prepared statements should be initialized before calling the appendRow method, and a good place to do this is in the class constructor. Because the constructor is used, the organization of the code is a little different than in DxTable.java
.
The first thing that happens in the class constructor is the loading of the DataStore JDBC driver using Class.forName. Both the driver name and the beginning of the connection URL are defined as class variables for convenience. A Connection to Tx.jds
is created, and from that, a generic Statement.
The next step is to determine if the table exists. One way to do this is through DatabaseMetaData.getTables. The code asks for a list of tables named "Accounts"; if that list is empty, that means there is no such table, and it must be created by calling the createTable method. The createTable method uses a fairly straightforward SQL CREATE TABLE statement. Note that the SQL parser usually converts identifiers to uppercase. To keep the proper casing used by DxTable.java
, the identifiers must be enclosed in quotes in this and other SQL statements. (In the long run, you may get the notion that case-sensitivity is more trouble than it's worth.) The final job of the constructor is to create the two prepared statements.
The demo method calls appendRow to add a couple of test rows. As in DxTable.java
, the last/largest ID value is retrieved and incremented for the new row. But instead of using a sort order and going to the last row, the JDBC approach uses an SQL SELECT statement that fetches the maximum value. As a result, the empty table condition, when there is no last value, must be handled specifically.
Finally, the contents of the table are displayed using an SQL SELECT statement to fetch the rows and a loop that's very similar to the one in DxTable.java
, and the statement and connection are closed as required by JDBC.
You can run this program interchangeably with DxTable.java
. Both of them add two more test rows to the same table.
Each JDBC connection actually uses its own internal instance of DataStoreConnection for the connection; that's how the DataStore JDBC driver is implemented. But this internal object is not accessible, so you must use the JDBC API.
For control over transactions, disable auto-commit mode by calling Connection.setAutoCommit(false). You can then call commit and rollback on the Connection object.
pubsweb@borland.com
Copyright © 1999, Inprise Corporation. All rights reserved.