open-hand/choerodon-starters

choerodon-starter-mybatis-mapper简介

Opened this issue · 0 comments

choerodon-starter-mybatis-mapper

mybatis基础工具包,集成通用MapperPageHelper两个开源项目,对源代码根据自身业务逻辑需求进行了精简和修改,扩展了审计字段、多语言功能,并修改了分页插件,添加了插入或更新指定列等功能。

Choerodon微服务里有数据库操作的都使用了这个工具包

Feature

PageHelper嵌套结果查询分页处理,分页查询通用方法抽象。

To get the code

git clone https://rdc.hand-china.com/gitlab/io.choerodon/choerodon-starter-parent.git

Installation and Getting Started

<dependency>
    <groupId>io.choerodon</groupId>
    <artifactId>choerodon-starter-mybatis-mapper</artifactId>
    <version>0.1.0</version>
</dependency>

Documentation

通用Mapper原作者文档

PageHelper原作者使用方法

通用Mapper实现原理的介绍

Usage

通用Mapper用法:

新建user表:

CREATE TABLE `user`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'name',
  `object_version_number` bigint(20) UNSIGNED NULL DEFAULT 1,
  `created_by` bigint(20) UNSIGNED NULL DEFAULT 0,
  `creation_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP,
  `last_updated_by` bigint(20) UNSIGNED NULL DEFAULT 0,
  `last_update_date` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 867 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `user` VALUES (1, 'Daenerys Targaryen', 1, 0, '2018-04-11 03:09:38', 0, '2018-04-11 03:09:38');

对应的dataobject如下:

//对应数据库的表名
@Table(name = "user")
//支持4个审计字段(created_by, creation_date, last_update_by, last_update_date)
@ModifyAudit
//object_version_number
@VersionAudit
public class UserDO extends AuditDomain {
    //指定主键,用于插入后主键回查
    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private String name;

    //省略get和set方法
}

choerodon-starter-mybatis-mapper设置了扫描项目下以mapper结尾的文件夹,因此mapper文件和对应的xml文件应放倒mapper文件夹下面,iam结构如下:

.
+-- src
    +-- main
        +-- docker
        +-- java
        |    +-- io
        |        +-- choerodon
        |            +-- iam
        |               +-- infra
        |                   +-- mapper
        +-- resource
            +-- mapper

mapper文件如下:

public interface UserMapper extends BaseMapper<UserDO> {
    //只是个例子,其实可以用自动生成的方法selectAll()来替代
    List<UserDO> selectAllUsers();
}

继承了BaseMapper的mapper接口包含了绝大多数单表的增删改查操作,可以满足大多数的简单数据库操作,不用手写sql。

如果有复杂的业务逻辑需要手写sql,需要在resource下的mapper文件夹创建与mapper class同名的xml文件,本例中就新建UserMapper.xml,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="io.choerodon.iam.infra.mapper.UserMapper">
     <select id="selectAllUsers" resultType="io.choerodon.manager.infra.dataobject.UserDO">
        select * from user
    </select>
</mapper>

目前为止就可以在reposity里面调用userMapper做增删改查操作了。

@Component
public class UserRepositoryImpl implements UserRepository {
    private UserMapper userMapper;
    public UserRepositoryImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    List<UserDO> selectAllUsers() {
        return userMapper.selectAllUsers();
    }
}

同时我们还新建了BaseService类,以组合的方式封装了BaseMapper的方法。需要注意的是insertOptionalupdateOptional两个方法,他们都有一个可变参数,用来传如数据库对应的列,只插入或更新指定列的数据。

如果不使用BaseService里面的insertOptionalupdateOptional方法,而是直接调用userMapper.insertOptional(userDO),如下:

String columns = "id,name,object_version_number";
OptionalHelper.optional(Arrays.asList(columns));
userMapper.insertOptional(userDO);

PageHelper用法:

假如有如下请求

http://localhost:8030/v1/organization/1/users?page=0&size=10&sort=id,desc&sort=organizationId,phone,asc

page是起始页,默认值为0,size是当前页数显示记录数,默认值为20。sort为排序字段,权重从左向右递减,上例表示该查询先按id降序排列,id相同按organizationId升序排列,organizationId相同按phone升序排列。

controller如下:

@ApiOperation(value = "分页查询")
//自定义swagger中pageRequest对象的显示
@CustomPageRequest
@GetMapping
public ResponseEntity<Page<UserDTO>> list(@PathVariable(name = "organization_id") Long organizationId,
                                          @ApiIgnore
                                          //如果请求url里面没有传sort字段,设置默认排序为根据id升序排列
                                          @SortDefault(value = "id", direction = Sort.Direction.ASC)
                                                PageRequest pageRequest,
                                          @RequestParam(required = false) String name)
    return new ResponseEntity<>(organizationUserService.pagingQuery(pageRequest, name), HttpStatus.OK);
}

PageRequest对象对前端传入的page,size和sort参数进行封装。分页查询使用PageHelper对象调用分页方法。

    //只做分页
    int page = pageRequest.getPage();
    int size = pageRequest.getSize();
    Page<UserDO> users = PageHelper.doPage(page, size, () -> userMapper.selectAllUsers());
    //分页和排序
    Page<UserDO> users = PageHelper.doPageAndSort(pageRequest, () -> userMapper.selectAllUsers());
    //只排序
    Sort sort = PageHelper.getSort();
    List<UserDO> users = PageHelper.doSort(sort, () -> userMapper.selectAllUsers());

关于排序的用法下面做详细介绍:

1.单表情况:

单表查询操作不牵扯别名问题,用法简单。controller写法如下:

public ResponseEntity<Page<IconDTO>> pagingQuery(@SortDefault(value = "lastUpdateDate", direction = Sort.Direction.ASC) PageRequest pageRequest)

在controller参数中使用pageRequest对象接收url中的page、size和sort字段。@SortDefault注解value为默认排序字段,如果是驼峰,拼接sql会转为下划线;direction为升序(Sort.Direction.ASC)和降序(Sort.Direction.DESC)。page和size不传分配默认值,sort不传如果controller配置有@SortDefault注解,则以注解生成默认值,如果没有配置注解则sort为null。

方法调用如下:

//如果使用生成sort的方式,xml中的sql请不要写order by。
PageHelper.doPageAndSort(pageRequest, ()-> mapper.fulltextSearch(userDO, param));

2.关联多表查询(只适合简单的关联表查询,如果是极复杂查询请自己写sql):

    SELECT
        fo.id,
        fo.code,
        fo.name,
        fo.password_policy_id,
        fp.id AS project_id,
        fp.name AS project_name,
        fp.code AS project_code,
        fp.organization_id,
        fp.is_enabled
    FROM
        fd_organization as fo
    LEFT
        JOIN fd_project as fp
    ON
        fo.id = fp.organization_id
    WHERE
        fp.is_enabled = true

由于organization和project表都有code和name字段,如果要对organization表的code字段和project表的code字段进行排序,前端url对sort字段传参要做区分,假如是sort=organizationCode,projectCode,desc,由于sql中存在别名现象且前端传入的排序字段名也存在认为命名的因素,所以需要建立一个HashMap进行key-value映射,organizationCode指向fo.code,projectCode指向fp.code。
这里将fo定义为主表,fp定义为从表。下例是按照organization表的code、name字段和project表的code、name字段排序:

   Map<String, String> map = new HashMap<>();
   map.put("organizationCode", "fo.code");
   map.put("projectCode", "fp.code");
   map.put("organizationName", "fo.name");
   map.put("projectName", "fp.name");
   //设置主表的别名,@SortDefault的value=id时,值会拼接到fo.id
   pageRequest.resetOrder("fo", map);
   PageHelper.doPageAndSort(pageRequest, ()-> mapper.fulltextSearch());

pageRequest.resetOrder("fo", map)必须写,重置sort里面的order对象,第一个参数为主表别名,没有别名写为fd_organization,第二个参数为map映射关系。主表别名的作用是如果sort=id,asc,会在上面的sql末尾拼接 order by fo.id asc,如果要还想按project_id排序,只能放map里面,用
map.put("projectId", "fp.id")的形式实现。
设置完pageRequest后调用PageHelper.doPageAndSort()方法即可。

PageRequest在feign调用时传递

feign调用传递分页参数的�时候,传递PageRequest对象,feign会把这个对象当成body,调用post方法请求。但PageRequest解析器是从url里的?后面截取参数然后放到�PageRequest对象里,默认的feign编码器会导致调用的时候分页参数丢失,所以mapper里面提供了一个�PageRequestQueryEncoder,在客户端服务做feign配置如下:

@Configuration
public class FeignClientConfig {

    private ObjectFactory<HttpMessageConverters> messageConverters;

    FeignClientConfig(ObjectFactory<HttpMessageConverters> messageConverters) {
        this.messageConverters = messageConverters;
    }

    @Bean
    public Encoder feignEncoder() {
        return new PageRequestQueryEncoder(new SpringEncoder(messageConverters));
    }
}

feign调用的时候直接传PageRequest对象即可:

    @GetMapping("/v1/organizations")
    ResponseEntity list(PageRequest pageRequest);

xml中存在不兼容的数据库方言怎么处理

mapper提供了一些�常用的DatabaseIdProvider

    DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
    Properties properties = new Properties();
    properties.setProperty("Oracle", "oracle");
    properties.setProperty("MySQL", "mysql");
    properties.setProperty("DB2", "db2");
    properties.setProperty("Derby", "derby");
    properties.setProperty("H2", "h2");
    properties.setProperty("HSQL", "hsql");
    properties.setProperty("Informix", "informix");
    properties.setProperty("MS-SQL", "ms-sql");
    properties.setProperty("PostgreSQL", "postgresql");
    properties.setProperty("Sybase", "sybase");
    properties.setProperty("Hana", "hana");
    databaseIdProvider.setProperties(properties);

对于一些不兼容的sql,比如分页方言(这里只是做举例),mysql/oracle/sqlserver都不相同,这个时候只需要在xml里面提供databaseId即可

<select id="list" databaseId="mysql" resultMap="user">
    select * from user limit 10
</select>
<select id="list" databaseId="oracle" resultMap="user">
    <![CDATA[
    select * from user where ROWNUM <= 10
     ]]>
</select>

注意

单表动态排序有字段校验,如果传入字段不是数据库字段,抛异常。

多表动态排序暂时没有非法字段校验,如果字段非法,只有在执行sql的时候抛SqlGrammarException,但可以防止sql注入。

这种在sql后面拼order by的操作只能在sql语句的末尾拼接,如果有分页参数,就是在sql语句末尾,分页参数之前拼接。不支持嵌套结果查询在sql的中间部分拼接order by。如果要自定义order by位置目前只能自己手动写sql,把排序字段以参数的形式传入。

Dependencies

  • choerodon-starter-core: Page对象和PageInfo对象
  • mybatis-spring-boot-starter
  • com.github.jsqlparse
  • com.fasterxml.jackson.core
  • javax.persistence
  • mysql-connector-java
  • swagger-annotations

Reporting Issues

If you find any shortcomings or bugs, please describe them in the Issue.

How to Contribute

Pull requests are welcome! Follow this link for more information on how to contribute.

Note

  • 不是表中字段的属性必须加 @Transient注解,这样生成动态sql就不会获取到该列

  • 在实体类要在类前面加@MultiLanguage注解,开启多语言支持,多语言字段需要加@MultiLanguageField注解

  • 通用 Mapper 不支持 devtools 热加载,devtools 排除实体类包即可,配置方式参考:sing-boot-devtools-customizing-classload

  • 只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会被分页。

  • 请不要配置多个分页插件

  • 请不要在系统中配置多个分页插件(使用Spring时,mybatis-serviceConfig.xml和Spring配置方式,请选择其中一种,不要同时配置多个分页插件)!

  • 分页插件不支持带有for update语句的分页

  • 对于带有for update的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。

  • 分页插件不支持嵌套结果映射,由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。