| 
68
 | 
     1 /*
 | 
| 
 | 
     2 Copyright (c) 2008  Franklin Schmidt <fschmidt@gmail.com>
 | 
| 
 | 
     3 
 | 
| 
 | 
     4 Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
| 
 | 
     5 of this software and associated documentation files (the "Software"), to deal
 | 
| 
 | 
     6 in the Software without restriction, including without limitation the rights
 | 
| 
 | 
     7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
| 
 | 
     8 copies of the Software, and to permit persons to whom the Software is
 | 
| 
 | 
     9 furnished to do so, subject to the following conditions:
 | 
| 
 | 
    10 
 | 
| 
 | 
    11 The above copyright notice and this permission notice shall be included in
 | 
| 
 | 
    12 all copies or substantial portions of the Software.
 | 
| 
 | 
    13 
 | 
| 
 | 
    14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
| 
 | 
    15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
| 
 | 
    16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
| 
 | 
    17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
| 
 | 
    18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
| 
 | 
    19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
| 
 | 
    20 THE SOFTWARE.
 | 
| 
 | 
    21 */
 | 
| 
 | 
    22 
 | 
| 
 | 
    23 package fschmidt.db.base;
 | 
| 
 | 
    24 
 | 
| 
 | 
    25 import java.sql.SQLException;
 | 
| 
 | 
    26 import java.sql.PreparedStatement;
 | 
| 
 | 
    27 import java.sql.Connection;
 | 
| 
 | 
    28 import java.util.Map;
 | 
| 
 | 
    29 import java.util.HashMap;
 | 
| 
 | 
    30 import java.util.Iterator;
 | 
| 
 | 
    31 import org.slf4j.Logger;
 | 
| 
 | 
    32 import org.slf4j.LoggerFactory;
 | 
| 
 | 
    33 import fschmidt.db.DbTable;
 | 
| 
 | 
    34 import fschmidt.db.DbKey;
 | 
| 
 | 
    35 import fschmidt.db.DbKeySetter;
 | 
| 
 | 
    36 import fschmidt.db.DbObject;
 | 
| 
 | 
    37 import fschmidt.db.DbRecord;
 | 
| 
 | 
    38 import fschmidt.db.SQLRuntimeException;
 | 
| 
 | 
    39 import fschmidt.db.DbExpression;
 | 
| 
 | 
    40 import fschmidt.db.LongKey;
 | 
| 
 | 
    41 import fschmidt.db.DbUtils;
 | 
| 
 | 
    42 import fschmidt.db.DbArcana;
 | 
| 
 | 
    43 import fschmidt.db.ListenerList;
 | 
| 
 | 
    44 import fschmidt.db.extend.DbRecordExt;
 | 
| 
 | 
    45 import fschmidt.db.extend.DbTableExt;
 | 
| 
 | 
    46 import fschmidt.db.extend.DbTransaction;
 | 
| 
 | 
    47 
 | 
| 
 | 
    48 
 | 
| 
 | 
    49 final class DbRecordImpl<K extends DbKey,V extends DbObject<K,V>> implements DbRecordExt<K,V> {
 | 
| 
 | 
    50 	private static final Logger logger = LoggerFactory.getLogger(DbRecordImpl.class);
 | 
| 
 | 
    51 
 | 
| 
 | 
    52 	public static boolean hasStackTrace = false;
 | 
| 
 | 
    53 	private final V obj;
 | 
| 
 | 
    54 	private K key;
 | 
| 
 | 
    55 	private final DbTableExt<K,V> table;
 | 
| 
 | 
    56 	private Map<String,Object> fields = new HashMap<String,Object>();
 | 
| 
 | 
    57 	private boolean isInDb;
 | 
| 
 | 
    58 	private boolean isStale = false;
 | 
| 
 | 
    59 //	private final long whenLoaded = System.currentTimeMillis();
 | 
| 
 | 
    60 	private DbTransaction trans;
 | 
| 
 | 
    61 
 | 
| 
 | 
    62 	public DbRecordImpl(DbTableExt<K,V> table,V obj,K key) {
 | 
| 
 | 
    63 		this.table = table;
 | 
| 
 | 
    64 		this.obj = obj;
 | 
| 
 | 
    65 		this.key = key;
 | 
| 
 | 
    66 		this.isInDb = true;
 | 
| 
 | 
    67 		this.trans = table.getDbDatabaseExt().getTransaction();
 | 
| 
 | 
    68 	}
 | 
| 
 | 
    69 
 | 
| 
 | 
    70 	public DbRecordImpl(DbTableExt<K,V> table,V obj) {
 | 
| 
 | 
    71 		this.table = table;
 | 
| 
 | 
    72 		this.obj = obj;
 | 
| 
 | 
    73 		this.key = null;
 | 
| 
 | 
    74 		this.isInDb = false;
 | 
| 
 | 
    75 		this.trans = table.getDbDatabaseExt().getTransaction();
 | 
| 
 | 
    76 	}
 | 
| 
 | 
    77 
 | 
| 
 | 
    78 	public V getDbObject() {
 | 
| 
 | 
    79 		return obj;
 | 
| 
 | 
    80 	}
 | 
| 
 | 
    81 
 | 
| 
 | 
    82 	public K getPrimaryKey() {
 | 
| 
 | 
    83 		return key;
 | 
| 
 | 
    84 	}
 | 
| 
 | 
    85 
 | 
| 
 | 
    86 	public DbTable<K,V> getDbTable() {
 | 
| 
 | 
    87 		return table;
 | 
| 
 | 
    88 	}
 | 
| 
 | 
    89 
 | 
| 
 | 
    90 	public Map<String,Object> fields() {
 | 
| 
 | 
    91 		return fields;
 | 
| 
 | 
    92 	}
 | 
| 
 | 
    93 
 | 
| 
 | 
    94 /*
 | 
| 
 | 
    95 	public long whenLoaded() {
 | 
| 
 | 
    96 		return whenLoaded;
 | 
| 
 | 
    97 	}
 | 
| 
 | 
    98 */
 | 
| 
 | 
    99 	private void checkTrans() {
 | 
| 
 | 
   100 		if( trans != table.getDbDatabaseExt().getTransaction() ) {
 | 
| 
 | 
   101 			if( trans==null )
 | 
| 
 | 
   102 				throw new IllegalStateException("record read outside of a transaction cannot be updated inside a transaction");
 | 
| 
 | 
   103 			else
 | 
| 
 | 
   104 				throw new IllegalStateException("transaction mismatch");
 | 
| 
 | 
   105 		}
 | 
| 
 | 
   106 	}
 | 
| 
 | 
   107 
 | 
| 
 | 
   108 	public void insert() {
 | 
| 
 | 
   109 		if( isInDb() )
 | 
| 
 | 
   110 			throw new IllegalStateException("already in database");
 | 
| 
 | 
   111 		try {
 | 
| 
 | 
   112 			ListenerList<V> list = table.getPreInsertListeners();
 | 
| 
 | 
   113 			list.event(obj);
 | 
| 
 | 
   114 			store();
 | 
| 
 | 
   115 			this.trans = table.getDbDatabaseExt().getTransaction();
 | 
| 
 | 
   116 			final Map<String,Object> updatedFields = fields;
 | 
| 
 | 
   117 			final Exception ex = hasStackTrace ? new Exception("in "+Thread.currentThread()) : null;
 | 
| 
 | 
   118 			table.getDbDatabaseExt().runBeforeCommit( new Runnable() {
 | 
| 
 | 
   119 				public void run() {
 | 
| 
 | 
   120 					ListenerList<V> list = table.getPostInsertListeners();
 | 
| 
 | 
   121 					if( !list.isEmpty() ) {
 | 
| 
 | 
   122 						V obj2 = DbUtils.getGoodCopy(obj);
 | 
| 
 | 
   123 						if( obj2==null ) {
 | 
| 
 | 
   124 							DbRecord<K,V> rec = obj.getDbRecord();
 | 
| 
 | 
   125 							K key = rec.getPrimaryKey();
 | 
| 
 | 
   126 							if( ex != null )
 | 
| 
 | 
   127 								logger.error("insert null in "+Thread.currentThread()+" table="+table+" key="+key, ex);
 | 
| 
 | 
   128 							throw new NullPointerException("in "+Thread.currentThread()+" table="+table+" key="+key);
 | 
| 
 | 
   129 						}
 | 
| 
 | 
   130 						DbRecordImpl rec2 = (DbRecordImpl)obj2.getDbRecord();
 | 
| 
 | 
   131 						try {
 | 
| 
 | 
   132 							list.event(obj2);
 | 
| 
 | 
   133 						} finally {
 | 
| 
 | 
   134 							updatedFields.clear();
 | 
| 
 | 
   135 						}
 | 
| 
 | 
   136 					}
 | 
| 
 | 
   137 				}
 | 
| 
 | 
   138 			} );
 | 
| 
 | 
   139 		} finally {
 | 
| 
 | 
   140 			fields = new HashMap<String,Object>();
 | 
| 
 | 
   141 			makeStale();
 | 
| 
 | 
   142 		}
 | 
| 
 | 
   143 	}
 | 
| 
 | 
   144 
 | 
| 
 | 
   145 	public void update() {
 | 
| 
 | 
   146 		checkTrans();
 | 
| 
 | 
   147 		if( !isInDb() )
 | 
| 
 | 
   148 			throw new IllegalStateException("not in database");
 | 
| 
 | 
   149 		try {
 | 
| 
 | 
   150 			ListenerList<V> list = table.getPreUpdateListeners();
 | 
| 
 | 
   151 			list.event(obj);
 | 
| 
 | 
   152 			if( fields.isEmpty() ) {
 | 
| 
 | 
   153 				table.uncache(key);
 | 
| 
 | 
   154 			} else {
 | 
| 
 | 
   155 				store();
 | 
| 
 | 
   156 			}
 | 
| 
 | 
   157 			final Map<String,Object> updatedFields = fields;
 | 
| 
 | 
   158 			table.getDbDatabaseExt().runBeforeCommit( new Runnable() {
 | 
| 
 | 
   159 				public void run() {
 | 
| 
 | 
   160 					ListenerList<V> list = table.getPostUpdateListeners();
 | 
| 
 | 
   161 					if( !list.isEmpty() && isInDb ) {
 | 
| 
 | 
   162 						V obj2 = DbUtils.getGoodCopy(obj);
 | 
| 
 | 
   163 						if( obj2==null )
 | 
| 
 | 
   164 							return;  // presumably deleted later in transaction
 | 
| 
 | 
   165 						DbRecordImpl rec2 = (DbRecordImpl)obj2.getDbRecord();
 | 
| 
 | 
   166 						try {
 | 
| 
 | 
   167 							list.event(obj2);
 | 
| 
 | 
   168 						} finally {
 | 
| 
 | 
   169 							updatedFields.clear();
 | 
| 
 | 
   170 						}
 | 
| 
 | 
   171 					}
 | 
| 
 | 
   172 				}
 | 
| 
 | 
   173 			} );
 | 
| 
 | 
   174 		} finally {
 | 
| 
 | 
   175 			fields = new HashMap<String,Object>();
 | 
| 
 | 
   176 		}
 | 
| 
 | 
   177 	}
 | 
| 
 | 
   178 
 | 
| 
 | 
   179 	private void store() {
 | 
| 
 | 
   180 		StringBuilder buf = new StringBuilder();
 | 
| 
 | 
   181 		try {
 | 
| 
 | 
   182 			DbArcana arcana = table.getDbDatabase().arcana();
 | 
| 
 | 
   183 			int n = 0;
 | 
| 
 | 
   184 			if( isInDb() ) {  // update
 | 
| 
 | 
   185 				buf.append( "update " );
 | 
| 
 | 
   186 				buf.append( table.getTableName() );
 | 
| 
 | 
   187 				buf.append( " set " );
 | 
| 
 | 
   188 				boolean first = true;
 | 
| 
 | 
   189 				for( Map.Entry<String,Object> entry : fields.entrySet() ) {
 | 
| 
 | 
   190 					if( first ) {
 | 
| 
 | 
   191 						first = false;
 | 
| 
 | 
   192 					} else {
 | 
| 
 | 
   193 						buf.append( ',' );
 | 
| 
 | 
   194 					}
 | 
| 
 | 
   195 					buf.append( arcana.quoteIdentifier(entry.getKey()) );
 | 
| 
 | 
   196 					if( entry.getValue() instanceof DbExpression ) {
 | 
| 
 | 
   197 						buf.append( '=' );
 | 
| 
 | 
   198 						buf.append( entry.getValue() );
 | 
| 
 | 
   199 					} else {
 | 
| 
 | 
   200 						buf.append( "=?" );
 | 
| 
 | 
   201 						n++;
 | 
| 
 | 
   202 					}
 | 
| 
 | 
   203 				}
 | 
| 
 | 
   204 			} else {  // insert
 | 
| 
 | 
   205 				buf.append( "insert into " );
 | 
| 
 | 
   206 				buf.append( table.getTableName() );
 | 
| 
 | 
   207 				if( fields.isEmpty() ) {
 | 
| 
 | 
   208 					buf.append( " DEFAULT VALUES" );
 | 
| 
 | 
   209 				} else {
 | 
| 
 | 
   210 					buf.append( " (" );
 | 
| 
 | 
   211 					boolean first = true;
 | 
| 
 | 
   212 					for( Map.Entry<String,Object> entry : fields.entrySet() ) {
 | 
| 
 | 
   213 						if( first ) {
 | 
| 
 | 
   214 							first = false;
 | 
| 
 | 
   215 						} else {
 | 
| 
 | 
   216 							buf.append( ',' );
 | 
| 
 | 
   217 						}
 | 
| 
 | 
   218 						buf.append( arcana.quoteIdentifier(entry.getKey()) );
 | 
| 
 | 
   219 					}
 | 
| 
 | 
   220 					buf.append( ") values (" );
 | 
| 
 | 
   221 					first = true;
 | 
| 
 | 
   222 					for( Map.Entry<String,Object> entry : fields.entrySet() ) {
 | 
| 
 | 
   223 						if( first ) {
 | 
| 
 | 
   224 							first = false;
 | 
| 
 | 
   225 						} else {
 | 
| 
 | 
   226 							buf.append(',');
 | 
| 
 | 
   227 						}
 | 
| 
 | 
   228 						if( entry.getValue() instanceof DbExpression ) {
 | 
| 
 | 
   229 							buf.append( entry.getValue() );
 | 
| 
 | 
   230 						} else {
 | 
| 
 | 
   231 							buf.append( '?' );
 | 
| 
 | 
   232 							n++;
 | 
| 
 | 
   233 						}
 | 
| 
 | 
   234 					}
 | 
| 
 | 
   235 					buf.append( ")" );
 | 
| 
 | 
   236 				}
 | 
| 
 | 
   237 			}
 | 
| 
 | 
   238 			Connection con = table.getDbDatabaseExt().getConnection();
 | 
| 
 | 
   239 			try {
 | 
| 
 | 
   240 				DbKeySetter<K> keySetter = table.getKeySetter();
 | 
| 
 | 
   241 				PreparedStatement stmt;
 | 
| 
 | 
   242 				if( isInDb() ) {
 | 
| 
 | 
   243 					stmt = keySetter.prepareStatement(
 | 
| 
 | 
   244 						con, key, buf.toString(), 1+n
 | 
| 
 | 
   245 					);
 | 
| 
 | 
   246 				} else {
 | 
| 
 | 
   247 					stmt = con.prepareStatement( buf.toString() );
 | 
| 
 | 
   248 				}
 | 
| 
 | 
   249 				int idx = 1;
 | 
| 
 | 
   250 				for( Map.Entry<String,Object> entry : fields.entrySet() ) {
 | 
| 
 | 
   251 					Object value = entry.getValue();
 | 
| 
 | 
   252 					if( value instanceof DbExpression )
 | 
| 
 | 
   253 						continue;
 | 
| 
 | 
   254 					try {
 | 
| 
 | 
   255 						arcana.setValue(stmt,idx++,value);
 | 
| 
 | 
   256 					} catch(SQLException e) {
 | 
| 
 | 
   257 						throw new SQLRuntimeException("field="+entry.getKey()+" value="+value,e);
 | 
| 
 | 
   258 					}
 | 
| 
 | 
   259 				}
 | 
| 
 | 
   260 				stmt.executeUpdate();
 | 
| 
 | 
   261 				boolean wasInDb = isInDb;
 | 
| 
 | 
   262 				isInDb = true;
 | 
| 
 | 
   263 				stmt.close();
 | 
| 
 | 
   264 				if( !wasInDb ) {
 | 
| 
 | 
   265 					key = keySetter.refreshKeyAfterInsert(con,this);
 | 
| 
 | 
   266 				}
 | 
| 
 | 
   267 			} finally {
 | 
| 
 | 
   268 				try {
 | 
| 
 | 
   269 					con.close();
 | 
| 
 | 
   270 				} catch(SQLException e) {
 | 
| 
 | 
   271 					logger.error("couldn't close connection",e);
 | 
| 
 | 
   272 					throw e;
 | 
| 
 | 
   273 				} catch(RuntimeException e) {
 | 
| 
 | 
   274 					logger.error("couldn't close connection",e);
 | 
| 
 | 
   275 					throw e;
 | 
| 
 | 
   276 				}
 | 
| 
 | 
   277 			}
 | 
| 
 | 
   278 		} catch(SQLException e) {
 | 
| 
 | 
   279 			throw new SQLRuntimeException("key = "+key+"  sql = "+buf.toString(),e);
 | 
| 
 | 
   280 		} finally {
 | 
| 
 | 
   281 			table.uncache(key);
 | 
| 
 | 
   282 		}
 | 
| 
 | 
   283 	}
 | 
| 
 | 
   284 
 | 
| 
 | 
   285 	public void delete() {
 | 
| 
 | 
   286 		checkTrans();
 | 
| 
 | 
   287 		ListenerList<V> list = table.getPreDeleteListeners();
 | 
| 
 | 
   288 		list.event(obj);
 | 
| 
 | 
   289 		if( !isInDb() )
 | 
| 
 | 
   290 			throw new IllegalStateException("not in database");
 | 
| 
 | 
   291 		try {
 | 
| 
 | 
   292 			Connection con = table.getDbDatabaseExt().getConnection();
 | 
| 
 | 
   293 			PreparedStatement stmt = table.getKeySetter().prepareStatement(
 | 
| 
 | 
   294 				con, key, "delete from "+table.getTableName(), 1
 | 
| 
 | 
   295 			);
 | 
| 
 | 
   296 			stmt.executeUpdate();
 | 
| 
 | 
   297 			isInDb = false;
 | 
| 
 | 
   298 			stmt.close();
 | 
| 
 | 
   299 			con.close();
 | 
| 
 | 
   300 /*
 | 
| 
 | 
   301 			table.getDbDatabaseExt().runBeforeCommit( new Runnable() {
 | 
| 
 | 
   302 				public void run() {
 | 
| 
 | 
   303 					table.getPostDeleteListeners().event(obj);
 | 
| 
 | 
   304 				}
 | 
| 
 | 
   305 			} );
 | 
| 
 | 
   306 */
 | 
| 
 | 
   307 		} catch(SQLException e) {
 | 
| 
 | 
   308 			throw new SQLRuntimeException("key = "+key,e);
 | 
| 
 | 
   309 		} finally {
 | 
| 
 | 
   310 			table.uncache(key);
 | 
| 
 | 
   311 			fields.clear();
 | 
| 
 | 
   312 		}
 | 
| 
 | 
   313 		table.getDbDatabaseExt().runBeforeCommit( new Runnable() {
 | 
| 
 | 
   314 			public void run() {
 | 
| 
 | 
   315 				ListenerList<V> list = table.getPostDeleteListeners();
 | 
| 
 | 
   316 				list.event(obj);
 | 
| 
 | 
   317 			}
 | 
| 
 | 
   318 		} );
 | 
| 
 | 
   319 	}
 | 
| 
 | 
   320 
 | 
| 
 | 
   321 	public boolean isStale() {
 | 
| 
 | 
   322 		return isStale || (isInDb || trans!=null) && trans != table.getDbDatabaseExt().getTransaction();
 | 
| 
 | 
   323 	}
 | 
| 
 | 
   324 
 | 
| 
 | 
   325 	public void makeStale() {
 | 
| 
 | 
   326 		isStale = true;
 | 
| 
 | 
   327 	}
 | 
| 
 | 
   328 
 | 
| 
 | 
   329 	public boolean isInDb() {
 | 
| 
 | 
   330 		return isInDb;
 | 
| 
 | 
   331 	}
 | 
| 
 | 
   332 
 | 
| 
 | 
   333 }
 |