001: package org.garret.perst;
002:
003: import java.util.*;
004: import java.lang.reflect.*;
005: import org.garret.perst.impl.ClassDescriptor;
006:
007: /**
008: * This class emulates relational database on top of Perst storage
009: * It maintain class extends, associated indices, prepare queries.
010: */
011: public class Database {
012: /**
013: * Constructor of database. This method initialize database if it not initialized yet.
014: * Starting from 2.72 version of Perst.Net, it supports automatic
015: * creation of table descriptors when Database class is used.
016: * So now it is not necessary to explicitly create tables and indices -
017: * the Database class will create them itself on demand.
018: * Indexable attribute should be used to mark key fields for which index should be created.
019: * Table descriptor is created when instance of the correspondent class is first time stored
020: * in the database. Perst creates table descriptors for all derived classes up
021: * to the root java.lang.Object class.
022: * @param storage opened storage. Storage should be either empty (non-initialized, either
023: * previously initialized by the this method. It is not possible to open storage with
024: * root object other than table index created by this constructor.
025: * @param multithreaded <code>true</code> if database should support concurrent access
026: * to the data from multiple threads.
027: */
028: public Database(Storage storage, boolean multithreaded) {
029: this (storage, multithreaded, true);
030: }
031:
032: /**
033: * Constructor of database. This method initialize database if it not initialized yet.
034: * @param storage opened storage. Storage should be either empty (non-initialized, either
035: * previously initialized by the this method. It is not possible to open storage with
036: * root object other than table index created by this constructor.
037: * @param multithreaded <code>true</code> if database should support concurrent access
038: * to the data from multiple threads.
039: * @param autoRegisterTables automatically create tables descriptors for instances of new
040: * classes inserted in the database
041: */
042: public Database(Storage storage, boolean multithreaded,
043: boolean autoRegisterTables) {
044: this .storage = storage;
045: this .multithreaded = multithreaded;
046: this .autoRegisterTables = autoRegisterTables;
047: if (multithreaded) {
048: storage
049: .setProperty("perst.alternative.btree",
050: Boolean.TRUE);
051: }
052: metadata = (Index) storage.getRoot();
053: boolean schemaUpdated = false;
054: if (metadata == null) {
055: beginTransaction();
056: metadata = storage.createIndex(String.class, true);
057: storage.setRoot(metadata);
058: schemaUpdated = true;
059: }
060: Iterator iterator = metadata.entryIterator();
061: tables = new HashMap();
062: while (iterator.hasNext()) {
063: Map.Entry map = (Map.Entry) iterator.next();
064: Table table = (Table) map.getValue();
065: Class cls = ClassDescriptor.loadClass(storage, (String) map
066: .getKey());
067: tables.put(cls, table);
068: schemaUpdated |= addIndices(table, cls);
069: }
070: if (schemaUpdated) {
071: commitTransaction();
072: }
073: }
074:
075: /**
076: * Constructor of single threaded database. This method initialize database if it not initialized yet.
077: * @param storage opened storage. Storage should be either empty (non-initialized, either
078: * previously initialized by the this method. It is not possible to open storage with
079: * root object other than table index created by this constructor.
080: */
081: public Database(Storage storage) {
082: this (storage, false);
083: }
084:
085: /**
086: * Begin transaction
087: */
088: public void beginTransaction() {
089: if (multithreaded) {
090: storage
091: .beginThreadTransaction(Storage.SERIALIZABLE_TRANSACTION);
092: }
093: }
094:
095: /**
096: * Commit transaction
097: */
098: public void commitTransaction() {
099: if (multithreaded) {
100: storage.endThreadTransaction();
101: } else {
102: storage.commit();
103: }
104: }
105:
106: /**
107: * Rollback transaction
108: */
109: public void rollbackTransaction() {
110: if (multithreaded) {
111: storage.rollbackThreadTransaction();
112: } else {
113: storage.rollback();
114: }
115: }
116:
117: /**
118: * Create table for the specified class.
119: * This function does nothing if table for such class already exists
120: * @param table class corresponding to the table
121: * @return <code>true</code> if table is created, <code>false</code> if table
122: * alreay exists
123: * @deprecated Since version 2.75 of Perst it is not necessary to create table and index
124: * descriptors explicitly: them are automatically create when object is inserted in the
125: * database first time (to mark fields for which indices should be created, use Indexable
126: * annotation)
127: */
128: public boolean createTable(Class table) {
129: if (multithreaded) {
130: metadata.exclusiveLock();
131: }
132: if (tables.get(table) == null) {
133: Table t = new Table();
134: t.extent = storage.createSet();
135: t.indices = storage.createLink();
136: t.indicesMap = new HashMap();
137: tables.put(table, t);
138: metadata.put(table.getName(), t);
139: addIndices(t, table);
140: return true;
141: }
142: return false;
143: }
144:
145: private boolean addIndices(Table table, Class cls) {
146: boolean schemaUpdated = false;
147: for (Field f : cls.getDeclaredFields()) {
148: Indexable idx = (Indexable) f
149: .getAnnotation(Indexable.class);
150: if (idx != null) {
151: schemaUpdated |= createIndex(table, cls, f.getName(),
152: idx.unique(), idx.caseInsensitive());
153: }
154: }
155: return schemaUpdated;
156: }
157:
158: /**
159: * Drop table associated with this class. Do nothing if there is no such table in the database.
160: * @param table class corresponding to the table
161: * @return <code>true</code> if table is deleted, <code>false</code> if table
162: * is not found
163: */
164: public boolean dropTable(Class table) {
165: if (multithreaded) {
166: metadata.exclusiveLock();
167: }
168: if (tables.remove(table) != null) {
169: metadata.remove(table.getName());
170: return true;
171: }
172: return false;
173: }
174:
175: /**
176: * Add new record to the table. Record is inserted in table corresponding to the class of the object.
177: * Record will be automatically added to all indices existed for this table.
178: * If there is not table associated with class of this object, then
179: * database will search for table associated with superclass and so on...
180: * @param record object to be inserted in the table
181: * @return <code>true</code> if record was successfully added to the table, <code>false</code>
182: * if there is already such record (object with the same ID) in the table or there is some record with the same value of
183: * unique key field
184: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
185: * record class
186: */
187: public <T extends IPersistent> boolean addRecord(T record) {
188: return addRecord(record.getClass(), record);
189: }
190:
191: private Table locateTable(Class cls, boolean exclusive) {
192: return locateTable(cls, exclusive, true);
193: }
194:
195: private Table locateTable(Class cls, boolean exclusive,
196: boolean shouldExist) {
197: Table table = null;
198: if (multithreaded) {
199: metadata.sharedLock();
200: }
201: for (Class c = cls; c != null
202: && (table = (Table) tables.get(c)) == null; c = c
203: .getSuperclass())
204: ;
205: if (table == null) {
206: if (shouldExist) {
207: throw new StorageError(StorageError.CLASS_NOT_FOUND,
208: cls.getName());
209: }
210: return null;
211: }
212: if (exclusive) {
213: table.extent.exclusiveLock();
214: } else {
215: table.extent.sharedLock();
216: }
217: return table;
218: }
219:
220: private void registerTable(Class cls) {
221: if (multithreaded) {
222: metadata.sharedLock();
223: }
224: if (autoRegisterTables) {
225: boolean exclusiveLockSet = false;
226: for (Class c = cls; c != Object.class; c = c
227: .getSuperclass()) {
228: Table t = (Table) tables.get(c);
229: if (t == null) {
230: if (!exclusiveLockSet) {
231: metadata.unlock(); // try to avoid deadlock caused by concurrent insertion of objects
232: exclusiveLockSet = true;
233: }
234: createTable(c);
235: }
236: }
237: }
238: }
239:
240: /**
241: * Add new record to the specified table. Record is inserted in table corresponding to the specified class.
242: * Record will be automatically added to all indices existed for this table.
243: * @param table class corresponding to the table
244: * @param record object to be inserted in the table
245: * @return <code>true</code> if record was successfully added to the table, <code>false</code>
246: * if there is already such record (object with the same ID) in the table or there is some record with the same value of
247: * unique key field
248: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
249: * record class
250: */
251: public <T extends IPersistent> boolean addRecord(Class table,
252: T record) {
253: boolean added = false;
254: boolean found = false;
255: registerTable(table);
256: ArrayList wasInsertedIn = new ArrayList();
257: for (Class c = table; c != null; c = c.getSuperclass()) {
258: Table t = (Table) tables.get(c);
259: if (t != null) {
260: found = true;
261: if (multithreaded) {
262: t.extent.exclusiveLock();
263: }
264: if (t.extent.add(record)) {
265: wasInsertedIn.add(t.extent);
266: Iterator iterator = t.indicesMap.values()
267: .iterator();
268: while (iterator.hasNext()) {
269: FieldIndex index = (FieldIndex) iterator.next();
270: if (index.put(record)) {
271: wasInsertedIn.add(index);
272: } else {
273: iterator = wasInsertedIn.iterator();
274: while (iterator.hasNext()) {
275: Object idx = iterator.next();
276: if (idx instanceof IPersistentSet) {
277: ((IPersistentSet) idx)
278: .remove(record);
279: } else {
280: ((FieldIndex) idx).remove(record);
281: }
282: }
283: return false;
284: }
285: }
286: added = true;
287: }
288: }
289: }
290: if (!found) {
291: throw new StorageError(StorageError.CLASS_NOT_FOUND, table
292: .getName());
293: }
294: return added;
295: }
296:
297: /**
298: * Delete record from the table. Record is removed from the table corresponding to the class
299: * of the object. Record will be automatically added to all indices existed for this table.
300: * If there is not table associated with class of this object, then
301: * database will search for table associated with superclass and so on...
302: * Object represented the record will be also deleted from the storage.
303: * @param record object to be deleted from the table
304: * @return <code>true</code> if record was successfully deleted from the table, <code>false</code>
305: * if there is not such record (object with the same ID) in the table
306: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
307: * record class
308: */
309: public <T extends IPersistent> boolean deleteRecord(T record) {
310: return deleteRecord(record.getClass(), record);
311: }
312:
313: /**
314: * Delete record from the specified table. Record is removed from the table corresponding to the
315: * specified class. Record will be automatically added to all indices existed for this table.
316: * Object represented the record will be also deleted from the storage.
317: * @param table class corresponding to the table
318: * @param record object to be deleted from the table
319: * @return <code>true</code> if record was successfully deleted from the table, <code>false</code>
320: * if there is not such record (object with the same ID) in the table
321: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
322: * specified class
323: */
324: public <T extends IPersistent> boolean deleteRecord(Class table,
325: T record) {
326: boolean removed = false;
327: if (multithreaded) {
328: metadata.sharedLock();
329: }
330: for (Class c = table; c != null; c = c.getSuperclass()) {
331: Table t = (Table) tables.get(c);
332: if (t != null) {
333: if (multithreaded) {
334: t.extent.exclusiveLock();
335: }
336: if (t.extent.remove(record)) {
337: Iterator iterator = t.indicesMap.values()
338: .iterator();
339: while (iterator.hasNext()) {
340: FieldIndex index = (FieldIndex) iterator.next();
341: index.remove(record);
342: }
343: removed = true;
344: }
345: }
346: }
347: if (removed) {
348: record.deallocate();
349: }
350: return removed;
351: }
352:
353: /**
354: * Add new index to the table. If such index already exists this method does nothing.
355: * @param table class corresponding to the table
356: * @param key field of the class to be indexed
357: * @param unique if index is unique or not
358: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
359: * the specified class
360: * @return <code>true</code> if index is created, <code>false</code> if index
361: * already exists
362: * @deprecated since version 2.75 of Perst it is not necessary to create table and index
363: * descriptors explicitly: them are automatically create when object is inserted in the
364: * database first time (to mark fields for which indices should be created, use Indexable
365: * annotation)
366: */
367: public boolean createIndex(Class table, String key, boolean unique) {
368: return createIndex(locateTable(table, true), table, key,
369: unique, false);
370: }
371:
372: /**
373: * Add new index to the table. If such index already exists this method does nothing.
374: * @param table class corresponding to the table
375: * @param key field of the class to be indexed
376: * @param unique if index is unique or not
377: * @param caseInsensitive if string index is case insensitive
378: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
379: * the specified class
380: * @return <code>true</code> if index is created, <code>false</code> if index
381: * already exists
382: * @deprecated since version 2.75 of Perst it is not necessary to create table and index
383: * descriptors explicitly: them are automatically cerate when objct is inserted in the
384: * database first time (to mark fields for which indices should be created, use Indexable
385: * annotaion)
386: */
387: public boolean createIndex(Class table, String key, boolean unique,
388: boolean caseInsensitive) {
389: return createIndex(locateTable(table, true), table, key,
390: unique, caseInsensitive);
391: }
392:
393: private boolean createIndex(Table t, Class c, String key,
394: boolean unique, boolean caseInsensitive) {
395: if (t.indicesMap.get(key) == null) {
396: FieldIndex index = storage.createFieldIndex(c, key, unique,
397: caseInsensitive);
398: t.indicesMap.put(key, index);
399: t.indices.add(index);
400: return true;
401: }
402: return false;
403: }
404:
405: /**
406: * Drop index for the specified table and key.
407: * Does nothing if there is no such index.
408: * @param table class corresponding to the table
409: * @param key field of the class to be indexed
410: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
411: * the specified class
412: * @return <code>true</code> if index is deleted, <code>false</code> if index
413: * is not found
414: */
415: public boolean dropIndex(Class table, String key) {
416: Table t = locateTable(table, true);
417: FieldIndex index = (FieldIndex) t.indicesMap.remove(key);
418: if (index != null) {
419: t.indices.remove(t.indices.indexOf(index));
420: return true;
421: }
422: return false;
423: }
424:
425: /**
426: * Get indices for the specified table
427: * @param table class corresponding to the table
428: * @return map of table indices
429: */
430: public HashMap getIndices(Class table) {
431: Table t = locateTable(table, true, false);
432: return t == null ? new HashMap() : t.indicesMap;
433: }
434:
435: /**
436: * Exclude record from specified index. This method is needed to perform update of indexed
437: * field (key). Before updating the record, it is necessary to exclude it from indices
438: * which keys are affected. After updating the field, record should be reinserted in these indices
439: * using includeInIndex method.<P>
440: * If there is not table associated with class of this object, then
441: * database will search for table associated with superclass and so on...<P>
442: * This method does nothing if there is no index for the specified field.
443: * @param record object to be excluded from the specified index
444: * @param key name of the indexed field
445: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
446: * record class
447: * @return <code>true</code> if record is excluded from index, <code>false</code> if
448: * there is no such index
449: */
450: public boolean excludeFromIndex(IPersistent record, String key) {
451: return excludeFromIndex(record.getClass(), record, key);
452: }
453:
454: /**
455: * Exclude record from specified index. This method is needed to perform update of indexed
456: * field (key). Before updating the record, it is necessary to exclude it from indices
457: * which keys are affected. After updating the field, record should be reinserted in these indices
458: * using includeInIndex method.<P>
459: * If there is not table associated with class of this object, then
460: * database will search for table associated with superclass and so on...<P>
461: * This method does nothing if there is no index for the specified field.
462: * @param table class corresponding to the table
463: * @param record object to be excluded from the specified index
464: * @param key name of the indexed field
465: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
466: * the specified class
467: * @return <code>true</code> if record is excluded from index, <code>false</code> if
468: * there is no such index
469: */
470: public boolean excludeFromIndex(Class table, IPersistent record,
471: String key) {
472: Table t = locateTable(table, true);
473: FieldIndex index = (FieldIndex) t.indicesMap.get(key);
474: if (index != null) {
475: index.remove(record);
476: return true;
477: }
478: return false;
479: }
480:
481: /**
482: * Include record in the specified index. This method is needed to perform update of indexed
483: * field (key). Before updating the record, it is necessary to exclude it from indices
484: * which keys are affected using excludeFromIndex method. After updating the field, record should be
485: * reinserted in these indices using this method.<P>
486: * If there is not table associated with class of this object, then
487: * database will search for table associated with superclass and so on...<P>
488: * This method does nothing if there is no index for the specified field.
489: * @param record object to be excluded from the specified index
490: * @param key name of the indexed field
491: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
492: * the specified class
493: * @return <code>true</code> if record is included in index, <code>false</code> if
494: * there is no such index
495: */
496: public boolean includeInIndex(IPersistent record, String key) {
497: return includeInIndex(record.getClass(), record, key);
498: }
499:
500: /**
501: * Include record in the specified index. This method is needed to perform update of indexed
502: * field (key). Before updating the record, it is necessary to exclude it from indices
503: * which keys are affected using excludeFromIndex method. After updating the field, record should be
504: * reinserted in these indices using this method.<P>
505: * If there is not table associated with class of this object, then
506: * database will search for table associated with superclass and so on...<P>
507: * This method does nothing if there is no index for the specified field.
508: * @param table class corresponding to the table
509: * @param record object to be excluded from the specified index
510: * @param key name of the indexed field
511: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
512: * the specified class
513: * @return <code>true</code> if record is included in index, <code>false</code> if
514: * there is no such index
515: */
516: public boolean includeInIndex(Class table, IPersistent record,
517: String key) {
518: Table t = locateTable(table, true);
519: FieldIndex index = (FieldIndex) t.indicesMap.get(key);
520: if (index != null) {
521: index.put(record);
522: return true;
523: }
524: return false;
525: }
526:
527: /**
528: * Select record from specified table
529: * @param table class corresponding to the table
530: * @param predicate search predicate
531: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
532: * the specified class
533: * @return iterator through selected records. This iterator doesn't support remove() method
534: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
535: * @exception JSQLRuntimeException exception is thrown if there is runtime error during query execution
536: */
537: public <T extends IPersistent> IterableIterator<T> select(
538: Class table, String predicate) {
539: return select(table, predicate, false);
540: }
541:
542: /**
543: * Select record from specified table
544: * @param table class corresponding to the table
545: * @param predicate search predicate
546: * @param forUpdate <code>true</code> if records are selected for update - in this case exclusive lock is set
547: * for the table to avoid deadlock.
548: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
549: * the specified class
550: * @return iterator through selected records. This iterator doesn't support remove() method
551: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
552: * @exception JSQLRuntimeException exception is thrown if there is runtime error during query execution
553: */
554: public <T extends IPersistent> IterableIterator<T> select(
555: Class table, String predicate, boolean forUpdate) {
556: Query q = prepare(table, predicate, forUpdate);
557: return q.execute(getRecords(table));
558: }
559:
560: /**
561: * Prepare JSQL query. Prepare is needed for queries with parameters. Also
562: * preparing query can improve speed if query will be executed multiple times
563: * (using prepare, it is compiled only once).<P>
564: * To execute prepared query, you should use Query.execute(db.getRecords(XYZ.class)) method
565: * @param table class corresponding to the table
566: * @param predicate search predicate
567: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
568: * the specified class
569: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
570: */
571: public <T extends IPersistent> Query<T> prepare(Class table,
572: String predicate) {
573: return prepare(table, predicate, false);
574: }
575:
576: /**
577: * Prepare JSQL query. Prepare is needed for queries with parameters. Also
578: * preparing query can improve speed if query will be executed multiple times
579: * (using prepare, it is compiled only once).<P>
580: * To execute prepared query, you should use Query.execute(db.getRecords(XYZ.class)) method
581: * @param table class corresponding to the table
582: * @param predicate search predicate
583: * @param forUpdate <code>true</code> if records are selected for update - in this case exclusive lock is set
584: * for the table to avoid deadlock.
585: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
586: * the specified class
587: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
588: */
589: public <T extends IPersistent> Query<T> prepare(Class table,
590: String predicate, boolean forUpdate) {
591: Table t = locateTable(table, forUpdate, false);
592: Query q = storage.createQuery();
593: q.prepare(table, predicate);
594: if (t != null) {
595: Iterator iterator = t.indicesMap.entrySet().iterator();
596: while (iterator.hasNext()) {
597: Map.Entry entry = (Map.Entry) iterator.next();
598: FieldIndex index = (FieldIndex) entry.getValue();
599: String key = (String) entry.getKey();
600: q.addIndex(key, index);
601: }
602: }
603: return q;
604: }
605:
606: /**
607: * Get iterator through all table records
608: * @param table class corresponding to the table
609: * @return iterator through all table records.
610: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
611: * the specified class
612: */
613: public <T extends IPersistent> IterableIterator<T> getRecords(
614: Class table) {
615: return getRecords(table, false);
616: }
617:
618: /**
619: * Get iterator through all table records
620: * @param table class corresponding to the table
621: * @param forUpdate <code>true</code> if records are selected for update - in this case exclusive lock is set
622: * for the table to avoid deadlock.
623: * @return iterator through all table records.
624: * @exception StorageError(CLASS_NOT_FOUND) exception is thrown if there is no table corresponding to
625: * the specified class
626: */
627: public <T extends IPersistent> IterableIterator<T> getRecords(
628: Class table, boolean forUpdate) {
629: Table t = locateTable(table, forUpdate, false);
630: return new IteratorWrapper<T>(t == null ? new LinkedList<T>()
631: .iterator() : t.extent.iterator());
632: }
633:
634: /**
635: * Get storage associated with this database
636: * @return underlying storage
637: */
638: public Storage getStorage() {
639: return storage;
640: }
641:
642: static class Table extends Persistent {
643: IPersistentSet extent;
644: Link indices;
645:
646: transient HashMap indicesMap = new HashMap();
647:
648: public void onLoad() {
649: for (int i = indices.size(); --i >= 0;) {
650: FieldIndex index = (FieldIndex) indices.get(i);
651: indicesMap
652: .put(index.getKeyFields()[0].getName(), index);
653: }
654: }
655: }
656:
657: HashMap tables;
658: Storage storage;
659: Index metadata;
660: boolean multithreaded;
661: boolean autoRegisterTables;
662: }
|