SpringBoot20-基础案例10整合Mybatis框架集成分页助手插件

本文源码GitHub:知了一笑https://github.com/cicadasmile/spring-boot-base一、Mybatis框架1、mybatis简介MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 2、mybatis特点1)sql语句与代码分离,存放于xml配置文件中,方便管理2)用逻辑标签控制动态SQL的拼接,灵活方便3)查询的结果集与java对象自动映射4)编写原生态SQL,接近JDBC5)简单的持久化框架,框架不臃肿简单易学3、适用场景MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,MyBatis将是不错的选择。 二、与SpringBoot2.0整合1、项目结构图采用druid连接池,该连接池。 2、核心依赖<!-- mybatis依赖 --><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version></dependency><!-- mybatis的分页插件 --><dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.1.6</version></dependency>3、核心配置mybatis: # mybatis配置文件所在路径 config-location: classpath:mybatis.cfg.xml type-aliases-package: com.boot.mybatis.entity # mapper映射文件 mapper-locations: classpath:mapper/*.xml4、逆向工程生成的文件这里就不贴代码了。 5、编写基础测试接口// 增加int insert(ImgInfo record);// 组合查询List<ImgInfo> selectByExample(ImgInfoExample example);// 修改int updateByPrimaryKeySelective(ImgInfo record);// 删除int deleteByPrimaryKey(Integer imgId);6、编写接口实现@Servicepublic class ImgInfoServiceImpl implements ImgInfoService { @Resource private ImgInfoMapper imgInfoMapper ; @Override public int insert(ImgInfo record) { return imgInfoMapper.insert(record); } @Override public List<ImgInfo> selectByExample(ImgInfoExample example) { return imgInfoMapper.selectByExample(example); } @Override public int updateByPrimaryKeySelective(ImgInfo record) { return imgInfoMapper.updateByPrimaryKeySelective(record); } @Override public int deleteByPrimaryKey(Integer imgId) { return imgInfoMapper.deleteByPrimaryKey(imgId); }}7、控制层测试类@RestControllerpublic class ImgInfoController { @Resource private ImgInfoService imgInfoService ; // 增加 @RequestMapping("/insert") public int insert(){ ImgInfo record = new ImgInfo() ; record.setUploadUserId("A123"); record.setImgTitle("博文图片"); record.setSystemType(1) ; record.setImgType(2); record.setImgUrl("https://avatars0.githubusercontent.com/u/50793885?s=460&v=4"); record.setLinkUrl("https://avatars0.githubusercontent.com/u/50793885?s=460&v=4"); record.setShowState(1); record.setCreateDate(new Date()); record.setUpdateDate(record.getCreateDate()); record.setRemark("知了"); record.setbEnable("1"); return imgInfoService.insert(record) ; } // 组合查询 @RequestMapping("/selectByExample") public List<ImgInfo> selectByExample(){ ImgInfoExample example = new ImgInfoExample() ; example.createCriteria().andRemarkEqualTo("知了") ; return imgInfoService.selectByExample(example); } // 修改 @RequestMapping("/updateByPrimaryKeySelective") public int updateByPrimaryKeySelective(){ ImgInfo record = new ImgInfo() ; record.setImgId(11); record.setRemark("知了一笑"); return imgInfoService.updateByPrimaryKeySelective(record); } // 删除 @RequestMapping("/deleteByPrimaryKey") public int deleteByPrimaryKey() { Integer imgId = 11 ; return imgInfoService.deleteByPrimaryKey(imgId); }}8、测试顺序http://localhost:8010/inserthttp://localhost:8010/selectByExamplehttp://localhost:8010/updateByPrimaryKeySelectivehttp://localhost:8010/deleteByPrimaryKey三、集成分页插件1、mybatis配置文件<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <plugins> <!--mybatis分页插件--> <plugin interceptor="com.github.pagehelper.PageHelper"> <property name="dialect" value="mysql"/> </plugin> </plugins></configuration>2、分页实现代码@Overridepublic PageInfo<ImgInfo> queryPage(int page,int pageSize) { PageHelper.startPage(page,pageSize) ; ImgInfoExample example = new ImgInfoExample() ; // 查询条件 example.createCriteria().andBEnableEqualTo("1").andShowStateEqualTo(1); // 排序条件 example.setOrderByClause("create_date DESC,img_id ASC"); List<ImgInfo> imgInfoList = imgInfoMapper.selectByExample(example) ; PageInfo<ImgInfo> pageInfo = new PageInfo<>(imgInfoList) ; return pageInfo ;}3、测试接口http://localhost:8010/queryPage四、源代码地址GitHub地址:知了一笑https://github.com/cicadasmile/spring-boot-base码云地址:知了一笑https://gitee.com/cicadasmile/spring-boot-base ...

July 12, 2019 · 2 min · jiezi

手撕面试官系列二开源框架面试题SpringSpringMVCMyBatis

跳槽时时刻刻都在发生,但是我建议大家跳槽之前,先想清楚为什么要跳槽。切不可跟风,看到同事一个个都走了,自己也盲目的开始面试起来(期间也没有准备充分),到底是因为技术原因(影响自己的发展,偏移自己规划的轨迹),还是钱给少了,不受重视。 闲话不多说开始主题(面试题+答案领取方式见个人主页) 以下为常见spring面试题: 1 、什么是 Spring 框架?Spring 框架有哪些主要模块?2 、使用 Spring 框架能带来哪些好处?3 、什么是控制反转(IOC) ?什么是依赖注入?4 、请解释下 Spring 框架中的 IoC ?5 、BeanFactory 和 和 ApplicationContext 有什么区别?6 、Spring 有几种配置方式?7 、如何用基于 XML 配置的方式配置 Spring ?8 、如何用基于 Java 配置的方式配置 Spring?9 、怎样用注解的方式配置 Spring10 、请解释 Spring Bean 的生命周期?11 、Spring Bean 的作用域之间有什么区别?12 、什么是 Spring inner beans?13 、Spring 框架中的单例 Beans 是线程安全的么?14 、请举例说明如何在 Spring 中注入一个 Java Collection?15 、如何向 Spring Bean 中注入一个 Java.util.Properties ?16 、请解释 Spring Bean 的自动装配?17 、请解释自动装配模式的区别?18 、如何开启基于注解的自动装配?19 、请举例解释@Required 注解?20 、请举例解释@Autowired 注解?21 、请举例说明@Qualifier 注解?22 、构造方法注入和设值注入有什么区别?23 、Spring 框架中有哪些不同类型的事件?24 、FileSystemResource 和 和 ClassPathResource 有何区别?25 、Spring 框架中都用到了哪些设计模式? ...

July 11, 2019 · 2 min · jiezi

Spring-Boot-JDBC-Mybatis-配置多数据源-以及-采用Durid-作为连接池

1 配置文件在配置文件中配置两个数据源配置,以及mybatis xml配置文件路径 # mybatis 多数据源配置mybatis.config-location = classpath:mapper/config/mybatis-config.xml################# mysql 数据源1 #################spring.datasource.one.jdbc-url=jdbc:mysql://localhost:3306/user?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=truespring.datasource.one.username=rootspring.datasource.one.password=root#spring.datasource.one.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.one.driver-class-name=com.mysql.jdbc.Driver################# mysql 数据源1 ################################## mysql 数据源2 ################spring.datasource.second.jdbc-url=jdbc:mysql://xxxxxxxxxx:3306/user?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=truespring.datasource.second.username=rootspring.datasource.second.password=root#spring.datasource.second.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.second.driver-class-name=com.mysql.jdbc.Driver################# mysql 数据源1 #################2 数据库配置代码:1 步骤1 首先加载配置的数据源:手动将数据配置文件信息注入到数据源实例对象中。2 根据创建的数据源,配置数据库实例对象注入到SqlSessionFactory 中,构建对应的 SqlSessionFactory。3 配置数据库事务:将数据源添加到事务中。4 将SqlSessionFactory 注入到SqlSessionTemplate 模板中5 最后将上面创建的 SqlSessionTemplate 注入到对应的 Mapper 包路径下,这样这个包下面的 Mapper 都会使用第一个数据源来进行数据库操作。 basePackages 指明 Mapper 地址。sqlSessionTemplateRef 指定 Mapper 路径下注入的 sqlSessionTemplate。在多数据源的情况下,不需要在启动类添加:@MapperScan("com.xxx.mapper") 的注解。2 项目结构: 3 第一个数据源@Api("SqlSessionTemplate 注入到对应的 Mapper 包路径下")@Configuration@MapperScan(basePackages = "com.example.demo.mapper.one", sqlSessionTemplateRef = "oneSqlSessionTemplate")public class OneDataSourceConfig { //------------------ 1 加载配置的数据源: ------------------------------- @Bean("oneDatasource") @ConfigurationProperties(prefix = "spring.datasource.one") @Primary //默认是这个库 public DataSource DataSource1Config(){ return DataSourceBuilder.create().build(); } //---------------------- 2 创建的数据源 构建对应的 SqlSessionFactory。 ---------------------- @Bean(name = "oneSqlSessionFactory" ) @Primary public SqlSessionFactory oneSqlSessionFactory(@Qualifier("oneDatasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/one/*.xml")); return bean.getObject(); } //------------------------3 配置事务 -------------------------- @Bean(name = "oneTransactionManager") @Primary public DataSourceTransactionManager oneTransactionManager(@Qualifier("oneDatasource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } //------------------------------- 4 注入 SqlSessionFactory 到 SqlSessionTemplate 中--------------------------------- @Bean(name = "oneSqlSessionTemplate") @Primary public SqlSessionTemplate oneSqlSessionTemplate(@Qualifier("oneSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }}第二个数据源@Api("SqlSessionTemplate 注入到对应的 Mapper 包路径下")@Configuration@MapperScan(basePackages = "com.example.demo.mapper.second", sqlSessionTemplateRef = "secondSqlSessionTemplate")public class SecondDataSourceConfig { //------------------ 加载配置的数据源: ------------------------------- @Bean("secondDatasource") @ConfigurationProperties(prefix = "spring.datasource.second") public DataSource DataSource2Config(){ return DataSourceBuilder.create().build(); } //---------------------- 创建的数据源 构建对应的 SqlSessionFactory。 ---------------------- @Bean(name = "secondSqlSessionFactory") public SqlSessionFactory secondSqlSessionFactory(@Qualifier("secondDatasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/second/*.xml")); return bean.getObject(); } //------------------------ 配置事务 -------------------------- @Bean(name = "secondTransactionManager") public DataSourceTransactionManager secondTransactionManager(@Qualifier("secondDatasource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } //------------------------------- 注入 SqlSessionFactory 到 SqlSessionTemplate 中--------------------------------- @Bean(name = "secondSqlSessionTemplate") public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("secondSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }}3 xml文件<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration></configuration>4 mapper 类public interface User1Mapper { public void inserts(User user);}public interface User2Mapper { public void inserts(User user);}5 mybatis mapper.xml<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.demo.mapper.one.User1Mapper"> <insert id="inserts" parameterType="com.example.demo.pojo.User" useGeneratedKeys="true" keyProperty="id"> insert into user(`name`,age) VALUE (#{name},#{age}) </insert> </mapper><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.demo.mapper.one.User2Mapper"> <insert id="inserts" parameterType="com.example.demo.pojo.User" useGeneratedKeys="true" keyProperty="id"> insert into user(`name`,age) VALUE (#{name},#{age}) </insert> </mapper>3 启动成功表示数据源创建成功,这里连接池采用springboot默认的Hikari数据库连接池(不需要配置) ...

June 29, 2019 · 2 min · jiezi

犯罪心理解读Mybatis拦截器

原文链接:"犯罪心理"解读Mybatis拦截器 Mybatis拦截器执行过程解析 文章写过之后,我觉得 “Mybatis 拦截器案件”背后一定还隐藏着某种设计动机,里面大量的使用了 Java 动态代理手段,它是怎样应用这个手段优雅的设计出整个拦截事件的?就像抓到罪犯要了解它犯罪动机是什么一样,我们需要解读 Mybatis拦截器的设计理念: 设计解读Java 动态代理我们都懂得,我们先用它设计一个基本拦截器首先定义目标对象接口: public interface Target { public void execute();}然后,定义实现类实现其接口: public class TargetImpl implements Target { public void execute() { System.out.println("Execute"); }}最后,使用 JDK 动态代理定义一个代理类,用于为目标类生成代理对象: public class TargetProxy implements InvocationHandler { private Object target; private TargetProxy(Object target) { this.target = target; } //代理对象生成目标对象 public static Object bind(Object target) { return Proxy.newProxyInstance(target.getClass() .getClassLoader(), target.getClass().getInterfaces(), new TargetProxy(target)); } // public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Begin"); return method.invoke(target, args); }}这时,客户端调用方式如下: ...

June 27, 2019 · 4 min · jiezi

不得不知的责任链设计模式

世界上最遥远的距离,不是生与死,而是它从你的世界路过无数次,你却选择视而不见,你无情,你冷酷啊...... 被你忽略的就是责任链设计模式,希望它再次经过你身旁你会猛的发现,并对它微微一笑...... 责任链设计模式介绍抽象介绍初次见面,了解表象,深入交流之后(看完文中的 demo 和框架中的实际应用后),你我便是灵魂之交(重新站在上帝视角来理解这个概念会更加深刻) 责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象能或不能处理该请求,它都会把相同的请求传给下一个接收者,依此类推,直至责任链结束。接下来将概念图形化,用大脑图形处理区理解此概念 上图左侧的 UML 类图中,Sender 类不直接引用特定的接收器类。 相反,Sender 引用Handler 接口来处理请求handler.handleRequest(),这使得 Sender 独立于具体的接收器(概念当中说的解耦) Receiver1,Receiver2 和 Receiver3 类通过处理或转发请求来实现 Handler 接口(取决于运行时条件)上图右侧的 UML 序列图显示了运行时交互,在此示例中,Sender 对象在 receiver1 对象(类型为Handler)上调用 handleRequest(), 接收器 1 将请求转发给接收器 2,接收器 2 又将请求转发到处理(执行)请求的接收器3具象介绍大家小时候都玩过击鼓传花的游戏,游戏的每个参与者就是责任链中的一个处理对象,花球就是待处理的请求,花球就在责任链(每个参与者中)进行传递,只不过责任链的结束时间点是鼓声的结束. 来看 Demo 和实际案例 Demo设计程序猿和 log 是老交情了,使用 logback 配置日志的时候有 ConsoleAppender 和 RollingFileAppender,这两个 Appender 就组成了一个 log 记录的责任链。下面的 demo 就是模拟 log 记录:ConsoleLogger 打印所有级别的日志;EmailLogger 记录特定业务级别日志 ;FileLogger 中只记录 warning 和 Error 级别的日志 抽象概念介绍中,说过实现责任链要有一个抽象接收器接口,和具体接收器,demo 中 Logger 就是这个抽象接口,由于该接口是 @FunctionalInterface (函数式接口), 它的具体实现就是 Lambda 表达式,关键代码都已做注释标注 ...

June 21, 2019 · 4 min · jiezi

Mybatis拦截器执行过程解析

上一篇文章 Mybatis拦截器之数据加密解密 介绍了 Mybatis 拦截器的简单使用,这篇文章将透彻的分析 Mybatis 是怎样发现拦截器以及调用拦截器的 intercept 方法的 小伙伴先按照文章内容细致但不入微的了解整个拦截器执行过程,在纸上勾勒出各个点,再细致入微的读源码,将这些点用线串起来,这样站在上帝视角后,理解的更加深刻发现拦截器按照官网说明,我们通过实现 org.apache.ibatis.plugin.Interceptor 接口自定义的拦截器,有两种方式将自定义拦截器添加到 Mybatis 的 configuration 中 配置文件方式在 mybatis-config.xml 中添加 plugin <!-- mybatis-config.xml --><plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin></plugins>XMLConfigBuilder 负责解析 Mybatis 全局配置文件,其中有 pluginElement 方法: private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } }从该方法中可以看出,遍历 XML XNode,如果该 node 有 interceptor 属性,说明我们配置了拦截器,通过 configuration.addInterceptor(interceptorInstance); 将拦截器实例添加到 Mybatis Configuration 中 ...

June 18, 2019 · 6 min · jiezi

鱼与熊掌得兼Hibernate与Mybatis共存

鱼与熊掌得兼:Hibernate与Mybatis共存很长一段时间,网上有很多关于Hibernate与Mybatis孰优孰劣的争论,两个阵营的人谁也不能说服谁,每个人的理由都很有道理。今天,我分享的主题是:在一个项目中同时使用Hibernate和Mybatis两个ORM框架。 作为一个开发者,没有必要花费过多的时间去证明技术无用论,当你开始指责某个框架垃圾,另外一个框架最好时,隐性的暴露出你对某个框架没有深入的研究,无知的指责对于技术的提升没有任何的帮助。框架本身没有对错一说,只有适合和更适合项目的选择。任何框架都有自身的能力范围,就拿Hibernate和Mybatis这两个ORM框架来说,Hibernate封装了很多有用的API给开发者,降低了操作数据库的难度和复杂度,同时也减少了模板代码的数量,但Hibernate留给开发者可操作的空间相对Mybatis少了很多;Mybatis框架使用起来很灵活,开发者可以自定义查询语句,但增加了模板代码的数量,看起来没有Hibernate那么便捷。两种框架在便捷与灵活两个指标上做出了取舍与妥协,这不能说是框架的错。对于一个框架而言,需要有自身专注的领域和设计愿景,不可能面面俱到。 使用任何一种技术框架,都需要贴合现实的业务需求以及自身的技术能力。当你还没有深入的去了解一门技术或者当前业务需求无法与框架契合时,不要盲目的批判框架的好坏。今天,我不再去对比Hibernate与Mybatis两者之间的优劣,而是给出一个比较中庸的放方案,将两个ORM框架同时整合在一个项目中。 一、准备开发环境 如果你想成功运行本文中的源代码,需要满足一下的几个条件: 1、JDK : JDK 1.8.x及以上版本2、Maven : Maven 3.x或更高版本3、Git:版本控制工具,选择一个你喜欢的4、IDE : 选择你比较喜欢的一个代码编辑器,如STS、IntelliJ IDEA。笔者使用的是IntelliJ IDEA5、Databases : 选择一个你熟练使用的数据库系统。笔者在本文中使用的是MySQL 5.1.x版本的数据库系统如需获取本次分享内容的源代码进调试,可以到文章末尾找到源代码仓库连接 二、搭建项目2-1、引入依赖 为了快速构建项目,笔者采用Spring Boot来构建项目,同时使用加入Spring Data JPA和Mybatis两个ORM框架的依赖包。在此需要特别说明,Hibernate是一个JPA标准的实现,尔Spring Data JPA是一个JPA数据访问抽象,通过Spring Data JPA,可以轻松使用Hibernate框架。 你可以通过Spring Initializer来初始化项目,也可以通过IDEA自带的Spring Initializer功能构建项目,项目构建完成之后,pom.xml文件中的配置如下(包含但不限于文中给出的依赖项): 2-2、定义实体类-User.java 为了演示同时使用Hibernate和Mybatis操作数据库,需要提供一个实体类User.java,代码如下所示: 说明:在本次演示的项目中,使用到了Lombok插件,它可以让开发者减少模板代码的书写,提高开发速度。@Data注解可以自动生成类属性的getter、setter和toString方法。@NoArgsConstructor会自动为类生成无参构造函数,@AllArgsConstructor则会生成带全部属性的构造函数。 2-3、定义数据持久化接口 在本次课程中,将使用Spring Data JPA来完成写操作,如新增、修改、删除;使用Mybatis来完成读操作,如根据用户ID查询、查询所有的用户等。Spring Data JPA和MyBatis的持久化接口都位于com.ramostear.hm.orm包下,Spring Data JPA的持久化接口相对比较简单,之间继承JpaRepository类即可,代码如下: 说明:因为JPA只负责写操作,所以直接继承并使用JpaRepository提供的API即可,不需要额外的定义其他的接口方法。 下面是Mybatis的映射接口,定义了两个方法:根据ID查询用户信息和查询所有的用户信息。代码如下所示: 说明:此接口需要注意的地方是@Component和@Mapper注解,@Component注解标注此接口后,Spring会自动扫描并配置此类;@Mapper注解是把这个mapper的DAO交由Spring进行管理。 定义完Mybatis 映射接口后,需要提供一个进行数据库查询的xml配置文件。该文件位于resources/mapper文件夹中,UserMapper.xml完整代码如下: 2-4、定义UserService 在UserService接口中,提供三个方法:保存用户信息、根据ID查询用户、查询所有的用户。UserService接口代码如下: 在UserService接口的实现类中,需要同时注入UserRepository和UserMapper两个依赖。我们使用构造函数的方式来注入这两个依赖。代码如下: 说明:@Transactional注解用于设置每个方法的事务控制方式。@Service注解声明该类是一个服务提供类,且设置了该类被Spring初始化时Bean对象的名称为“userService”。 2-5、定义控制器 最后,提供一个控制器,用于处理客户端的相关请求。在控制器中,提供了三个请求处理方法,分别处理客户端新增用户、根据ID查询用户和查询所有用户的请求。控制器代码如下: 说明:在本次教程中,为了编码IDEA报警告,所有的依赖注入都采用构造函数的方式注入相关的依赖。 三、配置Hibernate和Mybatis 网络上有很多关于在Spring Boot项目中配置Hibernate和Mybatis的教程,但同时配置Hibernate和Mybatis的文章很少,有一些是通过Java代码的方式对这两个ORM框架进行配置,采用的是多数据源的方法来整合两个框架。其实整合这两个框架没有想象中的那么难,只需要在application.yml或者application.properties配置文件中加入几行代码,就可以完成两个框架的整合。以application.yml配置文件为例,配置代码如下: ...

June 14, 2019 · 1 min · jiezi

JeecgBoot-201-版本发布企业级快速开发平台

Jeecg-Boot 2.0.1 版本发布,前后端分离快速开发平台Jeecg-Boot项目简介Jeecg-boot 是一款基于代码生成器的快速开发平台!采用前后端分离技术:SpringBoot,Mybatis,Shiro,JWT,Vue & AntDesign。提供强大的代码生成器, 前端页面和后台代码一键生成,不需要写任何代码,保持jeecg一贯的强大,绝对是全栈开发者福音!!JeecgBoot的宗旨是降低前后端分离的开发成本,提高UI能力的同时提高开发效率,追求更高的能力,No代码概念,一系列智能化在线开发。源码下载源码下载:https://github.com/zhangdaisc...在线文档:http://jeecg-boot.mydoc.io在线演示:http://boot.jeecg.org入门视频:http://t.cn/E9mFjet常见问题:http://t.cn/EITSuH8升级日志本版本主要录制全新教学视频和修复2.0.0版本的一系列bug,提供稳定版本供大家使用录制新版入门教程视频用户列表无编辑按钮,编辑提示无权限非admin用户,角色授权、部门、用户等操作提示无权限提示 Parameter useId not found (SysAnnouncementMapper的userid找不到)Type definition error: [simple type, class org.jeecg.modules.system.model.SysUserDepartsVO]无构造器问题oracle下部门数据列表是空问题系统公告提示资源找不到问题类别统计报表菜单删除,此功能是测试未完全实现部门管理怎么添加子部门: 选中部门,鼠标右键,添加子部门或删除子部门老菜单访问404,因为V2.0版本,菜单增加了一个类型“是否路由”,请设置“是”项目打可执行jar包,缺少maven plugin插件的问题添加新菜单访问404问题解决,改造程序,默认设置菜单路由类型默认值“是”系统展示

June 7, 2019 · 1 min · jiezi

Springboot2快速集成minidao持久层

Springboot2 快速集成minidao持久层这里采用springboot版本号: 2.0.4.RELEASEminidao已经提供自定义starter,集成非常简单,直接pom引入minidao-spring-boot-starter依赖即可集成步骤: 第一步: pom引入starter依赖包<dependency> <groupId>org.jeecgframework</groupId> <artifactId>minidao-spring-boot-starter</artifactId> <version>1.6.7.RELEASE</version></dependency>此starterd对应的minidao最新版本 1.6.7 默认提供了mysql的依赖。 第二步:配置minidao的配置参数 (application.properties 或者 application.yml) minidao: base-package: com.springBoot.* db-type: mysql show-sql: true第三步: 配置springjdbc所需数据源 ( application.yml) spring: datasource: url: jdbc:mysql://localhost:3306/minidao-pe username: root password: root driver-class-name: com.mysql.jdbc.Driver通过以上三步,minidao集成完毕。 参考源码下载: https://download.csdn.net/dow...技术交流群:325978980 Minidao常见配置参数说明:参数名用途默认值base-packageminidao扫描路径*db-type数据库类型,常用配置: mysql/postgresql/oracle/sqlservermysqlshow-sql是否打印sqltrueempty-interceptorminidao拦截器的bean名字空keyType是使用什么字母做关键字Map的关键字 默认值origin 即和sql保持一致,lower小写(推荐),upper 大写origin

June 7, 2019 · 1 min · jiezi

我就是不看好jpa

知乎看到问题《SpringBoot开发使用Mybatis还是Spring Data JPA??》,顺手一答,讨论激烈。我实在搞不懂spring data jpa为啥选了hibernate作为它的实现,是“Gavin King”的裙带关系么?DAO层搞来搞去,从jdbc到hibernate,从toplink到jdo,到现在MyBatis胜出,是有原因的。 目前,一些狗屁培训公司,还有一些网络课程,包括一些洋课程,为了做项目简单,很多会使用jpa。但到了公司发现根本就不是这样,这就是理论和现实的区别。 顶着被骂的风险,我整理发出来。这可不是争论php好还是java好性质了。 忠告:精力有限,一定要先学MyBatis啊。jpa那一套,表面简单而已。以下是原始回答如果你经历过多个公司的产品迭代,尤其是复杂项目,你就会发现Spring Data JPA这种东西是多么的不讨好。说实话,Mybatis的功能有时候都嫌多,有些纯粹是画蛇添足。 jpa虽然是规范,但和hibernate这种ORM是长得比较像的(不说一些特殊场景的用法)。 Spring Data JPA 是个玩具,只适合一些简单的映射关系。也不得不提一下被人吹捧的querydsl,感觉有些自作聪明的讨巧。实在不想为了一个简单的DAO层,要学一些费力不讨好的东西。 List<Person> persons = queryFactory.selectFrom(person) .where(person.children.size().eq( JPAExpressions.select(parent.children.size().max()) .from(parent))) .fetch();看看上面的查询语句,完全不如普通SQL表达的清晰。要是紧急排查个问题,妈蛋... jpa虽然有很多好处,比如和底层的SQL无关。但我觉得Spring Data JPA有以下坏处: 1、 屏蔽了SQL的优雅,发明了一种自己的查询方式。这种查询方式并不能够覆盖所有的SQL场景。 2、 增加了代码的复杂度,需要花更多的时间来理解DAO 3、DAO操作变的特别的分散,分散到多个java文件中,或者注解中(虽然也支持XML)。如果进行一些扫描,或者优化,重构成本大 4、不支持复杂的SQL,DBA流程不好切入 Mybatis虽然也有一些问题,但你更像是在写确切的SQL。它比Spring Data JPA更加轻量级。 你只要在其他地方调试好了SQL,只需要写到配置文件里,起个名字就可以用了,少了很多烧脑的转换过程。 Mybatis完全能够应对工业级的复杂SQL,甚至存储过程(不推荐)。个人认为Mybatis依然是搞复杂了,它其中还加了一些类似if else的编程语言特性。 你的公司如果有DBA,是不允许你乱用SQL的。用Mybatis更能切入到公司的流程上。 所以我认为:玩具项目或者快速开发,使用Spring Boot JPA。反之,Mybatis是首选。 一些有用的评论你说的if else是指的SQL拼接吧,这可是MyBatis的重要功能之一,学起来一点儿也不复杂好吗? 最近也在研究持久层,可以充分利用这个jpa这个玩具,两者结合是不错的选择,jpa基本的单表操作,mybatis做复杂查询,开发效率高,降低sql维护成本,也为优化留下空间,当然这需要对spring-data-jpa做一些扩展 查询直接sql,其他的还是orm方便 mybatis主要是原生sql,对于其他没学习过jpa的开发人员而言降低了学习维护门槛,而且说真的jpa写了个锅你去追其实还是挺头疼的... mybatis-plus整合之后基本curd不用纠结了,很多对对象操作然后直接save就好。复杂场景、联表就直接用原生sql就好,至于性能问题可以用sqlAdvice优化下。 jdbc template+代码生成器,更简单更高效 jpa这玩意,写一个简单的数据库操作,就比如说单表的操作,很好用。如果是多表,那就算了吧 spring boot 推荐jpa,知道为什么吗 native=true 想用原生查询也没人拦着你啊 你好像不是很懂hibernate和jpa… 不知道你是否清楚 jpa,hibernate,spring data jpa,还有querydsl 之间的关系。 总有一天你会知道数据库优先和程序优先的区别 jpa还有一个好处,那就是帅锅啊 END来,越年轻越狂妄的家伙们,来喷我啊。 ...

June 3, 2019 · 1 min · jiezi

MyBatis的原理

MyBatis核心类SqlSessionFactory每一个MyBatis应用都是以一个SqlSessionFactory的实例为核心构建的。SqlSessionFactory的核心作用是什么? 从类的名称上可以看出来,SqlSessionFactory是产生SqlSession的工厂。SqlSessionFactory是通过SqlSessionFactoryBuilder这个构建器来构建的。SqlSessionFactory是一个接口,其中定义了获取SqlSession的方法。 public interface SqlSessionFactory { //获取一个SqlSession SqlSession openSession(); //获取一个SqlSession,参数设置事务是否自动提交 SqlSession openSession(boolean var1); //通过指定Connection中的参数获取 SqlSession openSession(Connection var1); //获取SqlSession,设置事务的隔离级别,NONE(0),READ_COMMITTED(2),READ_UNCOMMITTED(1),REPEATABLE_READ(4),SERIALIZABLE(8); SqlSession openSession(TransactionIsolationLevel var1); //获取SqlSession,同时制定执行的类别,支持三种SIMPLE(这种模式下,将为每个语句创建一个PreparedStatement),REUSE(这个模式下重复使用preparedStatment),BATCH(批量更新,insert时候,如果没有提交,无法获取自增id); SqlSession openSession(ExecutorType var1); SqlSession openSession(ExecutorType var1, boolean var2); SqlSession openSession(ExecutorType var1, TransactionIsolationLevel var2); SqlSession openSession(ExecutorType var1, Connection var2); //获取所有的配置项 Configuration getConfiguration();}SqlSessionFactory包含两个实现:DefaultSqlSessionFactory和SqlSessionManagerSqlSessionFactory的实例如何创建呢?1、通常我们是在只有MyBatis的项目中是使用下面的代码段来创建的: String resource = "org/mybatis/example/mybatis-config.xml";//读取mybatis配置的问题InputStream inputStream = Resources.getResourceAsStream(resource);//通过SqlSessionFactoryBuilder的build方式创建SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);2、在Spring boot和MyBatis结合的项目中我会使用下面的代码段来创建: MyBatis-Spring-Boot-Starter依赖将会提供如下自动检测现有的DataSource将创建并注册SqlSessionFactory的实例,该实例使用SqlSessionFactoryBean将该DataSource作为输入进行传递将创建并注册从SqlSessionFactory中获取的SqlSessionTemplate的实例。自动扫描您的mappers,将它们链接到SqlSessionTemplate并将其注册到Spring上下文,以便将它们注入到您的bean中。就是说,使用了该Starter之后,只需要定义一个DataSource即可(application.properties中可配置),它会自动创建使用该DataSource的SqlSessionFactory Bean以及SqlSessionTemplate。会自动扫描你的Mappers,连接到SqlSessionTemplate,并注册到Spring上下文中.@Beanpublic SqlSessionFactory sqlSessionFactory(@Qualifier("druidDataSource") DataSource druidDataSource) throws Exception { //先创建一个SqlSessionFactoryBean SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); //在这个bean里设置需要的参数 fb.setDataSource(this.dynamicDataSource(druidDataSource)); fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package")); fb.setTypeHandlersPackage(env.getProperty("mybatis.type-handlers-package")); fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations"))); //通过这个方法获取一个SqlSessionFactory return fb.getObject();}在代码里一直向下查找的时候就会发现:这个过程其实和上面的过程一样。SqlSessionFactoryBean会将一系列的属性封装成一个Configuration对象,然后调用this.sqlSessionFactoryBuilder.build(configuration) 来创建。而在sqlSessionFactoryBuilder里主要就是解析资源的内容,然后进行创建。 ...

May 30, 2019 · 3 min · jiezi

MyBatis连接管理2

MyBatis连接管理(1) MySQL连接管理在调用SqlSessionFactory的openSession函数时,只是创建了一个DefaultSqlSession实例,并没有真正去连接MySQL: private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; DefaultSqlSession var8; try { Environment environment = this.configuration.getEnvironment(); TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); Executor executor = this.configuration.newExecutor(tx, execType); var8 = new DefaultSqlSession(this.configuration, executor, autoCommit); } catch (Exception var12) { this.closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12); } finally { ErrorContext.instance().reset(); } return var8; }只有当真正执行SQL语句时,比如query,才通过SimpleExecutor调用doQuery,最终通过Transaction调用DataSource的popConnection: ...

May 27, 2019 · 3 min · jiezi

MyBatis连接管理1

DateSource的创建过程MyBatis的数据源(Date Source)分为:UNPOOLED、POOLED和JNDI,我们主要看下POOLED也就是连接池方式,它的配置如下: <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <package name="core.mappers"/> </mappers></configuration>这里注意,MyBatis底层也是使用MySQL提供的JDBC driver,所以要记得在工程里包含这个库。它还可以配置以下一些属性: poolMaximumActiveConnections – 在任意时间可以存在的活动(也就是正在使用)连接数量,默认值:10poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。poolMaximumCheckoutTime – 在被强制Roll Back(认为是Overdue)之前,池中连接可以被检出(checked out使用)的时间,默认值:20000 毫秒(即 20 秒)poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直安静的失败),默认值:20000 毫秒(即 20 秒)。poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。 默认值:3 (新增于 3.4.5)poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动失败时带有一个恰当的错误消息。poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。在利用SqlSessionFactoryBuilder创建DefaultSqlSessionFactory时,通过XMLConfigBuilder来读取上面config文件里的内容,看到dataSource的类型为POOLED,则利用反射创建PooledDataSourceFactory: ...

May 27, 2019 · 1 min · jiezi

Spring系列实战篇4你有多了解MyBatis

1. 概念在我上大学的时候,最流行的JavaEE框架是 SSH (Struts+Spring+Hibernate),现在同学们应该都在学 SSM(Spring+SpringMVC+MyBatis)了。从历史演变来看,Spring是越来越强大,而MyBatis则是顶替了Hibernate的地位。今天的“主角”就是MyBatis。 1.1. ORM的历史演变我们先聊一聊ORM(Object Relational Mapping),翻译为“对象关系映射”,就是通过实例对象的语法,完成关系型数据库的操作的技术。ORM用于实现面向对象编程语言里不同类型系统的数据之间的转换,其实是创建了一个可在编程语言里使用的"虚拟对象数据库"。 ORM 把数据库映射成对象: 数据库的表(table) --> 类(class)记录(record,行数据)--> 对象(object)字段(field)--> 对象的属性(attribute)基于传统ORM框架的产品有很多,其中就有耳熟能详的Hibernate。ORM通过配置文件,使数据库表和JavaBean类对应起来,提供简便的操作方法,增、删、改、查记录,不再拼写字符串生成sql,编程效率大大提高,同时减少程序出错机率,增强数据库的移植性,方便测试。 但是有些时候我还是喜欢原生的JDBC,因为在某些特殊的应用场景中,对于sql的应用复杂性比较高,或者需要对sql的性能进行优化,这些ORM框架就显得很笨重。Hibernate这类“全自动化”框架,对数据库结构封装的较为完整,这种一站式的解决方案未必适用于所有的业务场景。 幸运的是,不只我一个人有这种感受,很久之前大家开始关注一个叫 iBATIS 的开源项目,它相对传统ORM框架而言更加的灵活,被定义为“半自动化”的ORM框架。2010年,谷歌接管了iBATIS,MyBatis就随之诞生了。虽然2010年我都还没上大学,但很可惜,MyBatis在国内的大火的比较晚,我在校园期间都没有接触过。 1.2. 开启MyBatisMyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 MyBatis为半自动化,需要自己书写sql语句,需要自己定义映射。增加了程序员的一些操作,但是带来了设计上的灵活。并且也是支持Hibernate的一些特性,如延迟加载,缓存和映射等,而且随之SSM架构的成熟,MyBatis肯定会被授予有越来越多新的特性。那么接下来就开始 MyBatis 的实战演练吧! 2. MyBatis 基本使用下面讲解在SpringBoot 中,使用MyBatis的基本操作。 2.1. 基础配置在SpringBoot中集成 MyBatis 的方式很简单,只需要引用 MyBatis的starter包即可,不过针对不同的数据源,需要导入所依赖的驱动jar包(如:mysql(mysql-connector-java-x.jar)/oracle(ojdbcx.jar)/sql server(sqljdbcx.jar)等) pom.xml(示例) <!--mybatis--><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version></dependency><!--oracle jdbc--><dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>6</version></dependency><!--druid 数据源--><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version></dependency>对于相关数据源的连接信息,需要在application.properties中配置,同样提供示例 # Oracle数据库的连接信息spring.datasource.url=jdbc:oracle:thin:@ip:port/instancespring.datasource.username=usernamespring.datasource.password=passwordspring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver#mybatis 驼峰式命名映射,可将ResultMap返回值通过驼峰式映射给pojomybatis.configuration.map-underscore-to-camel-case=true#mybatis xml文件路径mybatis.mapper-locations=classpath:mapper/*Mapper.xml#开启mybatis dao层的日志logging.level.com.df.stage.tasktimer.mapper=debug2.2. 使用MyBatis方式一:xml配置MyBatis3 之前,需要手动获取SqlSession,并通过命名空间来调用MyBatis方法,比较麻烦。而MyBatis3 就开始支持接口的方式来调用方法,这也成为当前即为普遍的用法,本文就以此为例。 ...

May 27, 2019 · 2 min · jiezi

Mybatis源码分析一一条sql语句如何被执行

本篇为原创文章,如需转载,请标明原创地址。 我先写一个简单的例子来执行一条sql语句mapper.xml <mapper namespace="com.example.demo1.mybatis.ArticleMapper"> <select id="selectById" resultType="com.example.demo1.mybatis.Article" parameterType="java.lang.Long"> select <include refid="baseColumns"/> from article where 1= 1 and id = #{id} </select> <sql id="baseColumns"> id,title </sql></mapper>实体类 @Datapublic class Article { private Long id; private String title;}测试类 public class MybatisTest { public static void main(String[] args) throws IOException { // sqlSessionFactory是一个复杂对象,通常创建一个复杂对象会使用建造器来构建,这里首先创建建造器 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // configuration对象对应mybatis的config文件,为了测试简便,我这里直接创建Configuration对象而不通过xml解析获得 Configuration configuration = new Configuration(); configuration.setEnvironment(buildEnvironment()); // 解析一个mapper.xml为MappedStatement并加入到configuration中 InputStream inputStream = Resources.getResourceAsStream("mybatis/Article.xml"); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "mybatis/Article.xml", configuration.getSqlFragments()); mapperParser.parse(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(configuration); // 创建一个sqlSession,这里使用的是简单工厂设计模式 SqlSession sqlSession = sqlSessionFactory.openSession(); // 执行最终的sql,查询文章id为1的文章 Article article = sqlSession.selectOne("com.example.demo1.mybatis.ArticleMapper.selectById",1L); // 打印文件的标题 System.out.println(article.getTitle()); // sqlSession默认不会自动关闭,我们需要手动关闭 sqlSession.close(); } private static Environment buildEnvironment() { return new Environment.Builder("test") .transactionFactory(getTransactionFactory()) .dataSource(getDataSource()).build(); } private static DataSource getDataSource() { String url = "url"; String user = "user"; String password = "password"; Properties properties = new Properties(); properties.setProperty("url", url); properties.setProperty("username", user); properties.setProperty("password", password); properties.setProperty("driver", "com.mysql.jdbc.Driver"); properties.setProperty("driver.encoding", "UTF-8"); PooledDataSourceFactory factory = new PooledDataSourceFactory(); factory.setProperties(properties); DataSource dataSource = factory.getDataSource(); return dataSource; } private static TransactionFactory getTransactionFactory() { return new JdbcTransactionFactory(); }分析sqlSession.selectOne("com.example.demo1.mybatis.ArticleMapper.selectById",1L); ...

May 23, 2019 · 3 min · jiezi

springboot结合MyBatis中使用foreach

废话少说,直接建项目上代码 先建一个springboot web项目或者模块,目录结构如下 在pom.xml中自行加入需要的依赖,application.properties的配置如下 server.port=8888mybatis.mapper-locations=classpath*:mapper/*Mapper.xmlspring.datasource.url=jdbc:mysql://localhost:3306/ssm?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8spring.datasource.username=rootspring.datasource.password=Panbing936@spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver实体类User.java @Data@NoArgsConstructor@AllArgsConstructorpublic class User { private int id; private String name;}UserMapper.xml中使用foreach接收list参数 <!-- 使用foreach接收list参数 --> <select id="getUsersByListParam" resultType="cn.niit.mybatisforeach.pojo.User"> select * from t_user <foreach collection="list" item="item" open="where id in(" close=")" separator=","> #{item} </foreach> </select>使用foreach批量插入用户 <!-- 使用foreach批量插入用户--> <insert id="insertMultiUsers"> insert into t_user(id,name) values <foreach collection="users" item="user" separator=","> (#{user.id},#{user.name}) </foreach> </insert>使用foreach接收array数组 <!--使用foreach接收array数组--> <select id="getUserByArrayList" parameterType="java.util.ArrayList" resultType="cn.niit.mybatisforeach.pojo.User"> select * from t_user where id in <foreach collection="array" index="index" item="item" open="(" separator="," close=")"> #{item} </foreach> </select>使用foreach接收Map <!--使用foreach接收Map--> <select id="getUserByMap" parameterType="java.util.HashMap" resultType="cn.niit.mybatisforeach.pojo.User"> select * from t_user where id in <foreach collection="ids" index="index" item="item" open="(" separator="," close=")"> #{item} </foreach> </select>UserMapper.java@Mapper@Componentpublic interface UserMapper { /** * 多个参数查询user集合 */ List<User> getUsersByListParam(@Param("list") List list); /** * 使用foreach批量插入 */ int insertMultiUsers(@Param("users") List<User> users); /** * 使用foreach接收数组参数 */ List getUserByArrayList(int[] ids); /** * 使用foreach接收map */ List getUserByMap(Map params);}测试代码MybatisForeachApplicationTests.java 中的代码如下 ...

May 22, 2019 · 2 min · jiezi

Intellij-Idea-中进行-Mybatis逆向工程

开篇Mybatis有个实用的功能就是逆向工程,能根据表结构反向生成实体类,这样能避免手工生成出错。市面上的教程大多都很老了,大部分都是针对mysql5的,以下为我执行mysql8时的经验。 引入工程这里使用的是maven包管理工具,在pom.xml添加以下配置,以引入mybatis.generator <build> <finalName>SpringMVCBasic</finalName> <!-- 添加mybatis-generator-maven-plugin插件 --> <plugins> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> </plugin> </plugins> </build>配置文件在maven项目下的src/main/resources 目录下新建generatorConfig.xml和generator.properties文件 generator.properties jdbc.driverLocation=F:\\maven-repository\\mysql\\mysql-connector-java\\8.0.16\\mysql-connector-java-8.0.16.jarjdbc.driverClass=com.mysql.cj.jdbc.Driverjdbc.connectionURL=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8jdbc.userId=testjdbc.password=test123注意:1,generator.properties里面的jdbc.driverLocation指向是你本地maven库对应mysql-connector地址2,与老版本不同,这里driversClass为com.mysql.cj.jdbc.Driver generatorConfig.xml <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <!--导入属性配置--> <properties resource="generator.properties"></properties> <!--指定特定数据库的jdbc驱动jar包的位置(绝对路径)--> <classPathEntry location="${jdbc.driverLocation}"/> <context id="default" targetRuntime="MyBatis3"> <!-- optional,旨在创建class时,对注释进行控制 --> <commentGenerator> <!--是否去掉自动生成的注释 true:是--> <property name="suppressDate" value="true"/> <property name="suppressAllComments" value="true"/> </commentGenerator> <!--jdbc的数据库连接:驱动类、链接地址、用户名、密码--> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.connectionURL}" userId="${jdbc.userId}" password="${jdbc.password}"> </jdbcConnection> <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制--> <javaTypeResolver> <property name="forceBigDecimals" value="false"/> </javaTypeResolver> <!-- Model模型生成器,用来生成含有主键key的类,记录类 以及查询Example类 targetPackage 指定生成的model生成所在的包名 targetProject 指定在该项目下所在的路径 --> <javaModelGenerator targetPackage="com.ifly.outsourcing.entity" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> <property name="trimStrings" value="true"/> </javaModelGenerator> <!--Mapper映射文件生成所在的目录 为每一个数据库的表生成对应的SqlMap文件 --> <sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources"> <property name="enableSubPackages" value="false"/> </sqlMapGenerator> <!-- 客户端代码,生成易于使用的针对Model对象和XML配置文件 的代码 type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper对象 type="MIXEDMAPPER",生成基于注解的Java Model 和相应的Mapper对象 type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.ifly.outsourcing.dao" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 数据表进行生成操作 tableName:表名; domainObjectName:对应的DO --> <table tableName="user" domainObjectName="user" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"> </table> </context></generatorConfiguration>注意:这里主要注意修改对应的javaModelGenerator ,sqlMapGenerator,javaClientGenerator 为自己的生成路径。以及添加自己的数据表。 ...

May 21, 2019 · 1 min · jiezi

19051501记录一次日常犯的错

Parameter 'array' not found. Available parameters are [collection, list]莫名其妙,今天写代码遇到个低级错误,困扰了好久,测试突然给提了个缺陷,说业务逻辑有问题于是,就启动了缺陷排查的流程1.问题复现   根据问题复现步骤,确实发现业务逻辑不对2.代码排查   根据代码排查,业务逻辑确实写了,对表的更新3.日志排查   根据日志排查,发现新增的代码并没有执行,而且,也没有报错。随后就进行了纠结(现在都想敲死自己,应该不用纠结,在编辑器debug跑一遍,问题就暴露出来了)。4.解决问题   先使用单测,跑了一遍对应的方法,发现确实没有问题,所以怀疑,是因为MOCK掉的DAO方法,抛了一个异常,然后没有显式的抛出来,所以就手动debug启动了下应用,就是POSTMAN测试,果然,报错如下: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'array' not found. Available parameters are [collection, list]这里是因为,在mybaits传集合参数,进行循环时,一定要指定集合类型,目前mybaits对List集合和Array集合,是不同,需要在循环时指定对应的集合,如果使用类似于Long[] 等进行传参时,一定要指定collection="array",如果使用List进行传参时,需要指定collection="list",否则就会抛异常。至于为什么在服务器上没有抛异常出来,很可能是被框架给吃掉了,需要进一步排查。

May 15, 2019 · 1 min · jiezi

mybatis处理枚举类

mybatis自带对枚举的处理类org.apache.ibatis.type.EnumOrdinalTypeHandler<E> :该类实现了枚举类型和Integer类型的相互转换。但是给转换仅仅是将对应的枚举转换为其索引位置,也就是"ordinal()"方法获取到的值。对应自定义的int值,该类无能为力。 org.apache.ibatis.type.EnumTypeHandler<E>:该类实现了枚举类型和String类型的相互转换。对于想将枚举在数据库中存储为对应的int值的情况,该类没办法实现。 基于以上mybatis提供的两个枚举处理类的能力有限,因此只能自己定义对枚举的转换了。 自定义mybatis的枚举处理类EnumValueTypeHandler该类需要继承org.apache.ibatis.type.BaseTypeHandler<E>,然后在重定义的方法中实现自有逻辑。 import java.sql.CallableStatement;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import org.apache.ibatis.type.MappedTypes;import org.apache.ibatis.type.BaseTypeHandler;import org.apache.ibatis.type.JdbcType;/** * 处理实现了{@link EsnBaseEnum}接口的枚举类 * @author followtry * @time 2016年8月16日 下午8:06:49 * @since 2016年8月16日 下午8:06:49 */ //在 xml 中添加该 TypeHandler 时需要使用该注解@MappedTypes(value = { QcListTypeEnum.class, SellingQcBizTypeEnum.class})public class EnumValueTypeHandler<E extends EsnBaseEnum> extends BaseTypeHandler<E> { private Class<E> type; private final E[] enums; public EnumValueTypeHandler(Class<E> type) { if (type == null) { throw new IllegalArgumentException("Type argument cannot be null"); } this.type = type; this.enums = type.getEnumConstants(); if (this.enums == null) { throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type."); } } @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { //获取非空的枚举的int值并设置到statement中 ps.setInt(i, parameter.getValue()); } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { int i = rs.getInt(columnName); if (rs.wasNull()) { return null; } else { try { return getEnumByValue(i); } catch (Exception ex) { throw new IllegalArgumentException( "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex); } } } /** * 通过枚举类型的int值,获取到对应的枚举类型 * @author jingzz * @param i */ protected E getEnumByValue(int i) { for (E e : enums) { if (e.getValue() == i) { return e; } } return null; } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int i = rs.getInt(columnIndex); if (rs.wasNull()) { return null; } else { try { return getEnumByValue(i); } catch (Exception ex) { throw new IllegalArgumentException( "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex); } } } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int i = cs.getInt(columnIndex); if (cs.wasNull()) { return null; } else { try { return getEnumByValue(i); } catch (Exception ex) { throw new IllegalArgumentException( "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex); } } }}该处理器是处理继承了EsnBaseEnum接口的枚举类,因为该接口中定义了获取自定义int值的方法。 ...

May 13, 2019 · 3 min · jiezi

浅析MyBatis的动态代理原理

前言一直以来都在使用MyBatis做持久化框架,也知道当我们定义XXXMapper接口类并利用它来做CRUD操作时,Mybatis是利用了动态代理的技术帮我们生成代理类。那么动态代理内部的实现细节到底是怎么的呀?XXXMapper.java类和XXXMapper.xml到底是如何关联起来的呀?本篇文章就来详细剖析下MyBatis的动态代理的具体实现机制。 MyBatis的核心组件及应用在详细探究MyBatis中动态代理机制之前,先来补充一下基础知识,认识一下MyBatis的核心组件。 SqlSessionFactoryBuilder(构造器): 它可以从XML、注解或者手动配置Java代码来创建SqlSessionFactory。SqlSessionFactory: 用于创建SqlSession (会话) 的工厂SqlSession: SqlSession是Mybatis最核心的类,可以用于执行语句、提交或回滚事务以及获取映射器Mapper的接口SQL Mapper: 它是由一个Java接口和XML文件(或注解)构成的,需要给出对应的SQL和映射规则,它负责发送SQL去执行,并返回结果注意: 现在我们使用Mybatis,一般都是和Spring框架整合在一起使用,这种情况下,SqlSession将被Spring框架所创建,所以往往不需要我们使用SqlSessionFactoryBuilder或者SqlSessionFactory去创建SqlSession下面展示一下如何使用MyBatis的这些组件,或者如何快速使用MyBatis: 数据库表CREATE TABLE user( id int, name VARCHAR(255) not NULL , age int , PRIMARY KEY (id))ENGINE =INNODB DEFAULT CHARSET=utf8;声明一个User类@Datapublic class User { private int id; private int age; private String name; @Override public String toString() { return "User{" + "id=" + id + ", age=" + age + ", name='" + name + '\'' + '}'; }}定义一个全局配置文件mybatis-config.xml (关于配置文件中具体属性标签解释参阅官方文档)<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><!--全局配置文件的根元素--><configuration> <!--enviroments表示环境配置,可以配置成开发环境(development)、测试环境(test)、生产环境(production)等--> <environments default="development"> <environment id="development"> <!--transactionManager: 事务管理器,属性type只有两个取值:JDBC和MANAGED--> <transactionManager type="MANAGED" /> <!--dataSource: 数据源配置--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root" /> <property name="password" value="root" /> </dataSource> </environment> </environments> <!--mappers文件路径配置--> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers></configuration>UserMapper接口public interface UserMapper { User selectById(int id);}UserMapper文件<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--namespace属性表示命令空间,不同xml映射文件namespace必须不同--><mapper namespace="com.pjmike.mybatis.UserMapper"> <select id="selectById" parameterType="int" resultType="com.pjmike.mybatis.User"> SELECT id,name,age FROM user where id= #{id} </select></mapper>测试类public class MybatisTest { private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream("mybatis-config.xml")); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectById(1); System.out.println("User : " + user); } }}// 结果:User : User{id=1, age=21, name='pjmike'}上面的例子简单的展示了如何使用MyBatis,与此同时,我也将用这个例子来进一步探究MyBatis动态原理的实现。 ...

May 9, 2019 · 5 min · jiezi

解密Mybatis手写Mybatis框架二

简化版Mybatis实现思路1.创建SqlSessionFactory实例.2.实例化过程中,加载配置文件创建configuration对象.3.通过factory创建SqlSession对象,把configuaration传入SqlSession.4.通过SqlSession获取mapper接口动态代理5.通过代理对调sqlsession中查询方法;6.sqlsession将查询方法转发给executor;7.executor基于JDBC访问数据库获取数据;8.executor通过反射将数据转换成POJO并返回给sqlsession;9.数据返回给调用者 上节讲到快速入门mybatis的demo三大阶段 // 1.读取mybatis配置文件创SqlSessionFactoryString resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);inputStream.close();//-------------第二阶段-------------// 2.获取sqlSessionSqlSession sqlSession = sqlSessionFactory.openSession();// 3.获取对应mapperTUserMapper mapper = sqlSession.getMapper(TUserMapper.class);//-------------第三阶段-------------// 4.执行查询语句并返回结果TUser user = mapper.selectByPrimaryKey(1);System.out.println(user.toString());第一阶段:第一阶段先把配置文件加载到内存,包括数据库信息和mapper.xml。 针对mapper.xml我们定义一个MappedStatement类来存入相应信息. public class MappedStatement { //此处忽略getset方法 private String namespace; private String sourceId;//mapper接口路径+xml里面的每一个id private String sql;//sql语句 private String resultType;//返回类型 }再定义一个全局配置信息即Configuration存放所有配置信息: public class Configuration { //记录mapper xml文件存放的位置 public static final String MAPPER_CONFIG_LOCATION = "config"; //记录数据库连接信息文件存放位置 public static final String DB_CONFIG_FILE = "db.properties"; private String dbUrl; private String dbUserName; private String dbPassword; private String dbDriver; //mapper xml解析完以后select节点的信息存放在mappedStatements,key为MappedStatement里面 //的sourceId protected final Map<String, MappedStatement> mappedStatements = new HashMap<String, MappedStatement>(); //为mapper接口生成动态代理的方法public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return MapperProxyFactory.getMapperProxy(sqlSession, type);}}SqlSessionFactory实例化,并加载configuaration对象信息,这样就把所有的配置信息加载到内存里 ...

May 9, 2019 · 4 min · jiezi

解密Mybatis手写Mybatis框架一

Mybatis快速入门步骤: 1.加入mybatis的依赖2.添加Mybatis的配置文件3.场景介绍4.编写实体类丶mapper接口及mapper.xml文件5.编写测试代码demo: public class TUser { private Integer id; private String userName; private String realName; private Byte sex; private String mobile; private String email; private String note; private Integer positionId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getRealName() { return realName; } public void setRealName(String realName) { this.realName = realName; } public Byte getSex() { return sex; } public void setSex(Byte sex) { this.sex = sex; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } public Integer getPositionId() { return positionId; } public void setPositionId(Integer positionId) { this.positionId = positionId; } }public interface TUserMapper { TUser selectByPrimaryKey(Integer id); }TUserMapper.xml: ...

May 8, 2019 · 2 min · jiezi

从零开始搭建SSM框架Spring-Spring-MVC-Mybatis

最近在回顾和总结一些技术,想到了把之前比较火的 SSM 框架重新搭建出来,作为一个小结,同时也希望本文章写出来能对大家有一些帮助和启发,因本人水平有限,难免可能会有一些不对之处,欢迎各位大神拍砖指教,共同进步。 本文章示例使用 IntelliJ IDEA 来开发,JDK 使用 11 版本,其余各框架和技术基本上使用了文章撰写当时的最新版本。 好的,下面直接进入正题。 打开 IntelliJ IDEA,File > New > Project > Maven,选中“Create from archetype”,然后再选中“org.apache.maven.archetypes:maven-archetype-webapp”: Next,输入项目的“GroupId”、“ArtifactId”和Version: Next,指定“Maven home directory”等配置: Next,修改Project Name: Finish,打开项目,添加一些必要的目录,最终项目框架目录图如下: 修改pom.xml文件,指定各依赖和插件的版本等信息: <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <spring.version>5.1.6.RELEASE</spring.version> <junit.version>4.12</junit.version> <lombok.version>1.18.6</lombok.version> <mybatis-plus.version>3.1.1</mybatis-plus.version> <freemarker.version>2.3.28</freemarker.version> <druid.version>1.1.16</druid.version> <jsqlparser.version>2.0</jsqlparser.version> <mysql-connector.version>8.0.16</mysql-connector.version> <jstl-api.version>1.2</jstl-api.version> <servlet-api.version>4.0.1</servlet-api.version> <jsp-api.version>2.3.3</jsp-api.version> <springfox-swagger.version>2.9.2</springfox-swagger.version> <commons-lang3.version>3.9</commons-lang3.version> <jackson.version>2.9.8</jackson.version> <mapstruct.version>1.3.0.Final</mapstruct.version> <log4j.version>2.11.2</log4j.version> <slf4j.version>1.7.26</slf4j.version> <clean.plugin.version>3.1.0</clean.plugin.version> <resources.plugin.version>3.1.0</resources.plugin.version> <compiler.plugin.version>3.8.0</compiler.plugin.version> <surefire.plugin.version>3.0.0-M3</surefire.plugin.version> <war.plugin.version>3.2.2</war.plugin.version> <install.plugin.version>3.0.0-M1</install.plugin.version> <deploy.plugin.version>3.0.0-M1</deploy.plugin.version></properties>在<dependencyManagement>标签里面管理各依赖的版本号: <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis-plus.version}</version> <scope>test</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>${freemarker.version}</version> <scope>test</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>${jsqlparser.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector.version}</version> </dependency> <dependency> <groupId>javax.servlet.jsp.jstl</groupId> <artifactId>jstl-api</artifactId> <version>${jstl-api.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>${jsp-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${springfox-swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${springfox-swagger.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons-lang3.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${jackson.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${mapstruct.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>${log4j.version}</version> </dependency> </dependencies></dependencyManagement>添加项目依赖: ...

May 3, 2019 · 8 min · jiezi

MyBatis中主键回填的两种实现方式

主键回填其实是一个非常常见的需求,特别是在数据添加的过程中,我们经常需要添加完数据之后,需要获取刚刚添加的数据 id,无论是 Jdbc 还是各种各样的数据库框架都对此提供了相关的支持,本文我就来和和大家分享下数据库主键回填在 MyBatis 中的两种实现思路。 原生写法框架来源于我们学过的基础知识,主键回填实际上是一个在 JDBC 中就被支持的写法,有的小伙伴可能不知道这一点,因此这里我先来说说在 JDBC 中如何实现主键回填。 JDBC 中实现主键回填其实非常容易,主要是在构造 PreparedStatement 时指定需要主键回填,然后在插入成功后,查询刚刚插入数据的 id ,示例代码如下: public int insert(Person person) { Connection con = null; PreparedStatement ps = null; ResultSet rs = null; con = DBUtils.getConnection(); ps = con.prepareStatement("INSERT INTO person(username,password,money) VALUES(?,?,?)", PreparedStatement.RETURN_GENERATED_KEYS); ps.setObject(1, person.getUsername()); ps.setObject(2, person.getPassword()); ps.setObject(3, person.getMoney()); int i = ps.executeUpdate(); rs = ps.getGeneratedKeys(); int id = -1; if (rs.next()) { id = rs.getInt(1); } return id;}和普通的插入 SQL 不同之处主要体现在两个地方: ...

April 24, 2019 · 1 min · jiezi

从 PageHelper 学到的不侵入 Signature 的 AOP

从 PageHelper 学到的不侵入 Signature 的 AOP前言最近搭新项目框架,之前 Mybatis 的拦截器都是自己写的,一般是有个 Page 类型做判断是否增加分页 sql。但是这样同样的业务开放给页面和 api 可能要写两个,一种带分页类型 Page 一种不带分页。发现开源项目 PageHelper 不需要侵入方法的 Signature 就可以做分页,特此来源码分析一下。P.S: 看后面的源码分析最好能懂 mybatis 得拦截器分页插件原理PageHelper 的简答使用 public PageInfo<RpmDetail> listRpmDetailByCondition(String filename, Date startTime, Date endTime, Integer categoryId, Integer ostypeId, Integer statusId, Integer pageNo, Integer pageSize) { PageHelper.startPage(pageNo, pageSize); List<RpmDetail> result = rpmDetailMapper.listRpmDetailByCondition(filename, startTime, endTime, categoryId, ostypeId, statusId); return new PageInfo(result); }PageHelper.startPage 解析 public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; } protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal(); protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); }可以看出在真正使用分页前,生成了一个 page 对象,然后放入了 ThreadLocal 中,这个思想很巧妙,利用每个请求 Web 服务,每个请求由一个线程处理的原理。利用线程来决定是否进行分页。是否用 ThreadLocal 来判断是否分页的猜想? //调用方法判断是否需要进行分页,如果不需要,直接返回结果 if (!dialect.skip(ms, parameter, rowBounds)) { //判断是否需要进行 count 查询 if (dialect.beforeCount(ms, parameter, rowBounds)) { //查询总数 Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql); //处理查询总数,返回 true 时继续分页查询,false 时直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //当查询总数为 0 时,直接返回空的结果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页 resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } @Override private PageParams pageParams; public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) { if (ms.getId().endsWith(MSUtils.COUNT)) { throw new RuntimeException(“在系统中发现了多个分页插件,请检查系统配置!”); } Page page = pageParams.getPage(parameterObject, rowBounds); if (page == null) { return true; } else { //设置默认的 count 列 if (StringUtil.isEmpty(page.getCountColumn())) { page.setCountColumn(pageParams.getCountColumn()); } autoDialect.initDelegateDialect(ms); return false; } } /** * 获取分页参数 * * @param parameterObject * @param rowBounds * @return / public Page getPage(Object parameterObject, RowBounds rowBounds) { Page page = PageHelper.getLocalPage(); if (page == null) { if (rowBounds != RowBounds.DEFAULT) { if (offsetAsPageNum) { page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount); } else { page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount); //offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false page.setReasonable(false); } if(rowBounds instanceof PageRowBounds){ PageRowBounds pageRowBounds = (PageRowBounds)rowBounds; page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount()); } } else if(parameterObject instanceof IPage || supportMethodsArguments){ try { page = PageObjectUtil.getPageFromObject(parameterObject, false); } catch (Exception e) { return null; } } if(page == null){ return null; } PageHelper.setLocalPage(page); } //分页合理化 if (page.getReasonable() == null) { page.setReasonable(reasonable); } //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果 if (page.getPageSizeZero() == null) { page.setPageSizeZero(pageSizeZero); } return page; } /* * 获取 Page 参数 * * @return / public static <T> Page<T> getLocalPage() { return LOCAL_PAGE.get(); }果然如此,至此真相已经揭开。怎么保证之后的 sql 不使用分页呢?如: 先查出最近一个月注册的 10 个用户,再根据这些用户查出他们所有的订单。第一次查询需要分页,第二次并不需要来看看作者怎么实现的? try{ # 分页拦截逻辑 } finally { dialect.afterAll(); } @Override public void afterAll() { //这个方法即使不分页也会被执行,所以要判断 null AbstractHelperDialect delegate = autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); autoDialect.clearDelegate(); } clearPage(); } /* * 移除本地变量 */ public static void clearPage() { LOCAL_PAGE.remove(); }总结逻辑将分页对象放入 ThreadLocal根据 ThreadLocal 判断是否进行分页无论分页与不分页都需要清除 ThreadLocal备注 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.8</version> </dependency> ...

April 19, 2019 · 3 min · jiezi

Mybatis-Plus 真好用(乡村爱情加持)

写在前面MyBatis的增强方案确实有不少,甚至有种感觉是现在如果只用 “裸MyBatis”,不来点增强插件都不好意思了。这不,在上一篇文章《Spring Boot项目利用MyBatis Generator进行数据层代码自动生成》 中尝试了一下 MyBatis Generator。这次来点更加先进的 Mybatis-Plus,SQL语句都不用写了,分页也是自动完成,嗯,真香!数据库准备CREATE TABLE tbl_user( user_id BIGINT(20) NOT NULL COMMENT ‘主键ID’, user_name VARCHAR(30) NULL DEFAULT NULL COMMENT ‘姓名’, user_age INT(11) NULL DEFAULT NULL COMMENT ‘年龄’, PRIMARY KEY (user_id)) charset = utf8;MyBatis-Plus加持工程搭建 (不赘述了)依赖引入<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.12</version></dependency>主要是 Mybatis Plus、Lombok(不知道Lombok干嘛的?可以看这里)、Druid连接池 等依赖。MyBatis Plus配置项目配置mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml新增 MyBatis Plus配置类@Configuration@MapperScan(“cn.codesheep.springbtmybatisplus.mapper”)public class MyBatisConfig {}看到没,几乎零配置啊,下面就可以写业务逻辑了业务编写实体类@Data@TableName(“tbl_user”)public class User { @TableId(value = “user_id”) private Long userId; private String userName; private Integer userAge;}Mapper类public interface UserMapper extends BaseMapper<User> {}这里啥接口方法也不用写,就可以实现增删改查了!Service类Service接口:public interface UserService extends IService<User> { int insertUser( User user ); int updateUser( User user ); int deleteUser( User user ); User findUserByName( String userName ); IPage getUserPage( Page page, User user );}Service实现:@Service@AllArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { // 增 @Override public int insertUser(User user) { return baseMapper.insert( user ); } // 改 @Override public int updateUser(User user) { return baseMapper.updateById( user ); } // 删 @Override public int deleteUser(User user) { return baseMapper.deleteById( user.getUserId() ); } // 查 @Override public User findUserByName( String userName ) { return baseMapper.getUserByName( userName ); }}Controller类@RestController@RequestMapping("/user")public class UserContorller { @Autowired private UserService userService; // 增 @PostMapping( value = “/insert”) public Object insert( @RequestBody User user ) { return userService.insertUser( user ); } // 改 @PostMapping( value = “/update”) public Object update( @RequestBody User user ) { return userService.updateUser( user ); } // 删 @PostMapping( value = “/delete”) public Object delete( @RequestBody User user ) { return userService.deleteUser( user ); } // 查 @GetMapping( value = “/getUserByName”) public Object getUserByName( @RequestParam String userName ) { return userService.findUserByName( userName ); }}通过以上几个简单的步骤,我们就实现了 tbl_user表的增删改查,传统 MyBatis的 XML文件一个都不需要写!实际实验【《乡爱》加持】启动项目很牛批的 logo就会出现接下来通过 Postman来发送增删改查的请求插入记录通过 Postman随便插入几条记录 POST localhost:8089/user/insert{“userId”:3,“userName”:“刘能”,“userAge”:“58”}{“userId”:4,“userName”:“赵四”,“userAge”:“58”}{“userId”:5,“userName”:“谢广坤”,“userAge”:“58”}{“userId”:6,“userName”:“刘大脑袋”,“userAge”:“58”}修改记录修改记录时需要带用户ID,比如我们修改 赵四 那条记录的名字为 赵四(Zhao Four)删除记录修改记录时同样需要带用户ID,比如删除ID=6 那条 刘大脑袋的记录查询记录(普通查询,下文讲分页查询)比如,按照名字来查询:GET localhost:8089/user/getUserByName?userName=刘能最关心的分页问题首先装配分页插件@Beanpublic PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor();}Mapper类public interface UserMapper extends BaseMapper<User> { // 普通查询 User getUserByName( String userName ); // 分页查询 IPage<List<User>> getUsersPage( Page page, @Param(“query”) User user );}Service类@Service@AllArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { // 查:普通查 @Override public User findUserByName( String userName ) { return baseMapper.getUserByName( userName ); } // 分页查 @Override public IPage getUserPage(Page page, User user) { return baseMapper.getUsersPage( page, user ); }}Controller类@GetMapping( value = “/page”)public Object getUserPage( Page page, User user ) { return userService.getUserPage( page, user );}实际实验一下,我们分页查询 年龄 = 58 的多条记录:可以看到结果数据中,除了给到当前页数据,还把总记录条数,总页数等一并返回了,很是优雅呢 !写在最后由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

April 12, 2019 · 2 min · jiezi

PageHelper插件一对多查询时的分页问题

项目中经常会使用到一对多的查询场景,但是PageHelper对这种嵌套查询的支持不够,如果是一对多的列表查询,返回的分页结果是不对的参考Github上的说明:https://github.com/pagehelper…对于一对多的列表查询,有两种方式解决1、在代码中处理。单独修改分页查询的resultMap,删除collection标签,然后在代码中遍历结果,查询子集2、使用mybatis提供的方法解决,具体如下定义两个resultMap,一个给分页查询使用,一个给其余查询使用<resultMap id=“BaseMap” type=“com.xx.oo.Activity”> <id column=“id” property=“id” jdbcType=“INTEGER”/> ….</resultMap><resultMap id=“ResultMap” type=“com.xx.oo.Activity” extends=“BaseMap”> <collection property=“templates” ofType=“com.xx.oo.Template”> <id column=“pt_id” property=“id” jdbcType=“INTEGER”/> <result column=“pt_title” property=“title” jdbcType=“VARCHAR”/> </collection></resultMap><resultMap id=“RichResultMap” type=“com.xx.oo.Activity” extends=“BaseMap”> <!–property:对应JavaBean中的字段–> <!–ofType:对应JavaBean的类型–> <!–javaType:对应返回值的类型–> <!–column:对应数据库column的字段,不是JavaBean中的字段–> <!–select:对应查询子集的sql–> <collection property=“templates” ofType=“com.xx.oo.Template” javaType=“java.util.List” column=“id” select=“queryTemplateById”> <id column=“pt_id” property=“id” jdbcType=“INTEGER”/> <result column=“pt_title” property=“title” jdbcType=“VARCHAR”/> </collection></resultMap><resultMap id=“template” type=“com.xx.oo.Template”> <id column=“pt_id” property=“id” jdbcType=“INTEGER”/> <result column=“pt_title” property=“title” jdbcType=“VARCHAR”/></resultMap>需要分页的查询,使用RichResultMap。先定义一个查询子集的sql<!–这里的#{id}参数就是collection中定义的column字段–><select id=“queryTemplateById” parameterType=“java.lang.Integer” resultMap=“template”> select id pt_id, title pt_title from t_activity_template where is_delete=0 and activity_id = #{id} order by sort_number desc</select><select id=“queryByPage” parameterType=“com.xx.oo.ActivityPageRequest” resultMap=“RichResultMap”> SELECT t.,t1.real_name creator_name FROM t_activity t left join user t1 on t1.user_id = t.creator <where> t.is_delete = 0 <if test=“criteria != null and criteria.length()>0”>AND (t.activity_name like concat("%",#{criteria},"%"))</if> </where> ORDER BY t.id desc</select>不需要分页的普通查询,使用ResultMap<select id=“queryById” parameterType=“java.lang.Integer” resultMap=“ResultMap”> SELECT t., t6.id pt_id, t1.title pt_title FROM t_activity t left join t_activity_template t1 on t.id=t6.activity_id and t1.is_delete=0 WHERE t.is_delete = 0 AND t.id = #{id}</select>欢迎订阅「K叔区块链」 - 专注于区块链技术学习 博客地址:http://www.jouypub.com简书主页:https://www.jianshu.com/u/756c9c8ae984segmentfault主页:https://segmentfault.com/blog/jouypub腾讯云主页:https://cloud.tencent.com/developer/column/72548 ...

April 10, 2019 · 1 min · jiezi

SpringBoot+MySQL+MyBatis的入门教程

本博客 猫叔的博客,转载请申明出处本系列教程为HMStrange项目附带。历史文章如何在VMware12安装Centos7.6最新版Centos7.6安装Java8Centos7.6安装MySQL+Redis(最新版)教程内容备注:本系列开发工具均为IDEA1、构建项目,选择Lombok、Web、MySQL、MyBatis四个基本的Maven依赖。大家可以看看pom文件<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.myself.mybatis</groupId> <artifactId>datademo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>datademo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2、准备MySQL,这里可以参考历史文章的安装MySQL环节,我新建了一个数据库,针对这个项目,构建了一张简单的表。DDLCREATE TABLE t_msg ( id int(11) NOT NULL, message varchar(255) DEFAULT NULL COMMENT ‘信息’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3、构建项目目录,我构建了一个经典的web项目目录结构,entity实体类、mapper映射、service接口、impl接口实现、controller业务访问、resources/mapper包用于存放xml4、填写application.yml,默认生成不是yml,不过我觉得yml视觉效果好一些,就改了一下,我们需要填写数据库信息,还有mybatis的数据库映射地址,实体类地址spring: datasource: url: jdbc:mysql://192.168.192.133:3306/datademo?characterEncoding=utf-8&useSSL=false username: root password: password driver-class-name: com.mysql.cj.jdbc.Drivermybatis: mapper-locations: classpath*:mapper/Mapper.xml type-aliases-package: com.myself.mybatis.entity5、构建数据库对应的实体类TMsg,这个类放在entitypackage com.myself.mybatis.entity;import lombok.Data;import java.io.Serializable;/* * Created by MySelf on 2019/4/9. /@Datapublic class TMsg implements Serializable { private Integer id; private String message;}6、构建对应的Mapper接口(其实就类似dao层),这里与TMsgMapper.xml文件对应关系package com.myself.mybatis.mapper;import com.myself.mybatis.entity.TMsg;import org.apache.ibatis.annotations.Mapper;/* * Created by MySelf on 2019/4/9. /@Mapperpublic interface TMsgMapper { public TMsg findById(Integer id);}<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“com.myself.mybatis.mapper.TMsgMapper”> <select id=“findById” resultType=“com.myself.mybatis.entity.TMsg”> SELECT id,message from t_msg WHERE id = #{id} </select></mapper>我这边就单纯一个方法,大家可以扩展自己的方法。7、service层与其实现,这个比较简单,一般做过web项目的都了解package com.myself.mybatis.service;import com.myself.mybatis.entity.TMsg;/* * Created by MySelf on 2019/4/9. /public interface TMsgService { public TMsg findById(Integer id);}package com.myself.mybatis.service.impl;import com.myself.mybatis.entity.TMsg;import com.myself.mybatis.mapper.TMsgMapper;import com.myself.mybatis.service.TMsgService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/* * Created by MySelf on 2019/4/9. /@Servicepublic class TMsgServiceImpl implements TMsgService { @Autowired private TMsgMapper tMsgMapper; @Override public TMsg findById(Integer id) { return tMsgMapper.findById(id); }}8、controller层,我这边构建了一个get方法,通过id获取信息。package com.myself.mybatis.controller;import com.myself.mybatis.entity.TMsg;import com.myself.mybatis.service.TMsgService;import org.apache.ibatis.annotations.Param;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * Created by MySelf on 2019/4/9. */@RestController@RequestMapping("/msg”)public class TMsgController { @Autowired private TMsgService tMsgService; @GetMapping("/getMsg”) public String getMsg(@Param(“id”) Integer id){ TMsg tMsg = tMsgService.findById(id); return tMsg.getMessage(); }}9、启动项目,并使用Postman测试10、项目下载地址欢迎到HMStrange项目进行下载:https://github.com/UncleCatMy…公众号:Java猫说学习交流群:728698035现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。 ...

April 10, 2019 · 2 min · jiezi

MyBatis逆向工程

通过逆向工程生成mapper文件,大大提升了开发效率一、引入依赖 <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.5</version> </dependency>二、编写配置文件<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <context id=“DB2Tables” targetRuntime=“MyBatis3”> <commentGenerator> <property name=“suppressAllComments” value=“true” /> </commentGenerator> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://localhost:3306/shortvideo?characterEncoding=utf-8&amp;serverTimezone=GMT%2B8” userId=“root” password=“123456”> </jdbcConnection> <javaTypeResolver > <property name=“forceBigDecimals” value=“false” /> </javaTypeResolver> <!– 指定javabean生成的位置 –> <javaModelGenerator targetPackage=“com.imooc.pojo” targetProject=”.\src\main\java"> <property name=“enableSubPackages” value=“true” /> <property name=“trimStrings” value=“true” /> </javaModelGenerator> <!– 指定mapper文件生成的位置 –> <sqlMapGenerator targetPackage=“mapper” targetProject=".\src\main\resources"> <property name=“enableSubPackages” value=“true” /> </sqlMapGenerator> <!– 指定mapper接口生成的位置 –> <javaClientGenerator type=“XMLMAPPER” targetPackage=“com.imooc.mapper” targetProject=".\src\main\java"> <property name=“enableSubPackages” value=“true” /> </javaClientGenerator> <table tableName=“vuser” domainObjectName=“Users”></table> <table tableName=“bgm”></table> <table tableName=“comments”></table> <table tableName=“search_records”></table> <table tableName=“users_fans”></table> <table tableName=“users_like_videos”></table> <table tableName=“users_report”></table> <table tableName=“videos”></table> </context></generatorConfiguration>三、编写启动类import org.mybatis.generator.api.MyBatisGenerator;import org.mybatis.generator.config.Configuration;import org.mybatis.generator.config.xml.ConfigurationParser;import org.mybatis.generator.exception.InvalidConfigurationException;import org.mybatis.generator.exception.XMLParserException;import org.mybatis.generator.internal.DefaultShellCallback;import java.io.File;import java.io.IOException;import java.sql.SQLException;import java.util.ArrayList;import java.util.List;public class MybatisGenerator { public static void main(String[] args) throws IOException, XMLParserException, InvalidConfigurationException, SQLException, InterruptedException { List<String> warnings = new ArrayList<String>(); boolean overwrite = true; //指定配置文件的路径,相对或者绝对路径都可以 File configFile = new File(“E:\idea\short-video\src\main\java\com\imooc\utils\mybatisGenerator\mbg”); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null); }} ...

April 7, 2019 · 1 min · jiezi

第三课(spring-boot+mybatis+jqgrid)

课程目标完成与spring boot 与的mybatis的集成处理数据curd课程计划使用mybatis完成博客后台管理员列表的jqgird搜索课程分析想要完成列表的搜索,就必须对sql按提交搜索条件进行逻辑判断组织sql,也就是动态sql步骤1.加入依赖// build.gradledependencies { compile(“org.springframework.boot:spring-boot-starter-web”) compile(“org.springframework.boot:spring-boot-starter-thymeleaf”) compile(“org.springframework.boot:spring-boot-devtools”) // JPA Data (We are going to use Repositories, Entities, Hibernate, etc…) compile ‘org.springframework.boot:spring-boot-starter-data-jpa’ // Use MySQL Connector-J compile ‘mysql:mysql-connector-java’ // 使用mybatis compile(“org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.2”) developmentOnly(“org.springframework.boot:spring-boot-devtools”) testCompile(“junit:junit”)}2. 配置mybatisspring: jpa: hibernate: ddl-auto: update datasource: url: jdbc:mysql://localhost:3306/db_sptest?useSSL=false username: mysqluser password: mysqlpwd mvc: static-path-pattern: /static/**mybatis: type-aliases-package: hello.model3. 使用mybatis mapper// model/AdminMapper@Mapperpublic interface AdminMapper { //使用注解方式 @Select(“SELECT * FROM ADMIN WHERE name = #{name} LIMIT 1”) Admin findByName(String name); @Select(“SELECT * FROM ADMIN WHERE id = #{id}”) Admin findById(Integer id); //动态sql @SelectProvider(type = AdminService.class,method = “selectAdminLike”) List<Admin> findBySearch(Admin admin); //动态sql 返回行数 @SelectProvider(type = AdminService.class,method = “countAdminSearch”) @ResultType(Integer.class) int countBySearch(Admin admin);}4.编写动态sql语句// service/AdminServiceimport org.apache.ibatis.jdbc.SQL;public class AdminService { // With conditionals (note the final parameters, required for the anonymous inner class to access them) public String selectAdminLike(Admin admin) { return new SQL() {{ SELECT(“A.name,A.email,A.id”); FROM(“ADMIN A”); if (admin.getName() != null) { WHERE(“A.name = ‘” + admin.getName() + “’”); } if (admin.getEmail() != null) { WHERE(“A.email = " + admin.getEmail()); } }}.toString(); } public String countAdminSearch(Admin admin){ return new SQL() {{ SELECT(“count(*)”); FROM(“ADMIN A”); if (admin.getName() != null) { WHERE(“A.name = ‘” + admin.getName() + “’”); } if (admin.getEmail() != null) { WHERE(“A.email = " + admin.getEmail()); } }}.toString(); }}5.使用mybatis查询方法 // AdminController @GetMapping(path = “get_list”) @ResponseBody public DataResponse<Admin> getAdminList( Admin adminReq, @RequestParam(defaultValue = “1”, value = “page”) String page, @RequestParam(defaultValue = “10”, value = “rows”) String rows) { String total; //页数 List<Admin> admin_list; int records; records = adminMapper.countBySearch(adminReq); int pageSize = Integer.valueOf(rows); total = Integer.toString((records + pageSize - 1) / pageSize); DataResponse<Admin> response = new DataResponse<>(); admin_list = adminMapper.findBySearch(adminReq); response.setTotal(total); response.setRows(admin_list); response.setPage(page); response.setRecords(records); return response; }课程成果完成使用jqgrid+spring boot+mybatis 的数据列表搜索遗留page 和 pageSize 的传参控制,下节课对代码进行稍微的改动就可以支持目前使用了jpa的Hibernate entity 这么用合理么? ...

April 7, 2019 · 2 min · jiezi

Egg集成Mybatis

Egg集成Mybatis就一张图稍后再放出关于分页查询的处理吧,目前部分代码在express的一个小项目中,搬过来之后再分享一下,本人刚刚接触Egg这套框架,希望大家多提提意见。

April 5, 2019 · 1 min · jiezi

Mybatis源码分析(1) - Mybatis包目录简介

Mybatis核心包包名称包内内容简介annotation注解目录。包括所有的注解。如@SELECT,@UPDATE等bindingMapper类的实例反射生成工具目录builder主要是注解,mapper和SqlSuorce的构造器及转换器cacheMybatis内部缓存接口。实现了一些特定的缓存策略。FifoCache,LruCache,BlockingCache,LoggingCache等cursor默认的游标处理类dataSource数据源工厂类及实现。实现类包括JndiDataSourceFactory、PooledDataSourceFactory、UnpooledDataSourceFactory。 数据源实现类: UnpooledDataSource、PooledDataSourceexceptionsMybatis自定义的三个异常类。ExceptionFactory、PersistenceException、TooManyResultsException、IbatisException。都继承自RuntimeExceptionexecutor执行器相关包。包括Key生成器、加载器(包括Cglib、Javassist的代理,结果加载器)、参数处理器接口、结果处理器、结果集(resultSet)处理器、Statement处理器(实现类:BaseStatementHandler、CallableStatementHandler、PreparedStatementHandler、RoutingStatementHandler、SimpleStatementHandler)、执行器(SimpleExecutor、ReuseExecutor、CachingExecutor、BatchExecutor、BaseExecutor)io主要是定义的几个VFS(VFS、DefaultVFS、ClassLoaderWrapper)javassistjavassist的字节码处理包jdbc与Sql相关的操作。如Sql运行器,脚本运行器和Sql封装类等lang指定是用java7还是java8的API的注解.UsesJava7、UsesJava8logging各个类型的日志适配器,都实现了Log接口。StdOutImpl、Slf4jImpl、NoLoggingImpl、Log4j2Impl、Log4jImpl、Jdk14LoggingImpl、BaseJdbcLogger、JakartaCommonsLoggingImplmapping主要是接口参数,sql和返回结果的映射类,主要类包括:MappedStatement,ParameterMap,ParameterMapping,ResultMap,ResultMapping,BoundSql,SqlSource等类ognlognl包在Mybatis中的内部代码引用parsing变量解析.如解析${},#{}等plugin主要包含插件的定义接口。如Interceptor,Plugin,InterceptorChain等reflection主要是一些反射操作的工具方法和对象工厂类,以及一些常用的包装类,如BaseWrapper,BeanWrapper,CollectionWrapper,MapWrapper,ObjectWrapper,,,scripting执行驱动和动态Sql解析的老巢session主要是SqlSession和SqlSessionFactorytransaction主要是mybatis简单封装的jdbc事务操作类type各个类型数据的处理器。用于动态的设置参数和转换数据。如IntegerTypeHandler用来处理Integer类型的值的set和get操作。除了八大基本类型。还有常用的集合及Map类型,还增加了各种时间类型的处理器总结通过整理每个包的主要功能。通览整个mybatis的代码结构,了解各个组件的位置和大概的处理关系。为后续分析源码打下基础。

April 3, 2019 · 1 min · jiezi

一次奇怪的mybatis 驼峰 映射问题研究

使用mybatis的映射用resultMap,上面需要定义(type指定)使用resultType,直接指定目标实体类但是数据库定义的AB_C,按说应该对应abC,但是用resultType就没有映射过去,其他的可以改回了resultMap,,,就行了

March 27, 2019 · 1 min · jiezi

介绍 mybatis 执行 SQL 的大致过程

本文通过 main() 方法来用 mybatis 执行带参数的 SQL 命令,来介绍 mybatis 执行 SQL 的大致过程。准备数据库准备一个 MySQL 数据库,十分钟内快速运行起一个 MySQL 的方法可以看这篇文章。数据库内创建一张很简单的表:create database test;use test;create table t1(int id);insert into t1 set id=11;创建项目首先创建一个空的 Maven 项目,加上下面的依赖关系:<dependencies> <!– mybatis 本身 –> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.0</version> </dependency> <!– 连接池数据源 –> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.5.0</version> </dependency> <!– 数据库驱动 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> <!– 日志输出 –> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency></dependencies>创建 main() 方法在项目中随便创建一个类,写一个空的 main() 方法,确认这个方法可以正常运行。接下来我们在 main() 方法中,一步一步完成这个使用 mybatis 执行 SQL 命令的例子。1. 初始化数据源要连接数据库,自然要先准备好数据源对象,这里就不多做解释了:String url = “jdbc:mysql://localhost/test?serverTimezone=UTC”;String username = “root”;String password = “root123”;org.apache.commons.dbcp2.BasicDataSource dataSource = new BasicDataSource();dataSource.setUrl(url);dataSource.setUsername(username);dataSource.setPassword(password);dataSource 变量就是准备好的数据源对象。2. 定义要执行的 SQL 命令SqlCommandType commandType = SqlCommandType.SELECT;String commandId = “sql1”;String commandStatement = “select * from t1 where id=#{id}";String parameterName = “id”;Class<?> parameterType = Integer.class;Object parameterValue = 11;Class<?> resultType = HashMap.class;List<ResultMapping> resultPropertyMappings = Collections.emptyList();这个定义分为三部分:一是命令本身,包括命令的类型、ID、语句。在 mybatis 中,每条 SQL 命令都有唯一的 ID,因为在 SqlSession 里面,执行哪条 SQL 命令就是通过 ID 来指定的。二是参数,包括参数的名称、类型、参数值。这个例子中只有一个参数,参数名称 id 和语句里面的 #{id} 名称必须保持一致。三是返回值的类型。返回值的类型包括两部分,一是返回结果本身的类型,二是返回结果的每个字段各是什么类型(用于转换)。这个例子当中,我们简单的将返回结果包装为 Map 对象,所以里面的字段值就不去指定类型转换了,即 resultPropertyMappings 变量是个空的 List。上面这 8 个变量,会在组装 mybatis 的 SQL 命令过程中用到。3. 初始化 mybatis 配置mybatis 的所有配置都在 org.apache.ibatis.session.Configuration 对象当中。Configuration 对象是一个很复杂的对象,涵盖了执行 SQL 命令需要的所有东西。正常使用的情况下,我们要为它配置 XML 路径、Mapper 所在的包、查询结果的类型转换等等。在本文的例子当中,我们用不到 XML,所以只做最简单的配置:JdbcTransactionFactory transactionFactory = new JdbcTransactionFactory();Environment environment = new Environment(“default”, transactionFactory, dataSource);Configuration configuration = new Configuration(environment);可以看出,dataSource 变量是先放入 Environment 对象,然后再放入 Configuration 对象的。到这里,mybatis 的数据源就配置好了。接下来我们将之前定义的 SQL 命令加入 mybatis。4. 构建 MappedStatementmybatis 当中所有的 SQL 命令最终都是一个 MappedStatement 对象。但要构建它可不简单,我们要把前面定义的 SQL 命令三部分分别构建相应的对象,然后再组装成 MappedStatement 对象。// 1. 构建 SqlSource 对象SqlSource sqlSource = new SqlSourceBuilder( configuration).parse( commandStatement, parameterType, null);// 2. 构建 ResultMap 对象String resultMapId = commandId + “-Inline”;ResultMap resultMap = new ResultMap.Builder( configuration, resultMapId, resultType, resultPropertyMappings, null).build();// 3. 构建 ParameterMap 对象ParameterMap parameterMap = new ParameterMap.Builder( configuration, commandId, null, Collections.singletonList( new ParameterMapping.Builder( configuration, parameterName, Integer.class ).build() )).build();// 4. 将 SqlSource 对象、 ResultMap 对象和 ParameterMap 对象// 组合成最终的 MappedStatement 对象MappedStatement mappedStatement = new MappedStatement.Builder( configuration, commandId, sqlSource, commandType).resultMaps( Collections.singletonList(resultMap)).parameterMap( parameterMap).build();看得出,因为构建起来很复杂,所以 mybatis 定义了大量 Builder 类来用。5. 将 MappedStatement 加入 mybatisconfiguration.addMappedStatement(mappedStatement);所有的 MappedStatement 都要放入 Configuration。6. 初始化 SqlSession 并执行 SQL 命令这一步相信熟悉 mybatis 的同学都会用,就不多解释了。SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);SqlSession sqlSession = sqlSessionFactory.openSession(true);List<Object> result = sqlSession.selectList(commandId, parameterValue);System.out.println(result);上面这六个片段依次加入 main() 方法,即可完整执行。这就是 mybatis 执行 SQL 命令的基本套路。有的同学就会问,那我在 XML 里面定义的 SQL 语句是怎么解析执行的呢?mybatis 有个叫 org.apache.ibatis.scripting.LanguageDriver 的接口类,负责将 XML 或其他语言的动态 SQL 模板解析为最终的 SQL 语句,然后交给 SqlSourceBuilder 生成 SqlSource 对象。具体可以看看源码。相信理解了本文之后再去看 mybatis 的源码,会对它的执行机制有更清晰的了解。 ...

March 27, 2019 · 2 min · jiezi

最简单的SpringBoot整合MyBatis教程

前面两篇文章和读者聊了Spring Boot中最简单的数据持久化方案JdbcTemplate,JdbcTemplate虽然简单,但是用的并不多,因为它没有MyBatis方便,在Spring+SpringMVC中整合MyBatis步骤还是有点复杂的,要配置多个Bean,Spring Boot中对此做了进一步的简化,使MyBatis基本上可以做到开箱即用,本文就来看看在Spring Boot中MyBatis要如何使用。工程创建首先创建一个基本的Spring Boot工程,添加Web依赖,MyBatis依赖以及MySQL驱动依赖,如下: 创建成功后,添加Druid依赖,并且锁定MySQL驱动版本,完整的依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.28</version> <scope>runtime</scope></dependency>如此,工程就算是创建成功了。读者注意,MyBatis和Druid依赖的命名和其他库的命名不太一样,是属于xxx-spring-boot-stater模式的,这表示该starter是由第三方提供的。基本用法MyBatis的使用和JdbcTemplate基本一致,首先也是在application.properties中配置数据库的基本信息:spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.datasource.password=rootspring.datasource.type=com.alibaba.druid.pool.DruidDataSource配置完成后,MyBatis就可以创建Mapper来使用了,例如我这里直接创建一个UserMapper2,如下:public interface UserMapper2 { @Select(“select * from user”) List<User> getAllUsers(); @Results({ @Result(property = “id”, column = “id”), @Result(property = “username”, column = “u”), @Result(property = “address”, column = “a”) }) @Select(“select username as u,address as a,id as id from user where id=#{id}”) User getUserById(Long id); @Select(“select * from user where username like concat(’%’,#{name},’%’)”) List<User> getUsersByName(String name); @Insert({“insert into user(username,address) values(#{username},#{address})”}) @SelectKey(statement = “select last_insert_id()”, keyProperty = “id”, before = false, resultType = Integer.class) Integer addUser(User user); @Update(“update user set username=#{username},address=#{address} where id=#{id}”) Integer updateUserById(User user); @Delete(“delete from user where id=#{id}”) Integer deleteUserById(Integer id);}这里是通过全注解的方式来写SQL,不写XML文件,@Select、@Insert、@Update以及@Delete四个注解分别对应XML中的select、insert、update以及delete标签,@Results注解类似于XML中的ResultMap映射文件(getUserById方法给查询结果的字段取别名主要是向小伙伴们演示下@Results注解的用法),另外使用@SelectKey注解可以实现主键回填的功能,即当数据插入成功后,插入成功的数据id会赋值到user对象的id属性上。 UserMapper2创建好之后,还要配置mapper扫描,有两种方式,一种是直接在UserMapper2上面添加@Mapper注解,这种方式有一个弊端就是所有的Mapper都要手动添加,要是落下一个就会报错,还有一个一劳永逸的办法就是直接在启动类上添加Mapper扫描,如下:@SpringBootApplication@MapperScan(basePackages = “org.sang.mybatis.mapper”)public class MybatisApplication { public static void main(String[] args) { SpringApplication.run(MybatisApplication.class, args); }}好了,做完这些工作就可以去测试Mapper的使用了。mapper映射当然,开发者也可以在XML中写SQL,例如创建一个UserMapper,如下:public interface UserMapper { List<User> getAllUser(); Integer addUser(User user); Integer updateUserById(User user); Integer deleteUserById(Integer id);}然后创建UserMapper.xml文件,如下:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“org.sang.mybatis.mapper.UserMapper”> <select id=“getAllUser” resultType=“org.sang.mybatis.model.User”> select * from t_user; </select> <insert id=“addUser” parameterType=“org.sang.mybatis.model.User”> insert into user (username,address) values (#{username},#{address}); </insert> <update id=“updateUserById” parameterType=“org.sang.mybatis.model.User”> update user set username=#{username},address=#{address} where id=#{id} </update> <delete id=“deleteUserById”> delete from user where id=#{id} </delete></mapper>将接口中方法对应的SQL直接写在XML文件中。 那么这个UserMapper.xml到底放在哪里呢?有两个位置可以放,第一个是直接放在UserMapper所在的包下面: 放在这里的UserMapper.xml会被自动扫描到,但是有另外一个Maven带来的问题,就是java目录下的xml资源在项目打包时会被忽略掉,所以,如果UserMapper.xml放在包下,需要在pom.xml文件中再添加如下配置,避免打包时java目录下的XML文件被自动忽略掉:<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources></build>当然,UserMapper.xml也可以直接放在resources目录下,这样就不用担心打包时被忽略了,但是放在resources目录下,又不能自动被扫描到,需要添加额外配置。例如我在resources目录下创建mapper目录用来放mapper文件,如下: 此时在application.properties中告诉mybatis去哪里扫描mapper:mybatis.mapper-locations=classpath:mapper/.xml如此配置之后,mapper就可以正常使用了。注意第二种方式不需要在pom.xml文件中配置文件过滤。原理分析在SSM整合中,开发者需要自己提供两个Bean,一个SqlSessionFactoryBean,还有一个是MapperScannerConfigurer,在Spring Boot中,这两个东西虽然不用开发者自己提供了,但是并不意味着这两个Bean不需要了,在org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration类中,我们可以看到Spring Boot提供了这两个Bean,部分源码如下:@org.springframework.context.annotation.Configuration@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })@ConditionalOnSingleCandidate(DataSource.class)@EnableConfigurationProperties(MybatisProperties.class)@AutoConfigureAfter(DataSourceAutoConfiguration.class)public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); return factory.getObject(); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } } @org.springframework.context.annotation.Configuration @Import({ AutoConfiguredMapperScannerRegistrar.class }) @ConditionalOnMissingBean(MapperFactoryBean.class) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { @Override public void afterPropertiesSet() { logger.debug(“No {} found.”, MapperFactoryBean.class.getName()); } }}从类上的注解可以看出,当当前类路径下存在SqlSessionFactory、 SqlSessionFactoryBean以及DataSource时,这里的配置才会生效,SqlSessionFactory和SqlTemplate都被提供了。为什么要看这段代码呢?下篇文章,松哥和大伙分享Spring Boot中MyBatis多数据源的配置时,这里将是一个重要的参考。 好了,本文就先说到这里,关于在Spring Boot中整合MyBatis,这里还有一个小小的视频教程,加入我的星球即可免费观看: 关于我的星球【Java达摩院】,大伙可以参考这篇文章推荐一个技术圈子,Java技能提升就靠它了. ...

March 18, 2019 · 2 min · jiezi

mybatis-plus源码分析之sql注入器

mybatis-plus是完全基于mybatis开发的一个增强工具,它的设计理念是在mybatis的基础上只做增强不做改变,为简化开发、提高效率而生,它在mybatis的基础上增加了很多实用性的功能,比如增加了乐观锁插件、字段自动填充功能、分页插件、条件构造器、sql注入器等等,这些在开发过程中都是非常实用的功能,mybatis-plus可谓是站在巨人的肩膀上进行了一系列的创新,我个人极力推荐。下面我会详细地从源码的角度分析mybatis-plus(下文简写成mp)是如何实现sql自动注入的原理。温故知新我们回顾一下mybatis的Mapper的注册与绑定过程,我之前也写过一篇「Mybatis源码分析之Mapper注册与绑定」,在这篇文章中,我详细地讲解了Mapper绑定的最终目的是将xml或者注解上的sql信息与其对应Mapper类注册到MappedStatement中,既然mybatis-plus的设计理念是在mybatis的基础上只做增强不做改变,那么sql注入器必然也是在将我们预先定义好的sql和预先定义好的Mapper注册到MappedStatement中。现在我将Mapper的注册与绑定过程用时序图再梳理一遍:解析一下这几个类的作用:SqlSessionFactoryBean:继承了FactoryBean和InitializingBean,符合spring loc容器bean的基本规范,可在获取该bean时调用getObject()方法到SqlSessionFactory。XMLMapperBuilder:xml文件解析器,解析Mapper对应的xml文件信息,并将xml文件信息注册到Configuration中。XMLStatementBuilder:xml节点解析器,用于构建select/insert/update/delete节点信息。MapperBuilderAssistant:Mapper构建助手,将Mapper节点信息封装成statement添加到MappedStatement中。MapperRegistry:Mapper注册与绑定类,将Mapper的类信息与MapperProxyFactory绑定。MapperAnnotationBuilder:Mapper注解解析构建器,这也是为什么mybatis可以直接在Mapper方法添加注解信息就可以不用在xml写sql信息的原因,这个构建器专门用于解析Mapper方法注解信息,并将这些信息封装成statement添加到MappedStatement中。从时序图可知,Configuration配置类存储了所有Mapper注册与绑定的信息,然后创建SqlSessionFactory时再将Configuration注入进去,最后经过SqlSessionFactory创建出来的SqlSession会话,就可以根据Configuration信息进行数据库交互,而MapperProxyFactory会为每个Mapper创建一个MapperProxy代理类,MapperProxy包含了Mapper操作SqlSession所有的细节,因此我们就可以直接使用Mapper的方法就可以跟SqlSession进行交互。饶了一圈,发现我现在还没讲sql注入器的源码分析,你不用慌,你得体现出老司机的成熟稳定,之前我也跟你说了sql注入器的原理了,只剩下源码分析,这时候我们应该在源码分析之前做足前戏,前戏做足就剩下撕、拉、扯、剥开源码的外衣了,来不及解释了快上车!源码分析从Mapper的注册与绑定过程的时序图看,要想将sql注入器无缝链接地添加到mybatis里面,那就得从Mapper注册步骤添加,果然,mp很鸡贼地继承了MapperRegistry这个类然后重写了addMapper方法:com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { // TODO 如果之前注入 直接返回 return; // throw new BindingException(“Type " + type + // " is already known to the MybatisPlusMapperRegistry.”); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<>(type)); // It’s important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won’t try. // TODO 自定义无 XML 注入 MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }}方法中将MapperAnnotationBuilder替换成了自家的MybatisMapperAnnotationBuilder,在这里特别说明一下,mp为了不更改mybatis原有的逻辑,会用继承或者直接粗暴地将其复制过来,然后在原有的类名上加上前缀“Mybatis”。com.baomidou.mybatisplus.MybatisMapperAnnotationBuilder#parse:public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); // TODO 注入 CURD 动态 SQL (应该在注解之前注入) if (BaseMapper.class.isAssignableFrom(type)) { GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type); } for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods();}sql注入器就是从这个方法里面添加上去的,首先判断Mapper是否是BaseMapper的超类或者超接口,BaseMapper是mp的基础Mapper,里面定义了很多默认的基础方法,意味着我们一旦使用上mp,通过sql注入器,很多基础的数据库操作都可以直接继承BaseMapper实现了,开发效率爆棚有木有!com.baomidou.mybatisplus.toolkit.GlobalConfigUtils#getSqlInjector:public static ISqlInjector getSqlInjector(Configuration configuration) { // fix #140 GlobalConfiguration globalConfiguration = getGlobalConfig(configuration); ISqlInjector sqlInjector = globalConfiguration.getSqlInjector(); if (sqlInjector == null) { sqlInjector = new AutoSqlInjector(); globalConfiguration.setSqlInjector(sqlInjector); } return sqlInjector;}GlobalConfiguration是mp的全局缓存类,用于存放mp自带的一些功能,很明显,sql注入器就存放在GlobalConfiguration中。这个方法是先从全局缓存类中获取自定义的sql注入器,如果在GlobalConfiguration中没有找到自定义sql注入器,就会设置一个mp默认的sql注入器AutoSqlInjector。sql注入器接口:// SQL 自动注入器接口public interface ISqlInjector { // 根据mapperClass注入SQL void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); // 检查SQL是否注入(已经注入过不再注入) void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass); // 注入SqlRunner相关 void injectSqlRunner(Configuration configuration);}所有自定义的sql注入器都需要实现ISqlInjector接口,mp已经为我们默认实现了一些基础的注入器:com.baomidou.mybatisplus.mapper.AutoSqlInjectorcom.baomidou.mybatisplus.mapper.LogicSqlInjector其中AutoSqlInjector提供了最基本的sql注入,以及一些通用的sql注入与拼装的逻辑,LogicSqlInjector在AutoSqlInjector的基础上复写了删除逻辑,因为我们的数据库的数据删除实质上是软删除,并不是真正的删除。com.baomidou.mybatisplus.mapper.AutoSqlInjector#inspectInject:public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { String className = mapperClass.toString(); Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) { inject(builderAssistant, mapperClass); mapperRegistryCache.add(className); }}该方法是sql注入器的入口,在入口处添加了注入过后不再注入的判断功能。// 注入单点 crudSql@Overridepublic void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) { this.configuration = builderAssistant.getConfiguration(); this.builderAssistant = builderAssistant; this.languageDriver = configuration.getDefaultScriptingLanguageInstance(); // 驼峰设置 PLUS 配置 > 原始配置 GlobalConfiguration globalCache = this.getGlobalConfig(); if (!globalCache.isDbColumnUnderline()) { globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase()); } Class<?> modelClass = extractModelClass(mapperClass); if (null != modelClass) { // 初始化 SQL 解析 if (globalCache.isSqlParserCache()) { PluginUtils.initSqlParserInfoCache(mapperClass); } TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass); injectSql(builderAssistant, mapperClass, modelClass, table); }}注入之前先将Mapper类提取泛型模型,因为继承BaseMapper需要将Mapper对应的model添加到泛型里面,这时候我们需要将其提取出来,提取出来后还需要将其初始化成一个TableInfo对象,TableInfo存储了数据库对应的model所有的信息,包括表主键ID类型、表名称、表字段信息列表等等信息,这些信息通过反射获取。com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectSql:protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { if (StringUtils.isNotEmpty(table.getKeyProperty())) { /** 删除 / this.injectDeleteByIdSql(false, mapperClass, modelClass, table); /* 修改 / this.injectUpdateByIdSql(true, mapperClass, modelClass, table); /* 查询 / this.injectSelectByIdSql(false, mapperClass, modelClass, table); } /* 自定义方法 / this.inject(configuration, builderAssistant, mapperClass, modelClass, table);}所有需要注入的sql都是通过该方法进行调用,AutoSqlInjector还提供了一个inject方法,自定义sql注入器时,继承AutoSqlInjector,实现该方法就行了。com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectDeleteByIdSql:protected void injectSelectByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID; SqlSource sqlSource; if (batch) { sqlMethod = SqlMethod.SELECT_BATCH_BY_IDS; StringBuilder ids = new StringBuilder(); ids.append("\n<foreach item="item" index="index" collection="coll" separator=",">"); ids.append("#{item}"); ids.append("\n</foreach>"); sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), ids.toString()), modelClass); } else { sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), table.getKeyProperty()), Object.class); } this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource, modelClass, table);}我随机选择一个删除sql的注入,其它sql注入都是类似这么写,SqlMethod是一个枚举类,里面存储了所有自动注入的sql与方法名,如果是批量操作,SqlMethod的定义的sql语句在添加批量操作的语句。再根据table和sql信息创建一个SqlSource对象。com.baomidou.mybatisplus.mapper.AutoSqlInjector#addMappedStatement:public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType, KeyGenerator keyGenerator, String keyProperty, String keyColumn) { // MappedStatement是否存在 String statementName = mapperClass.getName() + “.” + id; if (hasMappedStatement(statementName)) { System.err.println("{" + statementName + “} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.”); return null; } /* 缓存逻辑处理 */ boolean isSelect = false; if (sqlCommandType == SqlCommandType.SELECT) { isSelect = true; } return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null, parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn, configuration.getDatabaseId(), languageDriver, null);}sql注入器的最终操作,这里会判断MappedStatement是否存在,这个判断是有原因的,它会防止重复注入,如果你的Mapper方法已经在Mybatis的逻辑里面注册了,mp不会再次注入。最后调用MapperBuilderAssistant助手类的addMappedStatement方法执行注册操作。到这里,一个sql自动注入器的源码就分析完了,其实实现起来很简单,因为它利用了Mybatis的机制,站在巨人的肩膀上进行创新。我希望在你们今后的职业生涯里,不要只做一个只会调用API的crud程序员,我们要有一种刨根问底的精神。阅读源码很枯燥,但阅读源码不仅会让你知道API底层的实现原理,让你知其然也知其所以然,还可以开阔你的思维,提升你的架构设计能力,通过阅读源码,可以看到大佬们是如何设计一个框架的,为什么会这么设计。 ...

March 18, 2019 · 3 min · jiezi

重拾-MyBatis-配置文件解析

前言我们知道在使用 Mybatis 时,我们需要通过 SqlSessionFactoryBuild 去创建 SqlSessionFactory 实例,譬如:// resource 为 mybatis 的配置文件 InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);那么我们看下 build 方法的具体实现public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { // 创建 XMLConfigBuilder 实例并执行解析 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException(“Error building SqlSession.”, e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { } }}public Configuration parse() { if (parsed) { throw new BuilderException(“Each XMLConfigBuilder can only be used once.”); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration;}Mybatis 主要通过 XMLConfigBuilder 执行对配置文件的解析,具体实现如下文:配置文件解析private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 解析 properties 标签 propertiesElement(root.evalNode(“properties”)); // 解析 settings 标签 Properties settings = settingsAsProperties(root.evalNode(“settings”)); loadCustomVfs(settings); loadCustomLogImpl(settings); // 解析 typeAliases 别名标签 typeAliasesElement(root.evalNode(“typeAliases”)); // 解析 plugins 插件标签 pluginElement(root.evalNode(“plugins”)); objectFactoryElement(root.evalNode(“objectFactory”)); objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”)); reflectorFactoryElement(root.evalNode(“reflectorFactory”)); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 解析 environments 标签 environmentsElement(root.evalNode(“environments”)); databaseIdProviderElement(root.evalNode(“databaseIdProvider”)); // 解析 typeHandlers 标签 typeHandlerElement(root.evalNode(“typeHandlers”)); // 解析 mappers 标签 mapperElement(root.evalNode(“mappers”)); } catch (Exception e) { throw new BuilderException(“Error parsing SQL Mapper Configuration. Cause: " + e, e); }}从 XMLConfigBuilder 的方法 parseConfiguration 实现我们知道,MyBatis 会依次解析配置文件中的相应标签,本文将针对开发中常用的配置进行分析;主要包括 properties, typeAliases, enviroments, typeHandlers, mappers 。properties 解析配置示例<configuration> <!– 可以指定 resource 属性,也可以指定 url 属性 –> <properties resource=“org/mybatis/example/config.properties”> <property name=“username” value=“dev_user”/> <property name=“password” value=“F2Fa3!33TYyg”/> </properties></configuration>从配置示例可以看出 properties 属性变量的来源可以是外部的配置文件,也可以是配置文件中自定义的,也可以是 SqlSessionFactoryBuilder 的 build 方法传参譬如:public SqlSessionFactory build(InputStream inputStream, Properties properties) { return build(inputStream, null, properties); }那么当存在同名的属性时,将采用哪种方式的属性值呢?解析private void propertiesElement(XNode context) throws Exception { if (context != null) { // 获取 properties 标签下的所有 property 子标签 Properties defaults = context.getChildrenAsProperties(); // 获取 resource,url 属性 String resource = context.getStringAttribute(“resource”); String url = context.getStringAttribute(“url”); // resource url 两个属性不能同时存在 if (resource != null && url != null) { throw new BuilderException(“The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.”); } if (resource != null) { // 加载 resource 指定的配置文件 defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { // 加载 url 指定的配置文件 defaults.putAll(Resources.getUrlAsProperties(url)); } /** * 获取传参的 properties * 构建 sqlSessionFactory 时可以传参 properties * * @see SqlSessionFactoryBuilder.build(InputStream inputStream, Properties properties) / Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); // 将 properties 赋值 configuration 中的 variables 变量 configuration.setVariables(defaults); } }public Properties getChildrenAsProperties() { Properties properties = new Properties(); // 遍历 properties 标签下的 propertry 子标签 for (XNode child : getChildren()) { // 获取 propertry 的 name value 属性 String name = child.getStringAttribute(“name”); String value = child.getStringAttribute(“value”); if (name != null && value != null) { properties.setProperty(name, value); } } return properties; }从 properties 标签解析的实现来看,MyBatis 加载 properties 属性的过程如下:首先加载 properties 标签内所有子标签的 property其次加载 properties 标签属性 resource 或 url 指定的外部属性配置最后加载 SqlSessionFactoryBuilder 的方法 build 传参的属性配置因此,通过方法参数传递的 properties 具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 标签内的子标签 property 指定的属性。typeAliases 解析类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余配置示例<typeAliases> <typeAlias alias=“Author” type=“domain.blog.Author”/> <typeAlias alias=“Blog” type=“domain.blog.Blog”/> <typeAlias alias=“Comment” type=“domain.blog.Comment”/></typeAliases>也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:<typeAliases> <package name=“domain.blog”/></typeAliases>解析private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { // 如果是 package 标签,对整个包下的 java bean 进行别名处理 // 若 java bean 没有配置注解的话,使用 bean 的首字母小写类名作为别名 // 若 java bean 配置了注解,使用注解值作为别名 if (“package”.equals(child.getName())) { // 获取指定的包名 String typeAliasPackage = child.getStringAttribute(“name”); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { // 别名 String alias = child.getStringAttribute(“alias”); // 别名对应的类 String type = child.getStringAttribute(“type”); try { Class<?> clazz = Resources.classForName(type); if (alias == null) { // 默认别名为类名,若配置了别名注解则取注解值映射类 typeAliasRegistry.registerAlias(clazz); } else { // 通过指定的别名映射类 typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException(“Error registering typeAlias for ‘” + alias + “’. Cause: " + e, e); } } } } }typeAliasesElement 在对 typeAliases 标签解析时,针对采用 package 和 typeAlias 两种配置方式进行了不同的解析。 下面我们先看下通过包名的配置方式通过包名解析public void registerAliases(String packageName) { registerAliases(packageName, Object.class); } public void registerAliases(String packageName, Class<?> superType) { // 获取包下所有的类 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); for (Class<?> type : typeSet) { // Ignore inner classes and interfaces (including package-info.java) // Skip also inner classes. See issue #6 // 忽略内部类 接口 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { registerAlias(type); } } } public void registerAlias(Class<?> type) { // 别名为类名 String alias = type.getSimpleName(); // 是否配置了别名注解,若配置了则别名取注解值 Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } registerAlias(alias, type); }当通过 package 指定包名时,MyBatis 会扫描包下所有的类(忽略内部类,接口),若类没有采用 @Alias 注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名, 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。public void registerAlias(String alias, Class<?> value) { if (alias == null) { throw new TypeException(“The parameter alias cannot be null”); } // issue #748 // 别名小写处理 String key = alias.toLowerCase(Locale.ENGLISH); if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { throw new TypeException(“The alias ‘” + alias + “’ is already mapped to the value ‘” + typeAliases.get(key).getName() + “’.”); } // 别名与类映射 typeAliases.put(key, value); }在完成别名的解析之后会将其注册到 typeAliasRegistry 的变量 typeAliases Map 集合中。配置环境 environments 解析environments 用于事务管理器及数据源相关配置配置示例<environments default=“development”> <environment id=“development”> <transactionManager type=“JDBC”> <property name=”…” value="…"/> </transactionManager> <dataSource type=“POOLED”> <property name=“driver” value="${driver}"/> <property name=“url” value="${url}"/> <property name=“username” value="${username}"/> <property name=“password” value="${password}"/> </dataSource> </environment> <environment id=“test”> <transactionManager type=“JDBC”> <property name="…" value="…"/> </transactionManager> <dataSource type=“POOLED”> <property name=“driver” value="${driver}"/> <property name=“url” value="${url}"/> <property name=“username” value="${username}"/> <property name=“password” value="${password}"/> </dataSource> </environment></environments>从 environments 的配置来看 MyBatis 是支持多数据源的,但每个 SqlSessionFactory 实例只能选择其中一个; 若需要连接多个数据库,就得需要创建多个 SqlSessinFactory 实例。解析private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { /* * @see org.apache.ibatis.session.SqlSessionFactoryBuilder.build 时未指定 enviorment, 则取默认的 */ environment = context.getStringAttribute(“default”); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute(“id”); // 查找与 environment 匹配的配置环境 if (isSpecifiedEnvironment(id)) { // 解析事务管理 TransactionFactory txFactory = transactionManagerElement(child.evalNode(“transactionManager”)); // 解析数据源 DataSourceFactory dsFactory = dataSourceElement(child.evalNode(“dataSource”)); // 获取数据源实例 DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); // 设置配置环境 configuration.setEnvironment(environmentBuilder.build()); } } } }private boolean isSpecifiedEnvironment(String id) { if (environment == null) { // 若 environment 为空说明未指定当前 SqlSessionFactory 实例所需的配置环境;同时 environments 标签未配置 default 属性 throw new BuilderException(“No environment specified.”); } else if (id == null) { // environment 标签需要配置 id 属性 throw new BuilderException(“Environment requires an id attribute.”); } else if (environment.equals(id)) { // environment == id 说明当前匹配配置环境 return true; } return false; }因 environments 支持多数据源的配置,所以在解析时会先查找匹配当前 SqlSessionFactory 的 environment; 然后在解析当前配置环境所需的事务管理器和数据源。事务管理器解析private TransactionFactory transactionManagerElement(XNode context) throws Exception { if (context != null) { // 获取配置事务管理器的类别,也就是别名 String type = context.getStringAttribute(“type”); // 获取事务属性配置 Properties props = context.getChildrenAsProperties(); // 通过别名查找对应的事务管理器类并实例化 TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException(“Environment declaration requires a TransactionFactory.”); }事务管理器解析时会通过配置中指定的 type 别名去查找对应的 TransactionFactory 并实例化。那么 MyBatis 内部内置了哪些事务管理器呢?public Configuration() { typeAliasRegistry.registerAlias(“JDBC”, JdbcTransactionFactory.class); typeAliasRegistry.registerAlias(“MANAGED”, ManagedTransactionFactory.class); // 省略 }从 Configuration 的构造可以看出,其构造时会通过 typeAliasRegistry 注册了别名为 JDBC,MANAGED 的两种事务管理器。数据源解析private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null) { // 获取配置数据源的类别,也就是别名 String type = context.getStringAttribute(“type”); // 获取数据源属性配置 Properties props = context.getChildrenAsProperties(); // 通过别名查找数据源并实例化 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException(“Environment declaration requires a DataSourceFactory.”); }同事务管理器一样,数据源解析时也会通过指定的别名查找对应的数据源实现类同样其在 Configuration 构造时向 typeAliasRegistry 注册了三种数据源public Configuration() { // 省略 typeAliasRegistry.registerAlias(“JNDI”, JndiDataSourceFactory.class); typeAliasRegistry.registerAlias(“POOLED”, PooledDataSourceFactory.class); typeAliasRegistry.registerAlias(“UNPOOLED”, UnpooledDataSourceFactory.class); // 省略 }类型转换器 typeHandlers 解析配置示例<typeHandlers> <typeHandler handler=“org.mybatis.example.ExampleTypeHandler”/></typeHandlers><typeHandlers> <package name=“org.mybatis.example”/></typeHandlers>解析private void typeHandlerElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { if (“package”.equals(child.getName())) { String typeHandlerPackage = child.getStringAttribute(“name”); typeHandlerRegistry.register(typeHandlerPackage); } else { // 映射 java 对象类型 String javaTypeName = child.getStringAttribute(“javaType”); // 映射 jdbc 类型 String jdbcTypeName = child.getStringAttribute(“jdbcType”); // 类型转换器类名 String handlerTypeName = child.getStringAttribute(“handler”); Class<?> javaTypeClass = resolveClass(javaTypeName); JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class<?> typeHandlerClass = resolveClass(handlerTypeName); if (javaTypeClass != null) { if (jdbcType == null) { // 指定了 java type,未指定 jdbc type typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { // 指定了 java type,指定了 jdbc type typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { // 未指定 java type 按 typeHandlerClass 注册 typeHandlerRegistry.register(typeHandlerClass); } } } } }typeHandler 解析指定 javaType 和 jdbcTypepublic void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) { register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass)); }private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { // 一个 java type 可能会映射多个 jdbc type Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); typeHandlerMap.put(javaType, map); } map.put(jdbcType, handler); } // 存储 typeHandler allTypeHandlersMap.put(handler.getClass(), handler); }当指定了 javaType 和 jdbcType 最终会将二者及 typeHandler 映射并注册到 typeHandlerMap 中,从 typeHandlerMap 的数据结构来看,javaType 可能会与多个 jdbcType 映射。 譬如 String -> CHAR,VARCHAR 。指定 javaType 未指定 jdbcTypepublic void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) { // 将 type handler 实例化 register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass)); }private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) { // 获取 MappedJdbcTypes 注解 // 该注解用于设置类型转换器匹配的 jdbcType MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null) { // 遍历匹配的 jdbcType 并注册 for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { register(javaType, null, typeHandler); } } else { // 未指定 jdbcType 时按 null 处理 register(javaType, null, typeHandler); } }当类型转换器配置了 javaType 未配置 jdbcType 时,会判断类型转换器是否配置了 @MappedJdbcTypes 注解; 若配置了则使用注解值作为 jdbcType 并注册,若未配置则按 null 注册。未指定 javaType 和 jdbcTypepublic void register(Class<?> typeHandlerClass) { boolean mappedTypeFound = false; // 获取 MappedTypes 注解 // 该注解用于设置类型转换器匹配的 javaType MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class<?> javaTypeClass : mappedTypes.value()) { // 执行注册 register(javaTypeClass, typeHandlerClass); mappedTypeFound = true; } } if (!mappedTypeFound) { register(getInstance(null, typeHandlerClass)); } }当 javaType,jdbcType 均为指定时,会判断类型转换器是否配置了 @MappedTypes 注解; 若配置了则使用注解值作为 javaType 并注册。package 解析public void register(String packageName) { // 扫描指定包下的所有类 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName); Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses(); for (Class<?> type : handlerSet) { //Ignore inner classes and interfaces (including package-info.java) and abstract classes // 忽略内部类 接口 抽象类 if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) { // 执行注册 register(type); } } }当按指定包名解析时,会扫描包下的所有类(忽略内部类,接口,抽象类)并执行注册小结本文我们主要分析了 Mybatis 配置文件中标签 properties,typeAliases,enviroments,typeHandlers 的解析过程,由于 mappers 的解析比较复杂后续在进行分析;通过本文的分析我们了解到 Configuration 实例中包括以下内容:variables : Properties 类型,存储属性变量typeAliasRegistry : 别名注册中心,通过一个 Map 集合变量 typeAliases 存储别名与类的映射关系environment : 配置环境,绑定事务管理器和当前数据源typeHandlerRegistry : 类型转换器注册中心,存储 javaType 与 jdbcType,typeHandler 的映射关系,内置 jdbcType 与 typeHandler 的映射关系 ...

March 15, 2019 · 7 min · jiezi

mybatis-plus 的一种很别扭的用法

熟悉 mybatis-plus 的人都知道,mybatis-plus 提供两种包含预定义增删改查操作的接口:com.baomidou.mybatisplus.core.mapper.BaseMappercom.baomidou.mybatisplus.extension.service.IService对比这两个接口,操作都差不多,名字有一点点改变,比如 BaseMapper 里面叫 insert() 的方法,在 IService 里面叫 save()。其实我也不是很清楚为什么要单独设计 IService 接口,但是两者确实有区别,就是 IService 提供批处理操作,BaseMapper 没有。另外,IService 的默认实现 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl 就是调用 BaseMapper 来操作数据库,所以我猜 IService 是 Java 8 之前对 BaseMapper 所做的扩展,而 Java 8 之后,因为有了 default 方法,ServiceImpl 里面的东西其实都可以移到 BaseMapper 里面了。除此之外还有就是 IService 依赖于 Spring 容器,而 BaseMapper 不依赖;BaseMapper 可以继承并添加新的数据库操作,IService 要扩展的话还是得调用 Mapper,显得有些多此一举。所以,如果你既要使用批处理操作,又要添加自己的数据库操作,那就必须两个接口一起用。比如在下面一个示例项目中,就同时存在两者:// StudentService.java@Servicepublic class StudentService extends ServiceImpl<StudentMapper, Student> {}// StudentMapper.java@Componentpublic interface StudentMapper extends BaseMapper<Student> { @Select(“select * from STUDENT where FIRST_NAME=#{firstName}”) List<Student> selectByFirstName(@Param(“firstName”) String firstName);}这样每个实体都要创建两个文件,很麻烦。可不可以简化呢?可以,就像下面这样:// StudentService.java@Servicepublic class StudentService extends ServiceImpl<StudentMapper, Student> { public interface StudentMapper extends BaseMapper<Student> { @Select(“select * from STUDENT where FIRST_NAME=#{firstName}”) List<Student> selectByFirstName(@Param(“firstName”) String firstName); }}对,你没看错,就把 Mapper 直接写在 Service 里面就好。有人就会问了,这个 Mapper 能用吗?告诉你,能:@AutowiredStudentService.StudentMapper studentMapper;像上面这样引用过来,照常使用即可。另外还有一种方式就是通过 Service 把 Mapper 暴露出来:public class StudentService extends ServiceImpl<StudentMapper, Student> { public StudentMapper getMapper() { return this.baseMapper; } …这个 baseMapper 也是 StudentMapper 的实例。这样的话,使用的时候就只需要引用 StudentService 一个对象了:List list = studentService.getMapper().selectByFirstName(“First”); ...

March 14, 2019 · 1 min · jiezi

几个数据持久化框架Hibernate、JPA、Mybatis、JOOQ和JDBC Template的比较

因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的,可惜不是完全免费,最终选择JDBC Template。Hibernate和Mybatis是使用最多的两个主流框架,而JOOQ、Ebean等小众框架则知道的人不多,但也有很多独特的优点;而JPA则是一组Java持久层Api的规范,Spring Data JPA是JPA Repository的实现,本来和Hibernate、Mybatis、JOOQ之类的框架不在同一个层次上,但引入Spring Data JPA之类框架之后,我们会直接使用JPA的API查询更新数据库,就像我们使用Mybatis一样,所以这里也把JPA和其他框架放在一起进行比较。同样,JDBC和其他框架也在同一层次,位于所有持久框架的底层,但我们有时候也会直接在项目中使用JDBC,而Spring JDBC Template部分消除了使用JDBC的繁琐细节,降低了使用成本,使得我们更加愿意在项目中直接使用JDBC。一、SQL封装和性能在使用Hibernate的时候,我们查询的是POJO实体类,而不再是数据库的表,例如hql语句 select count(*) from User,里面的User是一个Java类,而不是数据库表User。这符合ORM最初的理想,ORM认为Java程序员使用OO的思维方式,和关系数据库的思维方式差距巨大,为了填补对象和关系思维方式的鸿沟,必须做一个对象到关系的映射,然后在Java的对象世界中,程序员可以使用纯的对象的思维方式,查询POJO对象,查询条件是对象属性,不再需要有任何表、字段等关系的概念,这样java程序员就更容易做持久层的操作。JPA可以视为Hibernate的儿子,也继承了这个思路,把SQL彻底封装起来,让Java程序员看不到关系的概念,用纯的面向对象思想,重新创造一个新的查询语言代替sql,比如hql,还有JPQL等。支持JPA的框架,例如Ebean都属于这种类型的框架。但封装SQL,使用另一种纯的面向对象查询语言代替sql,真的能够让程序员更容易实现持久层操作吗?MyBatis的流行证明了事实并非如此,至少在大多数情况下,使用hql并不比使用sql简单。首先,从很多角度上看,hql/JPQL等语言更加复杂和难以理解;其次就是性能上明显降低,速度更慢,内存占用巨大,而且还不好优化。最为恼火的是,当关系的概念被替换为对象的概念之后,查询语言的灵活性变得很差,表达能力也比sql弱很多。写查询语句的时候受到各种各样的限制,一个典型的例子就是多表关联查询。不管是hibernate还是jpa,表之间的连接查询,被映射为实体类之间的关联关系,这样,如果两个实体类之间没有(实现)关联关系,你就不能把两个实体(或者表)join起来查询。这是很恼火的事情,因为我们很多时候并不需要显式定义两个实体类之间的关联关系就可以实现业务逻辑,如果使用hql,只是为了join我们就必须在两个实体类之间添加代码,而且还不能逆向工程,如果表里面没有定义外键约束的话,逆向工程会把我们添加的关联代码抹掉。MyBatis则是另外一种类型的持久化框架,它没有封装SQL也没有创建一种新的面相对象的查询语言,而是直接使用SQL作为查询语言,只是把结果填入POJO对象而已。使用sql并不比hql和JPQL困难,查询速度快,可以灵活使用任意复杂的查询只要数据库支持。从SQL封装角度上看,MyBatis比Hibernate和JPA成功,SQL本不该被封装和隐藏,让Java程序员使用SQL既不麻烦也更容易学习和上手,这应该是MyBatis流行起来的重要原因。轻量级持久层框架JOOQ也和MyBatis一样,直接使用SQL作为查询语言,比起MyBatis,JOOQ虽然知名度要低得多,但JOOQ不但和MyBatis一样可以利用SQL的灵活性和高效率,通过逆向工程,JOOQ还可以用Java代码来编写SQL语句,利用IDE的代码自动补全功能,自动提示表名和字段名,减少程序员记忆负担,还可以在元数据发生变化时发生编译错误,提示程序员修改相应的SQL语句。Ebean作为一种基于JPA的框架,它也使用JPQL语言进行查询,多数情况下会让人很恼火。但据说Ebean不排斥SQL,可以直接用SQL查询,也可以用类似JOOQ的DSL方式在代码中构造SQL语句(还是JPQL语句?),但没用过Ebean,所以具体细节不清楚。JDBC Template就不用说了,它根本没做ORM,当然是纯SQL查询。利用Spring框架,可以把JDBC Template和JPA结合起来使用,在JPA不好查询的地方,或者效率低不好优化的地方使用JDBC,缓解了Hibernate/JPA封装SQL造成的麻烦,但我仍没看到任何封装SQL的必要性,除了给程序员带来一大堆麻烦和学习负担之外,没有太明显的好处。二、DSL和变化适应性为了实现复杂的业务逻辑,不论是用SQL还是hql或者JPQL,我们都不得不写很多简单的或者复杂的查询语句,ORM无法减少这部分工作,最多是用另一种面向对象风格的语言去表达查询需求,如前所述,用面向对象风格的语言不见得比SQL更容易。通常业务系统中会有很多表,每个表都有很多字段,即便是编写最简单的查询语句也不是一件容易的事情,需要记住数据库中有哪些表,有哪些字段,记住有哪些函数等。写查询语句很多时候成为一件头疼的事情。QueryDSL、JOOQ、Ebean甚至MyBatis和JPA都设计一些特性,帮助开发人员编写查询语句,有人称之为“DSL风格数据库编程”。最早实现这类功能的可能是QueryDSL,把数据库的表结构逆向工程为java的类,然后可以让java程序员能够用java的语法构造出一个复杂的查询语句,利用IDE的代码自动补全功能,可以自动提示表名、字段名、查询语句的关键字等,很成功的简化了查询语句的编写,免除了程序员记忆各种名字、函数和关键字的负担。QueryDSL有很多版本,但用得多的是QueryDSL JPA,可以帮助开发人员编写JPQL语句,如前所述,JPQL语句有很多局限不如SQL灵活高效。后来的JOOQ和Ebean,基本上继承了QueryDSL的思路,Ebean基本上还是JPA风格的ORM框架,虽然也支持SQL,但不清楚其DSL特性是否支持SQL语句编写,在官网上看到的例子都是用于构造JPQL语句。这里面最成功的应该是JOOQ,和QueryDSL不同,JOOQ的DSL编程是帮助开发人员编写SQL语句,抛弃累赘的ORM概念,JOOQ这个功能非常轻小,非常容易学习和使用,同时性能也非常好,不像QueryDSL和Ebean,需要了解复杂的JPA概念和各种奇异的限制,JOOQ编写的就是普通的SQL语句,只是把查询结果填充到实体类中(严格说JOOQ没有实体类,只是自动生成的Record对象),JOOQ甚至不一定要把结果转换为实体类,可以让开发人员按照字段取得结果的值,相对于JDBC,JOOQ会把结果值转换为合适的Java类型,用起来比JDBC更简单。传统主流的框架对DSL风格支持得很少,Hibernate里面基本上没有看到有这方面的特性。MyBatis提供了"SQL语句构建器"来帮助开发人员构造SQL语句,但和QueryDSL/JOOQ/Ebean差很多,不能提示表名和字段名,语法也显得累赘不像SQL。JPA给人的印象是复杂难懂,它的MetaModel Api继承了特点,MetaModel API+Criteria API,再配合Hibernate JPA 2 Metamodel Generator,让人有点QueryDSL JPA的感觉,只是绕了一个大大的弯,叠加了好几层技术,最后勉强实现了QueryDSL JPA的简单易懂的功能。很多人不推荐JPA+QueryDSL的用法,而是推荐JPA MetaModel API+Criteria API+Hibernate JPA 2 Metamodel Generator的用法,让人很难理解,也许是因为这个方案是纯的标准的JPA方案。数据库DSL编程的另一个主要卖点是变化适应性强,数据库表结构在开发过程中通常会频繁发生变化,传统的非DSL编程,字段名只是一个字符串,如果字段名或者类型改变之后,查询语句没有相应修改,编译不会出错,也容易被开发人员忽略,是bug的一个主要来源。DSL编程里面,字段被逆向工程为一个java类的属性,数据库结构改变之后,作为java代码一部分的查询语句会发生编译错误,提示开发人员进行修改,可以减少大量bug,减轻测试的负担,提高软件的可靠性和质量。三、跨数据库移植Hibernate和JPA使用hql和JPQL这类数据库无关的中间语言描述查询,可以在不同数据库中无缝移植,移植到一个SQL有巨大差别的数据库通常不需要修改代码或者只需要修改很少的代码。Ebean如果不使用原生SQL,而是使用JPA的方式开发,也能在不同数据库中平滑的移植。MyBatis和JOOQ直接使用SQL,跨数据库移植时都难免要修改SQL语句。这方面MyBatis比较差,只有一个动态SQL提供的特性,对于不同的数据库编写不同的sql语句。JOOQ虽然无法像Hibernate和JPA那样无缝移植,但比MyBatis好很多。JOOQ的DSL很大一部分是通用的,例如分页查询中,Mysql的limit/offset关键字是很方便的描述方式,但Oracle和SQLServer的SQL不支持,如果我们用JOOQ的DSL的limit和offset方法构造SQL语句,不修改移植到不支持limit/offset的Oracle和SQLServer上,我们会发现这些语句还能正常使用,因为JOOQ会把limit/offset转换成等价的目标数据库的SQL语句。JOOQ根据目标数据库转换SQL语句的特性,使得在不同数据库之间移植的时候,只需要修改很少的代码,明显优于MyBatis。JDBC Template应该最差,只能尽量使用标准sql语句来减少移植工作量。四、安全性一般来说,拼接查询语句都会有安全隐患,容易被sql注入攻击。不论是jdbc,还是hql/JPQL,只要使用拼接的查询语句都是不安全的。对于JDBC来说,使用参数化的sql语句代替拼接,可以解决问题。而JPA则应该使用Criteria API解决这个问题。对于JOOQ之类的DSL风格框架,最终会被render为参数化的sql,天生免疫sql注入攻击。Ebean也支持DSL方式编程,也同样免疫sql注入攻击。这是因为DSL风格编程参数化查询比拼接字符串查询更简单,没人会拼接字符串。而jdbc/hql/JPQL拼接字符串有时候比参数化查询更简单,特别是jdbc,很多人会偷懒使用不安全的方式。五、JOOQ的失败之处可能大部分人会不同意,虽然Hibernate、JPA仍然大行其道,是最主流的持久化框架,但其实这种封装SQL的纯正ORM已经过时,效益低于使用它们的代价,应该淘汰了。MyBatis虽然有很多优点,但它的优点JOOQ基本上都有,而且多数还更好。MyBatis最大的缺点是难以避免写xml文件,xml文件编写困难,容易出错,还不容易查找错误。相对于JOOQ,MyBatis在多数情况下没有任何优势。Ebean同时具有很多不同框架的优点,但它是基于JPA的,难免有JPA的各种限制,这是致命的缺点。JOOQ这个极端轻量级的框架技术上是最完美的,突然有一天几个Web系统同时崩了,最后发现是JOOQ试用期过期了,这是JOOQ的失败之处,它不是完全免费的,只是对MySql之类的开源数据库免费。最终,我决定选择JDBC Template。

March 12, 2019 · 1 min · jiezi

SpringBoot + MyBatisPlus + ShardingJDBC 分库分表读写分离整合

本文描述在本地数据库模拟分库分表、读写分离的整合实现,假定会员数据按照 ID 取模进行分库分表,分为 2 个主库,每个库分配一个读库,累计 100 张表。如下表所示:库主/从表user_1主t_user_00 ~ t_user_49user_slave_1从t_user_00 ~ t_user_49user_2主t_user_50 ~ t_user_99user_slave_2从t_user_50 ~ t_user_99本文主要展示核心代码,部分如 Controller、Service 层的测试代码实现非常简单,故而省略这部分代码。依赖版本<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc</artifactId> <version>3.0.0.M1</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version></dependency><dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.41</version></dependency>数据准备use user_1;CREATE TABLE t_user_00 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_01 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_02 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_2;CREATE TABLE t_user_50 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_51 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_52 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_slave_1;CREATE TABLE t_user_00 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_01 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_02 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;use user_slave_2;CREATE TABLE t_user_50 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_51 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE t_user_52 ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(45) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;代码实现数据源配置server: port: 23333spring: application: name: pt-framework-demo datasource: type: com.alibaba.druid.pool.DruidDataSourcedatasource: default: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user: master: user1: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user2: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true slave: user1: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_slave_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true user2: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3307/user_slave_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false username: root password: root test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 18800 filters: mergeStat,wall,slf4j connectionProperties: druid.stat.slowSqlMillis=2000 validationQuery: SELECT 1 poolPreparedStatements: true主从、读写分离/** * Created by Captain on 01/03/2019. /@Configuration@MapperScan(basePackages = {“com.xxxx.framework.usermapper”}, sqlSessionFactoryRef = “userShardingSqlSessionFactory”)public class UserShardingDBConfiguration { @Value("${spring.datasource.type}") private Class<? extends DataSource> dataSourceType; private static final String USER_1_MASTER = “dsUser1Master”; private static final String USER_1_SLAVE = “dsUser1Slave”; private static final String USER_2_MASTER = “dsUser2Master”; private static final String USER_2_SLAVE = “dsUser2Slave”; private static final String USER_SHARDING_1 = “dsMasterSlave1”; private static final String USER_SHARDING_2 = “dsMasterSlave2”; private static final String USER_SHARDING_DATA_SOURCE = “userSharding”; @Bean(USER_1_MASTER) @ConfigurationProperties(prefix = “datasource.user.master.user1”) public DataSource dsUser1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_2_MASTER) @ConfigurationProperties(prefix = “datasource.user.master.user2”) public DataSource dsUser2(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_1_SLAVE) @ConfigurationProperties(prefix = “datasource.user.slave.user1”) public DataSource dsUserSlave1(){ return DataSourceBuilder.create().type(dataSourceType).build(); } /* * user_2 * @return / @Bean(USER_2_SLAVE) @ConfigurationProperties(prefix = “datasource.user.slave.user2”) public DataSource dsUserSlave2(){ return DataSourceBuilder.create().type(dataSourceType).build(); } @Bean(USER_SHARDING_1) public DataSource masterSlave1(@Qualifier(USER_1_MASTER) DataSource dsUser1,@Qualifier(USER_1_SLAVE) DataSource dsUserSlave1) throws Exception { Map<String,DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(USER_1_MASTER, dsUser1); dataSourceMap.put(USER_1_SLAVE, dsUserSlave1); MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration(“dsUser1”, USER_1_MASTER, Lists.newArrayList(USER_1_SLAVE)); return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>()); } @Bean(USER_SHARDING_2) public DataSource masterSlave2(@Qualifier(USER_2_MASTER) DataSource dsUser2,@Qualifier(USER_2_SLAVE) DataSource dsUserSlave2) throws Exception { Map<String,DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(USER_2_MASTER, dsUser2); dataSourceMap.put(USER_2_SLAVE, dsUserSlave2); MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration(“dsUser2”, USER_2_MASTER, Lists.newArrayList(USER_2_SLAVE)); return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>()); } @Bean(USER_SHARDING_DATA_SOURCE) public DataSource dsUser(@Qualifier(USER_SHARDING_1) DataSource dsUser1, @Qualifier(USER_SHARDING_2) DataSource dsUser2) throws Exception { Map<String, DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(“dsUser1”, dsUser1); dataSourceMap.put(“dsUser2”, dsUser2); ShardingRuleConfiguration userRule = getUserRule(); userRule.setDefaultDataSourceName(“dsUser”); return ShardingDataSourceFactory.createDataSource(dataSourceMap, userRule, new ConcurrentHashMap<>(), new Properties()); } /* * 配置分片规则 * @return / private ShardingRuleConfiguration getUserRule(){ ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”, new MemberIdShardingSchemeAlgorithm())); shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”,new MemberIdShardingTableAlgorithm())); shardingRuleConfig.getBindingTableGroups().add(“t_user”); return shardingRuleConfig; } @Bean(“userShardingSqlSessionFactory”) public SqlSessionFactory userSqlSessionFactory(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource) throws Exception{ MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:usermapper/.xml”)); return sqlSessionFactoryBean.getObject(); } @Bean(“userTransaction”) public DataSourceTransactionManager userTransactionManager(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource){ return new DataSourceTransactionManager(dataSource); }}分库策略/** * CoreUser 分库策略 * Created by Captain on 01/03/2019. /public class MemberIdShardingSchemeAlgorithm implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { for ( String str : availableTargetNames ){ int index = shardingValue.getValue() % 100; return str + (index > 49 ? “2” : “1”); } return null; }}分表策略/* * 会员信息分表策略,按照 id 分表 * Created by Captain on 04/03/2019. /public class MemberIdShardingTableAlgorithm implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { int index = shardingValue.getValue() % 100; return shardingValue.getLogicTableName() + “_” + (index < 10 ? “0” + index : index + “”); }}实体类/* * Created by Captain on 01/03/2019. /@TableName(“t_user”)public class User { @TableId(type = IdType.INPUT) private Integer id; private String name; private Integer age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; }}Mapper/* * Created by Captain on 04/03/2019. */public interface UserMapper extends BaseMapper<User> {}测试预期模拟过程没有实际做主从同步,写入“主库”中的数据并不能自动同步至“从库”,因此,插入数据后,需要手动写入数据至对应的从库,并且可对数据进行差异写入,测试查询时可根据差异来判断读写分离是否生效。测试用例预期结果插入数据 id 指定为 8902user_1 中数据写入成功插入数据 id 指定为 8952user_2 中数据写入成功查询 id 为 8902 的数据查询到 user_slave_1 中的结果查询 id 为 8952 的数据查询到 user_slave_2 中的结果 ...

March 4, 2019 · 5 min · jiezi

SpringBoot 实战 (十三) | 整合 MyBatis (XML 版)

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 SpringBoot 与 Mybatis 的整合以及 Mybatis 的使用,之前介绍过了 SpringBoot 整合MyBatis 注解版的使用,上一篇介绍过 MyBatis 的理论,今天这篇就不介绍 MyBatis 的理论了,有兴趣的跳转阅读:SpringBoot 实战 (十三) | 整合 MyBatis (注解版)准备工作SpringBoot 2.1.3IDEAJDK 8创建表CREATE TABLE student ( id int(32) NOT NULL AUTO_INCREMENT, student_id varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘学号’, name varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘姓名’, age int(11) NULL DEFAULT NULL COMMENT ‘年龄’, city varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘所在城市’, dormitory varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘宿舍’, major varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘专业’, PRIMARY KEY (id) USING BTREE)ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8;引入依赖<dependencies> <!– jdbc 连接驱动 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!– web 启动类 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mybatis 依赖 –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!– druid 数据库连接池 –> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.14</version> </dependency> <!– Mysql 连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <!– 分页插件 –> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency> <!– test 依赖 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!– springboot maven 插件 –> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!– mybatis generator 自动生成代码插件 –> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> </plugin> </plugins> </build>代码解释很详细了,但这里提一嘴,mybatis generator 插件用于自动生成代码,pagehelper 插件用于物理分页。项目配置server: port: 8080spring: datasource: name: test url: jdbc:mysql://127.0.0.1:3306/test username: root password: 123456 #druid相关配置 type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select ‘x’ testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20## 该配置节点为独立的节点,有很多同学容易将这个配置放在spring的节点下,导致配置无法被识别mybatis: mapper-locations: classpath:mapping/*.xml #注意:一定要对应mapper映射xml文件的所在路径 type-aliases-package: com.nasus.mybatisxml.model # 注意:对应实体类的路径#pagehelper分页插件pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSqlmybatis generator 配置文件这里要注意,配置 pom.xml 中 generator 插件所对应的配置文件时,在 Pom.xml 加入这一句,说明 generator 插件所对应的配置文件所对应的配置文件路径。这里已经在 Pom 中配置了,请见上面的 Pom 配置。${basedir}/src/main/resources/generator/generatorConfig.xmlgeneratorConfig.xml :<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <!– 数据库驱动:选择你的本地硬盘上面的数据库驱动包–> <classPathEntry location=“D:\repository\mysql\mysql-connector-java\5.1.47\mysql-connector-java-5.1.47.jar”/> <context id=“DB2Tables” targetRuntime=“MyBatis3”> <commentGenerator> <property name=“suppressDate” value=“true”/> <!– 是否去除自动生成的注释 true:是 : false:否 –> <property name=“suppressAllComments” value=“true”/> </commentGenerator> <!–数据库链接URL,用户名、密码 –> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://127.0.0.1/test” userId=“root” password=“123456”> </jdbcConnection> <javaTypeResolver> <property name=“forceBigDecimals” value=“false”/> </javaTypeResolver> <!– 生成模型的包名和位置–> <javaModelGenerator targetPackage=“com.nasus.mybatisxml.model” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> <property name=“trimStrings” value=“true”/> </javaModelGenerator> <!– 生成映射文件的包名和位置–> <sqlMapGenerator targetPackage=“mapping” targetProject=“src/main/resources”> <property name=“enableSubPackages” value=“true”/> </sqlMapGenerator> <!– 生成DAO的包名和位置–> <javaClientGenerator type=“XMLMAPPER” targetPackage=“com.nasus.mybatisxml.mapper” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> </javaClientGenerator> <!– 要生成的表 tableName是数据库中的表名或视图名 domainObjectName是实体类名–> <table tableName=“student” domainObjectName=“Student” enableCountByExample=“false” enableUpdateByExample=“false” enableDeleteByExample=“false” enableSelectByExample=“false” selectByExampleQueryId=“false”></table> </context></generatorConfiguration>代码注释很详细,不多说。生成代码过程第一步:选择编辑配置第二步:选择添加 Maven 配置第三步:添加命令 mybatis-generator:generate -e 点击确定第四步:运行该配置,生成代码特别注意!!!同一张表一定不要运行多次,因为 mapper 的映射文件中会生成多次的代码,导致报错,切记。如要运行多次,请把上次生成的 mapper 映射文件代码删除再运行。第五步:检查生成结果遇到的问题请参照别人写好的遇到问题的解决方法,其中我就遇到数据库时区不对以及只生成 Insert 方法这两个问题。都是看以下这篇文章解决的:Mybatis Generator自动生成代码以及可能出现的问题生成的代码1、实体类:Student.javapackage com.nasus.mybatisxml.model;public class Student { private Long id; private Integer age; private String city; private String dormitory; private String major; private String name; private Long studentId; // 省略 get 和 set 方法}2、mapper 接口:StudentMapper.javapackage com.nasus.mybatisxml.mapper;import com.nasus.mybatisxml.model.Student;import java.util.List;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface StudentMapper { int deleteByPrimaryKey(Long id); int insert(Student record); int insertSelective(Student record); Student selectByPrimaryKey(Long id); // 我添加的方法,相应的要在映射文件中添加此方法 List<Student> selectStudents(); int updateByPrimaryKeySelective(Student record); int updateByPrimaryKey(Student record);}3、映射文件:StudentMapper.xml<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace=“com.nasus.mybatisxml.mapper.StudentMapper” > <resultMap id=“BaseResultMap” type=“com.nasus.mybatisxml.model.Student” > <id column=“id” property=“id” jdbcType=“BIGINT” /> <result column=“age” property=“age” jdbcType=“INTEGER” /> <result column=“city” property=“city” jdbcType=“VARCHAR” /> <result column=“dormitory” property=“dormitory” jdbcType=“VARCHAR” /> <result column=“major” property=“major” jdbcType=“VARCHAR” /> <result column=“name” property=“name” jdbcType=“VARCHAR” /> <result column=“student_id” property=“studentId” jdbcType=“BIGINT” /> </resultMap> <sql id=“Base_Column_List” > id, age, city, dormitory, major, name, student_id </sql> <select id=“selectByPrimaryKey” resultMap=“BaseResultMap” parameterType=“java.lang.Long” > select <include refid=“Base_Column_List” /> from student where id = #{id,jdbcType=BIGINT} </select> <delete id=“deleteByPrimaryKey” parameterType=“java.lang.Long” > delete from student where id = #{id,jdbcType=BIGINT} </delete> <insert id=“insert” parameterType=“com.nasus.mybatisxml.model.Student” > insert into student (id, age, city, dormitory, major, name, student_id) values (#{id,jdbcType=BIGINT}, #{age,jdbcType=INTEGER}, #{city,jdbcType=VARCHAR}, #{dormitory,jdbcType=VARCHAR}, #{major,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{studentId,jdbcType=BIGINT}) </insert> <insert id=“insertSelective” parameterType=“com.nasus.mybatisxml.model.Student” > insert into student <trim prefix=”(” suffix=")" suffixOverrides="," > <if test=“id != null” > id, </if> <if test=“age != null” > age, </if> <if test=“city != null” > city, </if> <if test=“dormitory != null” > dormitory, </if> <if test=“major != null” > major, </if> <if test=“name != null” > name, </if> <if test=“studentId != null” > student_id, </if> </trim> <trim prefix=“values (” suffix=")" suffixOverrides="," > <if test=“id != null” > #{id,jdbcType=BIGINT}, </if> <if test=“age != null” > #{age,jdbcType=INTEGER}, </if> <if test=“city != null” > #{city,jdbcType=VARCHAR}, </if> <if test=“dormitory != null” > #{dormitory,jdbcType=VARCHAR}, </if> <if test=“major != null” > #{major,jdbcType=VARCHAR}, </if> <if test=“name != null” > #{name,jdbcType=VARCHAR}, </if> <if test=“studentId != null” > #{studentId,jdbcType=BIGINT}, </if> </trim> </insert> <update id=“updateByPrimaryKeySelective” parameterType=“com.nasus.mybatisxml.model.Student” > update student <set > <if test=“age != null” > age = #{age,jdbcType=INTEGER}, </if> <if test=“city != null” > city = #{city,jdbcType=VARCHAR}, </if> <if test=“dormitory != null” > dormitory = #{dormitory,jdbcType=VARCHAR}, </if> <if test=“major != null” > major = #{major,jdbcType=VARCHAR}, </if> <if test=“name != null” > name = #{name,jdbcType=VARCHAR}, </if> <if test=“studentId != null” > student_id = #{studentId,jdbcType=BIGINT}, </if> </set> where id = #{id,jdbcType=BIGINT} </update> <update id=“updateByPrimaryKey” parameterType=“com.nasus.mybatisxml.model.Student” > update student set age = #{age,jdbcType=INTEGER}, city = #{city,jdbcType=VARCHAR}, dormitory = #{dormitory,jdbcType=VARCHAR}, major = #{major,jdbcType=VARCHAR}, name = #{name,jdbcType=VARCHAR}, student_id = #{studentId,jdbcType=BIGINT} where id = #{id,jdbcType=BIGINT} </update> <!– 我添加的方法 –> <select id=“selectStudents” resultMap=“BaseResultMap”> SELECT <include refid=“Base_Column_List” /> from student </select></mapper>serviec 层1、接口:public interface StudentService { int addStudent(Student student); Student findStudentById(Long id); PageInfo<Student> findAllStudent(int pageNum, int pageSize);}2、实现类@Servicepublic class StudentServiceImpl implements StudentService{ //会报错,不影响 @Resource private StudentMapper studentMapper; /** * 添加学生信息 * @param student * @return / @Override public int addStudent(Student student) { return studentMapper.insert(student); } /* * 根据 id 查询学生信息 * @param id * @return / @Override public Student findStudentById(Long id) { return studentMapper.selectByPrimaryKey(id); } /* * 查询所有学生信息并分页 * @param pageNum * @param pageSize * @return */ @Override public PageInfo<Student> findAllStudent(int pageNum, int pageSize) { //将参数传给这个方法就可以实现物理分页了,非常简单。 PageHelper.startPage(pageNum, pageSize); List<Student> studentList = studentMapper.selectStudents(); PageInfo result = new PageInfo(studentList); return result; }}controller 层@RestController@RequestMapping("/student")public class StudentController { @Autowired private StudentService studentService; @GetMapping("/{id}") public Student findStidentById(@PathVariable(“id”) Long id){ return studentService.findStudentById(id); } @PostMapping("/add") public int insertStudent(@RequestBody Student student){ return studentService.addStudent(student); } @GetMapping("/list") public PageInfo<Student> findStudentList(@RequestParam(name = “pageNum”, required = false, defaultValue = “1”) int pageNum, @RequestParam(name = “pageSize”, required = false, defaultValue = “10”) int pageSize){ return studentService.findAllStudent(pageNum,pageSize); }}启动类@SpringBootApplication@MapperScan(“com.nasus.mybatisxml.mapper”) // 扫描 mapper 接口,必须加上public class MybatisxmlApplication { public static void main(String[] args) { SpringApplication.run(MybatisxmlApplication.class, args); }}提一嘴,@MapperScan(“com.nasus.mybatisxml.mappe”) 这个注解非常的关键,这个对应了项目中 mapper(dao) 所对应的包路径,必须加上,否则会导致异常。Postman 测试1、插入方法:2、根据 id 查询方法:3、分页查询方法:源码下载https://github.com/turoDog/De…帮忙点个 star 可好?后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 1, 2019 · 5 min · jiezi

mybatis 使用foreach 数据类型不对导致报错

起因使用mybatis动态sql进行遍历条件的时候报了下面这个错误:Caused by: java.lang.IllegalStateException: Type handler was null on parameter mapping for property ‘__frch_item_0’. It was either not specified and/or could not be found for the javaType (com.test.Report) : jdbcType (null) combination. at org.apache.ibatis.mapping.ParameterMapping$Builder.validate (ParameterMapping.java:117) at org.apache.ibatis.mapping.ParameterMapping$Builder.build (ParameterMapping.java:104) at org.apache.ibatis.builder.SqlSourceBuilder$ParameterMappingTokenHandler.buildParameterMapping (SqlSourceBuilder.java:123) at org.apache.ibatis.builder.SqlSourceBuilder$ParameterMappingTokenHandler.handleToken (SqlSourceBuilder.java:67) at org.apache.ibatis.parsing.GenericTokenParser.parse (GenericTokenParser.java:69) at org.apache.ibatis.builder.SqlSourceBuilder.parse (SqlSourceBuilder.java:45) at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql (DynamicSqlSource.java:44) at org.apache.ibatis.mapping.MappedStatement.getBoundSql (MappedStatement.java:292) at org.apache.ibatis.executor.CachingExecutor.query (CachingExecutor.java:81) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList (DefaultSqlSession.java:148) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList (DefaultSqlSession.java:141) at sun.reflect.NativeMethodAccessorImpl.invoke0 (Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke (Method.java:498) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke (SqlSessionTemplate.java:434) at com.sun.proxy.$Proxy57.selectList (Unknown Source) at org.mybatis.spring.SqlSessionTemplate.selectList (SqlSessionTemplate.java:231)<select id=“getById” parameterType=“map” resultType=“Report”> select * from test where orderid in <foreach collection=“idlist” item=“item” index=“index” open="(" separator="," close=")"> #{item} </foreach> </select>怎么找都没找到原因,最后发现这个sql之所以报错并不是这个sql写错了,而是传入的参数报错了,我认为参数是这个样子的:{ “name”:“张三”, “idlsit”:{1,2,3}}最后发现,参数是这个样子的的:{ “name”:“张三”, “idlsit”:{Report对象,Report对象,Report对象}}看到这里可能会觉得怎么可能出现这种问题,编译都不会通过,但是通过特殊的情况确实产生了,我的代码如下: List<String> idList = reportDao.getIdList(start, end); if (null != idList && idList.size() >= 1) { result = orderReportDao.getById(idList, start); }sql: <select id=“getIdList” parameterType=“map” resultType=“Report”>//这个返回值类型错了 select id from test_id </select> <select id=“getById” parameterType=“map” resultType=“Report”> select * from test where orderid in <foreach collection=“idlist” item=“item” index=“index” open="(" separator="," close=")"> #{item} </foreach> </select>实际上在执行过程中getIdList的返回类型错了,但是没有报错误,返回值List < String > idList实际上是List < Report > idList,这竟然没有报错,可能反射有关。 ...

February 28, 2019 · 1 min · jiezi

spring boot 整合mybatis 无法输出sql的问题

使用spring boot整合mybatis,测试功能的时候,遇到到了sql问题,想要从日志上看哪里错了,但是怎么都无法输出执行的sql,我使用的是log4j2,百度了一下,很多博客都说,加上下面的日志配置: <logger name=“java.sql.Statement” level=“debug”/> <logger name=“java.sql.PreparedStatement” level=“debug”/> <logger name=“java.sql.Connection” level=“debug”/> <logger name=“ResultSet” level=“debug”/>经实际测试,没什么用。只好去官网找解决方案,在mybatis日志上看到,如果存在内置日志,就是用内置的日志,自己配置的日志就忽略了。MyBatis 内置日志工厂基于运行时自省机制选择合适的日志工具。它会使用第一个查找得到的工具(按上文列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。 不少应用服务器(如 Tomcat 和 WebShpere)的类路径中已经包含 Commons Logging,所以在这种配置环境下的 MyBatis 会把它作为日志工具,记住这点非常重要。这将意味着,在诸如 WebSphere 的环境中,它提供了 Commons Logging 的私有实现,你的 Log4J 配置将被忽略。MyBatis 将你的 Log4J 配置忽略掉是相当令人郁闷的(事实上,正是因为在这种配置环境下,MyBatis 才会选择使用 Commons Logging 而不是 Log4J)。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志工具,你可以通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择别的日志工具。<configuration> <settings> … <setting name=“logImpl” value=“LOG4J”/> … </settings></configuration> logImpl 可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了接口 org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名。(译者注:可以参考org.apache.ibatis.logging.slf4j.Slf4jImpl.java的实现)所以我就创建了一个mybatis-config文件:<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE configuration PUBLIC “-//mybatis.org//DTD Config 3.0//EN” “http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <settings> <setting name=“logImpl” value=“LOG4J2”/> </settings></configuration>配置mybatis:mybatis: type-aliases-package: com.demo mapper-locations: classpath:mapper/.xml configuration: cache-enabled: true lazy-loading-enabled: true multiple-result-sets-enabled: true default-executor-type: simple default-statement-timeout: 25000 config-location: classpath:mybatis-config.xml然而启动的时候报错:aused by: java.lang.IllegalStateException: Property ‘configuration’ and ‘configLocation’ can not specified with together at org.springframework.util.Assert.state(Assert.java:73) ~[spring-core-5.0.10.RELEASE.jar:5.0.10.RELEASE] at org.mybatis.spring.SqlSessionFactoryBean.afterPropertiesSet(SqlSessionFactoryBean.java:377) ~[mybatis-spring-1.3.2.jar:1.3.2] at org.mybatis.spring.SqlSessionFactoryBean.getObject(SqlSessionFactoryBean.java:547) ~[mybatis-spring-1.3.2.jar:1.3.2] at org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.sqlSessionFactory(MybatisAutoConfiguration.java:153) ~[mybatis-spring-boot-autoconfigure-1.3.2.jar:1.3.2] at org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration$$EnhancerBySpringCGLIB$$8eb42bd5.CGLIB$sqlSessionFactory$0(<generated>) ~[mybatis-spring-boot-autoconfigure-1.3.2.jar:1.3.2] at org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration$$EnhancerBySpringCGLIB$$8eb42bd5$$FastClassBySpringCGLIB$$1132516e.invoke(<generated>) ~[mybatis-spring-boot-autoconfigure-1.3.2.jar:1.3.2] 报错信息说configuration和configLocation不能同时存在,所以我就想两个功能一定是相同的,只不过配置方式不一样,一个是xml,一个是spring boot的风格,所以我就删掉了config-location,在configuration下面找到了一个log-impl,加上这个配置之后,问题就解决了,如下:mybatis: type-aliases-package: com.demo mapper-locations: classpath:mapper/.xml configuration: cache-enabled: true lazy-loading-enabled: true multiple-result-sets-enabled: true default-executor-type: simple default-statement-timeout: 25000 log-impl: org.apache.ibatis.logging.log4j2.Log4j2Impl输出日志: ==> Preparing: select prodid,prodname,isgroupinsur,isdtbprod from bx_product where prodid in ( 1 , 2 , 3 ) ==> Parameters: <== Total: 0只要加这个配置就能解决问题了。 ...

February 28, 2019 · 1 min · jiezi

SpringBoot 实战 (九) | 整合 Mybatis

微信公众号:一个优秀的废人如有问题或建议,请后台留言,我会尽力解决你的问题。前言如题,今天介绍 SpringBoot 与 Mybatis 的整合以及 Mybatis 的使用,本文通过注解的形式实现。什么是 MybatisMyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生 Map 使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java 对象)映射成数据库中的记录。优点:简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个 jar 文件+配置几个 sql 映射文件易于学习,易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。灵活:mybatis 不会对应用程序或者数据库的现有设计强加任何影响。 sql 写在 xml 里,便于统一管理和优化。通过 sql 基本上可以实现我们不使用数据访问框架可以实现的所有功能,或许更多。解除 sql 与程序代码的耦合:通过提供 DAL 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql 和代码的分离,提高了可维护性。提供映射标签,支持对象与数据库的 orm 字段关系映射提供对象关系映射标签,支持对象关系组建维护提供xml标签,支持编写动态 sql。缺点:编写 SQL 语句时工作量很大,尤其是字段多、关联表多时,更是如此。SQL 语句依赖于数据库,导致数据库移植性差,不能更换数据库。框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。二级缓存机制不佳准备工作IDEAJDK1.8SpringBoot 2.1.3sql 语句,创建表,插入数据:CREATE TABLE student ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(20) NOT NULL, age double DEFAULT NULL, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;INSERT INTO student VALUES (‘1’, ‘aaa’, ‘21’);INSERT INTO student VALUES (‘2’, ‘bbb’, ‘22’);INSERT INTO student VALUES (‘3’, ‘ccc’, ‘23’);pom.xml 文件配置依赖<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.nasus</groupId> <artifactId>mybatis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>mybatis</name> <description>mybatis Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!– 启动 web –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– mybatis 依赖 –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!– mysql 连接类 –> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– druid 数据库连接池–> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.9</version> </dependency> <!– lombok 插件 用于简化实体代码 –> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!– 单元测试 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> <version>1.0.0.Final</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>application.yaml 配置文件spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver实体类import javax.persistence.GeneratedValue;import javax.persistence.Id;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class Student { @Id @GeneratedValue private Integer id; private String name; private Integer age;}使用了 lombok 简化了代码。dao 层import com.nasus.mybatis.domain.Student;import java.util.List;import org.apache.ibatis.annotations.Delete;import org.apache.ibatis.annotations.Insert;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import org.apache.ibatis.annotations.Update;@Mapperpublic interface StudentMapper { @Insert(“insert into student(name, age) values(#{name}, #{age})”) int add(Student student); @Update(“update student set name = #{name}, age = #{age} where id = #{id}”) int update(@Param(“name”) String name, @Param(“age”) Integer age, @Param(“id”) Integer id); @Delete(“delete from student where id = #{id}”) int delete(int id); @Select(“select id, name as name, age as age from student where id = #{id}”) Student findStudentById(@Param(“id”) Integer id); @Select(“select id, name as name, age as age from student”) List<Student> findStudentList();}这里有必要解释一下,@Insert 、@Update、@Delete、@Select 这些注解中的每一个代表了执行的真实 SQL。 它们每一个都使用字符串数组 (或单独的字符串)。如果传递的是字符串数组,它们由每个分隔它们的单独空间串联起来。这就当用 Java 代码构建 SQL 时避免了“丢失空间”的问题。 然而,如果你喜欢,也欢迎你串联单独 的字符串。属性:value,这是字符串 数组用来组成单独的 SQL 语句。@Param 如果你的映射方法的形参有多个,这个注解使用在映射方法的参数上就能为它们取自定义名字。若不给出自定义名字,多参数(不包括 RowBounds 参数)则先以 “param” 作前缀,再加上它们的参数位置作为参数别名。例如 #{param1},#{param2},这个是默认值。如果注解是 @Param(“id”),那么参数就会被命名为 #{id}。service 层import com.nasus.mybatis.domain.Student;import java.util.List;public interface StudentService { int add(Student student); int update(String name, Integer age, Integer id); int delete(Integer id); Student findStudentById(Integer id); List<Student> findStudentList();}实现类:import com.nasus.mybatis.dao.StudentMapper;import com.nasus.mybatis.domain.Student;import com.nasus.mybatis.service.StudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; /** * 添加 Student * @param name * @param age * @return / @Override public int add(Student student) { return studentMapper.add(student); } /* * 更新 Student * @param name * @param age * @param id * @return / @Override public int update(String name, Integer age, Integer id) { return studentMapper.update(name,age,id); } /* * 删除 Student * @param id * @return / @Override public int delete(Integer id) { return studentMapper.delete(id); } /* * 根据 id 查询 Student * @param id * @return / @Override public Student findStudentById(Integer id) { return studentMapper.findStudentById(id); } /* * 查询所有的 Student * @return */ @Override public List<Student> findStudentList() { return studentMapper.findStudentList(); }}controller 层构建 restful APIimport com.nasus.mybatis.domain.Student;import com.nasus.mybatis.service.StudentService;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/Student”)public class StudentController { @Autowired private StudentService studentService; @PostMapping(””) public int add(@RequestBody Student student){ return studentService.add(student); } @PutMapping("/{id}") public int updateStudent(@PathVariable(“id”) Integer id, @RequestParam(value = “name”, required = true) String name, @RequestParam(value = “age”, required = true) Integer age){ return studentService.update(name,age,id); } @DeleteMapping("/{id}") public void deleteStudent(@PathVariable(“id”) Integer id){ studentService.delete(id); } @GetMapping("/{id}") public Student findStudentById(@PathVariable(“id”) Integer id){ return studentService.findStudentById(id); } @GetMapping("/list") public List<Student> findStudentList(){ return studentService.findStudentList(); }}测试结果其他接口已通过 postman 测试,无问题。源码下载:github 地址后语以上为 SpringBoot 实战 (九) | 整合 Mybatis 的教程,除了注解方式实现以外,Mybatis 还提供了 XML 方式实现。想了解更多用法请移步官方文档。最后,对 Python 、Java 感兴趣请长按二维码关注一波,我会努力带给你们价值,如果觉得本文对你哪怕有一丁点帮助,请帮忙点好看,让更多人知道。另外,关注之后在发送 1024 可领取免费学习资料。资料内容详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

February 20, 2019 · 3 min · jiezi

MyBatis分页插件PageHelper

MyBatis分页插件PageHelper如果你也在用 MyBatis,建议尝试该分页插件,这一定是最方便使用的分页插件。分页插件支持任何复杂的单表、多表分页。PageHelper是一个Mybatis的分页插件, 负责将已经写好的sql语句, 进行分页加工.PageHelper的使用优点:无需你自己去封装以及关心sql分页等问题,使用很方便,前端取数据也很方便。1.引入pagehelper依赖<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.2<ersion></dependency>2.配置applicationContext.xml文件在spring的sqlsessionfactory的bean中增加一个分页拦截器属性<bean id=“sqlSessionFactory” class=“org.mybatis.spring.SqlSessionFactoryBean”> <property name=“plugins”> <array> <bean class=“com.github.pagehelper.PageInterceptor”> <property name=“properties”> <value> <!– 这里设定你的数据库类型 –> helperDialect=mysql </value> </property> </bean> </array> </property></bean>3.调用PageHelper的方法在service方法中调用PageHelper的静态方法startPage(注意一定要在实际查询数据库之前调用该方法),传入需要查询的页号和每页大小,返回PageHelper插件提供的PageInfo对象。即可自动完成数据库物理分页,无须在你的sql语句中手工加limit子句4. PageInfo的结构关于PageInfo的结构请参看源码,这里通过返回的json来展示。根据需要取PageInfo对象的相应属性即可。

February 19, 2019 · 1 min · jiezi

Spring Boot项目利用MyBatis Generator进行数据层代码自动生成

概 述MyBatis Generator (简称 MBG) 是一个用于 MyBatis和 iBATIS的代码生成器。它可以为 MyBatis的所有版本以及 2.2.0之后的 iBATIS版本自动生成 ORM层代码,典型地包括我们日常需要手写的 POJO、mapper xml 以及 mapper 接口等。MyBatis Generator 自动生成的 ORM层代码几乎可以应对大部分 CRUD 数据表操作场景,可谓是一个生产力工具啊!注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站数据库准备与工程搭建首先我们准备一个 MySQL数据表 user_info用于下文实验里面插入了若干条数据:新建一个Spring Boot 工程引入 MyBatis Generator 依赖<dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> <scope>provided</scope></dependency>引入 MyBatis Generator Maven 插件<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile> <overwrite>true</overwrite> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.12</version> </dependency> </dependencies></plugin>MyBatis Generator Maven 插件引入以后,我们可以在 Spring Boot工程的 Maven插件工具栏中看到新增的插件选项,类似下图:准备 MyBatis Generator 配置文件MyBatis Generator 也需要一个 xml格式的配置文件,该文件的位置配在了上文 引入 MyBatis Generator Maven 插件的 xml配置里,即src/main/resources/mybatis-generator.xml,下面给出本文所使用的配置:<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <context id=“MySql” defaultModelType=“flat”> <plugin type=“org.mybatis.generator.plugins.SerializablePlugin” /> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://121.196.123.245:3306/demo” userId=“root” password=“xxxxxx” /> <javaModelGenerator targetPackage=“cn.codesheep.springbt_mybatis_generator.entity” targetProject=“src/main/java”></javaModelGenerator> <sqlMapGenerator targetPackage=“mapper” targetProject=“src/main/resources”></sqlMapGenerator> <javaClientGenerator targetPackage=“cn.codesheep.springbt_mybatis_generator.mapper” targetProject=“src/main/java” type=“XMLMAPPER”></javaClientGenerator> <table tableName=“user_info”> <property name=“modelOnly” value=“false”/> </table> </context></generatorConfiguration>上面 xml中几个关键的配置简介如下:< jdbcConnection /> 数据库连接配置,至关重要<javaModelGenerator /> 指定自动生成的 POJO置于哪个包下<sqlMapGenerator /> 指定自动生成的 mapper.xml置于哪个包下<javaClientGenerator /> 指定自动生成的 DAO接口置于哪个包下<table /> 指定数据表名,可以使用_和%通配符更多关于 MyBatis Generator 配置的内容,可以移步 官方文档。运行 MyBatis Generator直接通过 IDEA的 Maven图形化插件来运行 MyBatis Generator,其自动生成的过程 和 生成的结果如下图所示:很明显,通过 MyBatis Generator,已经很方便的帮我们自动生成了 POJO、mapper xml 以及 mapper接口,接下来我们看看自动生成的代码怎么用!自动生成的代码如何使用我们发现通过 MyBatis Generator自动生成的代码中带有一个 Example文件,比如上文中的 UserInfoExample,其实 Example文件对于平时快速开发还是有很大好处的,它能节省很多写 sql语句的时间,举几个实际的例子吧:单条件模糊搜索 + 排序在我们的例子中,假如我想通过用户名 user_name来在 MySQL数据表 user_info中进行模糊搜索,并对结果进行排序,此时利用UserInfoExample可以方便快速的实现:@Autowiredprivate UserInfoMapper userInfoMapper;public List<UserInfo> searchUserByUserName( String userName ) { UserInfoExample userInfoExample = new UserInfoExample(); userInfoExample.createCriteria().andUserNameLike( ‘%’+ userName +’%’ ); // 设置模糊搜索的条件 String orderByClause = “user_name DESC”; userInfoExample.setOrderByClause( orderByClause ); // 设置通过某个字段排序的条件 return userInfoMapper.selectByExample( userInfoExample );}多条件精确搜索再比如,我们想通过电话号码 phone和用户名 user_name 两个字段来在数据表 user_info中实现精确搜索,则可以如下实现:public List<UserInfo> multiConditionsSearch( UserInfo userInfo ) { UserInfoExample userInfoExample = new UserInfoExample(); UserInfoExample.Criteria criteria = userInfoExample.createCriteria(); if( !”".equals(userInfo.getPhone()) ) criteria.andPhoneEqualTo( userInfo.getPhone() ); if( !"".equals(userInfo.getUserName()) ) criteria.andUserNameEqualTo( userInfo.getUserName() ); return userInfoMapper.selectByExample( userInfoExample );}很明显可以看出的是,我们是通过直接编写代码逻辑来取代编写 SQL语句,因此还是十分直观和容易理解的!后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

February 14, 2019 · 2 min · jiezi

MyEclipse2018中安装Mybatis generator插件

前言在使用maven配置Mybatis generator插件时报以下错误,generator插件一直无法使用,查询资料说和eclipse版本有关系。The POM for org.eclipse.m2e:lifecycle-mapping🫙1.0.0 is missing, no dependency information available无奈之下选择安装eclipse的插件。安装步骤(基于MyEclipse2018)点击help–>install from catalog…在搜索框输入MyBatis Generator出现点击install–>Finish.如下图所示安装完成后在项目中右击新建MyBatis Generator configuration file.如下图所示点击next 选择文件生成的路径以及文件名,如下图所示点击Finfish。具体配置可参考以下配置进行修改<?xml version=“1.0” encoding=“UTF-8”?> <!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <context id=“context1”> <!– 数据库链接URL,用户名、密码 –> <jdbcConnection connectionURL=“jdbc:mysql://localhost:3306/miaosha?characterEncoding=utf8” driverClass=“com.mysql.jdbc.Driver” password=“jma3” userId=“root” /> <!– 生成model模型,对应的包 –> <javaModelGenerator targetPackage=“com.jian.miaosha.dataobject” targetProject=“miaosha/src/main/java” /> <!– 对应的xml mapper文件 –> <sqlMapGenerator targetPackage=“mapping” targetProject=“miaosha/src/main/resources” /> <!– 对应的dao接口 –> <javaClientGenerator targetPackage=“com.jian.miaosha.dao” targetProject=“miaosha/src/main/java” type=“XMLMAPPER” /> <!– 要生成的表 –> <table schema=”" tableName=“customer_password”> </table> <table schema="" tableName=“customer”> </table> </context></generatorConfiguration> ...

February 14, 2019 · 1 min · jiezi

Mybatis批量更新三种方式

Mybatis实现批量更新操作方式一:<update id=“updateBatch” parameterType=“java.util.List”> <foreach collection=“list” item=“item” index=“index” open="" close="" separator=";"> update tableName <set> name=${item.name}, name2=${item.name2} </set> where id = ${item.id} </foreach> </update>但Mybatis映射文件中的sql语句默认是不支持以" ; " 结尾的,也就是不支持多条sql语句的执行。所以需要在连接mysql的url上加 &allowMultiQueries=true 这个才可以执行。方式二:<update id=“updateBatch” parameterType=“java.util.List”> update tableName <trim prefix=“set” suffixOverrides=","> <trim prefix=“c_name =case” suffix=“end,"> <foreach collection=“list” item=“cus”> <if test=“cus.name!=null”> when id=#{cus.id} then #{cus.name} </if> </foreach> </trim> <trim prefix=“c_age =case” suffix=“end,"> <foreach collection=“list” item=“cus”> <if test=“cus.age!=null”> when id=#{cus.id} then #{cus.age} </if> </foreach> </trim> </trim> <where> <foreach collection=“list” separator=“or” item=“cus”> id = #{cus.id} </foreach> </where></update>这种方式貌似效率不高,但是可以实现,而且不用改动mysql连接效率参考文章:https://blog.csdn.net/xu19166…方式三:临时改表sqlSessionFactory的属性,实现批量提交的java,但无法返回受影响数量。public int updateBatch(List<Object> list){ if(list ==null || list.size() <= 0){ return -1; } SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean(“sqlSessionFactory”); SqlSession sqlSession = null; try { sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false); Mapper mapper = sqlSession.getMapper(Mapper.class); int batchCount = 1000;//提交数量,到达这个数量就提交 for (int index = 0; index < list.size(); index++) { Object obj = list.get(index); mapper.updateInfo(obj); if(index != 0 && index%batchCount == 0){ sqlSession.commit(); } } sqlSession.commit(); return 0; }catch (Exception e){ sqlSession.rollback(); return -2; }finally { if(sqlSession != null){ sqlSession.close(); } } }其中 SpringContextUtil 是自己定义的工具类 用来获取spring加载的bean对象,其中getBean() 获得的是想要得到的sqlSessionFactory。Mapper 是自己的更具业务需求的Mapper接口类,Object是对象。总结方式一 需要修改mysql的连接url,让全局支持多sql执行,不太安全方式二 当数据量大的时候 ,效率明显降低方式三 需要自己控制,自己处理,一些隐藏的问题无法发现。附件:SpringContextUtil.java@Componentpublic class SpringContextUtil implements ApplicationContextAware{ private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtil.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext(){ return applicationContext; } public static Object getBean(Class T){ try { return applicationContext.getBean(T); }catch (BeansException e){ return null; } } public static Object getBean(String name){ try { return applicationContext.getBean(name); }catch (BeansException e){ return null; } }} ...

February 1, 2019 · 2 min · jiezi

Mybatis单表CRUD与关联查询的通用方案之一:通用CRUD

在diboot 2.0版本框架的封装过程中,我们遇到的问题和最终的解决方案也许可以给此时的你提供些帮助和思路,于是就有了这些系列文章。此系列主题为“Mybatis单表CRUD与多表关联查询的通用方案”,目的是给出一套简单灵活易用的通用方案,可以做到1.利用通用Mapper框架实现单表CRUD,2.自己动手封装基于注解的多表关联实现方案。一、Mybatis的CRUD通用解决方案为什么需要通用Mapper?Mybatis中针对一个单表的CRUD操作一般要在其对应Mapper中写SQL,表的字段名会多次出现,当字段变更,增减时都需要同步修改其Mapper,繁琐且易出错。针对于单表的CRUD是比较简单的,可以在Entity中将字段与数据库表的列进行绑定,然后借助一个通用Mapper实现通用的CRUD,这样针对单表CRUD的操作将不再需要写SQL。通用Mapper框架的选择Mybatis-plus v3.x版本除了提供通用Mapper之外,还具备了方便易用的基于Lambda的条件构造器以及逻辑删除乐观锁等解决方案。另外也有其他轻量级的通用Mapper实现,比如Mapper 4.x,也是一个通用Mapper框架的选择。用上了通用Mapper,感觉越像JPA了?如果只是单表的CRUD,通用mapper方案确实跟JPA类似,不过JPA在Java代码里写SQL和对于复杂查询的处理不便,都带来了开发维护的麻烦。依然选用Mybatis方案,既借助通用Mapper实现了类似JPA的单表处理便利性,又保留了Mybatis自定义SQL的灵活性优势。多表关联如何处理?对于多表关联的处理,通用Mapper并没有给出较好的解决方案,你可以通过自定义SQL关联查询绑定,每个都要去写SQL,这样很繁琐。另外一个方案就是类似JPA的通过注解绑定表关联关系,然后实现查询结果的绑定。这个方案可以将关联查询拆解成多个单表查询,然后根据绑定关系组装最终结果,后续我们通过自定义封装实现这个方案,按需定制你的关联绑定方案。至于为何要将关联查询拆解成单表查询,可以到<高性能MySQL>一书中了解分解关联查询的好处Diboot 轻代码开发框架

January 29, 2019 · 1 min · jiezi

springboot+mybatis+mybaits plus 整合与基本应用

springboot+mybatis+mybaits plus 整合与基本应用引言在spring framework所支持的orm框架中,mybatis相比 hibernate,spring本身提供的支持是相对少的,这在开发过程中对使用mybatis进行开发的程序员来说无疑产生很多难处。为此,开源上也产生了很多三方对mybatis的一些增强工具,比如ourbatis、mybatis-generator等等。这篇我们主要来说下功能丰富、现在还在迭代的一款国人开发的增强工具mybatis-plus。就像官方文档说的那样我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。可以看出,mybatis-plus是了为了提高效率和简化配置而生的。下面就来展示下在springboot下如何整合mybatis-plus准备工作首先是创建一个springboot工程引入相关依赖(springboot相关、mybaits、mybatis-plus等等)<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– springboot对mybaits的自动配置依赖 –> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!– mybatis-plus相关依赖 –> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency> </dependencies>使用mybaits-plus的代码生成器映射生成代码,我们这里所使用的数据库为mysql。这个在另外一篇文章上说明,这里就不讲述了。在这里,mybaits-plus提供了BaseMapper、BaseService这些基类来提供一些操作的支持,比如save(T t)saveOrUpdate(T t)update(T t, Wrapper<T> wrapper)page(IPage<T> page, Wrapper<T> queryWrapper)等等。下面,我们就简单介绍下springboot中怎么运用mybaits-plus配置首先是说到配置,这里默认以yml文件来进行对mybaits-plus的配置。mybatis-plus: #MyBatis Mapper 所对应的 XML 文件位置 mapper-locations: classpath*:mapper/*.xml # MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名, #注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名(即 XML 中调用的时候不用包含包名) typeAliasesPackage: com.luwei.models # 与 typeAliasesPackage 一起使用,仅扫描以该类作为父类的类 # type-aliases-super-type: java.lang.Object # 配置扫描通用枚举,配置该属性,会对枚举类进行注入 typeEnumsPackage: com.luwei.demo.mybatisplusdemo.envm # 该包下的类注册为自定义类型转换处理类,用于属性类型转换 # type-handlers-package: com.luwei.demo.mybatisplusdemo.handler # 指定 mybatis 处理器 # executorType: simple configuration: #使用驼峰法映射属性,配置这个resultType可以映射 map-underscore-to-camel-case: true global-config: db-config: # 配置表明前缀,例如表设计时表名为tb_manager,对应entity为Manager table-prefix: tb_ #逻辑已删除值 logic-delete-value: 1 #逻辑未删除值 logic-not-delete-value: 0 # 是否开启like查询,即对 stirng 字段是否使用 like,默认不开启 # column-like: false logging: level: # 日志级别,显示操作sql com.luwei.demo.mybatisplusdemo.mapper: debug基本上这些配置都能满足一般的应用了。CRUD 接口上面说到,BaseMapper和BaseService已经实现了一些基本操作,下面简单说下这些接口的用法查询查询中Mybatis-plus提供多种封装好的方式,包括对主键查询、指定条件查询、分页查询等。Manager manager1 = managerService.getById(1);Assert.assertNotNull(manager1);LambdaQueryWrapper<Manager> wrapper = new LambdaQueryWrapper<Manager>().like(Manager::getName, “na”);List<Manager> managerList = managerService.list(wrapper);Assert.assertFalse(managerList.isEmpty());//先配置page分页插件配置Page page = new Page<>(1, 2);IPage<Manager> managerPage = managerService.page(page, wrapper);Assert.assertFalse(managerPage.getRecords().isEmpty());//获取map对象Map<String, Object> map = managerService.getMap(wrapper);System.out.println(map);Object obj = managerService.getObj(wrapper);System.out.println(obj);try { //若有多个结果,抛出异常 managerService.getOne(wrapper, true);}catch (RuntimeException e) { e.printStackTrace(); System.out.println(“异常捕获”);}增加save(T t)方法,实际就是将对象持久化到数据库中,这里会产生一条insert语句,并执行。@Transactionalpublic void add() { Manager manager = new Manager(); manager.setAccount(“account”); manager.setRole(RoleEnum.ROOT); manager.setPassword(“password”); manager.setName(“name”); save(manager);}日志输出:==> Preparing: INSERT INTO tb_manager ( account, name, password, role ) VALUES ( ?, ?, ?, ? ) ==> Parameters: account(String), name(String), password(String), 0(Integer)<== Updates: 1更改提供的update、updateOrSave、upateById都可以实现数据更新。各自的区别在于update:根据条件筛选并更新指定字段updateOrSave:对对象进行更新,对未存储的对象进行插入updateById:根据id对对象进行更新@Transactionalpublic void updateManager() { Manager manager = getById(1); manager.setName(“testUpdate”); updateById(manager); //saveOrUpdate(manager); //update(new Manager(), new UpdateWrapper<Manager>().lambda().set(Manager::getName, “test”).eq(Manager::getManagerId, 1));}删除在删除中,除了一般的物理删除外,mybaits-plus还提供了逻辑删除的支持。如果需要使用逻辑删除,除了上述配置外,还需要添加一个配置bean来装配插件。@Beanpublic ISqlInjector sqlInjector() { return new LogicSqlInjector();}这样在使用删除时,会对记录中标记为删除标识的字段进行更改,在查询和更新时,也只是针对删除标识为未删除的记录。public void deleteManager() { LambdaQueryWrapper<Manager> wrapper = new LambdaQueryWrapper<Manager>().eq(Manager::getManagerId, 4); System.out.println(baseMapper.delete(wrapper)); /Map<String, Object> deleteMap = new HashMap<>(); //使用表字段名 deleteMap.put(“manager_id”, 4); baseMapper.deleteByMap(deleteMap);/ /baseMapper.deleteById(4);/ //属于service下的方法 /LambdaQueryWrapper<Manager> wrapper = new LambdaQueryWrapper<Manager>().eq(Manager::getManagerId, 4); remove(wrapper);/}条件构造器在查询、更新、删除这些操作中,我们往往需要定义条件或者设置属性,也就是where子句和set语句,如果不是直接通过sql去处理,在mybatis-plus中,也提供了一种包装器来实现。AbstractWrapper囊括了几乎满足日常需要的条件操作,和jpa的specification一样,它支持动态产生条件,也支持使用lambda表达式去组装条件。他是QueryWrapper和UpdateWrapper的父类一些常用的wrapper方法Map<String, Object> conditionMap = new HashMap<>();//使用表字段名conditionMap.put(“name”, “name”);conditionMap.put(“manager_id”, 1);//allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)//filter:忽略字段//null2IsNull:为true则在map的value为null时调用 isNull 方法,为false时则忽略value为null的QueryWrapper<Manager> queryWrapper = new QueryWrapper<Manager>().allEq((r, v) -> r.indexOf(“name”) > 0, conditionMap, true);managerService.list(queryWrapper);//like(R column, Object val) -> column like ‘%na%’//likeLeft -> column like ‘%na’//likeRight -> column like ’na%’//and(Function<This, This> func) -> and (column = val)LambdaQueryWrapper<Manager> lambdaWrapper = new LambdaQueryWrapper<Manager>().like(Manager::getName, “na”).and((r) -> r.eq(Manager::getDisabled, false));managerService.list(lambdaWrapper);//orderBy(boolean condition, boolean isAsc, R… columns) -> order by columns isAscLambdaQueryWrapper<Manager> lambdaWrapper = new LambdaQueryWrapper<Manager>().orderBy(true, false, Manager::getManagerId);managerService.list(lambdaWrapper);//select 用于挑选属性LambdaQueryWrapper<Manager> lambdaWrapper = new LambdaQueryWrapper<Manager>().select(Manager::getName, Manager::getDisabled);managerService.list(lambdaWrapper);//set(R column, Object val) -> update T set colunm = valmanagerService.update(new Manager(), new UpdateWrapper<Manager>().lambda().set(Manager::getName, “newName”).eq(Manager::getManagerId, 4));诸如还有其他像eq,lte,isNull,orderBy,or,exists等限定方法,这里就不一一介绍了。其他分页插件引入分页,除了配置上,还需要添加插件bean@Beanpublic PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor();}在使用时,自定义查询中,参数中添加Page对象,并要求放在参数中的第一位IPage<ManagerPageVO> selectManagerPage(Page page, @Param(“roleEnum”)RoleEnum roleEnum, @Param(“managerId”) Integer managerId, @Param(“name”) String name);<select id=“selectManagerPage” resultType=“com.luwei.pojos.manager.ManagerPageVO”> <![CDATA[ select manager_id, account, name, role, disabled, create_time, last_login_time from tb_manager where role = #{roleEnum} and deleted = false ]]> <if test=“name != null”> <![CDATA[ and (name like CONCAT(’%’,#{name},’%’) or account like CONCAT(’%’,#{name},’%’)) ]]> </if> <if test=“managerId != null”> <![CDATA[ and manager_id = #{managerId} ]]> </if></select><select id=“selectForSpecialCondition” resultType=“com.luwei.entity.Manager”> select <include refid=“Base_Column_List” /> from tb_manager where name like ‘%admin%’</select>主键配置在mybaits-plus中,有多种主键生成策略,它们分别是IdType.AUTO:数据库id自增IdType.INPUT:用户输入IdType.ID_WORKER:唯一ID,自动填充(默认)IdType.UUID:唯一ID,自动填充由于我们公司使用的是mysql本身的自增策略,所以选择使用 IdType.AUTO。@ApiModelProperty(value = “管理员id”)@TableId(value = “manager_id”, type = IdType.AUTO)private Integer managerId;至于 ID_WORKER 和 UUID 的不同,在于它们的唯一键生成策略不同,ID_WORKER 按照官方的介绍,是使用Sequence作为基础产生唯一键。枚举属性为了让mybaits更好地使用枚举,mybatis-plus提供了枚举扫描注入具体配置,首先是配置扫描路径# 配置扫描通用枚举,配置该属性,会对枚举类进行注入typeEnumsPackage: com.luwei.demo.mybatisplusdemo.envm在枚举类中实现接口,用于获取具体值//实现此接口用于获取值public interface BaseEnum<E extends Enum<?>, T> { T getValue(); String getDisplayName();}这样,基本上就可以在mybatis上使用枚举类型了。总结上面描述了mybitis-plus基本用法,其实除了以上,它还提供了很多方便的插件和应用,包括xml热加载、乐观锁插件、性能分析插件等,这些由于篇幅和主题的原因我就不在这里阐述了。希望这篇可以带你很快地上手这个当前热门的mybaits增强工具,提高开发效率。 ...

January 28, 2019 · 3 min · jiezi

Mybatis增删改查之Oracle

Mybatis增删改查之Oracle一. 查询普通查询(返回普通的持久层对象,由于数据库字段风格和java不同,所以建立一个map映射)<resultMap type=“com.xxx.domain.RuleCondition” id=“RuleConditionResultMapWithoutBondList”> <id column=“RC_ID” jdbcType=“NUMERIC” property=“ruleConditionId”/> <result column=“RULE_CAT1” jdbcType=“VARCHAR” property=“ruleCatOne”/> <result column=“RULE_CAT2” jdbcType=“VARCHAR” property=“ruleCatTwo”/> <result column=“RC_OPER_TYPE” jdbcType=“VARCHAR” property=“ruleOperateSymbol”/> <result column=“RULE_REF” jdbcType=“VARCHAR” property=“ruleRef”/> <result column=“START_EFFECT_TIME” jdbcType=“VARCHAR” property=“effectTimeOfStart”/> <result column=“END_EFFECT_TIME” jdbcType=“VARCHAR” property=“effectTimeOfEnd”/> <result column=“BOND_CODE_1” jdbcType=“VARCHAR” property=“bondCodeOne”/> <result column=“BOND_CODE_2” jdbcType=“VARCHAR” property=“bondCodeTwo”/> <result column=“BP_THRESHOLD” jdbcType=“NUMERIC” property=“bpThreshold”/> <result column=“RC_STATUS” jdbcType=“VARCHAR” property=“ruleStatus”/> <result column=“OPERATOR_ID” jdbcType=“VARCHAR” property=“operatorId”/> <result column=“LAST_UPDATED_DATE” jdbcType=“DATE” property=“lastUpdateTime”/> </resultMap> <select id=“getRuleConditionWithoutBondListById” resultMap=“RuleConditionResultMapWithoutBondList”> select RC_ID, RULE_CAT1, RULE_CAT2, RC_OPER_TYPE, RULE_REF, START_EFFECT_TIME, END_EFFECT_TIME, BOND_CODE_1, BOND_CODE_2, BP_THRESHOLD, RC_STATUS, LAST_UPDATED_DATE FROM RULES_CONDITION WHERE RC_ID = #{ruleConditionId,jdbcType=NUMERIC} </select>带有自定义对象的查询(带了一个List)<!–collection中的就是查询附带的list的函数 property是java中list的属性名–><resultMap type=“com.xxx.domain.RuleCondition” id=“RuleConditionResultMap”> <id column=“RC_ID” jdbcType=“NUMERIC” property=“ruleConditionId”/> <result column=“RULE_CAT1” jdbcType=“VARCHAR” property=“ruleCatOne”/> <result column=“RULE_CAT2” jdbcType=“VARCHAR” property=“ruleCatTwo”/> <result column=“RC_OPER_TYPE” jdbcType=“VARCHAR” property=“ruleOperateSymbol”/> <result column=“RULE_REF” jdbcType=“VARCHAR” property=“ruleRef”/> <result column=“START_EFFECT_TIME” jdbcType=“VARCHAR” property=“effectTimeOfStart”/> <result column=“END_EFFECT_TIME” jdbcType=“VARCHAR” property=“effectTimeOfEnd”/> <result column=“BOND_CODE_1” jdbcType=“VARCHAR” property=“bondCodeOne”/> <result column=“BOND_CODE_2” jdbcType=“VARCHAR” property=“bondCodeTwo”/> <result column=“BP_THRESHOLD” jdbcType=“NUMERIC” property=“bpThreshold”/> <result column=“RC_STATUS” jdbcType=“VARCHAR” property=“ruleStatus”/> <result column=“OPERATOR_ID” jdbcType=“VARCHAR” property=“operatorId”/> <result column=“LAST_UPDATED_DATE” jdbcType=“DATE” property=“lastUpdateTime”/> <collection column=“RC_ID” property=“bondList” ofType=“com.xxx.domain.RuleBond” select=“getBondListByRuleConditionId”> </collection> </resultMap> <resultMap type=“com.xxx.domain.RuleBond” id=“RuleBondResultMap”> <id column=“RB_ID” jdbcType=“NUMERIC” property=“ruleBondId”/> <id column=“RC_ID” jdbcType=“NUMERIC” property=“ruleConditionId”/> <result column=“BOND_CODE” jdbcType=“VARCHAR” property=“bondCode”/> <result column=“SECURITY_TERM” jdbcType=“VARCHAR” property=“term”/> <result column=“BID_STRATEGY_ID” jdbcType=“VARCHAR” property=“bidStrategyId”/> <result column=“OFR_STRATEGY_ID” jdbcType=“VARCHAR” property=“ofrStrategyId”/> <result column=“STATUS” jdbcType=“VARCHAR” property=“status”/> <result column=“OPERATOR_ID” jdbcType=“VARCHAR” property=“operatorId”/> <result column=“LAST_UPDATED_DATE” jdbcType=“DATE” property=“lastUpdateTime”/> </resultMap><!–查询语句–> <select id=“getRuleConditionBOsByEnumValue” resultMap=“RuleConditionBOResultMap”> select RC_ID, RULE_CAT1, RULE_CAT2, RC_OPER_TYPE, RULE_REF, START_EFFECT_TIME, END_EFFECT_TIME, BOND_CODE_1, BOND_CODE_2, BP_THRESHOLD, RC_STATUS, LAST_UPDATED_DATE FROM RULES_CONDITION WHERE RULE_CAT1 = #{enumValue,jdbcType=VARCHAR} </select><!–附带List的查询语句–> <select id=“getBondListByRuleConditionId” resultMap=“RuleBondResultMap”> select RB_ID, RC_ID, t1.BOND_CODE, t2.SECURITY_TERM, BID_STRATEGY_ID, OFR_STRATEGY_ID, t1.STATUS, t1.LAST_UPDATED_DATE FROM RULES_BOND t1 left join BOND_BASIS_INFO t2 on t1.BOND_CODE = t2.BOND_CODE WHERE RC_ID = #{ruleConditionId,jdbcType=NUMERIC} </select>二. 新增普通新增 <insert id=“addRuleBond” parameterType=“com.xxx.domain.RuleBond”> insert into RULES_BOND (RB_ID, RC_ID, BOND_CODE, BID_STRATEGY_ID, OFR_STRATEGY_ID, STATUS, OPERATOR_ID, LAST_UPDATED_DATE) values (SEQ_RULES_BOND.nextVal, #{ruleConditionId,jdbcType=NUMERIC}, #{bondCode,jdbcType=VARCHAR}, #{bidStrategyId,jdbcType=VARCHAR}, #{ofrStrategyId,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}, #{operatorId,jdbcType=VARCHAR}, systimestamp) </insert>返回主键(多了一个selectkey)<!–selectkey中 keyproperty是写java中属性名称 后面的values中将得到的ruleBondId赋值即可–><insert id=“addRuleBond” parameterType=“com.xxx.domain.RuleBond”> <selectKey resultType=“java.lang.Integer” order=“BEFORE” keyProperty=“ruleBondId”> SELECT SEQ_RULES_BOND.Nextval from DUAL </selectKey> insert into RULES_BOND (RB_ID, RC_ID, BOND_CODE, BID_STRATEGY_ID, OFR_STRATEGY_ID, STATUS, OPERATOR_ID, LAST_UPDATED_DATE) values (#{ruleBondId,jdbcType=NUMERIC}, #{ruleConditionId,jdbcType=NUMERIC}, #{bondCode,jdbcType=VARCHAR}, #{bidStrategyId,jdbcType=VARCHAR}, #{ofrStrategyId,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}, #{operatorId,jdbcType=VARCHAR}, systimestamp) </insert>批量新增参照网上写了一下,一直报缺失表达式,原来是insert into后面 是不需要 values的;还有就是关于Oracle返回主键List ,我在网上暂时还没找到能正确执行的例子 ,求大佬告知<insert id=“addRuleBondList” parameterType=“java.util.List”> insert into RULES_BOND (RB_ID, RC_ID, BOND_CODE, BID_STRATEGY_ID, OFR_STRATEGY_ID, STATUS, OPERATOR_ID, LAST_UPDATED_DATE ) SELECT SEQ_RULES_BOND.NEXTVAL,t.* FROM ( <foreach close=")" collection=“ruleBonds” item=“item” index=“index” open="(" separator=“union ALL”> select #{item.ruleConditionId,jdbcType=NUMERIC}, #{item.bondCode,jdbcType=VARCHAR}, #{item.bidStrategyId,jdbcType=VARCHAR}, #{item.ofrStrategyId,jdbcType=VARCHAR}, #{item.status,jdbcType=VARCHAR}, #{item.operatorId,jdbcType=VARCHAR}, systimestamp from dual </foreach> ) t </insert>批量新增,存在则插入<insert id=“generateBaselines” parameterType=“java.util.List”> MERGE INTO RULES_CONDITION t USING ( <foreach collection=“ruleConditions” item=“item” index=“index” separator=“union”> select #{item.ruleConditionId,jdbcType=NUMERIC} id, #{item.ruleCatOne,jdbcType=VARCHAR} cat1, #{item.ruleCatTwo,jdbcType=VARCHAR} cat2, #{item.bondCodeOne,jdbcType=VARCHAR} code1, #{item.bondCodeTwo,jdbcType=VARCHAR} code2, #{item.ruleOperateSymbol,jdbcType=VARCHAR} symbol, #{item.operatorId,jdbcType=VARCHAR} u from DUAL </foreach>) t1 <!– 哪些条件相符–> ON (t.RULE_CAT1 = t1.cat1 AND t.RULE_CAT2 = t1.cat2 AND t.RC_OPER_TYPE = t1.symbol) <!–符合条件时–> WHEN MATCHED THEN UPDATE SET t.BOND_CODE_1 = t1.code1,t.BOND_CODE_2 = t1.code2,t.LAST_UPDATED_DATE = default <!–不符合条件时–> WHEN NOT MATCHED THEN INSERT(RC_ID, RULE_CAT1, RULE_CAT2, RC_OPER_TYPE, RULE_REF, BOND_CODE_1, BOND_CODE_2,RC_STATUS,OPERATOR_ID,LAST_UPDATED_DATE) VALUES (SEQ_RULES_CONDITION.nextval, t1.cat1, t1.cat2, t1.symbol, ‘1’, t1.code1, t1.code2, ‘0’, t1.u,default) </insert>三. 修改(begin,end最好还是加上,之前报错一直找不到错,加上begin,end就好了;end前后都加分号";",begin不用加)普通修改<update id=“modifyRuleBond” parameterType=“com.xxx.domain.RuleBond”> begin update RULES_BOND set <if test=“bidStrategyId!=null”> BID_STRATEGY_ID=#{bidStrategyId,jdbcType=VARCHAR}, </if> <if test=“ofrStrategyId!=null”> OFR_STRATEGY_ID=#{ofrStrategyId,jdbcType=VARCHAR}, </if> <if test=“operatorId!=null”> OPERATOR_ID=#{operatorId,jdbcType=VARCHAR}, </if> <if test=“status!=null”> STATUS=#{status,jdbcType=VARCHAR}, </if> LAST_UPDATED_DATE=SYSTIMESTAMP WHERE RB_ID = #{ruleBondId,jdbcType=NUMERIC}; end; </update>批量修改(begin,end加在 foreach的open和close处,记得加上分号)<update id=“modifyRuleCondition” parameterType=“java.util.List”> <foreach collection=“ruleConditions” item=“item” index=“index” open=“begin” close=";end;" separator=";"> UPDATE RULES_CONDITION <set> <if test=“item.ruleRef!=null”> RULE_REF=#{item.ruleRef,jdbcType=VARCHAR}, </if> <if test=“item.effectTimeOfStart!=null”> START_EFFECT_TIME=#{item.effectTimeOfStart,jdbcType=VARCHAR}, </if> <if test=“item.effectTimeOfEnd!=null”> END_EFFECT_TIME= #{item.effectTimeOfEnd,jdbcType=VARCHAR}, </if> <if test=“item.bpThreshold!=null”> BP_THRESHOLD= #{item.bpThreshold,jdbcType=NUMERIC}, </if> <if test=“item.ruleStatus!=null”> RC_STATUS= #{item.ruleStatus,jdbcType=VARCHAR}, </if> <if test=“item.operatorId!=null”> OPERATOR_ID= #{item.operatorId,jdbcType=VARCHAR}, </if> LAST_UPDATED_DATE=default, </set> WHERE RC_ID = #{item.ruleConditionId,jdbcType=INTEGER} </foreach> </update>四. 删除普通删除<delete id=“deleteRuleBond” parameterType=“com.xxx.domain.RuleBond”> delete from RULES_BOND where RB_ID = #{ruleBondId} AND TO_TIMESTAMP(TO_CHAR(LAST_UPDATED_DATE, ‘yyyy-MM-dd hh24:mi:ss’), ‘yyyy-MM-dd hh24:mi:ss’) = #{lastUpdateTime,jdbcType=TIMESTAMP} </delete>批量删除 1)批量执行语句<delete id=“batchDeleteRuleBond” parameterType=“java.util.List”> <foreach collection=“ruleBonds” open=“begin” close=";end;" item=“item” separator=";"> DELETE FROM RULES_BOND WHERE RB_ID = #{item.ruleBondId} and TO_TIMESTAMP(TO_CHAR(LAST_UPDATED_DATE, ‘yyyy-MM-dd hh24:mi:ss’), ‘yyyy-MM-dd hh24:mi:ss’) = #{item.lastUpdateTime,jdbcType=TIMESTAMP} </foreach> </delete> 2)综合成一条语句执行<delete id=“batchDeleteRuleBond” parameterType=“java.util.List”> DELETE FROM RULES_BOND WHERE RB_ID IN ( SELECT A.RB_ID FROM ( <foreach collection=“ruleBonds” item=“item” separator=“UNION All”> SELECT * FROM RULES_BOND WHERE RB_ID = #{item.ruleBondId} AND TO_TIMESTAMP(TO_CHAR(LAST_UPDATED_DATE, ‘yyyy-MM-dd hh24:mi:ss’), ‘yyyy-MM-dd hh24:mi:ss’) = #{item.lastUpdateTime,jdbcType=TIMESTAMP} </foreach> )A ) </delete> ...

January 23, 2019 · 3 min · jiezi

springboot 动态数据源(Mybatis+Druid)

git代码地址Spring多数据源实现的方式大概有2中,一种是新建多个MapperScan扫描不同包,另外一种则是通过继承AbstractRoutingDataSource实现动态路由。今天作者主要基于后者做的实现,且方式1的实现比较简单这里不做过多探讨。实现方式方式1的实现(核心代码):@Configuration@MapperScan(basePackages = “com.goofly.test1”, sqlSessionTemplateRef = “test1SqlSessionTemplate”)public class DataSource1Config1 { @Bean(name = “dataSource1”) @ConfigurationProperties(prefix = “spring.datasource.test1”) @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } // …..略}@Configuration@MapperScan(basePackages = “com.goofly.test2”, sqlSessionTemplateRef = “test1SqlSessionTemplate”)public class DataSourceConfig2 { @Bean(name = “dataSource2”) @ConfigurationProperties(prefix = “spring.datasource.test2”) @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } // …..略}方式2的实现(核心代码):public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private static final Logger log = Logger.getLogger(DynamicRoutingDataSource.class); @Override protected Object determineCurrentLookupKey() { //从ThreadLocal中取值 return DynamicDataSourceContextHolder.get(); }} 第1种方式虽然实现比较加单,劣势就是不同数据源的mapper文件不能在同一包名,就显得不太灵活了。所以为了更加灵活的作为一个组件的存在,作者采用的第二种方式实现。设计思路当请求经过被注解修饰的类后,此时会进入到切面逻辑中。切面逻辑会获取注解中设置的key值,然后将该值存入到ThreadLocal中执行完切面逻辑后,会执行AbstractRoutingDataSource.determineCurrentLookupKey()方法,然后从ThreadLocal中获取之前设置的key值,然后将该值返回。由于AbstractRoutingDataSource的targetDataSources是一个map,保存了数据源key和数据源的对应关系,所以能够顺利的找到该对应的数据源。源码解读org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,如下:public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); private Map<Object, DataSource> resolvedDataSources; private DataSource resolvedDefaultDataSource; protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, “DataSource router not initialized”); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException(“Cannot determine target DataSource for lookup key [” + lookupKey + “]”); } return dataSource; } /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction context. * <p>Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ protected abstract Object determineCurrentLookupKey(); //……..略targetDataSources是一个map结构,保存了key与数据源的对应关系;dataSourceLookup是一个DataSourceLookup类型,默认实现是JndiDataSourceLookup。点开该类源码会发现,它实现了通过key获取DataSource的逻辑。当然,这里可以通过setDataSourceLookup()来改变其属性,因为关于此处有一个坑,后面会讲到。public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup { public JndiDataSourceLookup() { setResourceRef(true); } @Override public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { try { return lookup(dataSourceName, DataSource.class); } catch (NamingException ex) { throw new DataSourceLookupFailureException( “Failed to look up JNDI DataSource with name ‘” + dataSourceName + “’”, ex); } }}组件使用多数据源# db1spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.master.username = rootspring.datasource.master.password = 123456spring.datasource.master.driverClassName = com.mysql.jdbc.Driverspring.datasource.master.validationQuery = truespring.datasource.master.testOnBorrow = true## db2spring.datasource.slave.url = jdbc:mysql://127.0.0.1:3306/test1?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.slave.username = rootspring.datasource.slave.password = 123456spring.datasource.slave.driverClassName = com.mysql.jdbc.Driverspring.datasource.slave.validationQuery = truespring.datasource.slave.testOnBorrow = true#主数据源名称spring.maindb=master#mapperper包路径mapper.basePackages =com.btps.xli.multidb.demo.mapper单数据源为了让使用者能够用最小的改动实现最好的效果,作者对单数据源的多种配置做了兼容。示例配置1(配置数据源名称):spring.datasource.master.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.master.username = rootspring.datasource.master.password = 123456spring.datasource.master.driverClassName = com.mysql.jdbc.Driverspring.datasource.master.validationQuery = truespring.datasource.master.testOnBorrow = true# mapper包路径mapper.basePackages = com.goofly.xli.multidb.demo.mapper# 主数据源名称spring.maindb=master示例配置2(不配置数据源名称):spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=falsespring.datasource.username = rootspring.datasource.password = 123456spring.datasource.driverClassName = com.mysql.jdbc.Driverspring.datasource.validationQuery = truespring.datasource.testOnBorrow = true# mapper包路径mapper.basePackages = com.goofly.xli.multidb.demo.mapper踩坑之路多数据源的循环依赖Description:The dependencies of some of the beans in the application context form a cycle: happinessController (field private com.db.service.HappinessService com.db.controller.HappinessController.happinessService) ↓ happinessServiceImpl (field private com.db.mapper.MasterDao com.db.service.HappinessServiceImpl.masterDao) ↓ masterDao defined in file [E:\GitRepository\framework-gray\test-db\target\classes\com\db\mapper\MasterDao.class] ↓ sqlSessionFactory defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]┌─────┐| dynamicDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]↑ ↓| firstDataSource defined in class path resource [com/goofly/xli/datasource/core/DynamicDataSourceConfiguration.class]↑ ↓| dataSourceInitializer解决方案:在Spring boot启动的时候排除DataSourceAutoConfiguration即可。如下:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})public class DBMain { public static void main(String[] args) { SpringApplication.run(DBMain.class, args); }} 但是作者在创建多数据源的时候由于并未创建多个DataSource的Bean,而是只创建了一个即需要做动态数据源的那个Bean。 其他的DataSource则直接创建实例然后存放在Map里面,然后再设置到DynamicRoutingDataSource#setTargetDataSources即可。因此这种方式也不会出现循环依赖的问题!动态刷新数据源 笔者在设计之初是想构建一个动态刷新数据源的方案,所以利用了SpringCloud的@RefreshScope去标注数据源,然后利用RefreshScope#refresh实现刷新。但是在实验的时候发现由Druid创建的数据源会因此而关闭,由Spring的DataSourceBuilder创建的数据源则不会发生任何变化。 最后关于此也没能找到解决方案。同时思考,如果只能的可以实现动态刷新的话,那么数据源的原有连接会因为刷新而中断吗还是会有其他处理?多数据源事务 有这么一种特殊情况,一个事务中调用了两个不同数据源,这个时候动态切换数据源会因此而失效。翻阅了很多文章,大概找了2中解决方案,一种是Atomikos进行事务管理,但是貌似性能并不是很理想。另外一种则是通过优先级控制,切面的的优先级必须要大于数据源的优先级,用注解@Order控制。此处留一个坑! ...

January 23, 2019 · 2 min · jiezi

MyBatis缓存介绍

MyBatis缓存介绍 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。mybatis的相关概念SqlSession : 代表和数据库的一次会话,向用户提供了操作数据库的方法。MappedStatement: 代表要发往数据库执行的指令,可以理解为是Sql的抽象表示。Executor: 具体用来和数据库交互的执行器,接受MappedStatement作为参数。映射接口: 在接口中会要执行的Sql用一个方法来表示,具体的Sql写在映射文件中。映射文件: 可以理解为是Mybatis编写Sql的地方,通常来说每一张单表都会对应着一个映射文件,在该文件中会定义Sql语句入参和出参的形式。一级缓存MyBatis的一级查询缓存(也叫作本地缓存)是基于org.apache.ibatis.cache.impl.PerpetualCache 类的 HashMap本地缓存,其作用域是SqlSession在同一个SqlSession中两次执行相同的 sql 查询语句,第一次执行完毕后,会将查询结果写入到缓存中,第二次会从缓存中直接获取数据,而不再到数据库中进行查询,这样就减少了数据库的访问,从而提高查询效率。当一个 SqlSession 结束后,该 SqlSession 中的一级查询缓存也就不存在了。myBatis 默认一级查询缓存是开启状态,且不能关闭。增删改会清空缓存,无论是否commit当SqlSession关闭和提交时,会清空一级缓存可能你会有疑惑,我的mybatis bean是由spring 来管理的,已经屏蔽了sqlSession这个东西了?那怎么的一次操作才算是一次sqlSession呢?spring整合mybatis后,非事务环境下,每次操作数据库都使用新的sqlSession对象。因此mybatis的一级缓存无法使用(一级缓存针对同一个sqlsession有效)在开启事物的情况之下,spring使用threadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的在开启以及缓存的时候查询得到的对象是同一个对象。这种情况下会出现一个问题。我们先看一下代码。public void listMybatisModel() { List<MybatisModel> mybatisModels = mapper.listMybatisModel(); List<MybatisModel> mybatisModelsOther = mapper.listMybatisModel(); System.out.println(mybatisModels == mybatisModelsOther); System.out.println(“list count: " + mybatisModels.size()); }System.out.println(mybatisModels == mybatisModelsOther);输出结果竟然是true,这样说来是同一个对象。 会出现这种场景,第一次查出来的对象然后修改了,第二次查出来的就是修改后的对象。一级缓存实现对SqlSession的操作mybatis内部都是通过Executor来执行的。Executor的生命周期和SqlSession是一致的。Mybatis在Executor中创建了一级缓存,基于PerpetualCache 类的 HashMap二级缓存MyBatis的二级缓存是mapper范围级别的SqlSession关闭后才会将数据写到二级缓存区域增删改操作,无论是否进行提交commit(),均会清空一级、二级缓存二级缓存是默认开启的。(想开启就不必做任何配置)二级缓存会使用 Least Recently Used (LRU,最近最少使用的)算法来收回。根据时间表(如 no Flush Interval ,没有刷新间隔),缓存不会以任何时间顺序来刷新 。缓存会存储集合或对象(无论查询方法返回什么类型的值)的 1024 个引用。缓存会被视为 read/write (可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改 。用下面这张图描述一级缓存和二级缓存的关系。配置二级缓存在保证二级缓存的全局配置开启的情况下,给某个xml开启二级缓存只需要在xml中添加<cache />即可// mybatis-config.xml 中配置<settings> 默认值为 true。即二级缓存默认是开启的 <setting name=“cacheEnabled” value=“true”/></settings>// 具体mapper.xml 中配置<mapper namespace=“cn.itcast.mybatis.mapper.UserMapper”> <!– 开启本mapper的namespace下的二级缓存 type:指定cache接口的实现类的类型,mybatis默认使用PerpetualCache 要和ehcache整合,需要配置type为ehcache实现cache接口的类型–> <cache /></mapper>如果想要设置增删改操作的时候不清空二级缓存的话,可以在其insert或delete或update中添加属性flushCache=”false”,默认为 true。<delete id=“deleteStudent” flushCache=“false”> DELETE FROM user where id=#{id}</delete>通过以下一些配置可以修改一些缓存参数。<cache eviction= “FIFO” flushlnterval=“600000” size=“512” readOnly=“true” />配置创建了一个FIFO缓存,并每隔 60 秒刷新一次,存储集合或对象的512个引用, 而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。cache可以配置的属性如下。eviction (收回策略)LRU (最近最少使用的: 移除最长时间不被使用的对象,这是默认值 。 FIFO (先进先出〉 : 按对象进入缓存的顺序来移除它们 。 SOFT (软引用) : 移除基于垃圾回收器状态和软引用规则的对象 。 WEAK (弱引用) : 更积极地移除基于垃圾收集器状态和弱引用规则的对象flushinterval(刷新间隔)。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。size (引用数目)。 可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是 1024 。readOnly (只读)。属性可以被设置为 true 或 false 。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是false。当只使用注解方式配置二级缓存时,如果在Mapper接口中,则需要增加如下配置 。@CacheNamespace (eviction = FifoCache.class ,flushinterval = 60000 ,size = 512 ,readWrite = true)public interface Mapper { }括号内的内容是配置缓存属性。Mapper 接口和对应的 XML 文件是相同的命名空间,想使用二级缓存,两者必须同时配置(如果接口不存在使用注解方式的方法,可以只在 XML 中配置〉,因此按照上面的方式进行配置就会出错 , 这个时候应该使用参照缓存。在 Mapper 接口中,参照缓存配置如下 。@CacheNarnespaceRef(RoleMapper.class)public interface RoleMapper {因为想让 RoleMapper 接口中的注解方法和 XML中的方法使用相同的缓存,因此使用参照缓存配置RoleMapper.class,这样就会使用命名空间为xx.xxx.xxx.xxx.RoleMapper的缓存配置,即RoleMapper.xml 中配置的缓存 。Mapper 接口可以通过注解引用XML 映射文件或者其他接口的缓存,在 XML 中也可以配置参照缓存,如可以在 RoleMapper.xml 中进行如下修改 。<cache-ref narnespace=“xxx.xxx.xxx.xxx.RoleMapper”/>这样配置后XML 就会引用 Mapper 接口中配置的二级缓存,同样可以避免同时配置二级缓存导致的冲突。MyBatis 中很少会同时使用 Mapper 接口注解方式和XML映射文件,所以参照缓存并不是为了解决这个问题而设计的。参照缓存除了能够通过引用其他缓存减少配置外,主要的作用是解决脏读。MyBatis使用SerializedCache(org.apache.ibaits.cache.decorators.SerializedCache)序列化缓存来实现可读写缓存类,井通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置为只读缓存,MyBatis就会使用Map来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。因为使用可读写缓存,可以使用SerializedCache序列化缓存。这个缓存类要求所有被序列化的对象必须实现 Serializable (java.io.Serializable)接口虽然使用序列化得到的对象都是不一样的对象修改时都是互不影响,但是还是不安全的。脏读的产生Mybatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都有自己的二级缓存,不同的mapper的二级缓存互不影响。引起脏读的操作通常发生在多表关联操作中,比如在两个不同的mapper中都涉及到同一个表的增删改查操作,当其中一个mapper对这张表进行查询操作,此时另一个mapper进行了更新操作刷新缓存,然后第一个mapper又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。脏读的避免mapper中的操作以单表操作为主,避免在关联操作中使用mapper使用参照缓存集成EhCache缓存缓存数据有内存和磁盘两级,无须担心容量问题。缓存数据会在虚拟机重启的过程中写入磁盘。可以通过RMI、可插入API等方式进行分布式缓存。具有缓存和缓存管理器的侦昕接口。支持多缓存管理器实例以及一个实例的多个缓存区域。1. 添加项目依赖 <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.0.3</version> </dependency>2. 配置 EhCache在 src/main/resources 目录下新增 ehcache.xml 文件。<?xml version=“1.0” encoding=“UTF-8”?><ehcache xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation=“ehcache.xsd” updateCheck=“false” monitoring=“autodetect” dynamicConfig=“true”> <diskStore path=“D:/cache” /> <defaultCache maxElementsInMemory=“3000” eternal=“false” copyOnRead=“true” copyOnWrite=“true” timeToIdleSeconds=“3600” timeToLiveSeconds=“3600” overflowToDisk=“true” diskPersistent=“true”/> </ehcache>有关EhCache的详细配置可以参考地址 http://www.ehcache.org/ehcach… 中的内容。copyOnRead 的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为 true ,那就是可读写缓存,每次读取缓存时都会复制一个新的实例 。copyOnWrite 的含义是 ,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。如果想使用可读写缓存,就需要将这两个属性配置为true,如果使用只读缓存,可以不配置这两个属性,使用默认值 false 即可 。修改Mapper.xml中的缓存配置ehcache-cache 提供了如下 2 个可选的缓存实现。org.mybatis.caches.ehcache.EhcacheCacheorg.mybatis.caches.ehcache.LoggingEhcache 这个是带日志的缓存。在xml中添加 <cache type =“org.mybatis.caches.ehcache.EhcacheCache” />只通过设置 type 属性就可 以使用 EhCache 缓存了,这时cache的其他属性都不会起到任何作用,针对缓存的配置都在ehcache.xml中进行。在ehcache.xml配置文件中,只有一个默认的缓存配置,所以配置使用EhCache缓存的Mapper映射文件都会有一个以映射文件命名空间命名的缓存。如果想针对某一个命名空间进行配置,需要在 ehcache.xml 中添加一个和映射文件命名空间一致的缓存配置,例如针对RoleMapper可以进行如下配置。 <cache name=“tk.mybatis.simple.mapper.RoleMapper” maxElementsInMemory=“3000” eternal=“false” copyOnRead=“true” copyOnWrite=“true” timeToIdleSeconds=“3600” timeToLiveSeconds=“3600” overflowToDisk=“true” diskPersistent=“true”/>集成Redis缓存添加依赖,目前只有bata版本。 <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>配置Redis使用 Redis 前,必须有一个 Redis 服务,有关Redis安装启动的相关内容,可参考如下地址中的官方文档:https://redis.io/topics/quick…。Redis服务启动后,在src/main/resources 目录下新增 redis.properties 文件 。host=localhostport=6379connectionTimeout=SOOOsoTimeout=SOOOpassword=database=OclientName=修改mapper.xml中的配置。<mapper namespace = ” tk.mybat 工 s.s 工 mple.mapper.RoleMapper ” 〉<cache type= “org.mybatis.caches.redis.RedisCache” />〈 !一其他自己直一 〉</mapper>配置依然很简单, RedisCache 在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此还需要保证被缓存的对象必须实现Serializable接口。改为RedisCache缓存配置后, testL2Cache 测试第一次执行时会全部成功,但是如果再次执行,就会出错。这是因为Redis作为缓存服务器,它缓存的数据和程序(或测试)的启动无关,Redis 的缓存并不会因为应用的关闭而失效。所以再次执行时没有进行一次数据库查询,所有查询都使用缓存,测试的第一部分代码中的rolel和role2都是直接从二级缓存中获取数据,因为是可读写缓存,所以不是相同的对象。当需要分布式部署应用时,如果使用MyBatis自带缓存或基础的EhCahca缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存 ,这种方式会消耗更多的服务器资源。如果使用类似 Redis 的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享 。 ...

January 18, 2019 · 2 min · jiezi

MyBatis 缓存详解

参考文档:MyBatis官方文档MyBatis的缓存主要分为两种一级缓存也叫本地缓存(local cache)和二级缓存(second level cache)。一级缓存、本地缓存一级缓存是session级缓存,即缓存只在session范围生效。每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询语句本身都会被保存在本地缓存中,那么,相同的查询语句和相同的参数所产生的更改就不会二度影响数据库了。本地缓存会被增删改、提交事务、关闭事务以及关闭 session 所清空。默认情况下,本地缓存数据可在整个 session 的周期内使用,这一缓存需要被用来解决循环引用错误和加快重复嵌套查询的速度,所以它不可以被禁用掉,但是你可以设置 localCacheScope=STATEMENT 表示缓存仅在语句执行时有效。注意,如果 localCacheScope 被设置为 SESSION,那么 MyBatis 所返回的引用将传递给保存在本地缓存里的相同对象。对返回的对象(例如 list)做出任何更新将会影响本地缓存的内容,进而影响存活在 session 生命周期中的缓存所返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。手动清空本地缓存:void clearCache()二级缓存二级缓存是namespace级缓存,二级缓存会在同一 namespace中生效。默认情况下,MyBatis 3 没有开启二级缓存,要开启二级缓存,你需要在你的 SQL 映射文件(mapper.xml)中添加一行:<cache/>其实还需要在配置文件中把mybatis.configuration.cache-enabled设置为true(默认为true),若添加<cache/>标签后缓存不生效,可以检查是否将其设置为了false字面上看就是这样。这个简单语句的效果如下:映射语句文件中的所有 select 语句将会被缓存。映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。所有的这些属性都可以通过缓存元素的属性来修改。比如:<cache eviction=“FIFO” flushInterval=“60000” size=“512” readOnly=“true”/>这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会 导致冲突。可用的收回策略有:LRU – 最近最少使用的:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的缓存回收策略是 LRU。flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。若在SqlSession关闭时,SqlSession对应的本地缓存会自动转化为二级缓存。自定义缓存使用自定缓存,只需要实现MyBatis的Cache接口并在<cache/>中配置缓存类型:<cache type=“com.domain.something.MyCustomCache”/>自定义缓存没有使用过,如果大家有兴趣可以参考MyBatis官方文档自定义缓存部分后记这篇文章主要由MyBatis官方文档整理而来,用于记录我的学习过程,作为2019年的开始,以后的学习都需要有产出物,否则学了之后很快就会忘记。 ...

January 17, 2019 · 1 min · jiezi

mybatis常见错误记录

Mybatis使用过程中,常见的一些问题汇总记录。1. Mybatis执行SQL,查出来的比使用PL/SQL等工具查出来的记录多。问题描述:由于查询要使用到not in,因此 not in ()括号里面的东西自己使用了前端拼接传值,传值格式为’’,‘‘问题处理:在Mapper.xml中使用了#{},这块涉及到#{}和${}的区别。替换成${} 就好了。2. MyBatis mapper文件中的变量引用方式#{}与${}的差别默认情况下,使用#{}语法,MyBatis会产生PreparedStatement语句中,并且安全的设置PreparedStatement参数,这个过程中MyBatis会进行必要的安全检查和转义。示例1:执行SQL:Select * from emp where name = #{employeeName}参数:employeeName传入值为:Smith解析后执行的SQL:Select * from emp where name = ?执行SQL:Select * from emp where name = ${employeeName}参数:employeeName传入值为:Smith解析后执行的SQL:Select * from emp where name = Smith综上所述、${}方式会引发SQL注入的问题、同时也会影响SQL语句的预编译,所以从安全性和性能的角度出发,能使用#{}的情况下就不要使用${}${}的使用场景:有时候可能需要直接插入一个不做任何修改的字符串到SQL语句中。这时候应该使用${}语法。比如,动态SQL中的字段名,如:ORDER BY ${columnName},not in ${items}注意:当使用${}参数作为字段名或表名时、需指定statementType为“STATEMENT”,如:<select id=“queryMetaList” resultType=“Map” statementType=“STATEMENT”> Select * from emp where name = ${employeeName} ORDER BY ${columnName}</select>3. ssm-web项目启动报错(一)Could not resolve type alias ‘map ‘. Cause: java.lang.ClassNotFoundException: Cannot find class: map<select id=“selectByCode” resultType=“com.lucifer.pojo.BaseParams” parameterType=“java.util.map”> SELECT BP.ID,BP.NAME FROM BASE_PARAMS BP WHERE BP.DOMAIN=#{domain} AND BP.IS_CANCEL=‘N’</select>处理方式:把resultType改为resultMap,把parameterType改为parameterMap,重新发布并运行。4. ssm-web项目启动报错(二)org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [] will not be managed by Spring处理方式: ...

January 15, 2019 · 1 min · jiezi

mybatis 延迟加载

mybatis 延迟加载什么是延迟加载延迟加载又叫懒加载,也叫按需加载,也就是说先加载主信息,需要的时候,再去加载从信息。代码中有查询语句,当执行到查询语句时,并不是马上去DB中查询,而是根据设置的延迟策略将查询向后推迟。什么时候会执行延迟加载配置之后在对关联对象进行查询时使用延迟加载。延迟加载策略直接加载遇到代码中查询语句,马上到DB中执行select语句进行查询。(这种只能用于多表单独查询)侵入式延迟加载将关联对象的详情(具体数据,如id、name)侵入到主加载对象,作为主加载对象的详情的一部分出现。当要访问主加载对象的详情时才会查询主表,但由于关联对象详情作为主加载对象的详情一部分出现,所以这个查询不仅会查询主表,还会查询关联表。深度延迟加载将关联对象的详情(具体数据,如id、name)侵入到主加载对象,作为主加载对象的详情的一部分出现。当要访问主加载对象的详情时才会查询主表,但由于关联对象详情作为主加载对象的详情一部分出现,所以这个查询不仅会查询主表,还会查询关联表。使用延迟加载的目的减轻DB服务器的压力,因为我们延迟加载只有在用到需要的数据才会执行查询操作。配置 <settings> <setting name =“aggressiveLazyLoading” value=“false”/> <!–开启延迟加载–> <setting name=“lazyLoadingEnabled” value=“true”/> </settings>我们用关联查询来实现延迟加载,假设我们现在要查出用户和用户角色。首先我们在user中添加查询userVo的方法和xml。<!–userMapper.xml–>….<resultMap id=“BaseResultMap” type=“com.redstar.basemapper.pojo.User”> <id column=“id” jdbcType=“VARCHAR” property=“id”/> <result column=“name” jdbcType=“VARCHAR” property=“name”/> <result column=“age” jdbcType=“INTEGER” property=“age”/> <result column=“role_id” jdbcType=“INTEGER” property=“roleId”/> </resultMap> <resultMap id=“userRoleMapSelect” type=“com.redstar.basemapper.pojo.UserVo”> <association property=“user” resultMap=“BaseResultMap”/> <association property=“role” fetchType=“lazy” column="{id=role_id}" select=“com.redstar.basemapper.dao.RoleMapper.getRoleById”/> </resultMap> <sql id=“Base_Column_List”> id, name, age, role_id </sql> <select id=“getUserVo” resultMap=“userRoleMapSelect”> select * from user where id=#{userId} </select>… <!–roleMapper.xml–>… <resultMap id=“BaseResultMap” type=“com.redstar.basemapper.pojo.Role”> <id column=“id” jdbcType=“INTEGER” property=“id” /> <result column=“role_name” jdbcType=“VARCHAR” property=“roleName” /> </resultMap> <sql id=“Base_Column_List”> id, role_name </sql> <select id=“getRoleById” resultMap=“BaseResultMap”> select * from role where id=#{id} </select>… 测试用例@RunWith(SpringRunner.class)@SpringBootTestpublic class BaseMapperApplicationTests { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Test public void getUserVo() { System.out.println(userMapper.getUserVo(“12312232”));// System.out.println(userMapper.getUserById(“12312232”));// System.out.println(roleMapper.getRoleById(1)); }}输出结果:UserVo{user=User [Hash = 1937575946, id=12312232, name=哇哈哈啊娃哈哈哇哈哈哈哈哈哈哈, age=48, roleId=1, serialVersionUID=1], role=Role [Hash = 317053574, id=1, roleName=admin, serialVersionUID=1]}注意许多对延迟加载原理不太熟悉的朋友会经常遇到一些莫名其妙的问题:有些时候延迟加载可以得到数据,有些时候延迟加载就会报错,为什么会出现这种情况呢?MyBatis 延迟加载是通过动态代理实现的,当调用配直为延迟加载的属性方法时, 动态代理的操作会被触发,这些额外的操作就是通过 MyBatis 的 SqlSessio口去执行嵌套 SQL 的 。由于在和某些框架集成时, SqlSession 的生命周期交给了框架来管理,因此当对象超出SqlSession 生命周期调用时,会由于链接关闭等问题而抛出异常 。 在和 Spring 集成时,要确保只能在 Service 层调用延迟加载的属性 。 当结果从 Service 层返回至 Controller 层时, 如果获取延迟加载的属性值,会因为 SqlSessio口已经关闭而抛出异常 。 ...

January 15, 2019 · 1 min · jiezi

Mybatis是怎么工作的(二)

目标:理清mybatis加载解析mapper文件的过程;理清mybatis执行SQL的过程。上一篇文章分析mybatis加载配置的源码时提到了org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration方法,现在继续分析其中的mapperElement方法。先看源码: private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if (“package”.equals(child.getName())) { String mapperPackage = child.getStringAttribute(“name”); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute(“resource”); String url = child.getStringAttribute(“url”); String mapperClass = child.getStringAttribute(“class”); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); // 生成XMLMapperBuilder XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 解析配置文件 mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException(“A mapper element may only specify a url, resource or class, but not more than one.”); } } } } }考虑到项目的配置,看下生成XMLMapperBuilder和mapperParser.parse()的代码。在生成XMLMapperBuilder的过程中,使用了MapperBuilderAssistant,这个类继承了BaseBuilder。在该类的构造器中加载了TypeAliasRegistry和TypeHandlerRegistry。下面重点看mapperParserparse() public void parse() { // 判断是否已经加载资源 if (!configuration.isResourceLoaded(resource)) { // 配置/mapper节点下的子节点 configurationElement(parser.evalNode("/mapper")); // 加载resource资源 configuration.addLoadedResource(resource); // 绑定命名空间 bindMapperForNamespace(); } // 加载未加载完成的资源 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }下面主要看下configurationElement的代码: private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute(“namespace”); if (namespace == null || namespace.equals("")) { throw new BuilderException(“Mapper’s namespace cannot be empty”); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode(“cache-ref”)); cacheElement(context.evalNode(“cache”)); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes(“select|insert|update|delete”)); } catch (Exception e) { throw new BuilderException(“Error parsing Mapper XML. The XML location is ‘” + resource + “’. Cause: " + e, e); } }可以看出主要是解析不同的节点,并放进builderAssistant里面去。下面看下执行SQL的过程。 ClipsDAO clipsDAO = session.getMapper(ClipsDAO.class); ClipsEntity clipsEntity = clipsDAO.selectById(1);查看session.getMapper()的实现: // org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } // org.apache.ibatis.session.Configuration#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } // org.apache.ibatis.binding.MapperRegistry#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException(“Type " + type + " is not known to the MapperRegistry.”); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException(“Error getting mapper instance. Cause: " + e, e); } } 可以看出,mybatis通过动态代理为接口生成了代理类,我们知道在加载配置时,bindMapperForNamespace方法调用了configuration.addMapper()方法把Class映射到org.apache.ibatis.binding.MapperRegistry#knownMappers中去的。下面看一下MapperProxy代码: @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 从缓存中获取MapperMethod对象 final MapperMethod mapperMethod = cachedMapperMethod(method); // 执行SQL return mapperMethod.execute(sqlSession, args); }下面是MapperMethod.execute()方法: public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException(“Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException(“Mapper method ‘” + command.getName() + " attempted to return null from a method with a primitive return type (” + method.getReturnType() + “).”); } return result; }至此,SQL执行完成。 ...

January 11, 2019 · 3 min · jiezi

Mybatis是如何工作的(一)

本文目标:使用纯Mybatis框架获取数据;理清Mybatis的工作过程。创建项目并运行首先创建maven项目,过程不再赘述。依赖如下:<dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> </dependencies>下面准备一张表:CREATE TABLE clips ( id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, content varchar(256) NOT NULL DEFAULT ’’ COMMENT ‘内容’, deleted tinyint(1) NOT NULL DEFAULT ‘0’ COMMENT ‘删除标识:0正常,1删除’, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新时间’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=‘clips’;添加一条数据:对应的实体类:public class ClipsEntity { private Integer id; private String content; private Integer deleted; private LocalDateTime createTime; private LocalDateTime updateTime; // 省略getter和setter}DAO:public interface ClipsDAO { ClipsEntity selectById(@Param(“id”) Integer id);}mapper文件:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“com.chunrun.dao.ClipsDAO”> <select id=“selectById” resultType=“com.chunrun.entity.ClipsEntity”> select * from clips where id = #{id} </select></mapper>Mybatis配置文件:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE configuration PUBLIC “-//mybatis.org//DTD Config 3.0//EN” “http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <settings> <!–使用jdbc的getGeneratekeys获取自增主键值–> <setting name=“useGeneratedKeys” value=“true”/> <!–使用列别名替换别名 默认true–> <setting name=“useColumnLabel” value=“true”/> <!–开启驼峰命名转换Table:create_time到 Entity(createTime)–> <setting name=“mapUnderscoreToCamelCase” value=“true”/> <!– 打印查询语句 –> <setting name=“logImpl” value=“org.apache.ibatis.logging.stdout.StdOutImpl” /> </settings> <typeAliases> <typeAlias alias=“ClipsEntity” type=“com.chunrun.entity.ClipsEntity”/> </typeAliases> <environments default=“development”> <environment id=“development”> <transactionManager type=“JDBC”/> <dataSource type=“POOLED”> <property name=“driver” value=“com.mysql.jdbc.Driver”/> <property name=“url” value=“jdbc:mysql://localhost:3306/bizu”/> <property name=“username” value=“chunrun”/> <property name=“password” value=“chunrun1s”/> </dataSource> </environment> </environments> <mappers> <mapper resource=“mapper/ClipsDAO.xml”/> </mappers></configuration>下面写个测试:public class Main { public static void main(String[] args) { String resource = “mybatis-config.xml”; try { InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = factory.openSession(); ClipsDAO clipsDAO = session.getMapper(ClipsDAO.class); ClipsEntity clipsEntity = clipsDAO.selectById(1); System.out.println(clipsEntity); } catch (Exception e) { System.out.println(e); } }}运行结果:运行成功。那么,在这个过程中,程序具体做了什么事呢?一步一步来看。首先,我们用配置文件生成了一个InputStream;然后用InputStream生成了生成SqlSessionFactory;然后获取Session;获取对应的mapper,执行SQL获取结果。Mybatis做的事情主要有三步:从配置文件中生成SqlSessionFactory;从SqlSessionFactory中获取session;获取对应的mapper,执行SQL。下面逐步看源码。加载mybatis配置,生成SqlSessionFactory // 首先调用的是这个方法: public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } // 然后是这个: public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // 根据参数获取一个XMLConfigBuilder,这部分是重点 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException(“Error building SqlSession.”, e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } // 返回的build方法如下,可以看出实现是DefaultSqlSessionFactory public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }注释部分已经很清楚了,下面重点看下XMLConfigBuilder,Mybatis通过这个类来解析mybatis对应的配置。 // 解析configuration节点下面的子节点,并返回最终的配置。 public Configuration parse() { if (parsed) { throw new BuilderException(“Each XMLConfigBuilder can only be used once.”); } parsed = true; parseConfiguration(parser.evalNode("/configuration”)); return configuration; } private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 加载properties节点下的属性, propertiesElement(root.evalNode(“properties”)); // 加载settings节点下的属性 Properties settings = settingsAsProperties(root.evalNode(“settings”)); loadCustomVfs(settings); // 加载别名配置 typeAliasesElement(root.evalNode(“typeAliases”)); // 加载插件配置 pluginElement(root.evalNode(“plugins”)); // 加载objectFactory配置 objectFactoryElement(root.evalNode(“objectFactory”)); // 加载objectWrapperFactory配置 objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”)); // 加载reflectorFactory配置 reflectorFactoryElement(root.evalNode(“reflectorFactory”)); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 加载environment配置,这里会配置事务管理器 environmentsElement(root.evalNode(“environments”)); // 加载databaseIdProvider配置 databaseIdProviderElement(root.evalNode(“databaseIdProvider”)); // 加载typeHandler是配置,自定义的typeHandler会在这注册 typeHandlerElement(root.evalNode(“typeHandlers”)); // 加载mapper配置 mapperElement(root.evalNode(“mappers”)); } catch (Exception e) { throw new BuilderException(“Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 至此,mybatis配置加载完成。小结本文主要介绍了如何使用纯Mybatis操作数据库,然后介绍了Mybatis加载配置的过程。内容相对粗浅,深入分析在下文。 ...

January 10, 2019 · 2 min · jiezi

mybatis增强工具MyBatis-plus

如果你正在用mybatis,那MyBatis-plus你不能错过,配合使用可极大简化开发、提高效率!简介MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。愿景我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。特性无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多种数据库支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题支持 XML 热加载:Mapper 对应的 XML 支持热加载,对于简单的 CRUD 操作,甚至可以无 XML 启动支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )支持关键词自动转义:支持数据库关键词(order、key……)自动转义,还可自定义关键词内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作内置 Sql 注入剥离器:支持 Sql 注入剥离,有效预防 Sql 注入攻击其中两大点可极大提高开发效率:代码生成器:采用代码或者 Maven 插件可快速生成 Dao、 Model 、 Service 、 Controller 层,Mapper.xml等代码,一键生成,无需重复编码强大的 CRUD 操作:内置通用 Mapper、通用 Service,无需编写其他代码,即用期做CRUD操作,以及自带分页插件,配置一下即可使用以下为CRUD 操作例子public interface FileTypeService extends IService<FileType> {}//注意:FileTypeService中,未写任何代码 //继承了IService通用Service public void addTest(){ //新增 FileType fileType = new FileType(); fileType.setName(“测试4”); fileTypeService.insert(fileType); } public void deleteTest(){ //根据id删除 fileTypeService.deleteById(1); //自定义条件删除 fileTypeService.delete(new EntityWrapper<FileType>().eq(“name”,“测试3”)); } public void getOneTest(){ //查询单个实体 条件为name为测试3 fileTypeService.selectOne(new EntityWrapper<FileType>().eq(“name”,“测试3”)); } public void listTest(){ //查询列表 只查"name"列 fileTypeService.selectList(new EntityWrapper<FileType>().setSqlSelect(“name”)); } public void selectPage(){ //分页查询 //参数Map Map<String,Object> param = new HashMap<>(); //当前页数 param.put(“page”,1); //每页显示 param.put(“limit”,20); //根据id字段倒序排序 param.put(“field”,“id”); param.put(“order”,“desc”); //分页查询 Page<FileType> page = fileTypeService.selectPage(new MapQuery(param).getPage()); }一行代码即可实现CRUD,单仅支持单表查询,如果需要关联多个表,还是得用以前的方法,写sql实现了支持SpringMvc和SpringBoot集成,具体使用请移步官网哈~ THANDKSEnd -一个立志成大腿而每天努力奋斗的年轻人伴学习伴成长,成长之路你并不孤单! ...

January 9, 2019 · 1 min · jiezi

MyBatis 源码阅读之数据库连接

MyBatis 源码阅读之数据库连接MyBatis 的配置文件所有配置会被 org.apache.ibatis.builder.xml.XMLConfigBuilder 类读取,我们可以通过此类来了解各个配置是如何运作的。而 MyBatis 的映射文件配置会被 org.apache.ibatis.builder.xml.XMLMapperBuilder 类读取。我们可以通过此类来了解映射文件的配置时如何被解析的。本文探讨 事务管理器 和 数据源 相关代码配置environment以下是 mybatis 配置文件中 environments 节点的一般配置。<!– mybatis-config.xml –><environments default=“development”> <environment id=“development”> <transactionManager type=“JDBC”> <property name="…" value="…"/> </transactionManager> <dataSource type=“POOLED”> <property name=“driver” value="${driver}"/> <property name=“url” value="${url}"/> <property name=“username” value="${username}"/> <property name=“password” value="${password}"/> </dataSource> </environment></environments>environments 节点的加载也不算复杂,它只会加载 id 为 development 属性值的 environment 节点。它的加载代码在 XMLConfigBuilder 类的 environmentsElement() 方法中,代码不多,逻辑也简单,此处不多讲。TransactionManager接下来我们看看 environment 节点下的子节点。transactionManager 节点的 type 值默认提供有 JDBC 和 MANAGED ,dataSource 节点的 type 值默认提供有 JNDI 、 POOLED 和 UNPOOLED 。它们对应的类都可以在 Configuration 类的构造器中找到,当然下面我们也一个一个来分析。现在我们大概了解了配置,然后来分析这些配置与 MyBatis 类的关系。TransactionFactorytransactionManager 节点对应 TransactionFactory 接口,使用了 抽象工厂模式 。MyBatis 给我们提供了两个实现类:ManagedTransactionFactory 和 JdbcTransactionFactory ,它们分别对应者 type 属性值为 MANAGED 和 JDBC 。TransactionFactory 有三个方法,我们需要注意的方法只有 newTransaction() ,它用来创建一个事务对象。void setProperties(Properties props);Transaction newTransaction(Connection conn);Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);其中 JdbcTransactionFactory 创建的事务对象是 JdbcTransaction 的实例,它是对 JDBC 事务的简单封装;ManagedTransactionFactory 创建的事务对象是 ManagedTransaction 的实例,它本身并不控制事务,即 commit 和 rollback 都是不做任何操作,而是交由 JavaEE 容器来控制事务,以方便集成。DataSourceFactoryDataSourceFactory 是获取数据源的接口,也使用了 抽象工厂模式 ,代码如下,方法极为简单:public interface DataSourceFactory { /** * 可传入一些属性配置 / void setProperties(Properties props); DataSource getDataSource();}MyBatis 默认支持三种数据源,分别是 UNPOOLED 、 POOLED 和 JNDI 。对应三个工厂类:UnpooledDataSourceFactory 、 PooledDataSourceFactory 和 JNDIDataSourceFactory 。其中 JNDIDataSourceFactory 是使用 JNDI 来获取数据源。我们很少使用,并且代码不是非常复杂,此处不讨论。我们先来看看 UnpooledDataSourceFactory :public class UnpooledDataSourceFactory implements DataSourceFactory { private static final String DRIVER_PROPERTY_PREFIX = “driver.”; private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length(); protected DataSource dataSource; public UnpooledDataSourceFactory() { this.dataSource = new UnpooledDataSource(); } @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); // MetaObject 用于解析实例对象的元信息,如字段的信息、方法的信息 MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); for (Object key : properties.keySet()) { String propertyName = (String) key; if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { // 添加驱动的配置属性 String value = properties.getProperty(propertyName); driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { // 为数据源添加配置属性 String value = (String) properties.get(propertyName); Object convertedValue = convertValue(metaDataSource, propertyName, value); metaDataSource.setValue(propertyName, convertedValue); } else { throw new DataSourceException(“Unknown DataSource property: " + propertyName); } } if (driverProperties.size() > 0) { metaDataSource.setValue(“driverProperties”, driverProperties); } } @Override public DataSource getDataSource() { return dataSource; } /* * 将 String 类型的值转为目标对象字段的类型的值 / private Object convertValue(MetaObject metaDataSource, String propertyName, String value) { Object convertedValue = value; Class<?> targetType = metaDataSource.getSetterType(propertyName); if (targetType == Integer.class || targetType == int.class) { convertedValue = Integer.valueOf(value); } else if (targetType == Long.class || targetType == long.class) { convertedValue = Long.valueOf(value); } else if (targetType == Boolean.class || targetType == boolean.class) { convertedValue = Boolean.valueOf(value); } return convertedValue; }}虽然代码看起来复杂,实际上非常简单,在创建工厂实例时创建它对应的 UnpooledDataSource 数据源。setProperties() 方法用于给数据源添加部分属性配置,convertValue() 方式时一个私有方法,就是处理 当 DataSource 的属性为整型或布尔类型时提供对字符串类型的转换功能而已。最后我们看看 PooledDataSourceFactory ,这个类非常简单,仅仅是继承了 UnpooledDataSourceFactory ,然后构造方法替换数据源为 PooledDataSource 。public class PooledDataSourceFactory extends UnpooledDataSourceFactory { public PooledDataSourceFactory() { this.dataSource = new PooledDataSource(); }}虽然它的代码极少,实际上都在 PooledDataSource 类中。DataSource看完了工厂类,我们来看看 MyBatis 提供的两种数据源类: UnpooledDataSource 和 PooledDataSource 。UnpooledDataSourceUnpooledDataSource 看名字就知道是没有池化的特征,相对也简单点,以下代码省略一些不重要的方法import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;public class UnpooledDataSource implements DataSource { private ClassLoader driverClassLoader; private Properties driverProperties; private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); private String driver; private String url; private String username; private String password; private Boolean autoCommit; // 事务隔离级别 private Integer defaultTransactionIsolationLevel; static { // 遍历所有可用驱动 Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); registeredDrivers.put(driver.getClass().getName(), driver); } } // …… private Connection doGetConnection(Properties properties) throws SQLException { // 每次获取连接都会检测驱动 initializeDriver(); Connection connection = DriverManager.getConnection(url, properties); configureConnection(connection); return connection; } /* * 初始化驱动,这是一个 同步 方法 / private synchronized void initializeDriver() throws SQLException { // 如果不包含驱动,则准备添加驱动 if (!registeredDrivers.containsKey(driver)) { Class<?> driverType; try { // 加载驱动 if (driverClassLoader != null) { driverType = Class.forName(driver, true, driverClassLoader); } else { driverType = Resources.classForName(driver); } Driver driverInstance = (Driver)driverType.newInstance(); // 注册驱动代理到 DriverManager DriverManager.registerDriver(new DriverProxy(driverInstance)); // 缓存驱动 registeredDrivers.put(driver, driverInstance); } catch (Exception e) { throw new SQLException(“Error setting driver on UnpooledDataSource. Cause: " + e); } } } private void configureConnection(Connection conn) throws SQLException { // 设置是否自动提交事务 if (autoCommit != null && autoCommit != conn.getAutoCommit()) { conn.setAutoCommit(autoCommit); } // 设置 事务隔离级别 if (defaultTransactionIsolationLevel != null) { conn.setTransactionIsolation(defaultTransactionIsolationLevel); } } private static class DriverProxy implements Driver { private Driver driver; DriverProxy(Driver d) { this.driver = d; } /* * Driver 仅在 JDK7 中定义了本方法,用于返回本驱动的所有日志记录器的父记录器 * 个人也不是十分明确它的用法,毕竟很少会关注驱动的日志 / public Logger getParentLogger() { return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); } // 其他方法均为调用 driver 对应的方法,此处省略 }}这里 DriverProxy 仅被注册到 DriverManager 中,这是一个注意点,我也不懂这种操作,有谁明白的可以留言相互讨论。这里的方法也不是非常复杂,我都已经标有注释,应该都可以看懂,不再细说。以上便是 UnpooledDataSource 的初始化驱动和获取连接关键代码。PooledDataSource接下来我们来看最后一个类 PooledDataSource ,它也是直接实现 DataSource ,不过因为拥有池化的特性,它的代码复杂不少,当然效率比 UnpooledDataSource 会高出不少。PooledDataSource 通过两个辅助类 PoolState 和 PooledConnection 来完成池化功能。PoolState 是记录连接池运行时的状态,定义了两个 PooledConnection 集合用于记录空闲连接和活跃连接。PooledConnection 内部定义了两个 Connection 分别表示一个真实连接和代理连接,还有一些其他字段用于记录一个连接的运行时状态。先来详细了解一下 PooledConnection/* * 此处使用默认的访问权限 * 实现了 InvocationHandler /class PooledConnection implements InvocationHandler { private static final String CLOSE = “close”; private static final Class<?>[] IFACES = new Class<?>[] { Connection.class }; /* hashCode() 方法返回 / private final int hashCode; private final Connection realConnection; private final Connection proxyConnection; // 省略 checkoutTimestamp、createdTimestamp、lastUsedTimestamp private boolean valid; / * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in * * @param connection - the connection that is to be presented as a pooled connection * @param dataSource - the dataSource that the connection is from / public PooledConnection(Connection connection, PooledDataSource dataSource) { this.hashCode = connection.hashCode(); this.realConnection = connection; this.dataSource = dataSource; this.createdTimestamp = System.currentTimeMillis(); this.lastUsedTimestamp = System.currentTimeMillis(); this.valid = true; this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); } / * 设置连接状态为不正常,不可使用 / public void invalidate() { valid = false; } / * Method to see if the connection is usable * * @return True if the connection is usable / public boolean isValid() { return valid && realConnection != null && dataSource.pingConnection(this); } /* * 自动上一次使用后经过的时间 / public long getTimeElapsedSinceLastUse() { return System.currentTimeMillis() - lastUsedTimestamp; } /* * 存活时间 / public long getAge() { return System.currentTimeMillis() - createdTimestamp; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 对于 close() 方法,将连接放回池中 dataSource.pushConnection(this); return null; } else { try { if (!Object.class.equals(method.getDeclaringClass())) { checkConnection(); } return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } } private void checkConnection() throws SQLException { if (!valid) { throw new SQLException(“Error accessing PooledConnection. Connection is invalid.”); } }}本类实现了 InvocationHandler 接口,这个接口是用于 JDK 动态代理的,在这个类的构造器中 proxyConnection 就是创建了此代理对象。来看看 invoke() 方法,它拦截了 close() 方法,不再关闭连接,而是将其继续放入池中,然后其他已实现的方法则是每次调用都需要检测连接是否合法。再来说说 PoolState 类,这个类没什么可说的,都是一些统计字段,没有复杂逻辑,不再讨论;注意该类是针对一个 PooledDataSource 对象统计的。也就是说 PoolState 的统计字段是关于整个数据源的,而一个PooledConnection 则是针对单个连接的。最后我们回过头来看 PooledDataSource 类,数据源的操作就只有两个,获取连接,释放连接,先来看看获取连接public class PooledDataSource implements DataSource { private final UnpooledDataSource dataSource; @Override public Connection getConnection() throws SQLException { return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return popConnection(username, password).getProxyConnection(); } /* * 获取一个连接 / private PooledConnection popConnection(String username, String password) throws SQLException { boolean countedWait = false; PooledConnection conn = null; long t = System.currentTimeMillis(); int localBadConnectionCount = 0; // conn == null 也可能是没有获得连接,被通知后再次走流程 while (conn == null) { synchronized (state) { // 是否存在空闲连接 if (!state.idleConnections.isEmpty()) { // 池里存在空闲连接 conn = state.idleConnections.remove(0); } else { // 池里不存在空闲连接 if (state.activeConnections.size() < poolMaximumActiveConnections) { // 池里的激活连接数小于最大数,创建一个新的 conn = new PooledConnection(dataSource.getConnection(), this); } else { // 最坏的情况,无法获取连接 // 检测最早使用的连接是否超时 PooledConnection oldestActiveConnection = state.activeConnections.get(0); long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); if (longestCheckoutTime > poolMaximumCheckoutTime) { // 使用超时连接,对超时连接的操作进行回滚 state.claimedOverdueConnectionCount++; state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; state.accumulatedCheckoutTime += longestCheckoutTime; state.activeConnections.remove(oldestActiveConnection); if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { try { oldestActiveConnection.getRealConnection().rollback(); } catch (SQLException e) { / * Just log a message for debug and continue to execute the following statement * like nothing happened. Wrap the bad connection with a new PooledConnection, * this will help to not interrupt current executing thread and give current * thread a chance to join the next competition for another valid/good database * connection. At the end of this loop, bad {@link @conn} will be set as null. */ log.debug(“Bad connection. Could not roll back”); } } conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); oldestActiveConnection.invalidate(); } else { // 等待可用连接 try { if (!countedWait) { state.hadToWaitCount++; countedWait = true; } long wt = System.currentTimeMillis(); state.wait(poolTimeToWait); state.accumulatedWaitTime += System.currentTimeMillis() - wt; } catch (InterruptedException e) { break; } } } } // 已获取连接 if (conn != null) { // 检测连接是否可用 if (conn.isValid()) { // 对之前的操作回滚 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); conn.setCheckoutTimestamp(System.currentTimeMillis()); conn.setLastUsedTimestamp(System.currentTimeMillis()); // 激活连接池数+1 state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; } else { // 连接坏掉了,超过一定阈值则抛异常提醒 state.badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { // 省略日志 throw new SQLException( “PooledDataSource: Could not get a good connection to the database.”); } } } } } if (conn == null) { // 省略日志 throw new SQLException( “PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.”); } return conn; }}上面的代码都已经加了注释,总体流程不算复杂:while => 连接为空能否直接从池里拿连接 => 可以则获取连接并返回不能,查看池里的连接是否没满 => 没满则创建一个连接并返回满了,查看池里最早的连接是否超时 => 超时则强制该连接回滚,然后获取该连接并返回未超时,等待连接可用检测连接是否可用释放连接操作,更为简单,判断更少protected void pushConnection(PooledConnection conn) throws SQLException { // 同步操作 synchronized (state) { // 从活动池中移除连接 state.activeConnections.remove(conn); if (conn.isValid()) { // 不超过空闲连接数 并且连接是同一类型的连接 if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { state.accumulatedCheckoutTime += conn.getCheckoutTime(); if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 废弃原先的对象 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); state.idleConnections.add(newConn); newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); // 该对象已经不能用于连接了 conn.invalidate(); if (log.isDebugEnabled()) { log.debug(“Returned connection " + newConn.getRealHashCode() + " to pool.”); } state.notifyAll(); } else { state.accumulatedCheckoutTime += conn.getCheckoutTime(); if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 关闭连接 conn.getRealConnection().close(); if (log.isDebugEnabled()) { log.debug(“Closed connection " + conn.getRealHashCode() + “.”); } conn.invalidate(); } } else { if (log.isDebugEnabled()) { log.debug(“A bad connection (” + conn.getRealHashCode() + “) attempted to return to the pool, discarding connection.”); } state.badConnectionCount++; } }}部分码注释已添加,这里就说一下总体流程:从活动池中移除连接如果该连接可用连接池未满,则连接放回池中满了,回滚,关闭连接总体流程大概就是这样其他还有两个方法代码较多,但逻辑都很简单:pingConnection() 执行一条 SQL 检测连接是否可用。forceCloseAll() 回滚并关闭激活连接池和空闲连接池中的连接 ...

January 7, 2019 · 7 min · jiezi

Mybatis从入门到精通——从JDBC编程开始

[toc]JDBC是什么 (Java DataBase Connectivity)jdbc是一种Java编程语言和各种数据库之间数据库无关连接的行业标准,JDBC API为基于SQL的数据库访问提供了调用级API数据库无关在没有JDBC之前,我们需要编写不同的程序对接不同厂商的数据库系统,像下图所示,需要针对不同的数据库api编程,可想而知,当我们需要更换数据库系统而产生的大量重复工作,增加不必要的开发成本而JDBC的出现,统一了Java程序访问不同数据库系统的api,应用程序通过调用JDBC来操作数据库时,实际上是右数据库系统厂商提供的JDBC驱动程序来完成的。这样一来,即使要更换数据库系统,也仅仅是更换相应的驱动程序就可以了。(本文使用mysql的驱动程序)JDBC API主要完成以下三个工作:建立与数据库的连接或访问任何表格数据源发送SQL语句到数据库处理数据返回的结果这三个工作中都有其对应的jdbc api来完成各自的任务。应用程序可以使用这些api 来操作数据库系统了JDBC APIDriverManager管理JDBC驱动的服务类,主要功能是获取Connection对象(示例程序中的第2步)getConnection方法Connection getConnection(url,username,password)url写法:jdbc:mysql://localhost:3306/mydb本地数据库简写:jdbc:mysql:///mydbjdbc:协议mysql:子协议localhost:主机名3306:端口号mydb:数据库名称Connection数据库连接对象,每个Connection对象表示一个连接会话。Connection的常用方法1. 返回Statement对象Statement createStatement()PreparedStatement PreparedStatement(String sql):Statement的子类,将SQL语句提交到数据库进行预编译CallableStatement prepareCall(String sql):Statement的子类,处理存储过程2. 处理事务的常用方法void setAutoCommit(boolean autoCommit):设置是否自动提交,默认truevoid commit:提交事务void rollback:事务回滚Savepoint setSavepoint:创建一个保存点Savepoint setSavepoint(Stirng name):指定名称来创建一个保存点void rollback(Savepoint savepoint):将事务回滚到指定的保存点void setTransactionIsolation(int level):设置事务隔离级别Statement执行具体的SQL语句,以及执行DDL、DCL、DML语句。Statement子类 PreparedStatement预编译的Statement对象SQL语句一次编译多次执行允许数据库预编译SQL语句(常带有参数或者叫占位符),这样一来,以后每次只需要改变SQL命令的参数,而不需要每次都编译SQL语句,性能得到了提升PreparedStatement主要方法void setXXX(int parmIndex,XXX value):设置预编译语句的参数Statement常用方法ResultSet executeQuery(String sql):只能执行查询语句,返回ResultSetint executeUpdate(String sql):主要用于执行DML语句的,返回受影响行数。boolean execute(String sql):执行任何SQL语句addBatch(String sql):添加到批处理executeBatch():执行批处理clearBatch():清空批处理ResultSet结果集:查询结果的封装ResultSet主要方法移动指针的方法next():移动指针到ResultSet记录的下一行,如果存在该条记录返回true…….获取当前行、指定列的值getInt()、getString()针对不同数据类型的方法,通用的两个泛型方法<T> T getObject(int columnIndex,Class<T> type)、<T> T getObject(String columnLabel,Class<T> type)JDBC实际操作建库建表的SQL语句– 建库CREATE DATABASE mydb CHARACTER SET utf8 COLLATE utf8_general_ci;– 建表CREATE TABLE IF NOT EXISTS user( id INT UNSIGNED AUTO_INCREMENT, username VARCHAR(100) NOT NULL, password VARCHAR(40) NOT NULL, name VARCHAR(40) NOT NULL, PRIMARY KEY ( id ))ENGINE=InnoDB DEFAULT CHARSET=utf8;示例程序框架我们会在业务逻辑代码start的地方开始具体的执行操作public void excute() throws SQLException { Connection conn = null; PreparedStatement smt = null; ResultSet resultset = null; try { //1、加载驱动 Class.forName(“com.mysql.jdbc.Driver”); //2、获取Connection对象 conn = DriverManager.getConnection(“jdbc:mysql://localhost:3306/mydb?characterEncoding=utf-8”, “root”, “******”); if (conn != null) { System.out.println(“连接成功”); } conn.setAutoCommit(false); //业务逻辑代码start //插入数据操作 String insertSql = “insert into user(username,password,name) values(?,?,?)”; smt = conn.prepareStatement(insertSql); smt.setString(1, “xiaoming”); smt.setString(2, “123”); smt.setString(3, “小明”); int result = smt.executeUpdate(); if (result > 0) { System.out.println(“添加成功”); } smt.clearParameters(); //修改数据操作 String updateSql = “update user set name=? where id=?”; smt = conn.prepareStatement(updateSql); smt.setString(1, “小牛”); smt.setInt(2, 2); int updateResult = smt.executeUpdate(); if (updateResult > 0) { System.out.println(“修改成功”); } smt.clearParameters(); //查询数据操作 String sql = “select *from user where id =?”; smt= conn.prepareStatement(sql); smt.setInt(1,3); resultset = smt.executeQuery(); while (resultset.next()) { int uid = resultset.getInt(“id”); System.out.println(String.format(“id:%s,username:%s,password:%s,name:%s”, uid, resultset.getString(“username”), resultset.getString(“password”), resultset.getString(“name”))); } //业务逻辑代码end conn.commit(); } catch (Exception e) { System.out.println(e); conn.rollback(); } //4、释放资源 写到finally里面,防止报错不能执行资源回收 finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } if (smt != null) { try { smt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (resultset != null) { try { resultset.close(); } catch (SQLException e) { e.printStackTrace(); } } } }执行结果连接成功添加成功修改成功id:3,username:xiaoming,password:123,name:小明业务逻辑代码中如果有错误,连接对象将执行回滚操作,保证数据的一致性。 ...

December 27, 2018 · 2 min · jiezi

MyBatis 源码解析(二):SqlSession 执行流程

简介上一篇文章(MyBatis 源码解析(一):初始化和动态代理)分析了 MyBatis 解析配置文件以及 Mapper 动态代理相关的源码,这一篇接着上一篇探究 SqlSession 的执行流程,另外了解一下 MyBatis 中的缓存。openSessionMyBatis 在解析完配置文件后生成了一个 DefaultSqlSessionFactory 对象,后续执行 SQL 请求的时候都是调用其 openSession 方法获得 SqlSessison,相当于一个 SQL 会话。 SqlSession 提供了操作数据库的一些方法,如 select、update 等。先看一下 DefaultSqlSessionFactory 的 openSession 的代码: public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 从 configuration 取出配置 final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 每个 SqlSession 都有一个单独的 Executor 对象 final Executor executor = configuration.newExecutor(tx, execType, autoCommit); // 返回 DefaultSqlSession 对象 return new DefaultSqlSession(configuration, executor); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException(“Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }主要代码在 openSessionFromDataSource,首先是从 Configuration 中取出相关的配置,生成 Transaction,接着又创建了一个 Executor,最后返回了 DefaultSqlSession 对象。这里的 Executor 是什么呢?它其实是一个执行器,SqlSession 的操作会交给 Executor 去执行。MyBatis 的 Executor 常用的有以下几种:SimpleExecutor: 默认的 Executor,每个 SQL 执行时都会创建新的 StatementResuseExecutor: 相同的 SQL 会复用 StatementBatchExecutor: 用于批处理的 ExecutorCachingExecutor: 可缓存数据的 Executor,用代理模式包装了其它类型的 Executor了解了 Executor 的类型后,看一下 configuration.newExecutor(tx, execType, autoCommit) 的代码: public Executor newExecutor(Transaction transaction, ExecutorType executorType) { // 默认是 SimpleExecutor executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } // 默认启动缓存 if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }MyBatis 默认启用一级缓存,即同一个 SqlSession 会共用同一个缓存,上面代码最终返回的是 CachingExecutor。getMapper在创建了 SqlSession 之后,下一步是生成 Mapper 接口的代理类,代码如下: public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }可以看出是从 configuration 中取得 Mapper,最终调用了 MapperProxyFactory 的 newInstance。MapperProxyFactory 在上一篇文章已经分析过,它是为了给 Mapper 接口生成代理类,其中关键的拦截逻辑在 MapperProxy 中,下面是其 invoke 方法: @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 过滤一些不需要被代理的方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 从缓存中获取 MapperMethod 然后调用 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }MapperProxy 中调用了 MapperMethod 的 execute,下面是部分代码:public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; …}可以看出,最终调用了 SqlSession 的对应方法,也就是 DefaultSqlSession 中的方法。select先看一下 DefaultSqlSession 中 select 的代码: public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { try { MappedStatement ms = configuration.getMappedStatement(statement); executor.query(ms, wrapCollection(parameter), rowBounds, handler); } catch (Exception e) { throw ExceptionFactory.wrapException(“Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }select 中调用了 executor 的 query,上面提到,默认的 Executor 是 CachingExecutor,看其中的代码: @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // 获取缓存的key CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 获取缓存 Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings(“unchecked”) List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 调用代理对象的缓存 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }首先检查缓存中是否有数据,如果没有再调用代理对象的 query,默认是 SimpleExecutor。Executor 是一个接口,下面有个实现类是 BaseExecutor,其中实现了其它 Executor 通用的一些逻辑,包括 doQuery 以及 doUpdate 等,其中封装了 JDBC 的相关操作。updateupdate 的执行与 select 类似, 都是从 CachingExecutor 开始,看代码: @Override public int update(MappedStatement ms, Object parameterObject) throws SQLException { // 检查是否需要刷新缓存 flushCacheIfRequired(ms); // 调用代理类的 update return delegate.update(ms, parameterObject); }update 会使得缓存的失效,所以第一步是检查是否需要刷新缓存,接下来再交给代理类去执行真正的数据库更新操作。总结本文主要分析了 SqlSession 的执行流程,结合上一篇文章基本了解了 MyBatis 的运行原理。对于 MyBatis 的源码,还有很多地方没有深入,例如SQL 解析时参数的处理、一级缓存与二级缓存的处理逻辑等,不过在熟悉 MyBatis 的整体框架之后,这些细节可以在需要用到的时候继续学习。 ...

December 19, 2018 · 3 min · jiezi

Spring Boot 集成 MyBatis和 SQL Server实践

文章共 509字,阅读大约需要 2分钟 !概 述Spring Boot工程集成 MyBatis来实现 MySQL访问的示例我们见过很多,而最近用到了微软的 SQL Server数据库,于是本文则给出一个完整的 Spring Boot + MyBatis + SQL Server 的工程示例。注: 本文首发于 My Personal Blog:CodeSheep·程序羊,欢迎光临 小站工程搭建新建 Spring Boot工程pom.xml 中添加 MyBatis和 SQL Server相关的依赖<!–for mybatis–><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version></dependency><!–for SqlServer–><dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>sqljdbc4</artifactId> <version>4.0</version></dependency>配置 application.properties这里同样主要是对于 MyBatis 和 SQL Server连接相关的配置server.port=89# mybatis 配置mybatis.type-aliases-package=cn.codesheep.springbt_mybatis_sqlserver.entitymybatis.mapper-locations=classpath:mapper/*.xmlmybatis.configuration.map-underscore-to-camel-case=true## ————————————————-## SqlServer 配置spring.datasource.url=jdbc:sqlserver://xxxx:1433;databasename=MingLispring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriverspring.datasource.username=xxxxspring.datasource.password=xxxx建立 SQL Server数据表和实体类首先在 SQL Server数据库中新建数据表 user_test作为测试用表DROP TABLE [demo].[user_test]GOCREATE TABLE [dbo].[user_test] ([user_id] int NOT NULL ,[user_name] varchar(50) NOT NULL ,[sex] tinyint NOT NULL ,[created_time] varchar(50) NOT NULL )GO然后在我们的工程中对应建立的 User实体类其字段和实际数据表的字段一一对应public class User { private Long userId; private String userName; private Boolean sex; private String createdTime; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Boolean getSex() { return sex; } public void setSex(Boolean sex) { this.sex = sex; } public String getCreatedTime() { return createdTime; } public void setCreatedTime(String createdTime) { this.createdTime = createdTime; }}Mybatis Mapper映射配置MyBatis映射配置的 XML文件如下:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace=“cn.codesheep.springbt_mybatis_sqlserver.mapper.UserMapper”> <resultMap id=“userMap” type=“cn.codesheep.springbt_mybatis_sqlserver.entity.User”> <id property=“userId” column=“user_id” javaType=“java.lang.Long”></id> <result property=“userName” column=“user_name” javaType=“java.lang.String”></result> <result property=“sex” column=“sex” javaType=“java.lang.Boolean”></result> <result property=“createdTime” column=“created_time” javaType=“java.lang.String”></result> </resultMap> <select id=“getAllUsers” resultMap=“userMap”> select * from user_test </select> <insert id=“addUser” parameterType=“cn.codesheep.springbt_mybatis_sqlserver.entity.User”> insert into user_test ( user_id, user_name, sex, created_time ) values ( #{userId}, #{userName}, #{sex}, #{createdTime} ) </insert> <delete id=“deleteUser” parameterType=“cn.codesheep.springbt_mybatis_sqlserver.entity.User”> delete from user_test where user_name = #{userName} </delete></mapper>与此同时,这里也给出对应 XML的 DAO接口public interface UserMapper { List<User> getAllUsers(); int addUser( User user ); int deleteUser( User user );}为了试验起见,这里给出了 增 / 删 / 查 三个数据库操作动作。编写 Service 和测试Controller上面这些准备工作完成之后,接下来编写数据库 CRUD的 Service类@Service@Primarypublic class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public List<User> getAllUsers() { return userMapper.getAllUsers(); } @Override public int addUser(User user) { SimpleDateFormat form = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); user.setCreatedTime( form.format(new Date()) ); return userMapper.addUser( user ); } @Override public int deleteUser(User user) { return userMapper.deleteUser( user ); }}这里的 Service功能同样主要关于数据表的 增 / 删 / 查 三个数据库操作动作。对照着上面的Service,我们编写一个对应接口测试的Controller@RestControllerpublic class UserController { @Autowired private IUserService userService; @RequestMapping(value = “/getAllUser”, method = RequestMethod.GET) public List<User> getAllUser() { return userService.getAllUsers(); } @RequestMapping(value = “/addUser”, method = RequestMethod.POST) public int addUser( @RequestBody User user ) { return userService.addUser( user ); } @RequestMapping(value = “/deleteUser”, method = RequestMethod.POST) public int deleteUser( @RequestBody User user ) { return userService.deleteUser( user ); }}实验测试插入数据依次用 POSTMAN通过 Post /addUser接口插入三条数据:{“userId”:1,“userName”:“刘能”,“sex”:true}{“userId”:2,“userName”:“赵四”,“sex”:false}{“userId”:3,“userName”:“王大拿”,“sex”:true}插入完成后去 SQL Server数据库里看一下数据插入情况如下:查询数据调用 Get /getAllUser接口,获取刚插入的几条数据删除数据调用 Post /deleteUser 接口,可以通过用户名来删除对应的用户后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊我的半年技术博客之路 ...

December 18, 2018 · 2 min · jiezi

MyBatis 源码阅读之 databaseId

MyBatis 源码阅读之 databaseIdMyBatis 的配置文件所有配置会被 org.apache.ibatis.builder.xml.XMLConfigBuilder 类读取,我们可以通过此类来了解各个配置是如何运作的。而 MyBatis 的映射文件配置会被 org.apache.ibatis.builder.xml.XMLMapperBuilder 类读取。我们可以通过此类来了解映射文件的配置时如何被解析的。databaseIddatabaseId 是用于项目中存在多种数据库 SQL 时区分同一条 SQL 对应的数据库。可以这样认为,在 Mybatis 中 SQL 的 id 和 databaseId 组合才是一条 SQL 的唯一标识。实际上 MyBatis 只会选择性加载指定 databaseId 的 SQL ,还有一些没有指定 databaseId 的 SQL。这里说的有点不是很准确,我们来慢慢分析便可以知晓。databaseId 的配置MyBatis 配置文件中 databaseId 的配置如下:<!– mybatis-config.xml –><databaseIdProvider type=“DB_VENDOR”> <property name=“SQL Server” value=“sqlserver”/> <property name=“DB2” value=“db2”/> <property name=“Oracle” value=“oracle” /></databaseIdProvider>读取的代码如下:private void databaseIdProviderElement(XNode context) throws Exception { DatabaseIdProvider databaseIdProvider = null; if (context != null) { String type = context.getStringAttribute(“type”); // 保持向后兼容 if (“VENDOR”.equals(type)) { type = “DB_VENDOR”; } // 属性设置 Properties properties = context.getChildrenAsProperties(); // 找到 type 配置对应的类 databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); databaseIdProvider.setProperties(properties); } Environment environment = configuration.getEnvironment(); if (environment != null && databaseIdProvider != null) { // 通过数据源确定使用的 databaseId ,之后 SQL 也只会加载这种 databaseId 的 SQL ,其他类型都会被忽略 String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); configuration.setDatabaseId(databaseId); }}这里的代码逻辑比较简单:读取 databaseIdProvider 节点的 type 值与子节点属性值根据 type 值找到与之匹配的 DatabaseIdProvider 子类,创建相应的实例,将子节点属性设置到实例中调用 DatabaseIdProvider 实例的 getDatabaseId() 方法获取值设置到 Configuration 实例中注:type 为 DB_VENDOR 表示使用 org.apache.ibatis.mapping.VendorDatabaseIdProvider 作为 DatabaseIdProvider 的实现类。这一点可以在 org.apache.ibatis.session.Configuration 的构造方法中找到证据。如果发现自己的 databaseId 没被正确识别,可以查看 getDatabaseId() 方法是否和预期一致。databaseId 的使用databaseId 在映射文件里要和上一节的配置的属性 value 值对应,如下:<!– mybatis-mapper.xml –><!– 指定 sql 和 select 节点的内容只适用于 oracle 数据库,那么使用 Oracle 的数据库时便会加载这些节点 –><sql id=“column” databaseId=“oracle”> <!– … –></sql><select id=“selectOne” databaseId=“oracle”> <!– … –></select>读取的代码在这,这只是 <sql> 节点加载的代码:private void sqlElement(List<XNode> list) throws Exception { if (configuration.getDatabaseId() != null) { // 加载 DataSource 对应的 databaseId 的 SQL 节点 sqlElement(list, configuration.getDatabaseId()); } // 记载 databaseId 为空的 SQL 节点 sqlElement(list, null);}private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception { for (XNode context : list) { String databaseId = context.getStringAttribute(“databaseId”); String id = context.getStringAttribute(“id”); id = builderAssistant.applyCurrentNamespace(id, false); if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { sqlFragments.put(id, context); } }}private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { if (requiredDatabaseId != null) { if (!requiredDatabaseId.equals(databaseId)) { // 两个 databaseId 一致才会返回 true,此处不一致 return false; } } else { // 一个为空,一个不为空,也不一致 if (databaseId != null) { return false; } // 如果先前已经加载过节点,则不再加载 // 是否视为同一个节点是由 id 决定 // 但 id 相同,databaseId 不同 mybatis也可以加载,所以有些地方说,id+databaseId 确定唯一一条 SQL if (this.sqlFragments.containsKey(id)) { XNode context = this.sqlFragments.get(id); if (context.getStringAttribute(“databaseId”) != null) { return false; } } } return true;}代码上已经有了详细的注释,这里就简单说一下。sqlElement() 方法会被调用两次,第一次用于处理 databaseId 与全局 Configuration 实例的 databaseId 一致的节点;另一次用于处理节点的 databaseId 为 null 的情况,针对同一个 id ,优先选择存在 databaseId 并且与数据源的一致。同样的,<select> 之类的节点解析代码也是类似,不过它们的解析代码在 org.apache.ibatis.builder.xml.XMLStatementBuilder 中。 ...

December 14, 2018 · 2 min · jiezi

新手也能实现,基于SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置

在上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》中,带着大家整合了 SpringBoot 和 Mybatis ,我们在当时使用的时单数据源的情况,这种情况下 Spring Boot的配置非常简单,只需要在 application.properties 文件中配置数据库的相关连接参数即可。但是往往随着业务量发展,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源。下面基于 SpringBoot+Mybatis ,带着大家看一下 SpringBoot 中如何配置多数据源。这篇文章所涉及的代码其实是基于上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》 的项目写的,但是为了考虑部分读者没有读过上一篇文章,所以我还是会一步一步带大家走完每一步,力争新手也能在看完之后独立实践。目录:<!– MarkdownTOC –>一 开发前的准备1.1 环境参数1.2 创建工程1.3 创建两个数据库和 user 用户表、money工资详情表1.4 配置 pom 文件中的相关依赖1.5 配置 application.properties1.6 创建用户类 Bean和工资详情类 Bean二 数据源配置三 Dao 层开发和 Service 层开发3.1 Dao 层3.2 Service 层四 Controller层五 启动类<!– /MarkdownTOC –>一 开发前的准备1.1 环境参数开发工具:IDEA基础工具:Maven+JDK8所用技术:SpringBoot+Mybatis数据库:MySQLSpringBoot版本:2.1.0. SpringBoot2.0之后会有一些小坑,这篇文章会给你介绍到。注意版本不一致导致的一些小问题。1.2 创建工程创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考下面这篇文章:https://blog.csdn.net/qq_34337272/article/details/79563606本项目结构:1.3 创建两个数据库和 user 用户表、money工资详情表我们一共创建的两个数据库,然后分别在这两个数据库中创建了 user 用户表、money工资详情表。我们的用户表很简单,只有 4 个字段:用户 id、姓名、年龄、余额。如下图所示:添加了“余额money”字段是为了给大家简单的演示一下事务管理的方式。我们的工资详情表也很简单,也只有 4 个字段: id、基本工资、奖金和惩罚金。如下图所示:建表语句:用户表:CREATE TABLE user ( id int(13) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, name varchar(33) DEFAULT NULL COMMENT ‘姓名’, age int(3) DEFAULT NULL COMMENT ‘年龄’, money double DEFAULT NULL COMMENT ‘账户余额’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8工资详情表:CREATE TABLE money ( id int(33) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, basic int(33) DEFAULT NULL COMMENT ‘基本工资’, reward int(33) DEFAULT NULL COMMENT ‘奖金’, punishment int(33) DEFAULT NULL COMMENT ‘惩罚金’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf81.4 配置 pom 文件中的相关依赖由于要整合 springboot 和 mybatis 所以加入了artifactId 为 mybatis-spring-boot-starter 的依赖,由于使用了Mysql 数据库 所以加入了artifactId 为 mysql-connector-java 的依赖。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>1.5 配置 application.properties配置两个数据源:数据库1和数据库2!注意事项:在1.0 配置数据源的过程中主要是写成:spring.datasource.url 和spring.datasource.driverClassName。而在2.0升级之后需要变更成:spring.datasource.jdbc-url和spring.datasource.driver-class-name!不然在连接数据库时可能会报下面的错误:### Error querying database. Cause: java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.另外在在2.0.2+版本后需要在datasource后面加上hikari,如果你没有加的话,同样可能会报错。server.port=8335# 配置第一个数据源spring.datasource.hikari.db1.jdbc-url=jdbc:mysql://127.0.0.1:3306/erp?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8spring.datasource.hikari.db1.username=rootspring.datasource.hikari.db1.password=153963spring.datasource.hikari.db1.driver-class-name=com.mysql.cj.jdbc.Driver# 配置第二个数据源spring.datasource.hikari.db2.jdbc-url=jdbc:mysql://127.0.0.1:3306/erp2?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8spring.datasource.hikari.db2.username=rootspring.datasource.hikari.db2.password=153963spring.datasource.hikari.db2.driver-class-name=com.mysql.cj.jdbc.Driver1.6 创建用户类 Bean和工资详情类 BeanUser.javapublic class User { private int id; private String name; private int age; private double money; … 此处省略getter、setter以及 toString方法}Money.javapublic class Money { private int basic; private int reward; private int punishment; … 此处省略getter、setter以及 toString方法}二 数据源配置通过 Java 类来实现对两个数据源的配置,这一部分是最关键的部分了,这里主要提一下下面这几点:@MapperScan 注解中我们声明了使用数据库1的dao类所在的位置,还声明了 SqlSessionTemplate 。SqlSessionTemplate是MyBatis-Spring的核心。这个类负责管理MyBatis的SqlSession,调用MyBatis的SQL方法,翻译异常。SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用。由于我使用的是全注解的方式开发,所以下面这条找并且解析 mapper.xml 配置语句被我注释掉了bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:mybatis/mapper/db2/*.xml”));比如我们要声明使用数据1,直接在 dao 层的类上加上这样一个注释即可:@Qualifier(“db1SqlSessionTemplate”)我们在数据库1配置类的每个方法前加上了 @Primary 注解来声明这个数据库时默认数据库,不然可能会报错。DataSource1Config.java@Configuration@MapperScan(basePackages = “top.snailclimb.db1.dao”, sqlSessionTemplateRef = “db1SqlSessionTemplate”)public class DataSource1Config { /** * 生成数据源. @Primary 注解声明为默认数据源 / @Bean(name = “db1DataSource”) @ConfigurationProperties(prefix = “spring.datasource.hikari.db1”) @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } /* * 创建 SqlSessionFactory / @Bean(name = “db1SqlSessionFactory”) @Primary public SqlSessionFactory testSqlSessionFactory(@Qualifier(“db1DataSource”) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); // bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:mybatis/mapper/db1/.xml”)); return bean.getObject(); } /** * 配置事务管理 / @Bean(name = “db1TransactionManager”) @Primary public DataSourceTransactionManager testTransactionManager(@Qualifier(“db1DataSource”) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = “db1SqlSessionTemplate”) @Primary public SqlSessionTemplate testSqlSessionTemplate(@Qualifier(“db1SqlSessionFactory”) SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }}DataSource2Config.java@Configuration@MapperScan(basePackages = “top.snailclimb.db2.dao”, sqlSessionTemplateRef = “db2SqlSessionTemplate”)public class DataSource2Config { @Bean(name = “db2DataSource”) @ConfigurationProperties(prefix = “spring.datasource.hikari.db2”) public DataSource testDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = “db2SqlSessionFactory”) public SqlSessionFactory testSqlSessionFactory(@Qualifier(“db2DataSource”) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“classpath:mybatis/mapper/db2/.xml”)); return bean.getObject(); } @Bean(name = “db2TransactionManager”) public DataSourceTransactionManager testTransactionManager(@Qualifier(“db2DataSource”) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = “db2SqlSessionTemplate”) public SqlSessionTemplate testSqlSessionTemplate(@Qualifier(“db2SqlSessionFactory”) SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); }}三 Dao 层开发和 Service 层开发新建两个不同的包存放两个不同数据库的 dao 和 service。3.1 Dao 层对于两个数据库,我们只是简单的测试一个查询这个操作。在上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》中,我带着大家使用注解实现了数据库基本的增删改查操作。UserDao.java@Qualifier(“db1SqlSessionTemplate”)public interface UserDao { /** * 通过名字查询用户信息 / @Select(“SELECT * FROM user WHERE name = #{name}”) User findUserByName(String name);}MoneyDao.java@Qualifier(“db2SqlSessionTemplate”)public interface MoneyDao { /* * 通过id 查看工资详情 / @Select(“SELECT * FROM money WHERE id = #{id}”) Money findMoneyById(@Param(“id”) int id);}3.2 Service 层Service 层很简单,没有复杂的业务逻辑。UserService.java@Servicepublic class UserService { @Autowired private UserDao userDao; /* * 根据名字查找用户 / public User selectUserByName(String name) { return userDao.findUserByName(name); }}MoneyService.java@Servicepublic class MoneyService { @Autowired private MoneyDao moneyDao; /* * 根据名字查找用户 */ public Money selectMoneyById(int id) { return moneyDao.findMoneyById(id); }}四 Controller层Controller 层也非常简单。UserController.java@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @RequestMapping("/query") public User testQuery() { return userService.selectUserByName(“Daisy”); }}MoneyController.java@RestController@RequestMapping("/money")public class MoneyController { @Autowired private MoneyService moneyService; @RequestMapping("/query") public Money testQuery() { return moneyService.selectMoneyById(1); }}五 启动类//此注解表示SpringBoot启动类@SpringBootApplicationpublic class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); }}这样基于SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置就已经完成了, 两个数据库都可以被访问了。 ...

December 3, 2018 · 3 min · jiezi

基于 SpringBoot2.0+优雅整合 SpringBoot+Mybatis

Github 地址:https://github.com/Snailclimb/springboot-integration-examples(SpringBoot和其他常用技术的整合,可能是你遇到的讲解最详细的学习案例,力争新手也能看懂并且能够在看完之后独立实践。基于最新的 SpringBoot2.0+,是你学习SpringBoot 的最佳指南。) ,欢迎各位 Star。SpringBoot 整合 Mybatis 有两种常用的方式,一种就是我们常见的 xml 的方式 ,还有一种是全注解的方式。我觉得这两者没有谁比谁好,在 SQL 语句不太长的情况下,我觉得全注解的方式一定是比较清晰简洁的。但是,复杂的 SQL 确实不太适合和代码写在一起。下面就开始吧!目录:一 开发前的准备1.1 环境参数1.2 创建工程1.3 创建数据库和 user 用户表1.4 配置 pom 文件中的相关依赖1.5 配置 application.properties1.6 创建用户类 Bean二 全注解的方式2.1 Dao 层开发2.2 service 层2.3 Controller 层2.4 启动类2.5 简单测试三 xml 的方式3.1 Dao 层的改动3.2 配置文件的改动一 开发前的准备1.1 环境参数开发工具:IDEA基础工具:Maven+JDK8所用技术:SpringBoot+Mybatis数据库:MySQLSpringBoot版本:2.1.01.2 创建工程创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考下面这篇文章:https://blog.csdn.net/qq_34337272/article/details/795636061.3 创建数据库和 user 用户表我们的数据库很简单,只有 4 个字段:用户 id、姓名、年龄、余额,如下图所示:添加了“余额money”字段是为了给大家简单的演示一下事务管理的方式。建表语句:CREATE TABLE user ( id int(13) NOT NULL AUTO_INCREMENT COMMENT ‘主键’, name varchar(33) DEFAULT NULL COMMENT ‘姓名’, age int(3) DEFAULT NULL COMMENT ‘年龄’, money double DEFAULT NULL COMMENT ‘账户余额’, PRIMARY KEY (id)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf81.4 配置 pom 文件中的相关依赖由于要整合 springboot 和 mybatis 所以加入了artifactId 为 mybatis-spring-boot-starter 的依赖,由于使用了Mysql 数据库 所以加入了artifactId 为 mysql-connector-java 的依赖。 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>1.5 配置 application.properties由于我使用的是比较新的Mysql连接驱动,所以配置文件可能和之前有一点不同。server.port=8333spring.datasource.url=jdbc:mysql://127.0.0.1:3306/erp?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8spring.datasource.username=rootspring.datasource.password=153963spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver注意:我们使用的 mysql-connector-java 8+ ,JDBC 连接到mysql-connector-java 6+以上的需要指定时区 serverTimezone=GMT%2B8。另外我们之前使用配置 Mysql数据连接是一般是这样指定driver-class-name=com.mysql.jdbc.Driver,但是现在不可以必须为 否则控制台下面的异常:Loading class com.mysql.jdbc.Driver'. This is deprecated. The new driver class is com.mysql.cj.jdbc.Driver’. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.上面异常的意思是:com.mysql.jdbc.Driver 被弃用了。新的驱动类是 com.mysql.cj.jdbc.Driver。驱动程序通过SPI自动注册,手动加载类通常是不必要。如果你非要写把com.mysql.jdbc.Driver 改为com.mysql.cj.jdbc.Driver 即可。1.6 创建用户类 Beanpublic class User { private int id; private String name; private int age; private double money; … 此处省略getter、setter以及 toString方法}二 全注解的方式先来看一下 全注解的方式,这种方式和后面提到的 xml 的方式的区别仅仅在于 一个将 sql 语句写在 java 代码中,一个写在 xml 配置文件中。全注方式解转换成 xml 方式仅需做一点点改变即可,我在后面会提到。项目结构:2.1 Dao 层开发UserDao.java@Mapperpublic interface UserDao { /** * 通过名字查询用户信息 / @Select(“SELECT * FROM user WHERE name = #{name}”) User findUserByName(@Param(“name”) String name); /* * 查询所有用户信息 / @Select(“SELECT * FROM user”) List<User> findAllUser(); /* * 插入用户信息 / @Insert(“INSERT INTO user(name, age,money) VALUES(#{name}, #{age}, #{money})”) void insertUser(@Param(“name”) String name, @Param(“age”) Integer age, @Param(“money”) Double money); /* * 根据 id 更新用户信息 / @Update(“UPDATE user SET name = #{name},age = #{age},money= #{money} WHERE id = #{id}”) void updateUser(@Param(“name”) String name, @Param(“age”) Integer age, @Param(“money”) Double money, @Param(“id”) int id); /* * 根据 id 删除用户信息 / @Delete(“DELETE from user WHERE id = #{id}”) void deleteUser(@Param(“id”) int id);}2.2 service 层@Servicepublic class UserService { @Autowired private UserDao userDao; /* * 根据名字查找用户 / public User selectUserByName(String name) { return userDao.findUserByName(name); } /* * 查找所有用户 / public List<User> selectAllUser() { return userDao.findAllUser(); } /* * 插入两个用户 / public void insertService() { userDao.insertUser(“SnailClimb”, 22, 3000.0); userDao.insertUser(“Daisy”, 19, 3000.0); } /* * 根据id 删除用户 / public void deleteService(int id) { userDao.deleteUser(id); } /* * 模拟事务。由于加上了 @Transactional注解,如果转账中途出了意外 SnailClimb 和 Daisy 的钱都不会改变。 / @Transactional public void changemoney() { userDao.updateUser(“SnailClimb”, 22, 2000.0, 3); // 模拟转账过程中可能遇到的意外状况 int temp = 1 / 0; userDao.updateUser(“Daisy”, 19, 4000.0, 4); }}2.3 Controller 层@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @RequestMapping("/query") public User testQuery() { return userService.selectUserByName(“Daisy”); } @RequestMapping("/insert") public List<User> testInsert() { userService.insertService(); return userService.selectAllUser(); } @RequestMapping("/changemoney") public List<User> testchangemoney() { userService.changemoney(); return userService.selectAllUser(); } @RequestMapping("/delete") public String testDelete() { userService.deleteService(3); return “OK”; }}2.4 启动类//此注解表示SpringBoot启动类@SpringBootApplication// 此注解表示动态扫描DAO接口所在包,实际上不加下面这条语句也可以找到@MapperScan(“top.snailclimb.dao”)public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); }}2.5 简单测试上述代码经过测试都没问题,这里贴一下根据姓名查询的测试的结果。三 xml 的方式项目结构:相比于注解的方式主要有以下几点改变,非常容易实现。3.1 Dao 层的改动我这里只演示一个根据姓名找人的方法。UserDao.java@Mapperpublic interface UserDao { /* * 通过名字查询用户信息 / User findUserByName(String name);}UserMapper.xml<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“top.snailclimb.dao.UserDao”> <select id=“findUserByName” parameterType=“String” resultType=“top.snailclimb.bean.User”> SELECT * FROM user WHERE name = #{name} </select></mapper>3.2 配置文件的改动配置文件中加入下面这句话:mybatis.mapper-locations=classpath:mapper/.xmlThoughtWorks准入职Java工程师。专注Java知识分享!开源 Java 学习指南——JavaGuide(12k+ Star)的作者。公众号多篇文章被各大技术社区转载。公众号后台回复关键字“1”可以领取一份我精选的Java资源哦! ...

November 30, 2018 · 3 min · jiezi

Spring Boot (八)MyBatis + Docker + MongoDB 4.x

一、MongoDB简介1.1 MongoDB介绍MongoDB是一个强大、灵活,且易于扩展的通用型数据库。MongoDB是C++编写的文档型数据库,有着丰富的关系型数据库的功能,并在4.0之后添加了事务支持。随着存储数据量不断的增加,开发者面临一个困难:如何扩展数据库?而扩展数据库分为横向扩展和纵向扩展,纵向扩展就是使用计算能力更强大的机器,它的缺点就是:机器性能的提升有物理极限的制约,而且大型机通常都是非常昂贵的,而MongoDB的设计采用的是横向扩展的模式,面向文档的数据模型使它很容易的在多台服务器上进行数据分割。MongoDB能自动处理夸集群的数据和负载,自动重新分配文档,这样开发者就能集中精力编写应用程序,而不需要考虑如果扩展的问题。<!–more–>1.2 MongoDB安装MongoDB的安装简单来说分为两种:官网下载对应物理机的安装包,直接安装使用Docker镜像,安装到Docker上推荐使用第二种,直接使用MongoDB镜像安装到Docker上,这样带来的好处是:安装简单、方便,且快速更容易进行数据迁移,使用Docker可以很容易的导入和导出整个MongoDB到任何地方所以本文将重点介绍MongoDB在Docker上的安装和使用。如果想要直接在物理机安装Docker,可以查看我之前的一篇文章《MongoDB基础介绍安装与使用》:https://www.cnblogs.com/vipst…1.3 Docker上安装MongoDB在Docker上安装软件一般需要两步:pull(下载)对应的镜像(相对于下载软件)装载镜像到容器(相对于安装软件)1.3.1 下载镜像下载镜像,需要到镜像市场:https://hub.docker.com/,如要要搜索的软件“mongo”,选择官方镜像“Official”,点击详情,获取相应的下载方法,我们得到下载MongoDB的命令如下:docker pull mongo:latest1.3.2 装载镜像到容器使用命令:docker run –name mongodb1 -p 27018:27017 -d mongo:latest–name 指定容器名称-p 27018:27017 映射本地端口27018到容器端口27017-d 后台运行mongo:latest 镜像名称和标签使用“docker images”查看镜像名称和标签,如下图:容器装载成功之后,就可以使用Robo 3T客户端进行连接了,是不需要输入用户名和密码的,如下图:表示已经连接成功了。Robo 3T为免费的连接MongoDB的数据库工具,可以去官网下载:https://robomongo.org/download1.3.3 开启身份认证如果是生成环境,没有用户名和密码的MongoDB是非常不安全的,因此我们需要开启身份认证。Setp1:装载容器我们还是用之前下载的镜像,重新装载一个容器实例,命令如下:docker run –name mongodb2 -p 27019:27017 -d mongo:latest –auth其中“–auth”就是开启身份认证。装载完身份认证成功容器之后,我们需要进入容器内部,给MongoDB设置用户名和密码。Setp2:进入容器内部docker exec -it <容器id/名称> bashSetp3:进入mongo命令行模式mongo adminSetp4:创建用户db.createUser({ user: ‘admin’, pwd: ‘admin’, roles: [ { role: “userAdminAnyDatabase”, db: “admin” } ] });创建的用户名为“admin”密码为“admin”,指定的数据库为“admin”。这个时候,我们使用Robo 3T 输入相应的信息进行连接,如下图:表示已经连接成功了。1.3.4 创建数据库设置用户上面我们用“admin”账户使用了系统数据库“admin”,通常在生成环境我们不会直接使用系统的数据库,这个时候我们需要自己创建自己的数据库分配相应的用户。Setp1:首先需要进入容器docker exec -it <容器id/名称> bashSetp2:创建数据库use testdb如果没有testdb就会自动创建数据库。Setp3:创建用户分配数据库db.createUser({ user: ‘admin’, pwd: ‘admin’, roles: [ { role: “readWrite”, db: “testdb” } ] });其中 role: “readWrite” 表式给用户赋值操作和读取的权限,当然增加索引、删除表什么的也是完全没有问题的。到目前为止我们就可以使用admin/admin操作testdb数据库了。1.3.5 其他Docker命令删除容器:docker container rm <容器id/名称>停止容器:docker stop <容器id/名称>启动容器:docker start <容器id/名称>查看运行是容器:docker ps查询所有的容器:docker ps -a二、MyBatis集成MongoDBSpring Boot项目集成MyBatis前两篇文章已经做了详细的介绍,这里就不做过多的介绍,本文重点来介绍MongoDB的集成。Setp1:添加依赖在pom.xml添加如下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId></dependency>Setp2:配置MongoDB连接在application.properties添加如下配置:spring.data.mongodb.uri=mongodb://username:pwd@172.16.10.79:27019/testdbSetp3:创建实体类import java.io.Serializable;public class User implements Serializable { private Long id; private String name; private int age; private String pwd; //…略set、get}Setp4:创建Dao类import com.hello.springboot.entity.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.data.mongodb.core.query.Update;import org.springframework.stereotype.Component;import java.util.List;@Componentpublic class UserDao { @Autowired private MongoTemplate mongoTemplate; /** * 添加用户 * @param user User Object / public void insert(User user) { mongoTemplate.save(user); } /* * 查询所有用户 * @return / public List<User> findAll() { return mongoTemplate.findAll(User.class); } /* * 根据id 查询 * @param id * @return / public User findById(Long id) { Query query = new Query(Criteria.where(“id”).is(id)); User user = mongoTemplate.findOne(query, User.class); return user; } /* * 更新 * @param user / public void updateUser(User user) { Query query = new Query(Criteria.where(“id”).is(user.getId())); Update update = new Update().set(“name”, user.getName()).set(“pwd”, user.getPwd()); mongoTemplate.updateFirst(query, update, User.class); } /* * 删除对象 * @param id / public void deleteUserById(Long id) { Query query = new Query(Criteria.where(“id”).is(id)); mongoTemplate.remove(query, User.class); }}Setp4:创建Controllerimport com.hello.springboot.dao.IndexBuilderDao;import com.hello.springboot.dao.UserDao;import com.hello.springboot.entity.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.ModelAndView;@RestController@RequestMapping("/")public class UserController { @Autowired private UserDao userDao; @RequestMapping("/") public ModelAndView index() { User user = new User(); user.setId(new Long(1)); user.setAge(18); user.setName(“Adam”); user.setPwd(“123456”); userDao.insert(user); ModelAndView modelAndView = new ModelAndView("/index"); modelAndView.addObject(“count”, userDao.findAll().size()); return modelAndView; }}Setp5:创建页面代码<html><head> <title>王磊的博客</title></head><body>Hello ${count}</body></html>到此为止已经完成了MongoDB的集成,启动项目,输入“http://localhost:8080/”去数据库查看插入的数据吧。正常插入数据库如下图:三、MongoDB主键自增细心的用户可能会发现,虽然MongoDB已经集成完了,但插入数据库的时候user的id是手动set的值,接下来我们来看怎么实现MongoDB中的id自增。3.1 实现思路MongoDB 实现id自增和Spring Boot JPA类似,是在数据库创建一张表,来记录表的“自增id”,只需要保证每次都增加的id和返回的id的原子性,就能保证id实现“自增”的功能。3.2 实现方案有了思路之后,接下来我们来看具体的实现方案。3.2.1 创建实体类import org.springframework.data.annotation.Id;import org.springframework.data.mongodb.core.mapping.Document;@Document(collection = “IndexBuilder”)public class IndexBuilder { @Id private String id; private Long seq; //..省略get、set方法}其中collection = “IndexBuilder"是指数据库的集合名称,对应关系型数据库的表名。3.2.2 创建Dao类import com.hello.springboot.entity.IndexBuilder;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoOperations;import org.springframework.data.mongodb.core.query.Update;import org.springframework.stereotype.Component;import static org.springframework.data.mongodb.core.FindAndModifyOptions.options;import static org.springframework.data.mongodb.core.query.Criteria.where;import static org.springframework.data.mongodb.core.query.Query.query;@Componentpublic class IndexBuilderDao { @Autowired private MongoOperations mongo; /* * 查询下一个id * @param collectionName 集合名 * @return */ public Long getNextSequence(String collectionName) { IndexBuilder counter = mongo.findAndModify( query(where("_id”).is(collectionName)), new Update().inc(“seq”, 1), options().returnNew(true).upsert(true), IndexBuilder.class); return counter.getSeq(); }}3.2.3 使用“自增”的idUser user = new User();user.setId(indexBuilderDao.getNextSequence(“user”));//…其他设置核心代码:indexBuilderDao.getNextSequence(“user”) 使用“自增”的id,实现id自增。到此为止,已经完成了MongoDB的自增功能,如果使用正常,数据库应该是这样的:数据库的IndexBuilder就是用来记录每个集合的“自增id”的。MongoDB集成的源码:https://github.com/vipstone/s… ...

October 8, 2018 · 2 min · jiezi

MyBatis Generator 自定义生成注释

最近做项目,ORM 使用的是 MyBatis,为了偷懒,我自然而然的想到了使用 MyBatis Generator(MBG)来生成数据库表对应的实体代码和 Mapper 代码。于是做了如下的配置(对 MBG 配置不熟悉的同学可以参考 Mybatis Generator最完整配置详解):<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!– 指定数据库驱动的jdbc驱动jar包的位置 –> <classPathEntry location=”./mysql-connector-java-5.1.40.jar" /> <context id=“mysql” defaultModelType=“hierarchical” targetRuntime=“MyBatis3Simple” > <!– 生成的 Java 文件的编码 –> <property name=“javaFileEncoding” value=“UTF-8”/> <!– 格式化 Java 代码 –> <property name=“javaFormatter” value=“org.mybatis.generator.api.dom.DefaultJavaFormatter”/> <!– 格式化 XML 代码 –> <property name=“xmlFormatter” value=“org.mybatis.generator.api.dom.DefaultXmlFormatter”/> <!– 配置数据库连接 –> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://localhost:3306/test?characterEncoding=utf-8” userId=“root” password=“123456”> </jdbcConnection> <!– 生成实体的位置 –> <javaModelGenerator targetPackage=“me.mizhoux.model” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> </javaModelGenerator> <!– 生成 Mapper 接口的位置 –> <sqlMapGenerator targetPackage=“me.mizhoux.mapper” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> </sqlMapGenerator> <!– 生成 Mapper XML 的位置 –> <javaClientGenerator targetPackage=“me.mizhoux.mapper” type=“XMLMAPPER” targetProject=“src/main/java”> <property name=“enableSubPackages” value=“true”/> </javaClientGenerator> <!– 设置数据库的表名和实体类名 –> <table tableName=“t_user” domainObjectName=“User”> <!– generatedKey用于生成生成主键的方法 –> <generatedKey column=“id” sqlStatement=“SELECT LAST_INSERT_ID()”/> </table> </context></generatorConfiguration>数据库建库建表的代码:CREATE SCHEMA db_test DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ;CREATE TABLE db_test.t_user ( id INT NOT NULL AUTO_INCREMENT COMMENT ‘用户 ID’, username VARCHAR(30) NULL COMMENT ‘用户名称’, password VARCHAR(20) NULL COMMENT ‘用户密码’, birthday DATE NULL COMMENT ‘用户生日’, PRIMARY KEY (id), UNIQUE INDEX username_UNIQUE (username ASC)) COMMENT = ‘用户’;开开心心,执行命令,开始生成代码: java -jar mybatis-generator-core-1.3.7.jar -configfile generatorConfig.xml -overwrite然后查看生成的 Java 实体类:看着这个注释,让我有点纠结啊 —— 为什么不是数据库中每个字段对应的注释呢?查找相关资料,得知 MBG 生成的是由 org.mybatis.generator.api.CommentGenerator 来控制的。这是一个接口,MBG 的默认实现类是做 org.mybatis.generator.internal.DefaultCommentGenerator。当你在 generatorConfig.xml 中配置了 commentGenerator 标签,那么默认状态下,生成注释的工作,将由 DefaultCommentGenerator来完成。 所以我们来查看下这个 DefaultCommentGenerator 的源码:public class DefaultCommentGenerator implements CommentGenerator { // 属性,即配置在 commentGenerator 标签之内的 Property 标签 private Properties properties; // 是否不生成日期 private boolean suppressDate; // 是否不生成注释 private boolean suppressAllComments; // 是否添加数据库内的注释 private boolean addRemarkComments; // 日期格式化 private SimpleDateFormat dateFormat; public DefaultCommentGenerator() { super(); properties = new Properties(); suppressDate = false; suppressAllComments = false; addRemarkComments = false; } @Override public void addConfigurationProperties(Properties properties) { this.properties.putAll(properties); suppressDate = isTrue(properties .getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_DATE)); suppressAllComments = isTrue(properties .getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_ALL_COMMENTS)); addRemarkComments = isTrue(properties .getProperty(PropertyRegistry.COMMENT_GENERATOR_ADD_REMARK_COMMENTS)); String dateFormatString = properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_DATE_FORMAT); if (StringUtility.stringHasValue(dateFormatString)) { dateFormat = new SimpleDateFormat(dateFormatString); } } // 其他代码 …}addRemarkComments 这个属性,看来就是用来生成数据库注释用的 —— 好开心,那把它设置为 true 试试:<generatorConfiguration> <!– 指定数据库驱动的jdbc驱动jar包的位置 –> <classPathEntry location="./mysql-connector-java-5.1.40.jar" /> <context id=“mysql” defaultModelType=“hierarchical” targetRuntime=“MyBatis3Simple” > <property name=“javaFileEncoding” value=“UTF-8”/> <!– 其他 Property –> <commentGenerator> <property name=“suppressDate” value=“true”/> <property name=“addRemarkComments” value=“true”/> </commentGenerator> … </context></generatorConfiguration>运行命令: java -jar mybatis-generator-core-1.3.7.jar -configfile generatorConfig.xml -overwrite数据库注释倒是拿到了,但是生成的一堆其他信息,看着实在是太扎眼了。查看源码,发现这些内容已经写死在 DefaultCommentGenerator 中了,没有办法自定义。自己动手丰衣足食,我们为啥不自己写个类实现 CommentGenerator 接口,然后自定义自己想要的注释呢。查看 commentGenerator 的 DTD,发现正好 commentGenerator 有个 type 属性,可以用来指定自己的注释实现类:查看 CommentGenerator 接口,发现里面的方法非常多,不仅包含了生成 Java 实体注释对应的方法,还包括了生成 XML 中注释的方法。所以我们先写一个默认的实现类,实现CommentGenerator 接口,但不做任何操作 —— 因为 DefaultCommentGenerator 本文已经存在了,为了避免混淆,就叫它SimpleCommentGenerator吧。然后定义我们自己的注释类,MySQLCommentGenerator,继承 SimpleCommentGenerator,重写我们需要的方法:public class MySQLCommentGenerator extends SimpleCommentGenerator { private Properties properties; public MySQLCommentGenerator() { properties = new Properties(); } @Override public void addConfigurationProperties(Properties properties) { // 获取自定义的 properties this.properties.putAll(properties); } @Override public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) { String author = properties.getProperty(“author”); String dateFormat = properties.getProperty(“dateFormat”, “yyyy-MM-dd”); SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat); // 获取表注释 String remarks = introspectedTable.getRemarks(); topLevelClass.addJavaDocLine("/"); topLevelClass.addJavaDocLine(" * " + remarks); topLevelClass.addJavaDocLine(" *"); topLevelClass.addJavaDocLine(" * @author " + author); topLevelClass.addJavaDocLine(" * @date " + dateFormatter.format(new Date())); topLevelClass.addJavaDocLine(" */"); } @Override public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) { // 获取列注释 String remarks = introspectedColumn.getRemarks(); field.addJavaDocLine("/"); field.addJavaDocLine(" * " + remarks); field.addJavaDocLine(" */"); }}因为我们现在要使用到我们自己自定义的 CommentGenerator ,所以我们 通过代码的方式来操作 MBG:public class Generator { public static void main( String[] args ) throws Exception { List<String> warnings = new ArrayList<>(); File configFile = new File(“generatorConfig.xml”); ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(true); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null); }}然后配置 generatorConfig.xml设置我们自己的注释生成器:<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <!– 指定数据库驱动的jdbc驱动jar包的位置 –> <!– 不再需要,因为 jar 包已经在 classpath 中 <classPathEntry location=”./mysql-connector-java-5.1.40.jar" /> –> <context id=“mysql” defaultModelType=“hierarchical” targetRuntime=“MyBatis3Simple” > … <!– 自定义注释生成器 –> <commentGenerator type=“me.mizhoux.mbgcomment.MySQLCommentGenerator”> <property name=“author” value=“Michael Chow”/> <property name=“dateFormat” value=“yyyy/MM/dd”/> </commentGenerator> … </context></generatorConfiguration>完整的 Maven 项目在 我的 GitHub。现在,我们运行主类 Generator,成功生成了数据库中的注释:等等,好像有点不对劲!类的注释怎么没有了!想来应该是 JDBC 连接 MySQL 的时候需要添加什么属性才能获取表的注释,上网查询,发现是 useInformationSchema,需要将其设置为 true(看来是 MBG 给自己的 DefaultCommentGenerator 开了小灶):<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE generatorConfiguration PUBLIC “-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN” “http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration> <context id=“mysql” defaultModelType=“hierarchical” targetRuntime=“MyBatis3Simple” > … <!– 自定义注释生成器 –> <commentGenerator type=“me.mizhoux.mbgcomment.MySQLCommentGenerator”> <property name=“author” value=“Michael Chow”/> <property name=“dateFormat” value=“yyyy/MM/dd”/> </commentGenerator> <!– 配置数据库连接 –> <jdbcConnection driverClass=“com.mysql.jdbc.Driver” connectionURL=“jdbc:mysql://localhost:3306/test?characterEncoding=utf-8” userId=“root” password=“123456”> <!– 设置 useInformationSchema 属性为 true –> <property name=“useInformationSchema” value=“true” /> </jdbcConnection> … </context></generatorConfiguration>然后再次运行主类 Generator:成功的生成了类主食和字段注释我这里并没有处理注释是多行文本的情况 —— 留给有兴趣的读者吧小项目地址:https://github.com/mizhoux/mbg-comment ...

September 27, 2018 · 3 min · jiezi

基于 mysql 异步驱动的非阻塞 Mybatis

虽然 spring5 也推出了 WebFlux 这一套异步技术栈,这种极大提升吞吐的玩法在 node 里玩的风生水起,但 java 世界里异步依旧不是主流,Vertx 倒是做了不少对异步的支持,但是其对于数据访问层的封装依旧还是挺精简的,传统的 javaer 还是受不了这种没有对象映射的工具库,于是我尝试将 Mybatis 移植到了异步驱动上,让数据访问层的工作变得更简单一些。给个例子:@Sql(User.class)public interface CommonMapper { @Select(columns = “id,age,username”) @OrderBy(“id desc”) @Page @ModelConditions({ @ModelCondition(field = “username”, criterion = Criterions.EQUAL), @ModelCondition(field = “maxAge”, column = “age”, criterion = Criterions.LESS), @ModelCondition(field = “minAge”, column = “age”, criterion = Criterions.GREATER) }) void query(UserSearch userSearch, DataHandler<List<User>> handler);}上面是 mapper 接口定义,方法的最后一个参数因为异步的原因所以变成了一个回调,不同的是有很多注解来表达 sql,看到这些注解应该不难猜出 sql 语句吧。如果不喜欢你当然可以继续使用 mapper.xml 的方式来写 sql。更多内容移步代码库吧~AsyncDaoasyncDao是一款异步非阻塞模型下的数据访问层工具。MySQL only. 基于MySQL的异步驱动借鉴了Mybatis的mapping 和 dynamicSQL的内容,Mybatiser可以无缝切换注解表达SQL的能力事务支持SpringBoot支持Mybatis like使用上与Mybatis几乎一致,由于异步非阻塞的关系,数据的返回都会通过回调DataHandler来完成,所以方法定义参数的最后一个一定是DataHandler类型。由于需要提取方法的参数名,于是需要加上编译参数-parameters,请将它在IDE和maven里配置上。public interface CommonDao { void query(User user, DataHandler<List<User>> handler); void querySingle(User user, DataHandler<User> handler); void querySingleMap(User user, DataHandler<Map> handler); void insert(User user,DataHandler<Long> handler); void update(User user,DataHandler<Long> handler); void delete(User user,DataHandler<Long> handler);}mapper.xml与Mybatis几乎一致的写法(覆盖常见标签,一些不常用标签可能不支持,动态SQL建议使用注解SQL功能)<?xml version=“1.0” encoding=“UTF-8”?><mapper namespace=“com.tg.async.mapper.CommonDao”> <resultMap id=“BaseResultMap” type=“com.tg.async.mapper.User”> <id column=“id” property=“id”/> <result column=“old_address” property=“oldAddress”/> <result column=“created_at” property=“createdAt”/> <result column=“password” property=“password”/> <result column=“now_address” property=“nowAddress”/> <result column=“state” property=“state”/> <result column=“age” property=“age”/> <result column=“username” property=“username”/> <result column=“updated_at” property=“updatedAt”/> </resultMap> <select id=“query” resultMap=“BaseResultMap”>select * from T_User <where> <if test=“user.username!=null and user.username!=’’">AND username = #{user.username}</if> <if test=“user.age != null”>OR age > #{user.age}</if> </where> order by id desc </select> <insert id=“insert” useGeneratedKeys=“true” keyProperty=“id”>insert into T_User <trim prefix=”(" suffix=")" suffixOverrides=","> <if test=“user.oldAddress != null”>old_address,</if> <if test=“user.createdAt != null”>created_at,</if> <if test=“user.password != null”>password,</if> <if test=“user.nowAddress != null”>now_address,</if> <if test=“user.state != null”>state,</if> <if test=“user.age != null”>age,</if> <if test=“user.username != null”>username,</if> <if test=“user.updatedAt != null”>updated_at,</if> </trim> <trim prefix=“values (” suffix=")" suffixOverrides=","> <if test=“user.oldAddress != null”>#{user.oldAddress},</if> <if test=“user.createdAt != null”>#{user.createdAt},</if> <if test=“user.password != null”>#{user.password},</if> <if test=“user.nowAddress != null”>#{user.nowAddress},</if> <if test=“user.state != null”>#{user.state},</if> <if test=“user.age != null”>#{user.age},</if> <if test=“user.username != null”>#{user.username},</if> <if test=“user.updatedAt != null”>#{user.updatedAt},</if> </trim> </insert> <update id=“update”> update T_User <set> <if test=“user.password != null”>password=#{user.password},</if> <if test=“user.age != null”>age=#{user.age},</if> </set> where id = #{user.id} </update></mapper>注解SQL在XML里写SQL对于一些常见SQL实在是重复劳动,so这里允许你利用注解来表达SQL,该怎么做呢?Table与Model关联@Table(name = “T_User”)public class User { @Id(“id”) private Long id; //建议全部用包装类型,并注意mysql中字段类型与java类型的对应关系,mysql的int不会自动装换到这里的long private String username; private Integer age; @Column(“now_address”) private String nowAddress; @Column(“created_at”) private LocalDateTime createdAt; //asyncDao 里sql的时间类型都用joda,注意不是JDK8提供的那个,而是第三方包org.joda.time @Ignore private String remrk;@Table记录数据表的名字 @Id记录主键信息 @Column映射了表字段和属性的关系,如果表字段和类属性同名,那么可以省略这个注解 @Ingore忽略这个类属性,没有哪个表字段与它关联。定义接口@Sql(User.class)public interface CommonDao { @Select(columns = “id,age,username”) @OrderBy(“id desc”) @Page @ModelConditions({ @ModelCondition(field = “username”, criterion = Criterions.EQUAL), @ModelCondition(field = “maxAge”, column = “age”, criterion = Criterions.LESS), @ModelCondition(field = “minAge”, column = “age”, criterion = Criterions.GREATER) }) void query(UserSearch userSearch, DataHandler<List<User>> handler); @Select(columns = “age,username”) @OrderBy(“id desc”) void queryParam(@Condition String username, @Condition(criterion = Criterions.GREATER) Integer age, @OffSet int offset, @Limit int limit, DataHandler<List<User>> handler); @Select(columns = “username,age”, sqlMode = SqlMode.COMMON) void queryList(@Condition(criterion = Criterions.IN, column = “id”) int[] ids, DataHandler<List<User>> handler); @Insert(useGeneratedKeys = true, keyProperty = “id”) void insert(User user, DataHandler<Long> handler); @Update @ModelConditions(@ModelCondition(field = “id”)) void update(User user, DataHandler<Long> handler); @Delete @ModelConditions(@ModelCondition(field = “id”)) void delete(User user, DataHandler<Long> handler);}看到这些注解你应该能猜出来SQL长什么样,接下来解释一下这些注解查询@Select(columns = “id,age,username”)@OrderBy(“id desc”)@Page@ModelConditions({ @ModelCondition(field = “username”, criterion = Criterions.EQUAL), @ModelCondition(field = “maxAge”, column = “age”, criterion = Criterions.LESS), @ModelCondition(field = “minAge”, column = “age”, criterion = Criterions.GREATER)})void query(UserSearch userSearch, DataHandler<List<User>> handler);@Selectcolumns:默认 select *可以配置columns(“username,age”)选择部分字段;SqlMode:有两个选择,SqlMode.SELECTIVE 和 SqlMode.COMMON,区别是selective会检查查询条件的字段是否为null来实现动态的查询,即值为null时不会成为查询条件。并且@Select,@Count,@Update,@Delete都有selective这个属性。@Conditioncriterion:查询条件,=,<,>,in等,具体见Criterionscolumn:与表字段的对应,若与字段名相同可不配置attach:连接 and,or, 默认是andtest:SqlMode为selective下的判断表达式,类似Mybatis<if test=“username != null”>里的test属性,动态化查询条件@Limit,@OffSet为分页字段。方法的参数不加任何注解一样会被当做查询条件,如下面两个函数效果是一样的:@Select()void queryUser(Integer age,DataHandler<List<User>> handler);@Select()void queryUser(@Condition(criterion = Criterions.EQUAL, column = “age”) Integer age,DataHandler<List<User>> handler);查询Model上面的例子在查询条件比较多时方法参数会比较多,我们可以把查询条件封装到一个类里,使用@ModelConditions来注解查询条件,注意被@ModelConditions注解的方法只能有两个参数,一个是查询model,一个是DataHandler。@Select@Page@ModelConditions({ @ModelCondition(field = “username”, criterion = Criterions.EQUAL), @ModelCondition(field = “minAge”, column = “age”, criterion = Criterions.GREATER), @ModelCondition(field = “maxAge”, column = “age”, criterion = Criterions.LESS), @ModelCondition(field = “ids”, column = “id”, criterion = Criterions.IN)})void queryUser5(UserSearch userSearch,DataHandler<List<User>> handler);@ModelConditionfield:必填,查询条件中类对应的属性column:对应的表字段test:动态SQL的判断表达式@Page只能用在ModelConditions下的查询,并且方法参数的那个类应该有offset,limit这两个属性,或者 使用@Page(offsetField = “offset”,limitField = “limit”)指定具体字段统计@Countvoid count(DataHandler<Integer> handler);//返回Long类型插入@Insert(useGeneratedKeys = true, keyProperty = “id”)//返回自增idvoid insert(User user, DataHandler<Long> handler);更新@Update(columns = “username,age”)//选择更新某几个列void update(User user, DataHandler<Long> handler);//返回affectedRows删除@Deleteint delete(@Condition(criterion = Criterions.GREATER, column = “age”) int min, @Condition(criterion = Criterions.LESS, column = “age”) int max, DataHandler<Long> handler);@Delete@ModelConditions(@ModelCondition(field = “id”))void delete(User user, DataHandler<Long> handler);使用简单的编程使用AsyncConfig asyncConfig = new AsyncConfig();PoolConfiguration configuration = new PoolConfiguration(“username”, “localhost”, 3306, “password”, “database-name”);asyncConfig.setPoolConfiguration(configuration);asyncConfig.setMapperPackages(“com.tg.async.mapper”);//mapper接口asyncConfig.setXmlLocations(“mapper/”);//xml目录,classpath的相对路径,不支持绝对路径AsyncDaoFactory asyncDaoFactory = AsyncDaoFactory.build(asyncConfig);CommonDao commonDao = asyncDaoFactory.getMapper(CommonDao.class); UserSearch userSearch = new UserSearch();userSearch.setUsername(“ha”);userSearch.setMaxAge(28);userSearch.setMinAge(8);userSearch.setLimit(5);CountDownLatch latch = new CountDownLatch(1);commonDao.query(user, users -> { System.out.println(users); latch.countDown();});latch.await(); 事务Mybatis和Spring体系里有一个非常好用的@Translactional注解,我们知道事务本质就是依赖connection的rollback等操作,那么一个事务下多个SQL就要共用这一个connection,如何共享呢?传统的阻塞体系下ThreadLocal就成了实现这一点的完美解决方案。那么在异步世界里,要实现mybatis-spring一样的上层Api来完成事务操作是一件非常困难的事,难点就在于Api太上层,以至于无法实现connection共享。于是这里自能退而求其次,使用编程式的方式来使用事务,抽象出一个Translaction,具体的mapper通过translaction.getMapper()来获取,这样通过同一个Translaction得到的Mapper都将共用一个connection。CountDownLatch latch = new CountDownLatch(1);AsyncConfig asyncConfig = new AsyncConfig();PoolConfiguration configuration = new PoolConfiguration(“username”, “localhost”, 3306, “password”, “database-name”);asyncConfig.setPoolConfiguration(configuration);asyncConfig.setMapperPackages(“com.tg.async.mapper”);asyncConfig.setXmlLocations(“mapper/”);asyncDaoFactory = AsyncDaoFactory.build(asyncConfig);asyncDaoFactory.startTranslation(res -> { Translaction translaction = res.result(); System.out.println(translaction); CommonDao commonDao = translaction.getMapper(CommonDao.class); User user = new User(); user.setUsername(“insert”); user.setPassword(“1234”); user.setAge(28); commonDao.insert(user, id -> { System.out.println(id); translaction.rollback(Void -> { latch.countDown(); }); });});latch.await();SpringBoot虽然Spring5推出了WebFlux,但异步体系在Spring里依旧不是主流。在异步化改造的过程中,大部分人也往往会保留Spring的IOC,而将其他交给Vertx,所以asyncDao对于Spring的支持就是将Mapper注入IOC容器。quick startYAML配置文件:async: dao: mapperLocations: /mapper #xml目录,classpath的相对路径,不支持绝对路径 basePackages: com.tg.mapper #mapper所在包 username: username host: localhost port: 3306 password: pass database: database-name maxTotal: 12 maxIdle: 12 minIdle: 1 maxWaitMillis: 10000添加@Mapper来实现注入@Mapper@Sql(User.class)public interface CommonDao { @Select(columns = “id,age,username”) @OrderBy(“id desc”) @Page(offsetField = “offset”, limitField = “limit”) @ModelConditions({ @ModelCondition(field = “username”, criterion = Criterions.EQUAL), @ModelCondition(field = “maxAge”, column = “age”, criterion = Criterions.LESS), @ModelCondition(field = “minAge”, column = “age”, criterion = Criterions.GREATER) }) void query(UserSearch userSearch, DataHandler<List<User>> handler);}通过@EnableAsyncDao来开启支持,简单示例:@SpringBootApplication@EnableAsyncDaopublic class DemoApplication { public static void main(String[] args){ ApplicationContext applicationContext = SpringApplication.run(DemoApplication.class); CommonDao commonDao = applicationContext.getBean(CommonDao.class); UserSearch userSearch = new UserSearch(); userSearch.setUsername(“ha”); userSearch.setMaxAge(28); userSearch.setMinAge(8); userSearch.setLimit(5); commonDao.query(userSearch, users -> { System.out.println(“result: " + users); }); }} ...

August 30, 2018 · 4 min · jiezi