JPA、MyBatis、JDBC、MyBatis-Plus 的差异与操作说明
在 Java 后端开发中,数据访问层的技术选型直接影响开发效率与维护成本。下面从 JDBC、MyBatis、MyBatis-Plus、JPA 四种方案出发,对比它们的定位、差异,并给出各自的代码示例。
1. JDBC(Java Database Connectivity)
JDBC 是 Java 访问数据库的最底层 API,所有 ORM 框架底层最终都基于 JDBC 完成数据库交互。它的特点是灵活、可控,但需要开发者手动处理连接、预编译、结果集映射和异常关闭等大量样板代码。
// 原生 JDBC 示例Stringurl="jdbc:mysql://localhost:3306/test";Stringuser="root";Stringpassword="123456";try(Connectionconn=DriverManager.getConnection(url,user,password);PreparedStatementps=conn.prepareStatement("SELECT * FROM user WHERE id = ?")){ps.setLong(1,1L);try(ResultSetrs=ps.executeQuery()){while(rs.next()){System.out.println(rs.getString("name"));}}}catch(SQLExceptione){e.printStackTrace();}特点:性能最高、最灵活,但代码冗余、易出错,适合对 SQL 有极致控制需求的场景。
2. MyBatis
MyBatis 是半自动 ORM 框架,SQL 由开发者编写,框架负责参数映射与结果集映射。它保留了 SQL 的灵活性,同时大幅减少了 JDBC 样板代码。
// Mapper 接口publicinterfaceUserMapper{UserselectById(@Param("id")Longid);}<!-- UserMapper.xml --><selectid="selectById"resultType="com.example.User">SELECT * FROM user WHERE id = #{id}</select>// 调用Useruser=userMapper.selectById(1L);特点:SQL 完全可控,适合复杂查询、多表关联、动态 SQL 场景;但需要手写 XML 或注解 SQL,开发量相对较大。
3. MyBatis-Plus
MyBatis-Plus 是 MyBatis 的增强工具,在保留 MyBatis 全部能力的基础上,提供了通用 CRUD、条件构造器、分页插件等开箱即用的能力,大幅减少重复 SQL 编写。
// 实体类@Data@TableName("user")publicclassUser{@TableId(type=IdType.AUTO)privateLongid;privateStringname;privateIntegerage;}// Mapper 继承 BaseMapper,直接获得通用 CRUDpublicinterfaceUserMapperextendsBaseMapper<User>{}// 使用条件构造器查询LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getName,"张三").gt(User::getAge,18);List<User>users=userMapper.selectList(wrapper);// 分页查询Page<User>page=userMapper.selectPage(newPage<>(1,10),wrapper);特点:单表 CRUD 几乎零 SQL,开发效率极高;复杂 SQL 仍可手写,兼顾效率与灵活性,是目前国内最流行的 MyBatis 增强方案。
4. JPA(Java Persistence API)
JPA 是 Java 官方规范,Hibernate 是其最主流的实现。JPA 是全自动 ORM,开发者通过实体注解和接口方法命名约定即可完成数据操作,无需编写 SQL。
// 实体类@Entity@Table(name="user")publicclassUser{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateLongid;privateStringname;privateIntegerage;}// Repository 接口,继承 JpaRepository 获得通用 CRUDpublicinterfaceUserRepositoryextendsJpaRepository<User,Long>{// 方法命名约定自动生成查询List<User>findByNameAndAgeGreaterThan(Stringname,Integerage);}// 调用List<User>users=userRepository.findByNameAndAgeGreaterThan("张三",18);特点:全自动映射,开发效率高,跨数据库移植性好;但复杂 SQL 和动态查询控制力较弱,性能调优相对困难,学习曲线较陡。
5. 四种方案对比总结
| 维度 | JDBC | MyBatis | MyBatis-Plus | JPA |
|---|---|---|---|---|
| 自动化程度 | 无 | 半自动 | 半自动(增强) | 全自动 |
| SQL 控制力 | 完全控制 | 完全控制 | 完全控制 | 较弱 |
| 开发效率 | 低 | 中 | 高 | 高 |
| 学习成本 | 低 | 中 | 中 | 较高 |
| 复杂查询 | 手写 SQL | 手写 SQL | 手写 SQL + 条件构造器 | 较难 |
| 典型场景 | 底层框架/极致性能 | 复杂业务 SQL | 快速 CRUD + 复杂 SQL | 标准规范/快速开发 |
选型建议:追求极致 SQL 控制选 JDBC;复杂查询多选 MyBatis;国内快速开发首选 MyBatis-Plus;追求规范统一、跨库移植选 JPA。
6. 常见查询操作示例
下面补充几种最常见的用户查询场景,分别用 JDBC、MyBatis、MyBatis-Plus、JPA 四种方式实现,方便对比它们的写法差异。
6.1 根据 id 查询用户
JDBC
// 根据 id 查询用户Stringsql="SELECT * FROM user WHERE id = ?";try(Connectionconn=DriverManager.getConnection(url,user,password);PreparedStatementps=conn.prepareStatement(sql)){ps.setLong(1,1L);try(ResultSetrs=ps.executeQuery()){if(rs.next()){Useru=newUser();u.setId(rs.getLong("id"));u.setName(rs.getString("name"));u.setAge(rs.getInt("age"));System.out.println(u);}}}catch(SQLExceptione){e.printStackTrace();}MyBatis
// Mapper 接口publicinterfaceUserMapper{UserselectById(@Param("id")Longid);}<!-- UserMapper.xml --><selectid="selectById"resultType="com.example.User">SELECT * FROM user WHERE id = #{id}</select>// 调用Useruser=userMapper.selectById(1L);MyBatis-Plus
// 直接使用 BaseMapper 内置方法Useruser=userMapper.selectById(1L);JPA
// Repository 接口publicinterfaceUserRepositoryextendsJpaRepository<User,Long>{}// 调用Useruser=userRepository.findById(1L).orElse(null);6.2 根据用户名模糊查询用户
JDBC
// 根据用户名模糊查询Stringsql="SELECT * FROM user WHERE name LIKE ?";try(Connectionconn=DriverManager.getConnection(url,user,password);PreparedStatementps=conn.prepareStatement(sql)){ps.setString(1,"%张%");try(ResultSetrs=ps.executeQuery()){while(rs.next()){System.out.println(rs.getString("name"));}}}catch(SQLExceptione){e.printStackTrace();}MyBatis
// Mapper 接口publicinterfaceUserMapper{List<User>selectByNameLike(@Param("name")Stringname);}<!-- UserMapper.xml --><selectid="selectByNameLike"resultType="com.example.User">SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')</select>// 调用List<User>users=userMapper.selectByNameLike("张");MyBatis-Plus
// 使用条件构造器模糊查询LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.like(User::getName,"张");List<User>users=userMapper.selectList(wrapper);JPA
// Repository 接口publicinterfaceUserRepositoryextendsJpaRepository<User,Long>{List<User>findByNameContaining(Stringname);}// 调用List<User>users=userRepository.findByNameContaining("张");6.3 根据 id、用户名、密码三个条件一起查询(用户名和密码支持模糊查询)
JDBC
// 多条件组合查询,用户名和密码模糊匹配Stringsql="SELECT * FROM user WHERE id = ? AND name LIKE ? AND password LIKE ?";try(Connectionconn=DriverManager.getConnection(url,user,password);PreparedStatementps=conn.prepareStatement(sql)){ps.setLong(1,1L);ps.setString(2,"%张%");ps.setString(3,"%123%");try(ResultSetrs=ps.executeQuery()){while(rs.next()){System.out.println(rs.getString("name"));}}}catch(SQLExceptione){e.printStackTrace();}MyBatis
// Mapper 接口publicinterfaceUserMapper{List<User>selectByCondition(@Param("id")Longid,@Param("name")Stringname,@Param("password")Stringpassword);}<!-- UserMapper.xml --><selectid="selectByCondition"resultType="com.example.User">SELECT * FROM user WHERE id = #{id} AND name LIKE CONCAT('%', #{name}, '%') AND password LIKE CONCAT('%', #{password}, '%')</select>// 调用List<User>users=userMapper.selectByCondition(1L,"张","123");MyBatis-Plus
// 使用条件构造器组合查询LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getId,1L).like(User::getName,"张").like(User::getPassword,"123");List<User>users=userMapper.selectList(wrapper);JPA
// Repository 接口publicinterfaceUserRepositoryextendsJpaRepository<User,Long>{List<User>findByIdAndNameContainingAndPasswordContaining(Longid,Stringname,Stringpassword);}// 调用List<User>users=userRepository.findByIdAndNameContainingAndPasswordContaining(1L,"张","123");小结:从上面三个查询场景可以看出,JDBC 需要手写全部 SQL 和结果集映射;MyBatis 需要手写 SQL 但映射交给框架;MyBatis-Plus 通过条件构造器几乎零 SQL;JPA 则完全靠方法命名约定自动生成查询。实际项目中可根据对 SQL 控制力的需求灵活选型。
7. 用户表设计与登录注册实现
下面实现一个最简单的用户表,包含Id、Username、Password、Create_time、Update_time五个字段,并给出登录与注册的完整代码。
7.1 表结构设计
| 字段 | 类型 | 说明 |
|---|---|---|
| Id | 字符串(主键,uid 生成) | 主键,使用 UUID 生成 |
| Username | 字符串(Unique 唯一索引) | 用户名,唯一 |
| Password | 字符串 | 密码 |
| Create_time | Date 类型 | 创建时自动填写 |
| Update_time | Date 类型 | 创建时自动填写,更新时更新 |
-- 建表语句CREATETABLEuser(IdVARCHAR(32)PRIMARYKEYCOMMENT'主键,uid 生成',UsernameVARCHAR(50)NOTNULLUNIQUECOMMENT'用户名,唯一索引',PasswordVARCHAR(100)NOTNULLCOMMENT'密码',Create_timeDATETIMENOTNULLCOMMENT'创建时间,自动填写',Update_timeDATETIMENOTNULLCOMMENT'更新时间,创建时填写,更新时更新')ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='用户表';7.2 实体类(MyBatis-Plus 示例)
// 实体类@Data@TableName("user")publicclassUser{@TableId(type=IdType.ASSIGN_UUID)// uid 生成主键privateStringid;privateStringusername;privateStringpassword;@TableField(fill=FieldFill.INSERT)// 创建时自动填写privateDatecreateTime;@TableField(fill=FieldFill.INSERT_UPDATE)// 创建时填写,更新时更新privateDateupdateTime;}// 自动填充处理器@ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{@OverridepublicvoidinsertFill(MetaObjectmetaObject){this.strictInsertFill(metaObject,"createTime",Date.class,newDate());this.strictInsertFill(metaObject,"updateTime",Date.class,newDate());}@OverridepublicvoidupdateFill(MetaObjectmetaObject){this.strictUpdateFill(metaObject,"updateTime",Date.class,newDate());}}// Mapper 接口publicinterfaceUserMapperextendsBaseMapper<User>{}7.3 注册代码
// 注册:用户名唯一校验 + 密码加密 + 插入用户@ServicepublicclassUserService{@AutowiredprivateUserMapperuserMapper;@AutowiredprivatePasswordEncoderpasswordEncoder;publicbooleanregister(Stringusername,Stringpassword){// 1. 校验用户名是否已存在LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getUsername,username);if(userMapper.selectCount(wrapper)>0){returnfalse;// 用户名已存在}// 2. 创建用户,密码加密存储Useruser=newUser();user.setUsername(username);user.setPassword(passwordEncoder.encode(password));// 3. 插入数据库(Id 由 ASSIGN_UUID 自动生成,时间字段自动填充)returnuserMapper.insert(user)>0;}}7.4 登录代码
// 登录:根据用户名查询,校验密码publicUserlogin(Stringusername,Stringpassword){// 1. 根据用户名查询用户LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getUsername,username);Useruser=userMapper.selectOne(wrapper);// 2. 用户不存在或密码不匹配则登录失败if(user==null||!passwordEncoder.matches(password,user.getPassword())){returnnull;}// 3. 登录成功,返回用户信息(实际项目中可在此生成 Token)returnuser;}说明:主键Id使用IdType.ASSIGN_UUID自动生成 uid;Username在数据库层面加了UNIQUE唯一索引,注册时再配合代码校验保证唯一;Create_time和Update_time通过MetaObjectHandler自动填充,创建时都写入当前时间,更新时只刷新Update_time。
8. 修改密码与注销账号
下面补充两个最常见的账号操作:修改密码和注销账号,均基于 MyBatis-Plus 实现。
8.1 修改密码
// 修改密码:校验原密码 + 更新为新密码publicbooleanchangePassword(StringuserId,StringoldPassword,StringnewPassword){// 1. 根据 id 查询用户Useruser=userMapper.selectById(userId);if(user==null){returnfalse;// 用户不存在}// 2. 校验原密码是否正确if(!passwordEncoder.matches(oldPassword,user.getPassword())){returnfalse;// 原密码错误}// 3. 更新为新密码(加密存储,Update_time 自动刷新)Userupdate=newUser();update.setId(userId);update.setPassword(passwordEncoder.encode(newPassword));returnuserMapper.updateById(update)>0;}8.2 注销账号
// 注销账号:根据 id 删除用户publicbooleandeleteUser(StringuserId){// 直接调用 BaseMapper 内置删除方法returnuserMapper.deleteById(userId)>0;}说明:修改密码时先校验原密码,避免被他人恶意篡改;新密码同样使用PasswordEncoder加密后存储,Update_time由MetaObjectHandler自动刷新。注销账号直接按主键Id删除即可,实际项目中可改为逻辑删除(@TableLogic)以保留历史数据。