20-BaseEntity基类

20-BaseEntity基类 20-BaseEntity333行的ActiveRecord基类Row是Map行BaseEntity是类型化实体——同一个脏标记协议的两种载体。save()一个方法按_t分发insert/update/delete到MyBatis Mapper、反射字段读写、自动类型转换、fromMap/toMap协议序列化。这篇拆完333行重点讲它和Row的协议同构与载体差异。文章目录20-BaseEntity333行的ActiveRecord基类一、协议字段从Map变成注解字段二、save()18行的ActiveRecord三、setItemValue反射版脏标记四、findField/getAllFields沿继承链爬五、fromMap/toMap协议的序列化边界六、BaseEntity vs Row一张对照表源码browise-data/src/main/java/com/browise/data/BaseEntity.java333行一、协议字段从Map变成注解字段publicabstractclassBaseEntity{TableField(existfalse)// MyBatis-Plus这不是数据库列protectedRowStatus_tRowStatus.NONE;TableField(existfalse)protectedfinalMapString,Object_onewLinkedHashMap();TableField(existfalse)protectedStringinsertMethodinsert;TableField(existfalse)protectedStringupdateMethodupdateById;TableField(existfalse)protectedStringdeleteMethoddeleteById;TableField(existfalse)protectedLongbaz001;// 社保标准主键列名}三个设计点TableField(existfalse)——_t/_o/方法名/主键都不是数据库列必须告诉MyBatis-Plus别把它们拼进SQL。协议字段寄生在实体里又不污染持久化——注解是隔离层。三个method名可覆盖——默认insert/updateById/deleteById是MyBatis-Plus BaseMapper的标准方法名实体有自定义SQL时setUpdateMethod(“updateLabel”)换名字Aa10字典实体就这么干——它没有TableIdMP的updateById不可用换成自定义Update SQL。getMapperClass()是唯一的抽象方法——子类告诉基类我的Mapper是谁TableName(SYS_USER)publicclassSysUserextendsBaseEntity{privateStringpsnName;// getter/setter...OverridepublicClass?getMapperClass(){returnSysUserMapper.class;}}二、save()18行的ActiveRecordpublicvoidsave(){MyBatisHelperhelperBrowiseContext.getHelper();// ①静态上下文取helperif(helpernull)thrownewRuntimeException(MyBatisHelper 未注入);Class?mapperClassgetMapperClass();if(mapperClassnull)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){FieldffindField(field);if(fnull)return;f.setAccessible(true);if(_tRowStatus.NONE){Objectoldf.get(this);if(old!null!old.equals(value)){_o.put(field,old);// 旧值非null且真的变了才记}_tRowStatus.UPDATE;}ObjectconvertedconvertType(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/DateprivateObjectconvertType(Class?type,Objectvalue){if(typeInteger.class||typeint.class)returnInteger.parseInt(s);if(typeBoolean.class){if(1.equals(s)||true.equalsIgnoreCase(s))returntrue;// 政务码值习惯if(0.equals(s)||false.equalsIgnoreCase(s))returnfalse;}if(typeDate.classvalueinstanceofLong)returnnewDate((Long)value);// ...}“1”/0当boolean——政务系统数据库里状态列就是VARCHAR存的0和1这层转换是业务习惯的适配。异常处理是catch (Exception ignored) {}——反射失败/类型转换失败静默吞掉。这个取舍有争议吞异常保证批量fromMap不会因一个坏字段崩但坏字段静默丢值难排查。实测的折中——开发期看日志convertType失败前有debug日志生产期保批量导入不中断。四、findField/getAllFields沿继承链爬privateFieldfindField(Stringname){Class?clsthis.getClass();while(cls!nullcls!BaseEntity.class){// 到BaseEntity为止try{returncls.getDeclaredField(name);}catch(NoSuchFieldExceptione){clscls.getSuperclass();}}returnnull;}停在中断点BaseEntity.class——子类可以有中间父类Person→SysUser反射沿链向上找到BaseEntity为止。协议字段(_t/_o)不在搜索范围——它们属于BaseEntitygetItemValue(“_t”)返回null——协议字段只能通过getRowStatus()/getOriginals()读不混入业务字段通道。getAllFields同构——收集全部非静态字段同样不含BaseEntity的供toMap序列化。五、fromMap/toMap协议的序列化边界publicvoidfromMap(MapString,Objectmap){for(Map.EntryString,Objectentry:map.entrySet()){Stringkeyentry.getKey();if(_t.equals(key)){// 协议字段特殊处理_tRowStatus.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转并记_okey的遍历顺序影响_o内容LinkedHashMap保序JSON解析器按出现序。约定协议JSON里_t放最前面——前端useCenter的toPayload保证这一点。六、BaseEntity vs Row一张对照表RowBaseEntity字段载体LinkedHashMap类型化Java字段类型转换无全是ObjectconvertType自动转_o记录条件containsKey即记非null且值变了才记持久化无靠commonSavesave()直接调Mapper适用元数据通道/动态表低代码模式/固定实体旧值null记录null不记录两套载体一个协议——前端永远只发_t/_o后端按场景选载体动态查询EA引擎落Map走Row固定业务实体继承BaseEntity。转换器是fromMap/toMap——Row.getMap()灌进BaseEntity.fromMap()协议无损过桥。✅ 亮点333行ActiveRecord基类拆成协议字段寄生TableField existfalse、save()18行按_t分发、反射版脏标记与Row版的两处差异null不记/同值不算脏、convertType的政务0/1布尔适配、fromMap的key顺序影响_o的隐蔽约定。适合做实体基类设计的人。扩展方向第24篇MyBatisHelper、第30篇CryptoAspect对BaseEntity的加解密拦截、第53篇审计监听。