20-BaseEntity:333行的ActiveRecord基类
Row是Map行,BaseEntity是类型化实体——同一个脏标记协议的两种载体。save()一个方法按_t分发insert/update/delete到MyBatis Mapper、反射字段读写、自动类型转换、fromMap/toMap协议序列化。这篇拆完333行,重点讲它和Row的协议同构与载体差异。
文章目录
- 20-BaseEntity:333行的ActiveRecord基类
- 一、协议字段从Map变成注解字段
- 二、save():18行的ActiveRecord
- 三、setItemValue:反射版脏标记
- 四、findField/getAllFields:沿继承链爬
- 五、fromMap/toMap:协议的序列化边界
- 六、BaseEntity vs Row:一张对照表
源码:
browise-data/src/main/java/com/browise/data/BaseEntity.java(333行)
一、协议字段从Map变成注解字段
publicabstractclassBaseEntity{@TableField(exist=false)// MyBatis-Plus:这不是数据库列protectedRowStatus_t=RowStatus.NONE;@TableField(exist=false)protectedfinalMap<String,Object>_o=newLinkedHashMap<>();@TableField(exist=false)protectedStringinsertMethod="insert";@TableField(exist=false)protectedStringupdateMethod="updateById";@TableField(exist=false)protectedStringdeleteMethod="deleteById";@TableField(exist=false)protectedLongbaz001;// 社保标准主键列名}三个设计点:
@TableField(exist=false)——_t/_o/方法名/主键都不是数据库列,必须告诉MyBatis-Plus别把它们拼进SQL。协议字段寄生在实体里又不污染持久化——注解是隔离层。
三个method名可覆盖——默认insert/updateById/deleteById是MyBatis-Plus BaseMapper的标准方法名;实体有自定义SQL时setUpdateMethod(“updateLabel”)换名字(Aa10字典实体就这么干——它没有@TableId,MP的updateById不可用,换成自定义@Update SQL)。
getMapperClass()是唯一的抽象方法——子类告诉基类"我的Mapper是谁":
@TableName("SYS_USER")publicclassSysUserextendsBaseEntity{privateStringpsnName;// getter/setter...@OverridepublicClass<?>getMapperClass(){returnSysUserMapper.class;}}二、save():18行的ActiveRecord
publicvoidsave(){MyBatisHelperhelper=BrowiseContext.getHelper();// ①静态上下文取helperif(helper==null)thrownewRuntimeException("MyBatisHelper 未注入");Class<?>mapperClass=getMapperClass();if(mapperClass==null)thrownewRuntimeException("getMapperClass() 返回 null");if(isInsert())helper.insert(mapperClass,insertMethod,this);// ②按_t分发elseif(isUpdate())helper.update(mapperClass,updateMethod,this);elseif(isDelete())helper.delete(mapperClass,deleteMethod,this);resetUpdate();// ③成功后归零}这就是ActiveRecord的全部——user.save()一行完成三种持久化,按_t自动选路。对比传统写法:
// 传统——调用方判断状态、选Mapper方法if(user.isNew())userMapper.insert(user);elseif(user.isDirty())userMapper.updateById(user);状态判断的知识被吸收进基类——调用方只说"保存它",不说"怎么保存"。
注意③resetUpdate()无条件执行——save()内部没有事务。事务在调用方(Spring @Transactional的Service方法)。如果外层事务最终回滚,实体的_t已经归零但数据库没变——内存和数据库短暂失配。约定:事务边界内不要再读实体状态做判断,以数据库为准。
三、setItemValue:反射版脏标记
publicvoidsetItemValue(Stringfield,Objectvalue){Fieldf=findField(field);if(f==null)return;f.setAccessible(true);if(_t==RowStatus.NONE){Objectold=f.get(this);if(old!=null&&!old.equals(value)){_o.put(field,old);// 旧值非null且真的变了才记}_t=RowStatus.UPDATE;}Objectconverted=convertType(f.getType(),value);// 自动类型转换f.set(this,converted);}和Row.setItemValue的两处差异:
①旧值记录多一个条件——Row版只要data.containsKey(key)就记;BaseEntity版要求old != null && !old.equals(value)。字段原值是null的不记进_o——reject恢复时_o里没有的字段保持data值,null字段恢复后还是null(原值就是null),跳过记录省内存。值没变(equals)的也不记——没变化的赋值不算脏,避免"set了同值导致行莫名变UPDATE"。
②convertType自动转换——HTTP来的值全是字符串,实体字段是Integer/Date:
privateObjectconvertType(Class<?>type,Objectvalue){if(type==Integer.class||type==int.class)returnInteger.parseInt(s);if(type==Boolean.class){if("1".equals(s)||"true".equalsIgnoreCase(s))returntrue;// 政务码值习惯if("0".equals(s)||"false".equalsIgnoreCase(s))returnfalse;}if(type==Date.class&&valueinstanceofLong)returnnewDate((Long)value);// ...}“1”/"0"当boolean——政务系统数据库里状态列就是VARCHAR存的0和1,这层转换是业务习惯的适配。
异常处理是catch (Exception ignored) {}——反射失败/类型转换失败静默吞掉。这个取舍有争议:吞异常保证批量fromMap不会因一个坏字段崩,但坏字段静默丢值难排查。实测的折中——开发期看日志(convertType失败前有debug日志),生产期保批量导入不中断。
四、findField/getAllFields:沿继承链爬
privateFieldfindField(Stringname){Class<?>cls=this.getClass();while(cls!=null&&cls!=BaseEntity.class){// 到BaseEntity为止try{returncls.getDeclaredField(name);}catch(NoSuchFieldExceptione){cls=cls.getSuperclass();}}returnnull;}停在中断点BaseEntity.class——子类可以有中间父类(Person→SysUser),反射沿链向上找到BaseEntity为止。协议字段(_t/_o)不在搜索范围——它们属于BaseEntity,getItemValue(“_t”)返回null——协议字段只能通过getRowStatus()/getOriginals()读,不混入业务字段通道。
getAllFields同构——收集全部非静态字段(同样不含BaseEntity的),供toMap序列化。
五、fromMap/toMap:协议的序列化边界
publicvoidfromMap(Map<String,Object>map){for(Map.Entry<String,Object>entry:map.entrySet()){Stringkey=entry.getKey();if("_t".equals(key)){// 协议字段特殊处理_t=RowStatus.fromCode(((Number)val).intValue());continue;}if("_o".equals(key)){_o.putAll((Map)val);continue;}setItemValue(key,val);// 业务字段走反射+类型转换+脏标记}}fromMap是HTTP JSON→实体的入口——前端提交的{_t:3, psnName:"李四", _o:{...}}反序列化:_t/_o按协议还原,业务字段经setItemValue(自动类型转换在这里发生)。
一个隐蔽点:fromMap走setItemValue会触发脏标记——如果实体的_t先被设为UPDATE再fromMap,字段赋值不会二次记_o(状态已是UPDATE,第17篇状态机);如果_t最后才设——前面的字段赋值会触发NONE→UPDATE转并记_o!key的遍历顺序影响_o内容(LinkedHashMap保序,JSON解析器按出现序)。约定:协议JSON里_t放最前面——前端useCenter的toPayload保证这一点。
六、BaseEntity vs Row:一张对照表
| Row | BaseEntity | |
|---|---|---|
| 字段载体 | LinkedHashMap | 类型化Java字段 |
| 类型转换 | 无(全是Object) | convertType自动转 |
| _o记录条件 | containsKey即记 | 非null且值变了才记 |
| 持久化 | 无(靠commonSave) | save()直接调Mapper |
| 适用 | 元数据通道/动态表 | 低代码模式/固定实体 |
| 旧值null | 记录null | 不记录 |
两套载体一个协议——前端永远只发_t/_o,后端按场景选载体:动态查询(EA引擎)落Map走Row,固定业务实体继承BaseEntity。转换器是fromMap/toMap——Row.getMap()灌进BaseEntity.fromMap(),协议无损过桥。
✅ 亮点:333行ActiveRecord基类拆成协议字段寄生(@TableField exist=false)、save()18行按_t分发、反射版脏标记与Row版的两处差异(null不记/同值不算脏)、convertType的政务0/1布尔适配、fromMap的key顺序影响_o的隐蔽约定。适合做实体基类设计的人。扩展方向:第24篇MyBatisHelper、第30篇CryptoAspect对BaseEntity的加解密拦截、第53篇审计监听。