View Javadoc

1   package org.apache.torque.oid;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.math.BigDecimal;
23  import java.sql.Connection;
24  import java.sql.ResultSet;
25  import java.sql.Statement;
26  import java.util.ArrayList;
27  import java.util.Hashtable;
28  import java.util.Iterator;
29  import java.util.List;
30  
31  import org.apache.commons.configuration.Configuration;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.torque.Database;
35  import org.apache.torque.Torque;
36  import org.apache.torque.TorqueException;
37  import org.apache.torque.map.TableMap;
38  import org.apache.torque.util.Transaction;
39  
40  //!!
41  // NOTE:
42  // It would be nice to decouple this from
43  // Torque. This is a great stand-alone utility.
44  
45  /***
46   * This method of ID generation is used to ensure that code is
47   * more database independent.  For example, MySQL has an auto-increment
48   * feature while Oracle uses sequences.  It caches several ids to
49   * avoid needing a Connection for every request.
50   *
51   * This class uses the table ID_TABLE defined in
52   * conf/master/id-table-schema.xml.  The columns in ID_TABLE are used as
53   * follows:<br>
54   *
55   * ID_TABLE_ID - The PK for this row (any unique int).<br>
56   * TABLE_NAME - The name of the table you want ids for.<br>
57   * NEXT_ID - The next id returned by IDBroker when it queries the
58   *           database (not when it returns an id from memory).<br>
59   * QUANTITY - The number of ids that IDBroker will cache in memory.<br>
60   * <p>
61   * Use this class like this:
62   * <pre>
63   * int id = dbMap.getIDBroker().getNextIdAsInt(null, "TABLE_NAME");
64   *  - or -
65   * BigDecimal[] ids = ((IDBroker)dbMap.getIDBroker())
66   *     .getNextIds("TABLE_NAME", numOfIdsToReturn);
67   * </pre>
68   *
69   * NOTE: When the ID_TABLE must be updated we must ensure that
70   * IDBroker objects running in different JVMs do not overwrite each
71   * other.  This is accomplished using using the transactional support
72   * occuring in some databases.  Using this class with a database that
73   * does not support transactions should be limited to a single JVM.
74   *
75   * @author <a href="mailto:frank.kim@clearink.com">Frank Y. Kim</a>
76   * @author <a href="mailto:jmcnally@collab.net">John D. McNally</a>
77   * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
78   * @version $Id: IDBroker.java 552333 2007-07-01 16:24:19Z tv $
79   */
80  public class IDBroker implements Runnable, IdGenerator
81  {
82      /*** Name of the ID_TABLE = ID_TABLE */
83      public static final String ID_TABLE = "ID_TABLE";
84  
85      /*** Table_Name column name */
86      public static final String COL_TABLE_NAME = "TABLE_NAME";
87  
88      /*** Fully qualified Table_Name column name */
89      public static final String TABLE_NAME = ID_TABLE + "." + COL_TABLE_NAME;
90  
91      /*** ID column name */
92      public static final String COL_TABLE_ID = "ID_TABLE_ID";
93  
94      /*** Fully qualified ID column name */
95      public static final String TABLE_ID = ID_TABLE + "." + COL_TABLE_ID;
96  
97      /*** Next_ID column name */
98      public static final String COL_NEXT_ID = "NEXT_ID";
99  
100     /*** Fully qualified Next_ID column name */
101     public static final String NEXT_ID = ID_TABLE + "." + COL_NEXT_ID;
102 
103     /*** Quantity column name */
104     public static final String COL_QUANTITY = "QUANTITY";
105 
106     /*** Fully qualified Quantity column name */
107     public static final String QUANTITY = ID_TABLE + "." + COL_QUANTITY;
108 
109     /*** the name of the database in which this IdBroker is running. */
110     private String databaseName;
111 
112     /***
113      * The default size of the per-table meta data <code>Hashtable</code>
114      * objects.
115      */
116     private static final int DEFAULT_SIZE = 40;
117 
118     /***
119      * The cached IDs for each table.
120      *
121      * Key: String table name.
122      * Value: List of Integer IDs.
123      */
124     private Hashtable ids = new Hashtable(DEFAULT_SIZE);
125 
126     /***
127      * The quantity of ids to grab for each table.
128      *
129      * Key: String table name.
130      * Value: Integer quantity.
131      */
132     private Hashtable quantityStore = new Hashtable(DEFAULT_SIZE);
133 
134     /***
135      * The last time this IDBroker queried the database for ids.
136      *
137      * Key: String table name.
138      * Value: Date of last id request.
139      */
140     private Hashtable lastQueryTime = new Hashtable(DEFAULT_SIZE);
141 
142     /***
143      * Amount of time for the thread to sleep
144      */
145     private static final int SLEEP_PERIOD = 60000;
146 
147     /***
148      * The safety Margin
149      */
150     private static final float SAFETY_MARGIN = 1.2f;
151 
152     /***
153      * The houseKeeperThread thread
154      */
155     private Thread houseKeeperThread = null;
156 
157     /***
158      * Are transactions supported?
159      */
160     private boolean transactionsSupported = false;
161 
162     /***
163      * The value of ONE!
164      */
165     private static final BigDecimal ONE = new BigDecimal("1");
166 
167     /*** the configuration */
168     private Configuration configuration;
169 
170     /*** property name */
171     private static final String DB_IDBROKER_CLEVERQUANTITY =
172         "idbroker.clever.quantity";
173 
174     /*** property name */
175     private static final String DB_IDBROKER_PREFETCH =
176         "idbroker.prefetch";
177 
178     /*** property name */
179     private static final String DB_IDBROKER_USENEWCONNECTION =
180         "idbroker.usenewconnection";
181 
182     /*** the log */
183     private Log log = LogFactory.getLog(IDBroker.class);
184 
185     /***
186      * constructs an IdBroker for the given Database.
187      * @param database the database where this IdBroker is running in.
188      */
189     public IDBroker(Database database)
190     {
191         this(database.getName());
192     }
193 
194     /***
195      * Creates an IDBroker for the ID table.
196      *
197      * @param tMap A TableMap.
198      * @deprecated Use IDBroker(DatabaseInfo) instead. Will be removed
199      *             in a future version of Torque.
200      */
201     public IDBroker(TableMap tMap)
202     {
203         this(tMap.getDatabaseMap().getName());
204     }
205 
206     /***
207      * Constructor.
208      * Provided as long as both Constructors, IDBroker(DatabaseInfo) and
209      * IDBroker(TableMap), are around.
210      * @param databaseName the name of the database for which this IdBroker
211      *        provides Ids.
212      */
213     private IDBroker(String databaseName)
214     {
215         this.databaseName = databaseName;
216         configuration = Torque.getConfiguration();
217 
218         // Start the housekeeper thread only if prefetch has not been disabled
219         if (configuration.getBoolean(DB_IDBROKER_PREFETCH, true))
220         {
221             houseKeeperThread = new Thread(this);
222             // Indicate that this is a system thread. JVM will quit only when
223             // there are no more active user threads. Settings threads spawned
224             // internally by Torque as daemons allows commandline applications
225             // using Torque terminate in an orderly manner.
226             houseKeeperThread.setDaemon(true);
227             houseKeeperThread.setName("Torque - ID Broker thread");
228             houseKeeperThread.start();
229         }
230 
231         // Check for Transaction support.  Give warning message if
232         // IDBroker is being used with a database that does not
233         // support transactions.
234         Connection dbCon = null;
235         try
236         {
237             dbCon = Torque.getConnection(databaseName);
238         }
239         catch (Throwable t)
240         {
241             log.error("Could not open a connection to the database "
242                     + databaseName,
243                     t);
244             transactionsSupported = false;
245         }
246         try
247         {
248             transactionsSupported = dbCon.getMetaData().supportsTransactions();
249         }
250         catch (Exception e)
251         {
252             log.warn("Could not read from connection Metadata"
253                     + " whether transactions are supported for the database "
254                     + databaseName,
255                     e);
256             transactionsSupported = false;
257         }
258         finally
259         {
260             try
261             {
262                 // Return the connection to the pool.
263                 dbCon.close();
264             }
265             catch (Exception e)
266             {
267                 log.warn("Could not close the connection which was used "
268                         + "for testing whether transactions are supported",
269                         e);
270             }
271         }
272         if (!transactionsSupported)
273         {
274             log.warn("IDBroker is being used with db '" + databaseName
275                     + "', which does not support transactions. IDBroker "
276                     + "attempts to use transactions to limit the possibility "
277                     + "of duplicate key generation.  Without transactions, "
278                     + "duplicate key generation is possible if multiple JVMs "
279                     + "are used or other means are used to write to the "
280                     + "database.");
281         }
282     }
283 
284     /***
285      * Set the configuration
286      *
287      * @param configuration the configuration
288      */
289     public void setConfiguration(Configuration configuration)
290     {
291         this.configuration = configuration;
292     }
293 
294     /***
295      * Returns an id as a primitive int.  Note this method does not
296      * require a Connection, it just implements the KeyGenerator
297      * interface.  if a Connection is needed one will be requested.
298      * To force the use of the passed in connection set the configuration
299      * property torque.idbroker.usenewconnection = false
300      *
301      * @param connection A Connection.
302      * @param tableName an Object that contains additional info.
303      * @return An int with the value for the id.
304      * @exception Exception Database error.
305      */
306     public int getIdAsInt(Connection connection, Object tableName)
307         throws Exception
308     {
309         return getIdAsBigDecimal(connection, tableName).intValue();
310     }
311 
312 
313     /***
314      * Returns an id as a primitive long. Note this method does not
315      * require a Connection, it just implements the KeyGenerator
316      * interface.  if a Connection is needed one will be requested.
317      * To force the use of the passed in connection set the configuration
318      * property torque.idbroker.usenewconnection = false
319      *
320      * @param connection A Connection.
321      * @param tableName a String that identifies a table.
322      * @return A long with the value for the id.
323      * @exception Exception Database error.
324      */
325     public long getIdAsLong(Connection connection, Object tableName)
326         throws Exception
327     {
328         return getIdAsBigDecimal(connection, tableName).longValue();
329     }
330 
331     /***
332      * Returns an id as a BigDecimal. Note this method does not
333      * require a Connection, it just implements the KeyGenerator
334      * interface.  if a Connection is needed one will be requested.
335      * To force the use of the passed in connection set the configuration
336      * property torque.idbroker.usenewconnection = false
337      *
338      * @param connection A Connection.
339      * @param tableName a String that identifies a table..
340      * @return A BigDecimal id.
341      * @exception Exception Database error.
342      */
343     public BigDecimal getIdAsBigDecimal(Connection connection,
344                                         Object tableName)
345         throws Exception
346     {
347         BigDecimal[] id = getNextIds((String) tableName, 1, connection);
348         return id[0];
349     }
350 
351     /***
352      * Returns an id as a String. Note this method does not
353      * require a Connection, it just implements the KeyGenerator
354      * interface.  if a Connection is needed one will be requested.
355      * To force the use of the passed in connection set the configuration
356      * property torque.idbroker.usenewconnection = false
357      *
358      * @param connection A Connection should be null.
359      * @param tableName a String that identifies a table.
360      * @return A String id
361      * @exception Exception Database error.
362      */
363     public String getIdAsString(Connection connection, Object tableName)
364         throws Exception
365     {
366         return getIdAsBigDecimal(connection, tableName).toString();
367     }
368 
369 
370     /***
371      * A flag to determine the timing of the id generation     *
372      * @return a <code>boolean</code> value
373      */
374     public boolean isPriorToInsert()
375     {
376         return true;
377     }
378 
379     /***
380      * A flag to determine the timing of the id generation
381      *
382      * @return a <code>boolean</code> value
383      */
384     public boolean isPostInsert()
385     {
386         return false;
387     }
388 
389     /***
390      * A flag to determine whether a Connection is required to
391      * generate an id.
392      *
393      * @return a <code>boolean</code> value
394      */
395     public boolean isConnectionRequired()
396     {
397         return false;
398     }
399 
400     /***
401      * This method returns x number of ids for the given table.
402      *
403      * @param tableName The name of the table for which we want an id.
404      * @param numOfIdsToReturn The desired number of ids.
405      * @return A BigDecimal.
406      * @exception Exception Database error.
407      */
408     public synchronized BigDecimal[] getNextIds(String tableName,
409                                                 int numOfIdsToReturn)
410         throws Exception
411     {
412         return getNextIds(tableName, numOfIdsToReturn, null);
413     }
414 
415     /***
416      * This method returns x number of ids for the given table.
417      * Note this method does not require a Connection.
418      * If a Connection is needed one will be requested.
419      * To force the use of the passed in connection set the configuration
420      * property torque.idbroker.usenewconnection = false
421      *
422      * @param tableName The name of the table for which we want an id.
423      * @param numOfIdsToReturn The desired number of ids.
424      * @param connection A Connection.
425      * @return A BigDecimal.
426      * @exception Exception Database error.
427      */
428     public synchronized BigDecimal[] getNextIds(String tableName,
429                                                 int numOfIdsToReturn,
430                                                 Connection connection)
431         throws Exception
432     {
433         if (tableName == null)
434         {
435             throw new Exception("getNextIds(): tableName == null");
436         }
437 
438         // A note about the synchronization:  I (jmcnally) looked at
439         // the synchronized blocks to avoid thread issues that were
440         // being used in this and the storeId method.  I do not think
441         // they were being effective, so I synchronized the method.
442         // I have left the blocks that did exist commented in the code
443         // to make it easier for others to take a look, because it
444         // would be preferrable to avoid the synchronization on the
445         // method
446 
447         List availableIds = (List) ids.get(tableName);
448 
449         if (availableIds == null || availableIds.size() < numOfIdsToReturn)
450         {
451             if (availableIds == null)
452             {
453                 log.debug("Forced id retrieval - no available list");
454             }
455             else
456             {
457                 log.debug("Forced id retrieval - " + availableIds.size());
458             }
459             storeIDs(tableName, true, connection);
460             availableIds = (List) ids.get(tableName);
461         }
462 
463         int size = availableIds.size() < numOfIdsToReturn
464                 ? availableIds.size() : numOfIdsToReturn;
465 
466         BigDecimal[] results = new BigDecimal[size];
467 
468         // We assume that availableIds will always come from the ids
469         // Hashtable and would therefore always be the same object for
470         // a specific table.
471         //        synchronized (availableIds)
472         //        {
473         for (int i = size - 1; i >= 0; i--)
474         {
475             results[i] = (BigDecimal) availableIds.get(i);
476             availableIds.remove(i);
477         }
478         //        }
479 
480         return results;
481     }
482 
483     /***
484      * @param tableName a <code>String</code> value that is used to identify
485      * the row
486      * @return a <code>boolean</code> value
487      * @exception TorqueException if a Torque error occurs.
488      * @exception Exception if another error occurs.
489      */
490     public boolean exists(String tableName)
491         throws Exception
492     {
493         String query = new StringBuffer(100)
494             .append("select ")
495             .append(TABLE_NAME)
496             .append(" where ")
497             .append(TABLE_NAME).append("='").append(tableName).append('\'')
498             .toString();
499 
500         boolean exists = false;
501         Connection dbCon = null;
502         try
503         {
504             dbCon = Torque.getConnection(databaseName);
505             Statement statement = dbCon.createStatement();
506             ResultSet rs = statement.executeQuery(query);
507             exists = rs.next();
508             statement.close();
509         }
510         finally
511         {
512             // Return the connection to the pool.
513             try
514             {
515                 dbCon.close();
516             }
517             catch (Exception e)
518             {
519                 log.error("Release of connection failed.", e);
520             }
521         }
522         return exists;
523     }
524 
525     /***
526      * A background thread that tries to ensure that when someone asks
527      * for ids, that there are already some loaded and that the
528      * database is not accessed.
529      */
530     public void run()
531     {
532         log.debug("IDBroker thread was started.");
533 
534         Thread thisThread = Thread.currentThread();
535         while (houseKeeperThread == thisThread)
536         {
537             try
538             {
539                 Thread.sleep(SLEEP_PERIOD);
540             }
541             catch (InterruptedException exc)
542             {
543                 // ignored
544             }
545 
546             // logger.info("IDBroker thread checking for more keys.");
547             Iterator it = ids.keySet().iterator();
548             while (it.hasNext())
549             {
550                 String tableName = (String) it.next();
551                 if (log.isDebugEnabled())
552                 {
553                     log.debug("IDBroker thread checking for more keys "
554                             + "on table: " + tableName);
555                 }
556                 List availableIds = (List) ids.get(tableName);
557                 int quantity = getQuantity(tableName, null).intValue();
558                 if (quantity > availableIds.size())
559                 {
560                     try
561                     {
562                         // Second parameter is false because we don't
563                         // want the quantity to be adjusted for thread
564                         // calls.
565                         storeIDs(tableName, false, null);
566                         if (log.isDebugEnabled())
567                         {
568                             log.debug("Retrieved more ids for table: " + tableName);
569                         }
570                     }
571                     catch (Exception exc)
572                     {
573                         log.error("There was a problem getting new IDs "
574                                      + "for table: " + tableName, exc);
575                     }
576                 }
577             }
578         }
579         log.debug("IDBroker thread finished.");
580     }
581 
582     /***
583      * Shuts down the IDBroker thread.
584      *
585      * Calling this method stops the thread that was started for this
586      * instance of the IDBroker. This method should be called during
587      * MapBroker Service shutdown.
588      */
589     public void stop()
590     {
591         houseKeeperThread = null;
592     }
593 
594     /***
595      * Check the frequency of retrieving new ids from the database.
596      * If the frequency is high then we increase the amount (i.e.
597      * quantity column) of ids retrieved on each access.  Tries to
598      * alter number of keys grabbed so that IDBroker retrieves a new
599      * set of ID's prior to their being needed.
600      *
601      * @param tableName The name of the table for which we want an id.
602      */
603     private void checkTiming(String tableName)
604     {
605         // Check if quantity changing is switched on.
606         // If prefetch is turned off, changing quantity does not make sense
607         if (!configuration.getBoolean(DB_IDBROKER_CLEVERQUANTITY, true)
608             || !configuration.getBoolean(DB_IDBROKER_PREFETCH, true))
609         {
610             return;
611         }
612 
613         // Get the last id request for this table.
614         java.util.Date lastTime = (java.util.Date) lastQueryTime.get(tableName);
615         java.util.Date now = new java.util.Date();
616 
617         if (lastTime != null)
618         {
619             long thenLong = lastTime.getTime();
620             long nowLong = now.getTime();
621             int timeLapse = (int) (nowLong - thenLong);
622             if (timeLapse < SLEEP_PERIOD && timeLapse > 0)
623             {
624                 if (log.isDebugEnabled())
625                 {
626                     log.debug("Unscheduled retrieval of more ids for table: "
627                             + tableName);
628                 }
629                 // Increase quantity, so that hopefully this does not
630                 // happen again.
631                 float rate = getQuantity(tableName, null).floatValue()
632                     / (float) timeLapse;
633                 quantityStore.put(tableName, new BigDecimal(
634                     Math.ceil(SLEEP_PERIOD * rate * SAFETY_MARGIN)));
635             }
636         }
637         lastQueryTime.put(tableName, now);
638     }
639 
640     /***
641      * Grabs more ids from the id_table and stores it in the ids
642      * Hashtable.  If adjustQuantity is set to true the amount of id's
643      * retrieved for each call to storeIDs will be adjusted.
644      *
645      * @param tableName The name of the table for which we want an id.
646      * @param adjustQuantity True if amount should be adjusted.
647      * @param connection a Connection
648      * @exception Exception a generic exception.
649      */
650     private synchronized void storeIDs(String tableName,
651                           boolean adjustQuantity,
652                           Connection connection)
653         throws Exception
654     {
655         BigDecimal nextId = null;
656         BigDecimal quantity = null;
657 
658         // Block on the table.  Multiple tables are allowed to ask for
659         // ids simultaneously.
660         //        TableMap tMap = dbMap.getTable(tableName);
661         //        synchronized(tMap)  see comment in the getNextIds method
662         //        {
663         if (adjustQuantity)
664         {
665             checkTiming(tableName);
666         }
667 
668         boolean useNewConnection = (connection == null) || (configuration
669                 .getBoolean(DB_IDBROKER_USENEWCONNECTION, true));
670         try
671         {
672             if (useNewConnection)
673             {
674                 connection = Transaction.beginOptional(databaseName,
675                     transactionsSupported);
676             }
677 
678             // Write the current value of quantity of keys to grab
679             // to the database, primarily to obtain a write lock
680             // on the table/row, but this value will also be used
681             // as the starting value when an IDBroker is
682             // instantiated.
683             quantity = getQuantity(tableName, connection);
684             updateQuantity(connection, tableName, quantity);
685 
686             // Read the next starting ID from the ID_TABLE.
687             BigDecimal[] results = selectRow(connection, tableName);
688             nextId = results[0]; // NEXT_ID column
689 
690             // Update the row based on the quantity in the
691             // ID_TABLE.
692             BigDecimal newNextId = nextId.add(quantity);
693             updateNextId(connection, tableName, newNextId.toString());
694 
695             if (useNewConnection)
696             {
697                 Transaction.commit(connection);
698             }
699         }
700         catch (Exception e)
701         {
702             if (useNewConnection)
703             {
704                 Transaction.rollback(connection);
705             }
706             throw e;
707         }
708 
709         List availableIds = (List) ids.get(tableName);
710         if (availableIds == null)
711         {
712             availableIds = new ArrayList();
713             ids.put(tableName, availableIds);
714         }
715 
716         // Create the ids and store them in the list of available ids.
717         int numId = quantity.intValue();
718         for (int i = 0; i < numId; i++)
719         {
720             availableIds.add(nextId);
721             nextId = nextId.add(ONE);
722         }
723         //        }
724     }
725 
726     /***
727      * This method allows you to get the number of ids that are to be
728      * cached in memory.  This is either stored in quantityStore or
729      * read from the db. (ie the value in ID_TABLE.QUANTITY).
730      *
731      * Though this method returns a BigDecimal for the quantity, it is
732      * unlikey the system could withstand whatever conditions would lead
733      * to really needing a large quantity, it is retrieved as a BigDecimal
734      * only because it is going to be added to another BigDecimal.
735      *
736      * @param tableName The name of the table we want to query.
737      * @param connection a Connection
738      * @return An int with the number of ids cached in memory.
739      */
740     private BigDecimal getQuantity(String tableName, Connection connection)
741     {
742         BigDecimal quantity = null;
743 
744         // If prefetch is turned off we simply return 1
745         if (!configuration.getBoolean(DB_IDBROKER_PREFETCH, true))
746         {
747             quantity = new BigDecimal((double)1);
748         }
749         // Initialize quantity, if necessary.
750         else if (quantityStore.containsKey(tableName))
751         {
752             quantity = (BigDecimal) quantityStore.get(tableName);
753         }
754         else
755         {
756             Connection dbCon = null;
757             try
758             {
759                 if (connection == null || configuration
760                     .getBoolean(DB_IDBROKER_USENEWCONNECTION, true))
761                 {
762                     // Get a connection to the db
763                     dbCon = Torque.getConnection(databaseName);
764                 }
765 
766                 // Read the row from the ID_TABLE.
767                 BigDecimal[] results = selectRow(dbCon, tableName);
768 
769                 // QUANTITY column.
770                 quantity = results[1];
771                 quantityStore.put(tableName, quantity);
772             }
773             catch (Exception e)
774             {
775                 quantity = new BigDecimal((double)10);
776             }
777             finally
778             {
779                 // Return the connection to the pool.
780                 try
781                 {
782                     dbCon.close();
783                 }
784                 catch (Exception e)
785                 {
786                     log.error("Release of connection failed.", e);
787                 }
788             }
789         }
790         return quantity;
791     }
792 
793     /***
794      * Helper method to select a row in the ID_TABLE.
795      *
796      * @param con A Connection.
797      * @param tableName The properly escaped name of the table to
798      * identify the row.
799      * @return A BigDecimal[].
800      * @exception Exception a generic exception.
801      */
802     private BigDecimal[] selectRow(Connection con, String tableName)
803         throws Exception
804     {
805         StringBuffer stmt = new StringBuffer();
806         stmt.append("SELECT ")
807             .append(COL_NEXT_ID)
808             .append(", ")
809             .append(COL_QUANTITY)
810             .append(" FROM ")
811             .append(ID_TABLE)
812             .append(" WHERE ")
813             .append(COL_TABLE_NAME)
814             .append(" = '")
815             .append(tableName)
816             .append('\'');
817 
818         Statement statement = null;
819 
820         BigDecimal[] results = new BigDecimal[2];
821         try
822         {
823             statement = con.createStatement();
824             ResultSet rs = statement.executeQuery(stmt.toString());
825 
826             if (rs.next())
827             {
828                 // work around for MySQL which appears to support
829                 // getBigDecimal in the source code, but the binary
830                 // is throwing an NotImplemented exception.
831                 results[0] = new BigDecimal(rs.getString(1)); // next_id
832                 results[1] = new BigDecimal(rs.getString(2)); // quantity
833             }
834             else
835             {
836                 throw new TorqueException("The table " + tableName
837                         + " does not have a proper entry in the " + ID_TABLE);
838             }
839         }
840         finally
841         {
842             if (statement != null)
843             {
844                 statement.close();
845             }
846         }
847 
848         return results;
849     }
850 
851     /***
852      * Helper method to update a row in the ID_TABLE.
853      *
854      * @param con A Connection.
855      * @param tableName The properly escaped name of the table to identify the
856      * row.
857      * @param id An int with the value to set for the id.
858      * @exception Exception Database error.
859      */
860     private void updateNextId(Connection con, String tableName, String id)
861         throws Exception
862     {
863 
864 
865         StringBuffer stmt = new StringBuffer(id.length()
866                                              + tableName.length() + 50);
867         stmt.append("UPDATE " + ID_TABLE)
868             .append(" SET ")
869             .append(COL_NEXT_ID)
870             .append(" = ")
871             .append(id)
872             .append(" WHERE ")
873             .append(COL_TABLE_NAME)
874             .append(" = '")
875             .append(tableName)
876             .append('\'');
877 
878         Statement statement = null;
879 
880         if (log.isDebugEnabled())
881         {
882             log.debug("updateNextId: " + stmt.toString());
883         }
884 
885         try
886         {
887             statement = con.createStatement();
888             statement.executeUpdate(stmt.toString());
889         }
890         finally
891         {
892             if (statement != null)
893             {
894                 statement.close();
895             }
896         }
897     }
898 
899     /***
900      * Helper method to update a row in the ID_TABLE.
901      *
902      * @param con A Connection.
903      * @param tableName The properly escaped name of the table to identify the
904      * row.
905      * @param quantity An int with the value of the quantity.
906      * @exception Exception Database error.
907      */
908     private void updateQuantity(Connection con, String tableName,
909                                 BigDecimal quantity)
910         throws Exception
911     {
912         StringBuffer stmt = new StringBuffer(quantity.toString().length()
913                                              + tableName.length() + 50);
914         stmt.append("UPDATE ")
915             .append(ID_TABLE)
916             .append(" SET ")
917             .append(COL_QUANTITY)
918             .append(" = ")
919             .append(quantity)
920             .append(" WHERE ")
921             .append(COL_TABLE_NAME)
922             .append(" = '")
923             .append(tableName)
924             .append('\'');
925 
926         Statement statement = null;
927 
928         if (log.isDebugEnabled())
929         {
930             log.debug("updateQuantity: " + stmt.toString());
931         }
932 
933         try
934         {
935             statement = con.createStatement();
936             statement.executeUpdate(stmt.toString());
937         }
938         finally
939         {
940             if (statement != null)
941             {
942                 statement.close();
943             }
944         }
945     }
946 }