/* * Copyright 2018 Jobsz ([email protected]) * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.jfinal.ext.plugin.activerecord; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.google.common.collect.Lists; import com.jfinal.ext.kit.SqlpKit; import com.jfinal.ext.plugin.redis.ModelRedisMapping; import com.jfinal.kit.HashKit; import com.jfinal.kit.StrKit; import com.jfinal.plugin.activerecord.Db; import com.jfinal.plugin.activerecord.Model; import com.jfinal.plugin.activerecord.Record; import com.jfinal.plugin.activerecord.SqlPara; import com.jfinal.plugin.activerecord.Table; import com.jfinal.plugin.redis.Cache; import com.jfinal.plugin.redis.Redis; public abstract class ModelExt<M extends ModelExt<M>> extends Model<M> { private static final long serialVersionUID = -6061985137398460903L; private static final String RECORDS = "records:"; //default sync to redis private boolean syncToRedis = GlobalSyncRedis.syncState(); private String cacheName = null; //call back private List<CallbackListener> callbackListeners = Lists.newArrayList(); /** * add Call back listener * @param callbackListener */ public void addCallbackListener(CallbackListener callbackListener) { this.callbackListeners.add(callbackListener); } /** * redis key: records:tablename: id1 | id2 | id3... * eg: the user has three primarykeys id1=1,id2=2,id3=3, so the redisKey is 'records:user:1|2|3'. * @return redis key */ private String redisKey(ModelExt<?> m) { Table table = m.table(); StringBuilder key = new StringBuilder(); key.append(RECORDS); key.append(table.getName()); key.append(":"); //fetch primary keys' values String[] primaryKeys = table.getPrimaryKey(); //format key for (int idx = 0; idx < primaryKeys.length; idx++) { Object primaryKeyVal = m.get(primaryKeys[idx]); if (null != primaryKeyVal) { if (idx > 0) { key.append("|"); } key.append(primaryKeyVal); } } return key.toString(); } /** * redis key for attrs' values * @param flag : ids or id store * @return data[s]:md5(concat(columns' value)) */ private String redisColumnKey(SqlpKit.FLAG flag) { StringBuilder key = new StringBuilder(this._getUsefulClass().toGenericString()); String[] attrs = this._getAttrNames(); Object val; for (String attr : attrs) { val = this.get(attr); if (null == val) { continue; } key.append(val.toString()); } key = new StringBuilder(HashKit.md5(key.toString())); if (flag.equals(SqlpKit.FLAG.ONE)) { return "data:"+key; } return "datas:"+key; } protected void saveToRedis() { //save total data: generic save this.redis().set(this.redisKey(this), this.attrsCp()); } private Cache redis() { Cache redis = null; if (StrKit.notBlank(this.cacheName)) { redis = GlobalSyncRedis.getSyncCache(this.cacheName); } if (null != redis) { return redis; } if (StrKit.isBlank(this.cacheName)) { this.cacheName = ModelRedisMapping.me().getCacheName(this.tableName()); } if (StrKit.notBlank(this.cacheName)) { redis = Redis.use(this.cacheName); } else { redis = Redis.use(); } if (null == redis) { throw new IllegalArgumentException(String.format("The Cache with the name '%s' was Not Found.", this.cacheName)); } //sync to memory. GlobalSyncRedis.setSyncCache(this.cacheName, redis); return redis; } /** * Use the columns that must contains primary keys fetch Data from db, and use the fetched primary keys fetch from redis. * @param columns */ private List<M> fetchDatasFromRedis(String... columns) { // redis key String key = this.redisColumnKey(SqlpKit.FLAG.ALL); // fetch from redis List<M> fetchDatas = this.redis().get(key); if (null == fetchDatas) { // fetch ids from db. fetchDatas = this.find(SqlpKit.select(this, this.primaryKeys())); } if (null == fetchDatas || fetchDatas.size() == 0) { return fetchDatas; } //put ids to redis this.redis().setex(key, GlobalSyncRedis.syncExpire(), fetchDatas); for (M m : fetchDatas) { // fetch data from redis if (null == m) { continue; } m = this.fetchOne(m, columns); } return fetchDatas; } private M fetchOneFromRedis(String... columns) { Object pk = this.primaryKeyValue(); if (null != pk) { return this.fetchOne(this, columns); } // redis key String key = this.redisColumnKey(SqlpKit.FLAG.ONE); // fetch from redis M m = this.redis().get(key); if (null == m) { // fetch id from db. m = this.findFirst(SqlpKit.selectOne(this, this.primaryKeys())); } if (null == m) { return m; } //put id to redis this.redis().setex(key, GlobalSyncRedis.syncExpire(), m); return this.fetchOne(m, columns); } @SuppressWarnings("unchecked") private M fetchOne(ModelExt<?> m, String... columns) { // use primay key fetch from redis Map<String, Object> attrs = this.redis().get(this.redisKey(m)); if (null != attrs) { if (null == columns || columns.length == 0) { return (M)m.put(attrs); } return (M)m.put(attrs).keep(columns); } // fetch from db M tmp = this.findFirst(SqlpKit.selectOne(m)); // save to redis if (null != tmp) { if (null == columns || columns.length == 0) { m.put(tmp._getAttrs()); } else { m.put(tmp._getAttrs()).keep(columns); } tmp.saveToRedis(); } return (M)m; } /** * Get attr type * @param attr * @return attr type class */ public Class<?> attrType(String attr) { Table table = this.table(); if (null != table) { return table.getColumnType(attr); } Method[] methods = this._getUsefulClass().getMethods(); String methodName; for (Method method : methods) { methodName = method.getName(); if (methodName.startsWith("set") && methodName.substring(3).toLowerCase().equals(attr)) { return method.getParameterTypes()[0]; } } return null; } /** * Get attr names */ public List<String> attrNames() { String[] names = this._getAttrNames(); if (null != names && names.length != 0) { return Arrays.asList(names); } Method[] methods = this._getUsefulClass().getMethods(); String methodName; List<String> attrs = new ArrayList<String>(); for (Method method : methods) { methodName = method.getName(); if (methodName.startsWith("set") ) { String attr = methodName.substring(3).toLowerCase(); if (StrKit.notBlank(attr)) { attrs.add(attr); } } } return attrs; } /** * current instance is Null or not. */ public boolean _isNull() { return this._getAttrs().isEmpty(); } /** * Model attr copy version , the model just use default "DbKit.brokenConfig" */ public Map<Object, Object> attrsCp() { Map<Object, Object> attrs = new HashMap<Object, Object>(); String[] attrNames = this._getAttrNames(); for (String attr : attrNames) { attrs.put(attr, this.get(attr)); } return attrs; } /** * Model Attrs */ public Map<String, Object> attrs() { return this._getAttrs(); } /** * auto sync to the redis: true-sync,false-cancel, default true * @param syncToRedis */ public void syncToRedis(boolean syncToRedis) { this.syncToRedis = syncToRedis; } /** * shot cache's name. * if current cacheName != the old cacheName, will reset old cache, update cache use the current cacheName and open syncToRedis. */ public void shotCacheName(String cacheName) { //reset cache if (StrKit.notBlank(cacheName) && !cacheName.equals(this.cacheName)) { GlobalSyncRedis.removeSyncCache(this.cacheName); } this.cacheName = cacheName; //auto open sync to redis this.syncToRedis = true; //update model redis mapping ModelRedisMapping.me().put(this.tableName(), this.cacheName); } /** * get current model's table */ public Table table() { return this._getTable(); } /** * get current model's tablename */ public String tableName() { return this.table().getName(); } /** * All primary keys */ public String[] primaryKeys() { return this.table().getPrimaryKey(); } /** * get numeric primary key. * if there is not found the primary key will throw the IllegalArgumentException. */ public String primaryKey() { String[] primaryKeys = this.primaryKeys(); if (primaryKeys.length >= 1) { return primaryKeys[0]; } throw new IllegalArgumentException("Not Found ,The Primary Key[s]."); } /** * get primary key value * if there is not found the primary key will throw the IllegalArgumentException. */ public Object primaryKeyValue() { return this.get(this.primaryKey()); } /** * save to db, if `syncToRedis` is true , this model will save to redis in the same time. */ @Override public boolean save() { for (CallbackListener callbackListener : this.callbackListeners) { callbackListener.beforeSave(this); } boolean ret = super.save(); if (this.syncToRedis && ret) { this.saveToRedis(); } for (CallbackListener callbackListener : this.callbackListeners) { callbackListener.afterSave(this); } return ret; } /** * delete from db, if `syncToRedis` is true , this model will delete from redis in the same time. */ @Override public boolean delete() { for (CallbackListener callbackListener : this.callbackListeners) { callbackListener.beforeDelete(this); } boolean ret = super.delete(); if (this.syncToRedis && ret) { //delete from Redis this.redis().del(this.redisKey(this)); } for (CallbackListener callbackListener : this.callbackListeners) { callbackListener.afterDelete(this); } return ret; } /** * update db, if `syncToRedis` is true , this model will update redis in the same time. */ @Override public boolean update() { for (CallbackListener callbackListener : this.callbackListeners) { callbackListener.beforeUpdate(this); } boolean ret = super.update(); if (this.syncToRedis && ret) { this.saveToRedis(); } for (CallbackListener callbackListener : this.callbackListeners) { callbackListener.afterUpdate(this); } return ret; } /** * <b>Advanced Function</b>. * List All data. <br/> * 1. You can use any column value fetch Models;<br/> * 2. The fetched Models contains all columns, <br/> * 3. if `syncToRedis` is true, the data from Redis else from DB.<br/> */ public List<M> fetch() { //from db if (!this.syncToRedis) { return this.find(SqlpKit.select(this)); } //from redis return this.fetchDatasFromRedis(); } /** * <b>Advanced Function</b>. * List All data. <br/> * 1. The data from DB just contains `columns` and primary keys; <br/> * 2. The data from Redis will contain all columns. <br/> * 3. Use any column value can do this. <br/> * @param columns: will fetch columns */ public List<M> fetch(String... columns) { if (null == columns) { return this.fetch(); } //from db if (!this.syncToRedis) { return this.find(SqlpKit.select(this, columns)); } //from redis return this.fetchDatasFromRedis(columns); } /** * <b>Advanced Function</b>. * you can fetch FirstOne Model: use any column value can do this.the data from DB. */ public M fetchOne() { //from db if (!this.syncToRedis) { return this.findFirst(SqlpKit.selectOne(this)); } //from redis return this.fetchOneFromRedis(); } /** * <b>Advanced Function</b>. * you can fetch FirstOne Model:just contains columns, use any column value can do this.the data from DB. */ public M fetchOne(String... columns) { if (null == columns) { return this.fetchOne(); } //from db if (!this.syncToRedis) { return this.findFirst(SqlpKit.selectOne(this, columns)); } //from redis return this.fetchOneFromRedis(columns); } /** * List All data just contains the primary keys. */ public List<M> fetchPrimaryKeysOnly() { return this.find(SqlpKit.select(this, this.primaryKeys())); } /** * Data Count */ public Long dataCount() { SqlPara sql = SqlpKit.select(this, "count(*) AS cnt"); Record record = Db.findFirst(sql); if (null != record) { return record.get("cnt"); } return 0L; } @SuppressWarnings("unchecked") private M cp(boolean cpAttrs) { M m = null; try { m = (M) this._getUsefulClass().newInstance(); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } if (cpAttrs) { m.put(this._getAttrs()); } return m; } /** * Clone new instance[wrapper clone] */ public M cp() { return this.cp(true); } /** * Clone new instance[wrapper clone], just link attrs values * @param attrs */ public M cp(String... attrs) { M m = this.cp(false); for (String attr : attrs) { m.set(attr, this.get(attr)); } return m; } /** * check current instance is equal obj or not.[wrapper equal] * @param obj * @return true:equal, false:not equal. */ public boolean eq(Object obj) { if (this == obj) return true; if (obj == null) return false; if (this._getUsefulClass() != obj.getClass()) return false; Model<?> other = (Model<?>) obj; Table tableinfo = this.table(); Set<Entry<String, Object>> attrsEntrySet = this._getAttrsEntrySet(); for (Entry<String, Object> entry : attrsEntrySet) { String key = entry.getKey(); Object value = entry.getValue(); Class<?> clazz = tableinfo.getColumnType(key); if (clazz == Float.class) { } else if (clazz == Double.class) { } else if (clazz == Model.class) { } else { if (value == null) { if (other.get(key) != null) return false; } else if (!value.equals(other.get(key))) return false; } } return true; } /** * wrapper hash code */ public int hcode() { final int prime = 31; int result = 1; Table table = this.table(); Set<Entry<String, Object>> attrsEntrySet = this._getAttrsEntrySet(); for (Entry<String, Object> entry : attrsEntrySet) { String key = entry.getKey(); Object value = entry.getValue(); Class<?> clazz = table.getColumnType(key); if (clazz == Integer.class) { result = prime * result + (Integer) value; } else if (clazz == Short.class) { result = prime * result + (Short) value; } else if (clazz == Long.class) { result = prime * result + (int) ((Long) value ^ ((Long) value >>> 32)); } else if (clazz == Float.class) { result = prime * result + Float.floatToIntBits((Float) value); } else if (clazz == Double.class) { long temp = Double.doubleToLongBits((Double) value); result = prime * result + (int) (temp ^ (temp >>> 32)); } else if (clazz == Boolean.class) { result = prime * result + ((Boolean) value ? 1231 : 1237); } else if (clazz == Model.class) { result = this.hcode(); } else { result = prime * result + ((value == null) ? 0 : value.hashCode()); } } return result; } /** * Convert to Record */ public Record convertToRecord() { Record r = new Record(); r.setColumns(this._getAttrs()); return r; } }