MyBatis - 高级查询

news/2024/12/23 9:05:29/

文章目录

    • 1.一对一映射
    • 2.一对多映射
    • 3.多对多映射
    • 4.自定义类型映射
      • 4.1 枚举类型案例
      • 4.2 货币类型案例
    • 5.分页插件

当使用 MyBatis 进行对象关系映射(ORM)时,我们经常需要处理一对一映射、一对多映射和多对多映射的关系。同时还可能遇到需要进行自定义类型映射和分页查询的场景。

注意:为了节约篇幅,这里直接给出处理方案,不再进行完整程序的演示。

1.一对一映射

一对一映射指的是两个实体之间的关系,其中一个实体与另一个实体关联,每个实体实例只能关联一个对应的实体实例。假设有两个实体类 UserAccount,每个 User 有一个 Account,即一对一的关系。

如果是基于 XML 映射文件的方式,我们可以通过 ResultMap + association 标签来实现:

<!-- UserMapper.xml -->
<select id="getUser" resultMap="userAccountMap">SELECT u.*, a.* FROM user u LEFT JOIN account a ON u.id = a.user_id WHERE u.id = #{id}
</select><resultMap id="userAccountMap" type="User"><id property="id" column="id"/><result property="username" column="username"/><result property="password" column="password"/><association property="account" javaType="Account"><id property="id" column="account_id"/><result property="userId" column="user_id"/></association>
</resultMap>

这里的 <association> 标签就是一对一的映射关系,通过 javaType 属性指定类型,将查询结果映射到 Useraccount 属性上。

如果是基于注解则对应如下:

// UserMapper.java
@Select("SELECT u.*, a.* FROM user u LEFT JOIN account a ON u.id = a.user_id WHERE u.id = #{id}")
@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "username", column = "username"),@Result(property = "password", column = "password"),@Result(property = "account", column = "account_id", javaType = Account.class,one = @One(select = "cn.javgo.mapper.AccountMapper.selectAccount"))
})
User selectUser(Integer id);

这里的 @One 注解表示一对一的映射关系,可以看到 @One 中给定的是具体获取方法的全限定类名加方法名,会根据 Useraccount_id 字段作为参数查询对应的 Account

2.一对多映射

一对多映射指的是两个实体之间的关系,其中一个实体与多个另一个实体关联,而多个另一个实体实例只能关联一个实体实例。假设有两个实体类 ClassStudent,一个 Class 有多个 Student,即一对多的关系。

如果是基于 XML 映射文件的方式,处理一对多关系,我们可以使用 ResultMap + collection 标签来实现:

<!-- ClassMapper.xml -->
<select id="getClass" resultMap="classStudentMap">SELECT c.*, s.* FROM class c LEFT JOIN student s ON c.id = s.class_id WHERE c.id = #{id}
</select><resultMap id="classStudentMap" type="Class"><id property="id" column="id"/><result property="name" column="name"/><collection property="students" ofType="Student"><id property="id" column="student_id"/><result property="name" column="student_name"/></collection>
</resultMap>

这里的 <collection> 标签就是一对多的映射关系,将查询结果映射到 Classstudents 属性上。

如果是基于注解则对应如下:

// ClassMapper.java
@Select("SELECT c.*, s.* FROM class c LEFT JOIN student s ON c.id = s.class_id WHERE c.id = #{id}")
@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "name", column = "name"),@Result(property = "students", column = "id", javaType = List.class,many = @Many(select = "cn.javgo.StudentMapper.selectStudentsByClassId"))
})
Class selectClass(Integer id);

这里的 @Many 注解表示一对多的映射关系,可以看到 @Many 中给定的是具体获取方法的全限定类名加方法名,会根据 Classid 字段作为参数查询对应的所有 Student

3.多对多映射

多对多映射指的是两个实体之间的关系,其中一个实体与多个另一个实体关联,多个另一个实体实例也可以关联多个实体实例。例如,学生表(Student)和课程表(Course),一个学生可以选修多门课程,一门课程也可以被多个学生选修。这其实与上述的一对多关系本质上是一样的,因此处理方案也相同。

以下是基于 XML 的示例:

<select id="getStudent" resultMap="studentCourseResult">SELECT * FROM Student s LEFT JOIN StudentCourseRelation scr ON s.id = scr.student_id LEFT JOIN Course c ON scr.course_id = c.id WHERE s.id = #{id}
</select><resultMap id="studentCourseResult" type="Student"><id property="id" column="id" /><result property="name" column="name" /><collection property="courses" ofType="Course"><id property="id" column="course_id" /><result property="courseName" column="course_name" /></collection>
</resultMap>

以下是基于注解的示例:

public interface StudentMapper {@Select("SELECT * FROM Student s LEFT JOIN StudentCourseRelation scr ON s.id = scr.student_id LEFT JOIN Course c ON scr.course_id = c.id WHERE s.id = #{id}")@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "name", column = "name"),@Result(property = "courses", column = "student_id", javaType = List.class,many = @Many(select = "cn.javgo.mapper.CourseMapper.selectByStudentId"))})Student getStudent(Long id);
}

4.自定义类型映射

MyBatis 允许你在几乎任何时候都使用自定义的 TypeHandler 来处理 SQL 语句的参数绑定以及结果映射。如果你有一个特定的数据类型需要做一些特殊的处理,你可以编写自定义的 TypeHandler

4.1 枚举类型案例

首先,需要实现 TypeHandlerBaseTypeHandler 抽象类。例如,有一个枚举类型 State,包含了 ACTIVEINACTIVE 两个状态:

/*** 状态枚举*/
public enum State {ACTIVE(1, "激活"),INACTIVE(0, "未激活");private Integer code;private String desc;State(Integer code, String desc) {this.code = code;this.desc = desc;}public Integer getCode() {return code;}public String getDesc() {return desc;}public static State getByCode(Integer code) {for (State state : State.values()) {if (state.getCode().equals(code)) {return state;}}return null;}
}

但在数据库中,我们希望它们分别保存为 1 和 0,可以定义一个自定义的类型处理器重写该类的四个方法:

/*** 自定义枚举类型转换器({@link State} -> {@link Integer})*/
public class StateTypeHandler extends BaseTypeHandler<State> {/*** 用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型( Java -> DB )* @param preparedStatement 用于设置参数的PreparedStatement对象* @param i 参数的位置* @param state 参数的值* @param jdbcType JDBC类型* @throws SQLException 数据库异常*/@Overridepublic void setNonNullParameter(PreparedStatement preparedStatement, int i, State state, JdbcType jdbcType) throws SQLException {preparedStatement.setInt(i, state.getCode());}/*** 用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param s 字段名称* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic State getNullableResult(ResultSet resultSet, String s) throws SQLException {int code = resultSet.getInt(s);return State.getByCode(code);}/*** 用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic State getNullableResult(ResultSet resultSet, int i) throws SQLException {int code = resultSet.getInt(i);return State.getByCode(code);}/*** 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param callableStatement CallableStatement对象* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic State getNullableResult(CallableStatement callableStatement, int i) throws SQLException {int code = callableStatement.getInt(i);return State.getByCode(code);}
}

然后,在 Spring Boot 的配置文件中配置 TypeHandler 所在的包路径:

mybatis:type-handlers-package: cn.javgo.learningmybatis.support.handler

最后,在 mapper XML 文件中,你就可以直接使用 State 类型了:

<select id="selectByState" parameterType="cn.javgo.learningmybatis.enums.State" resultType="User">SELECT * FROM User WHERE state = #{state}
</select>

在注解方式中,你可以直接在 @Results 注解中使用 @Result 注解的 typeHandler 属性来指定 TypeHandler

public interface UserMapper {@Select("SELECT * FROM User WHERE state = #{state}")@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "username", column = "username"),@Result(property = "state", column = "state", javaType = State.class, typeHandler = StateTypeHandler.class)})List<User> selectByState(@Param("state") State state);
}

这样,当查询 User 并将结果映射到 User 对象时,state 字段将使用我们自定义的 StateTypeHandler 来处理。

TIP:

MyBatis 为 Java 枚举类型的处理提供了两种方式:EnumTypeHandlerEnumOrdinalTypeHandler

  • EnumTypeHandler:默认枚举处理器,它将枚举的名称(如 ACTIVE 或 INACTIVE)保存到数据库中。
  • EnumOrdinalTypeHandler:它将枚举的顺序(ordinal)保存到数据库中,如 1 或 0。

因此,我们其实可以省略上述的操作,直接使用现成的这两个处理器就可以实现枚举相关的操作了。

4.2 货币类型案例

在实际场景中,我们只有在 MyBatis 没有提供合适的内置 TypeHandler 时,才自定义自己的类型处理器。一个常见的例子就是讲 Money 类型的属性值和 Long 类型之间的转换,因为金额一般我们都是用 Money 类来表示,但是在数据库中一般却以分为单位进行存储,在取出时则以人民币为币种还原为 Money

这里需要使用一个货币相关的类库,Joda-Money 是一个开源的 Java 库,旨在提供强大而灵活的处理货币和货币金额的功能。它是 Joda-Time 日期和时间库的姊妹项目,专门用于处理货币价值和货币操作。Joda-Money 提供了一组类和方法,使您能够进行精确的货币计算和处理。

需要使用该类库需要添加如下依赖:

<!-- Joda Money -->
<dependency><groupId>org.joda</groupId><artifactId>joda-money</artifactId><version>1.0.1</version>   <!--请根据实际情况选择版本号-->
</dependency>

处理 Money 类型的 MoneyTypeHandler 代码如下:

/*** 自定义类型转换器({@link Money} -> {@link Long})*/
public class MoneyTypeHandler extends BaseTypeHandler<Money> {/*** 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param value 数据库中的数据* @return 转换后的Java对象*/private Money parseMoney(Long value) {// 创建Money对象(货币单位为分,货币类型为人民币)return Money.ofMinor(CurrencyUnit.of("CNY"), value);}/*** 用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型( Java -> DB )* @param preparedStatement 用于设置参数的PreparedStatement对象* @param i 参数的位置* @param money 参数的值* @param jdbcType JDBC类型* @throws SQLException 数据库异常*/@Overridepublic void setNonNullParameter(PreparedStatement preparedStatement, int i, Money money, JdbcType jdbcType) throws SQLException {// 获取金额(分),并设置到PreparedStatement对象中preparedStatement.setLong(i, money.getAmountMinorLong());}/*** 用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param s 字段名称* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic Money getNullableResult(ResultSet resultSet, String s) throws SQLException {return parseMoney(resultSet.getLong(s));}/*** 用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param resultSet 结果集* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic Money getNullableResult(ResultSet resultSet, int i) throws SQLException {return parseMoney(resultSet.getLong(i));}/*** 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型( DB -> Java )* @param callableStatement CallableStatement对象* @param i 字段索引* @return 转换后的Java对象* @throws SQLException 数据库异常*/@Overridepublic Money getNullableResult(CallableStatement callableStatement, int i) throws SQLException {return parseMoney(callableStatement.getLong(i));}
}

5.分页插件

PageHelper 是一个简单且易用的 MyBatis 分页插件。它的设计思想是只对紧跟在 PageHelper.startPage 方法后的第一个 MyBatis 查询方法进行分页。这就意味着如果再次调用查询方法,它就会返回所有的记录,而不是一个分页结果。PageHelper-Spring-Boot-Starter 是 PageHelper 与 Spring Boot 的集成。

PageHelper 官方 GItHub 地址:https://github.com/pagehelper/Mybatis-PageHelper

Spring Boot Starter GItHub 地址:https://github.com/pagehelper/pagehelper-spring-boot

首先,你需要在项目的 pom.xml 文件中添加 pagehelper-spring-boot-starter 的依赖:

<!--MyBatis分页插件-->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper-starter.version}</version>
</dependency>

注意:上面的 ${pagehelper-starter.version} 需要根据实际需求选择对应的版本。

然后在 Spring Boot 的配置文件中进行 PageHelper 相关的基本配置:

# PageHelper 分页插件配置
pagehelper:# 配置数据库方言helper-dialect: mysql# 配置分页合理化参数reasonable: true# 配置支持通过 Mapper 接口参数来传递分页参数support-methods-arguments: true# 配置参数映射,即从 Map 中根据指定的名字取值,用于从 Map 中取值时的 keyparams: count=countSql# 如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果page-size-zero: true

常用的配置项如下:

配置项说明
pagehelper.helper-dialect配置数据库的方言
pagehelper.page-size-zero如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果
pagehelper.reasonable配置分页合理化参数,默认值为 false。当该参数设置为 true 时,pageNum<=0 会查询第一页,pageNum>pages(超过总页数时)会查询最后一页。默认 false 时,直接根据参数进行查询。
pagehelper.support-methods-arguments支持通过 Mapper 接口方法参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会进行分页。
pagehelper.params为了支持 startPage(Object params) 方法,增加了该配置来配置参数映射,用于从对象中根据属性名取值(一般为 Map 中根据指定的名字取值,用于从 Map 中取值时的 key),可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用法可以参考 startPage(Object params) 方法的示例。

然后,在 Mapper 接口中,你可以直接进行分页查询:

@Mapper
public interface UserMapper {@Select("select * from user")List<User> selectAll();
}

在 Service 层,你可以通过调用 PageHelper.startPage 方法实现分页:

@Service
public class UserService {@Autowiredprivate UserMapper userMapper;/*** 查询所有用户(分页)* @param pageNum 当前页码* @param pageSize 每页大小* @return PageInfo<User>*/public PageInfo<User> selectAll(int pageNum, int pageSize) {// 开启分页(将会自动拦截到下面这查询sql)PageHelper.startPage(pageNum, pageSize);// 执行查询List<User> users = userMapper.selectAll();// 封装为PageInfo对象PageInfo<User> pageInfo = new PageInfo<>(users);// 返回return pageInfo;}
}

在这个示例中,我们首先调用了 PageHelper.startPage 方法,然后调用了 userMapper.selectAll 方法。PageHelper 会对这个方法进行分页,然后我们将结果包装成 PageInfo 对象并返回。

PageInfo 是一个包含了分页信息的对象,包括当前页码、每页的数量、总记录数、总页数、是否为第一页、是否为最后一页、是否有前一页、是否有下一页等。

PageHelper 底层实现分页的原理:

PageHelper 插件的实现是基于 MyBatis 的拦截器接口 Interceptor。它会对执行 SQL 操作的 StatementHandler 进行拦截。在进行 SQL 查询之前,插件会改写要执行的 SQL,加入对应数据库的分页查询语句(即 limit 条件),从而实现物理分页的效果。具体来说,使用 PageHelper 插件时,当调用 PageHelper.startPage(pageNum, pageSize) 方法后,会创建一个 Page 对象并保存到本地线程变量 ThreadLocal 中,因此操作需要在一个线程中。在执行查询之前,会取出 Page 对象的信息,并利用这些信息改写 SQL。在查询完成后,将分页信息清空。

方法源码如下:

/*** 分页查询* @param pageNum 当前页* @param pageSize 每页大小* @return 一个经过分页后的 Page 对象*/
public static <E> Page<E> startPage(int pageNum, int pageSize) {// 调用下面的 startPage 方法,传入 pageNum、pageSize、DEFAULT_COUNT 参数,返回一个 Page<E> 对象return startPage(pageNum, pageSize, DEFAULT_COUNT);
}
/*** 真正的分页方法* @param pageNum 当前页* @param pageSize 每页大小* @param count 数据表中的记录总数,用于计算分页的页数和当前页的数据数量,以便于实现分页功能* @return 一个经过分页后的 Page 对象*/
public static <E> Page<E> startPage(int pageNum, int pageSize, int count) {// 创建 Page 对象Page<E> page = new Page<>(pageNum, pageSize, count);// 将 Page 对象设置到 ThreadLocal 中,方便在同一线程的任意地方获得 Page 对象PAGE_LOCAL.set(page);// 返回 Page 对象return page;
}

当然,MyBatis 本身其实也提供了 RowBounds 对象和在 Mapper 方法参数中指定分页信息的方式进行分页。RowBounds 是 MyBatis 提供的一个用于物理分页的类,它有两个重要的属性,offsetlimit。其中 offset 表示开始读取的位置(偏移量),limit 表示读取的数量。例如,new RowBounds(10, 20) 表示从第10条记录开始,读取20条记录。

注意:当我们配置了 pagehelper.offset-as-page-num=true 后会将 offset 当成 pageNum 页码使用,第二个参数 limitpageSize 参数。

使用 RowBounds 对象的例子:

@Select("select * from user")
List<User> selectAllByRowBounds(RowBounds rowBounds);

在方法参数中指定分页信息的例子:

/*** 查询所有用户(分页)* @param pageNum 当前页码* @param pageSize 每页大小* @return PageInfo<User>*/
public PageInfo<User> selectAllByRowBounds(int pageNum, int pageSize) {// 执行查询List<User> users = userMapper.selectAllByRowBounds(new RowBounds(pageNum, pageSize));// 封装为PageInfo对象PageInfo<User> pageInfo = new PageInfo<>(users);// 返回return pageInfo;
}

PageHelper VS RowBounds:

  1. PageHelper 是基于 MyBatis 插件实现的,只需要在查询前调用 PageHelper.startPage 方法即可实现分页,无需修改原有的 SQL。它会自动识别数据库类型,并生成对应的分页 SQL。PageHelper 只返回需要的记录,不必查询所有数据,性能较好。
  2. RowBounds 是 MyBatis 提供的一个用于分页的对象,通过查询所有数据后,再返回指定范围的记录实现的,所以在数据量较大时,性能较差。

因此,大多数情况下,推荐使用 PageHelper 插件进行分页,不仅可以减少代码的复杂度,而且更加易用。


http://www.ppmy.cn/news/150112.html

相关文章

当贝Z1 PRO使用心得

大屏会议需要&#xff0c;采购了1个当贝Z1 PRO电视盒子&#xff0c;据说是目前性能最强的&#xff0c;也是极少数带摄像头的电视盒子。 得益于强大的处理器&#xff0c;整体比较流畅&#xff0c;但系统太拉跨&#xff0c; 64位处理器&#xff0c;跑着32位的老系统&#xff0c;…

zuk android os 流量,原生用户最爱 Cyanogen OS版ZUK Z1固件

11月16日消息&#xff0c;Cyanogen通过官方网站发布了ZUK Z1的Cyanogen OS 12.1固件&#xff0c;喜欢原生安卓风格的ZUK Z1用户可以通过刷新固件的方式&#xff0c;将ZUI变更为Cyanogen OS 12.1系统。 ZUK Z1是国产新晋品牌ZUK于今年8月推出的首款智能手机产品&#xff0c;在国…

sony z1 android 6.0,索尼最强旗舰Xperia Z1升级版初体验

索尼最强旗舰Xperia Z1升级版初体验 出处&#xff1a;快科技 2014-01-03 16:16:21 作者&#xff1a;雪花 编辑&#xff1a;雪花[爆料] 收藏文章 目前索尼智能手机阵营中最强的当属Xperia Z1&#xff0c;而前不久他们携手中国移动推出了这款手机的小幅升级版&#xff0c;其最…

iqooz1手机能搭载鸿蒙吗,iQOOZ1有NFC功能吗?iQOO Z1支持NFC刷公交卡与门禁卡吗

iQOOZ1有NFC功能吗?随着NFC实现了对于公交卡、门禁卡、银行卡等功能的支持&#xff0c;它已经成为了手机中必不可少的功能之一。作为iQOO Z系列的第一款产品&#xff0c;iQOO Z1在硬件创新、交互变革方面有着很大的突破。如此优秀的一款iQOO Z1&#xff0c;它有没有全功能NFC功…

HBuilderX踩坑记录 —— vivo iqoo z1或者iqooz1x开启adb调试

在打包运行APP的基座的时候&#xff0c;HBuilder X连接手机显示 未检测到手机或模拟器 按住shift右键&#xff0c;运行出此弹窗 输入.\adb devices&#xff0c; .\adb deviceslist of devices attached下无设备&#xff0c;表示未找到手机。 在手机拨号界面输入*#558#&#xff…

android 8.1.0怎么截屏,vivo Z1i怎么截屏?4种vivo Z1i截图方法

近日&#xff0c;vivo发布了第款Z系列手机——Z1i&#xff0c;简单来说就是vivo Z1的衍生版&#xff0c;存储空间升级到了128GB&#xff0c;不过处理器降为骁龙636&#xff0c;前置摄像头降为1600万像素&#xff0c;售价1898元&#xff0c;主打的是颜值、拍照和性价比。下面本文…

LIBEVENT 框架

LIBEVENT 框架 LAMPlibevent特点:libevent的功能libevent官网安装步骤Linux下libevent主要API介绍libevent使用步骤libevent 编程案例LAMP 从LAMP说起: 是一个缩写,它指一组通常一起使用来运行动态网站或者服务器的自由软件 Linux - 操作系统Apache - 网页服务器MySQL - 数据…

pynq z1+ov5640工程复现

上一篇中pynq z2ov5640的工程成功移植到了pynq z1&#xff0c;本篇展示如何将工程复现。 下载的压缩包中有cv_ov5640.tcl的文件&#xff0c;打开发现原工程是用2018.3版本编译的 打开vivado2018.3&#xff08;可以修改版本&#xff0c;不过还原的工程ip可能会出错&#xff09;…