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