| 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 } |