001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.dataAccess; 019 020import io.stallion.dataAccess.db.Col; 021import io.stallion.dataAccess.db.DB; 022import io.stallion.dataAccess.db.Schema; 023import io.stallion.dataAccess.filtering.FilterCache; 024import io.stallion.dataAccess.filtering.FilterChain; 025import io.stallion.exceptions.ConfigException; 026import io.stallion.exceptions.UsageException; 027import io.stallion.reflection.PropertyUtils; 028import io.stallion.services.Log; 029import org.apache.commons.lang3.StringUtils; 030 031import java.util.*; 032 033import static io.stallion.utils.Literals.*; 034 035 036/** 037 * PartialStash loads the most recently updated 50,000 items in memory, and also 038 * keeps in memory every item directly loaded by id. This is useful for largish tables 039 * with recent data accessed very frequently, but where you don't want to incur 040 * a really long load-time when booting the server during deployment. 041 * 042 * @param <T> 043 */ 044public class PartialStash<T extends Model> extends Stash<T> { 045 046 protected Map<Long, T> itemByPrimaryKey; 047 protected List<T> items; 048 protected Set<String> uniqueFields; 049 protected Map<String, Map<Object, T>> keyNameToUniqueKeyToValue; 050 protected List<Col> columns; 051 052 @Override 053 public void init(DataAccessRegistration registration, ModelController<T> controller, Persister<T> persister) { 054 super.init(registration, controller, persister); 055 if (registration.getDynamicModelDefinition() != null) { 056 columns = registration.getDynamicModelDefinition().getColumns(); 057 } 058 initialize(); 059 } 060 061 private void initialize() { 062 if (getPersister() == null) { 063 throw new ConfigException("A controller must have a persister before it is inited"); 064 } 065 if (StringUtils.isEmpty(getBucket())) { 066 throw new ConfigException("A controller must have a valid bucket before it is inited"); 067 } 068 FilterCache.initCache(getBucket()); 069 this.items = new ArrayList<>(); 070 this.uniqueFields = set(); 071 this.itemByPrimaryKey = new HashMap<Long, T>(); 072 // Use a hashtable to enforce uniqueness 073 this.keyNameToUniqueKeyToValue = new Hashtable<String, Map<Object, T>>(); 074 for(String key: getUniqueFields()) { 075 this.keyNameToUniqueKeyToValue.put(key, new Hashtable<Object, T>()); 076 } 077 078 // Get the unique keys and alternative keys from annotations 079 for(String propertyName: PropertyUtils.getPropertyNames(this.getPersister().getModelClass())) { 080 if (PropertyUtils.propertyHasAnnotation(this.getPersister().getModelClass(), propertyName, UniqueKey.class)) { 081 Log.fine("Model:{0} has uniquekey on {1}", this.getPersister().getModelClass(), propertyName); 082 this.keyNameToUniqueKeyToValue.put(propertyName, new HashMap<Object, T>()); 083 } 084 } 085 if (!empty(columns)) { 086 for (Col col: columns) { 087 if (col.getUniqueKey()) { 088 this.keyNameToUniqueKeyToValue.put(col.getPropertyName(), new HashMap<Object, T>()); 089 } 090 } 091 } 092 uniqueFields = this.keyNameToUniqueKeyToValue.keySet(); 093 } 094 095 096 @Override 097 public void sync(T obj) { 098 // TODO: exclude properties with annotation @SyncExclude 099 T existing = this.originalForId(obj.getId()); 100 if (existing != null) { 101 List<String> changedKeyFields = new ArrayList<>(); 102 cloneInto(obj, existing, null, false, changedKeyFields); 103 } 104 } 105 106 107 /** 108 * Clones all non-null values from "source" into "dest" 109 * @param source 110 * @param dest 111 * @param properties 112 * @param copyNulls 113 * @param changedKeyFields 114 */ 115 public boolean cloneInto(Object source, Object dest, Iterable<String> properties, Boolean copyNulls, List<String> changedKeyFields) { 116 if (changedKeyFields == null) { 117 changedKeyFields = new ArrayList<String>(); 118 } 119 boolean hasChanges = false; 120 if (properties == null){ 121 //properties = PropertyUtils.describe(dest).keySet(); 122 properties = PropertyUtils.getProperties(dest).keySet(); 123 } 124 for(String name: properties) { 125 if (name.equals("id")) { 126 continue; 127 } 128 if (name.equals("class")) { 129 continue; 130 } 131 if (name.equals("controller")) { 132 continue; 133 } 134 Object o = PropertyUtils.getProperty(source, name); 135 Object previous = PropertyUtils.getProperty(dest, name); 136 if (o != null || copyNulls) { 137 if (previous == o || o != null && o.equals(previous)) { 138 continue; 139 } 140 if (getKeyFields() != null && this.getKeyFields().contains(name) && previous != null && !previous.equals(o)) { 141 changedKeyFields.add(name); 142 } 143 hasChanges = true; 144 PropertyUtils.setProperty(dest, name, o); 145 } 146 } 147 return hasChanges; 148 } 149 150 151 152 @Override 153 public T detach(T obj) { 154 T existing = this.itemByPrimaryKey.get(obj.getId()); 155 if (existing == null) { 156 return obj; 157 } 158 T newItem = null; 159 try { 160 newItem = (T)existing.getClass().newInstance(); 161 } catch (Exception e) { 162 throw new RuntimeException(e); 163 } 164 newItem.setId(obj.getId()); 165 cloneInto(existing, newItem, null, true, null); 166 return newItem; 167 } 168 169 170 @Override 171 public void save(T obj) { 172 T existing = originalForId(obj.getId()); 173 if (existing != null) { 174 syncForSave(obj); 175 getPersister().persist(existing); 176 } else { 177 Map<String, Object> changedValues = map(); 178 // If these are not the same reference in memory, then we find the changed values, 179 // and sync values from the detached object into the permanent object 180 if (existing != null && obj != existing) { 181 for (Map.Entry<String, Object> entry: PropertyUtils.getProperties(obj).entrySet()) { 182 Object org = PropertyUtils.getPropertyOrMappedValue(existing, entry.getKey()); 183 if (org == null && entry.getValue() != null) { 184 changedValues.put(entry.getKey(), entry.getValue()); 185 } else if (org != null && org.equals(entry.getValue())) { 186 changedValues.put(entry.getKey(), entry.getValue()); 187 } 188 } 189 this.syncForSave(obj); 190 getPersister().update(obj, changedValues); 191 } else { 192 getPersister().persist(obj); 193 } 194 } 195 } 196 197 @Override 198 public void syncForSave(T obj) { 199 T existing = this.originalForId(obj.getId()); 200 List<String> changedKeyFields = new ArrayList<>(); 201 cloneInto(obj, existing, null, true, changedKeyFields); 202 } 203 204 @Override 205 public void hardDelete(T obj) { 206 getPersister().hardDelete(obj); 207 } 208 209 @Override 210 public void loadAll() { 211 if (!DB.isUseDummyPersisterForSqlGenerationMode()) { 212 for (T obj : DB.instance().query(getPersister().getModelClass(), getInitialLoadSql())) { 213 loadItem(obj); 214 } 215 } 216 } 217 218 public String getInitialLoadSql() { 219 Schema schema = DB.instance().getSchema(getPersister().getModelClass()); 220 String sql = "SELECT * FROM " + schema.getName() + " ORDER BY row_updated_at DESC LIMIT 50000"; 221 return sql; 222 } 223 224 225 226 227 228 229 public void loadForId(Long id) { 230 T item = (T) getPersister().fetchOne(id); 231 loadItem(item); 232 FilterCache.clearBucket(getBucket()); 233 } 234 235 236 237 public boolean loadItem(T item) { 238 //Log.fine("Pojo item: {0}:{1}", item.getClass().getName(), item.getId()); 239 boolean hasChanges = false; 240 if (item.getId() == null) { 241 Log.warn("Loading a pojo item with a null ID! bucket: {0} class:{1}", getBucket(), item.getClass().getName()); 242 } 243 T original = itemByPrimaryKey.getOrDefault(item.getId(), null); 244 if (original != null) { 245 hasChanges = cloneInto(item, original, null, true, list()); 246 item = original; 247 } else { 248 registerItem(item); 249 hasChanges = true; 250 } 251 getController().onPostLoadItem(item); 252 registerKeys(item); 253 return hasChanges; 254 } 255 256 /** 257 * This gets called before persist, to avoid a race-condition whereby: 258 * 259 * 1) Thread A saves to the database 260 * 2) Thread B syncs from the database and loads the new object 261 * 3) Thread A finishes the database save, and then adds the newly saved item to the registry 262 * 4) Now we have two copies of the object in memory 263 * 264 * 265 * 266 * @param item 267 */ 268 protected void preRegisterItem(T item) { 269 itemByPrimaryKey.put(item.getId(), item); 270 } 271 272 protected void registerItem(T item) { 273 itemByPrimaryKey.put(item.getId(), item); 274 items.add(item); 275 } 276 277 public void registerKeys(T item) { 278 for (String uniqueKey : keyNameToUniqueKeyToValue.keySet()) { 279 Object val = PropertyUtils.getPropertyOrMappedValue(item, uniqueKey); 280 if (val != null) { 281 this.keyNameToUniqueKeyToValue.get(uniqueKey).put(val, item); 282 } 283 } 284 } 285 286 287 288 289 @Override 290 public List<T> getItems() { 291 throw new UsageException("This PartialStash does not have all items in memory."); 292 } 293 294 295 @Override 296 public void reset() { 297 items = new ArrayList<>(); 298 itemByPrimaryKey = new HashMap<Long, T>(); 299 keyNameToUniqueKeyToValue = new Hashtable<String, Map<Object, T>>(); 300 initialize(); 301 loadAll(); 302 FilterCache.clearBucket(getBucket()); 303 } 304 305 306 307 @Override 308 public void onPreRead() { 309 getPersister().onPreRead(); 310 } 311 312 313 @Override 314 public T forId(Long id) { 315 T item = this.itemByPrimaryKey.get(id); 316 if (item != null) { 317 onPreRead(); 318 return detach(item); 319 } else { 320 item = filterChain().filter("id", id).first(); 321 if (item != null) { 322 loadItem(item); 323 } 324 if (item == null) { 325 return null; 326 } 327 return detach(item); 328 } 329 } 330 331 @Override 332 public T originalForId(Long id) { 333 T item = this.itemByPrimaryKey.get(id); 334 if (item != null) { 335 onPreRead(); 336 return item; 337 } else { 338 return getPersister().fetchOne(id); 339 } 340 } 341 342 @Override 343 public T forUniqueKey(String keyName, Object lookupValue) { 344 Map<Object, T> map = this.keyNameToUniqueKeyToValue.getOrDefault(keyName, null); 345 if (map == null) { 346 throw new ConfigException("There is no unique key '" + keyName + "' defined for bucket '" + getBucket() + "'"); 347 } 348 T value = map.get(lookupValue); 349 if (value != null) { 350 onPreRead(); 351 return detach(value); 352 } else { 353 return filterChain().filter(keyName, value).first(); 354 } 355 } 356 357 358 @Override 359 public List<T> listForKey(String keyName, Object value) { 360 return filterChain().filter(keyName, value).all(); 361 } 362 363 @Override 364 public int countForKey(String keyName, Object value) { 365 return filterChain().filter(keyName, value).count(); 366 } 367 368 @Override 369 public FilterChain<T> filterChain() { 370 return getPersister().filterChain(); 371 } 372 373 @Override 374 public FilterChain<T> filterByKey(String key, Object lookupValue) { 375 return filterChain().filter(key, lookupValue); 376 } 377 378 @Override 379 public FilterChain<T> filterChain(List<T> subset) { 380 return new FilterChain<T>(getBucket(), subset, null); 381 } 382 383 384}