ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Easy-OPM:一个轻量级纯JDBC的ORM框架,一行代码搞定CRUD

Easy-OPM:一个轻量级纯JDBC的ORM框架,一行代码搞定CRUD 简介面向Java开发者的轻量级ORM框架Easy-OPM压缩包定位中小型项目在数据持久化时不想引入Hibernate/MyBatis等重量级框架、又不愿手写大量SQL的场景。资源共5个文件压缩包仅4KB包含两个Java源文件、一个Maven配置xml、一个README文档及gitignore文件属于精简的入门示例或骨架代码已有329人学习下载。该框架核心亮点在于简单API、注解自动映射、基础CRUD、内置事务管理和查询构建器适合具备Java基础、熟悉注解与数据库操作并希望理解ORM底层实现机制的开发者。通过阅读源码与示例可掌握实体类到数据库表的映射思路、主键生成策略及轻量级事务处理方式结合README与Maven配置能快速在本地搭建运行环境作为后续扩展或迁移到正式项目的起点。整体来说这是一份足够轻巧、可直接对照学习的资源适合想用最小成本评估和上手Easy-OPM的人。1. 为什么还要写一个ORM框架先聊聊我为什么要做Easy-OPM干了这么多年Java开发ORM框架用过不少。MyBatis、MyBatis-Plus、Spring Data JPA、Hibernate各有各的优缺点。用得越多越觉得很多项目其实根本不需要那么重的ORM框架。就拿MyBatis来说写个简单的单表增删改查要配Mapper接口、写XML映射文件、处理结果集映射一套流程走下来光样板代码就占了半屏。JPA倒是省事但学起来成本高而且它的对象关系映射机制有时候会给你搞出意料之外的SQL排查起来特别痛苦。所以我自己写了一个叫Easy-OPM的轻量级ORM框架。OPM全称是Object-Property-Mapping也就是基于对象属性到数据库字段的映射。思路很简单不用XML配置不用注解满天飞核心就做一件事把Java对象和数据库表之间的映射关系用最直接的方式搞定。一条查询写一行代码一条插入写一行代码不需要任何Mapper XML文件。为什么要叫“轻量级”因为它的设计目标是启动快、依赖少、使用简单。整个框架核心代码压缩下来也就几千行没有任何第三方依赖纯JDBC实现。你把它丢到一个只有JDK 8及以上环境的老项目里也能直接用不做任何侵入。适合谁来用中小型项目的快速开发工具类项目、内部管理系统、教学演练项目以及那些需要在嵌入式场景里用数据库的应用。它不适合那种几百张表、复杂关联查询满天飞的超大型企业级应用——那种场景你还是老老实实用MyBatis或者JPA吧。2. 整体设计思路为什么选择“约定优于配置”这条路2.1 核心设计哲学少即是多我见过太多框架死在复杂配置上。MyBatis刚出来的时候很多人说它简单但真正用起来你的项目里会堆积一堆XML文件。一旦项目复杂起来这些XML就是开发效率的杀手改个SQL都要翻半天文件。Easy-OPM的核心理念是“约定优于配置”。什么意思就是我给你一套默认规则只要你遵守这套规则你几乎不用写任何配置就能跑起来。比如类名User对应表user属性userName对应字段user_name这是默认约定。你要是非要用不规范的命名映射也可以通过注解来覆盖默认行为。这套设计思路在实践中有几个明显的好处。第一上手成本极低新手看一遍文档就能写crud不需要背一大堆概念。第二项目里不需要维护一堆配置文件代码即配置看着清爽。第三出了问题排查路径短逻辑就摆在那儿一眼能看穿。2.2 技术选型为什么坚持用纯JDBC市面上很多ORM框架都是基于反射加动态代理做的我也没免俗但我的技术栈很简单JDBC加反射加动态代理三个东西就够了。不引入Spring不引入连接池不引入任何缓存组件。为什么因为一旦引入这些重量级依赖框架的通用性就大打折扣。如果一个项目本身没用Spring你的框架非要强制依赖Spring那这个框架基本就废了。Easy-OPM对连接的管理是通过DataSource接口实现你传入一个实现了javax.sql.DataSource的对象就能跑不管你底层是Druid、HikariCP、DBCP还是自己写的连接池实现。至于动态代理我只用在了DAO接口的自动实现上。在Easy-OPM里你只需要定义一个接口继承BaseMapper框架会在运行时自动生成这个接口的实现类你连实现类都不用写。这里用到的就是JDK动态代理接口方法调用会被拦截然后根据方法名加上参数自动生成对应的SQL。熟悉Spring Data JPA的应该对这个模式很熟悉但Easy-OPM的方法名解析规则要简单得多不玩那些复杂的衍生查询语法。3. 快速上手从零开始写一个Easy-OPM应用3.1 Maven坐标引入和基础配置先看怎么引入依赖。我把Easy-OPM发布到了Maven中央仓库你在pom.xml里加上坐标就行dependency groupIdio.github.easyopm/groupId artifactIdeasy-opm-core/artifactId version1.0.0/version /dependency这个坐标对新手可能会踩一个坑——groupId和artifactId一定不能拼错。我之前见过一个朋友把artifactId写成了easy-orm结果拉下来一个完全不相干的库报了一堆ClassNotFoundException。这里要说明一下版本号我用的是1.0.0你可以去中央仓库搜一下最新的版本号以官方仓库为准。3.2 实体类定义和数据源配置定义一个实体类用一个用户表来做例子Data Table(sys_user) public class User { Id private Long id; private String userName; private String password; private Integer age; private String email; Column(create_time) private LocalDateTime createTime; }这个类不强制继承任何父类只是一个普通的POJO。Table注解用来指定表名不写的话默认会用类名转下划线User变成user。Id标注主键目前框架支持自增主键和自定义主键两种模式。Column注解用来指定列名如果属性名和字段名正好符合驼峰转下划线的规则不写也行。接下来配置数据源这里以HikariCP为例HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/test); config.setUsername(root); config.setPassword(123456); config.setDriverClassName(com.mysql.cj.jdbc.Driver); DataSource dataSource new HikariDataSource(config); DatabaseConfig dbConfig new DatabaseConfig(dataSource); OpmFactory factory new OpmFactory(dbConfig);这行代码要手动指定driverClassName不然HikariCP有时会根据URL自动推断推断失败就报找不到驱动类。如果你是MySQL 8.x驱动类名是com.mysql.cj.jdbc.Driver注意中间多了一个cjMySQL 5.x则用com.mysql.jdbc.Driver。这个差异点我见过太多人搞混了新手尤其容易栽在这里。3.3 第一个增删改查一行代码完成有了实体和工厂后接下来的操作就非常简单了。BaseMapperUser userMapper factory.getMapper(User.class); // 插入一条记录 User user new User(); user.setUserName(张三); user.setPassword(123456); user.setAge(25); user.setEmail(zhangsanexample.com); userMapper.insert(user); // 根据ID查询 User fromDb userMapper.selectById(1L); // 更新记录 fromDb.setAge(26); userMapper.update(fromDb); // 删除 userMapper.deleteById(1L);没错就是这么简单。没有写一行SQL没有建一个Mapper XML。所有的CRUD操作全部由框架自动生成SQL并执行。这里需要重点提一下update方法的语义。现在很多ORM框架都有这个问题update传一个只有部分字段有值的对象时到底该不该覆盖其他字段Easy-OPM的做法是update操作默认只更新非null字段。什么意思就是说如果你只set了age属性其他属性为null那么底层SQL就只有“update user set age ? where id ?”这一句其他字段不会被改动。这个设计是我在项目实践中踩了坑才定的方案。早年用MyBatis的时候总有人不小心把对象里没赋值的字段传进去然后数据库里那一整行数据被覆盖成null到时候就等着挨骂吧。4. 核心实现细节条件构造器与动态SQL4.1 条件构造器的设计思路一个ORM框架光有简单的按ID操作是撑不起实际业务的。实际业务里最常见的就是各种条件查询按用户名查、按年龄范围查、按创建时间排序等。很多ORM框架推出了流式API的条件构造器Easy-OPM也做了这个功能但我的实现更简单直接。直接看代码效果ListUser users userMapper.selectList( new QueryWrapperUser() .eq(age, 25) .like(user_name, 张) .orderByDesc(id) .limit(10) );eq就是等于like就是模糊查询orderByDesc就是倒序排序limit是分页。全部用字符串指定字段名不搞类型安全那一套。为什么不搞Lambda表达式那种类型安全的方式因为字符串方式实现起来简单报错信息直观适合轻量级场景。Lambda方式虽然能避免字段名拼写出错但生成的字节码在低版本Android环境和部分Java 8以下环境兼容性不好。既然定位是轻量级就不为了追求完美而牺牲兼容性。4.2 分页查询手写limit还是物理分页再来说分页这是很多轻量级框架处理得最糊弄的地方。有些框架直接用Java层面做内存分页数据量一大就内存爆掉。Easy-OPM从设计之初就确定要做物理分页。物理分页的实现核心在于拿到Page参数之后拼接limit语句。MySQL的写法是limit offset, sizeOracle的写法是Rownum嵌套子查询SQL Server又不一样。为了解决不同数据库方言的问题框架内置了一个Dialect接口public interface Dialect { String buildPageSql(String originalSql, int offset, int size); }每种数据库一个实现类你初始化OpmFactory的时候可以通过DatabaseType指定数据库类型。目前内置了MySQL、PostgreSQL、SQLite、H2这四种够覆盖绝大多数轻量级项目了。4.3 事务处理摆脱繁琐的模板代码Java原生JDBC的事务处理写起来很繁琐要手动getConnection、setAutoCommit(false)、commit、finally里rollback。Easy-OPM提供了一个简单的TransactionTemplate来简化这个操作boolean success transactionProxy.execute(() - { userMapper.insert(user1); userMapper.insert(user2); return true; });如果你用过Spring的TransactionTemplate对这个模式一定不陌生。lambda块里的代码会在同一个事务里执行任何一个操作抛异常整个事务自动回滚。当然Spring项目不建议用这个直接用Spring的Transactional注解就行Easy-OPM的这套设计是给非Spring项目准备的。做事务这里有个关键细节同一个事务必须复用同一个Connection。框架内部用了ThreadLocal来保存数据库连接确保同一个线程里的事务操作使用同一个Connection这就是TransactionTemplate和直接调Mapper操作能保持事务一致性的底层原因。有一点大家要注意TransactionTemplate只保证同一个线程内的事务有效性如果你在lambda里开了子线程去查数据库那个子线程用的是新的连接和当前事务不在同一个事务里。5. 踩坑实录我遇到过的5个经典问题5.1 反射性能问题setAccessible引发的血案Java反射的性能一直是个绕不开的话题。Easy-OPM底层用反射去解析实体类里的字段和注解、调用setter和getter。一开始我用的是Field.get和Field.set这套API在实体类字段多、数据量大的时候性能确实不理想。后来我做了两个优化。第一EntityMetadata缓存机制。一个实体的字段列表、注解信息、映射关系在框架启动时解析一次缓存起来后续每次操作直接读缓存不再重复反射解析。第二字段读写尽量用MethodHandle而不是原生反射。MethodHandle是Java 7引入的性能和直接调用差不了太多。5.2 Lombok失效问题Java版本编译器的坑有同学用Easy-OPM时遇到一个很奇怪的现象代码里加了Data注解IDE里看着一切正常但运行时框架读取不到任何字段。一查日志发现实体的字节码里根本没有getter和setter方法。这就是Lombok编译不生效的经典问题。Lombok是通过注解处理器在编译阶段修改字节码来生成getter和setter的但前提是JDK版本和Lombok版本要匹配。如果你的JDK是17Lombok还是老版本1.16那就会遇到控制台提示You arent using a compiler supported by lombok然后它直接罢工不生成任何方法。解决办法就是升级Lombok到1.18.20以上。这里建议大家把Lombok稳定版本固定在1.18.30左右兼容JDK 8到21都测试过。5.3 ClassNotFoundException: java.applet.Applet这个报错看着很吓人实际是MySQL驱动版本问题。新版MySQL Connector/J在某些版本里会引用java.applet.Applet类而JDK 9之后Applet API被移除了于是启动时直接抛ClassNotFoundException。所以用Easy-OPM碰到这个异常不用怀疑框架有问题先去检查你引入的mysql-connector-java版本。建议用8.0.33以上的版本这个版本的驱动已经把Applet相关代码清理干净了。5.4 Redis的increment()报错干扰排查有一次我在项目里同时用了Easy-OPM和Redis在RedisTemplate里调用increment()方法时报了一个奇怪错误说返回值不是Integer或out of range。我一开始以为是ORM框架的问题排查了半天才发现是我往Redis里存的初始值已经是字符串类型increment方法处理不了非数字类型的值。这个问题的教训是当框架出了诡异问题先把数据本身的可能性排查掉再看框架自身。很多时间浪费在怀疑工具上其实数据早就埋了雷。5.5 主键回填失效插入一条数据后你需要立刻拿到数据库自增后的主键。Easy-OPM是通过JDBC的getGeneratedKeys方法来拿自增主键的。这个方法有个局限不是所有数据库的驱动都支持。MySQL驱动默认是支持的PostgreSQL也支持但SQLite的某些驱动版本就不支持。如果你用SQLite并且插入后拿不到主键先别急着报bug看一下你的sqlite-jdbc驱动版本换成3.40.0以上的版本基本就正常了。6. 常见问题速查表问题现象可能原因解决方法启动报ClassNotFoundError: AppletMySQL驱动版本过低引用已删除的JDK类升级mysql-connector-java到8.0.33Lombok的getter/setter不存在Lombok版本和JDK版本不匹配升级Lombok到1.18.20插入数据后主键为null数据库JDBC驱动不支持getGeneratedKeys升级驱动版本检查是否配置了自增主键批量插入100条以上时变慢使用了逐条插入没有开启批量提交使用batchInsert切口开启rewriteBatchedStatements查询结果某些字段映射不上数据库字段名和实体属性名不一致使用Column注解显式指定字段名分页查询丢数据没有指定数据库类型导致方言用了默认策略初始化工厂显式设置DatabaseTypeN1查询性能问题循环里单条查询大量数据用in查询或join查询代替循环查询事务内的子线程查不到数据子线程使用新的Connection不在同一事务避免在事务里开子线程操作同一库7. 环境配置提醒Java环境本身要注意的事刚才说了这么多代码层面的坑最后再说一个基础到容易被忽略的地方——Java环境配置。用Easy-OPM或者任何一个Java框架之前首先要保证JDK安装正确。很多同学把JRE和JDK搞混只装了JRE然后跑Maven构建时报“javac不是内部或外部命令”。这里明确说JDK才是完整的Java开发工具包内置了编译器javacJRE只是运行环境只能跑jar包不能编译源码。Windows环境下配置JAVA_HOME环境变量时路径一定不要带上bin目录。我见过有人把JAVA_HOME设成C:\Program Files\Java\jdk-17\bin结果运行java命令时找个类库都找不到各种莫名其妙的NoClassDefFoundError。正确的JAVA_HOME应该是C:\Program Files\Java\jdk-17系统会自动在这个路径下找bin目录。Mac和Linux用户相对没那么容易踩路径坑但也要检查一下用java -version和javac -version两个命令把环境确认好再开工能避免后续所有跟“找不到类”相关的诡异问题。项目里用Maven的话建议在pom.xml里显式声明maven.compiler.source和maven.compiler.target不然Maven默认用JDK 1.5编译级别你的lambda表达式和var关键字全都会编译不通过。8. 站在现在的角度回看这个项目我把Easy-OPM发在Github上之后陆陆续续收到了一些使用者的反馈。有拿来写毕设的有团队内部工具系统用的还有一个做嵌入式设备的老哥说在资源受限的环境里这个框架跑得很稳因为没有任何多余的依赖。我个人的实际体会是做一个ORM框架最大的收获不是代码本身而是重新理解了数据访问层应该是什么样。很多功能看上去很简单真正动手做才发现坑很深。比如批量插入的rewriteBatchedStatements优化在MySQL中默认是关闭的你不去了解底层JDBC的批处理逻辑写出来的批量插入和单条插入性能没有区别。再比如ResultSet映射这个看起来简单的工作要处理好类型转换、null值处理、字段对齐真的需要大量边界测试。最后再分享一个小技巧。如果你在研究Easy-OPM的过程中想调试SQL日志不用急着去接日志框架用最简单的方式自定义一个DataSource实现在getConnection方法附近打印一下原始的SQL语句。我把这个功能内置成了LoggingDataSource你只需要一行配置打开框架就会把每次执行的预编译SQL和执行参数全部打印到控制台。这比任何SQL日志分析工具都直观也非常适合用来学习一个ORM框架的执行逻辑。本文还有配套的精品资源点击获取
返回列表