SpringLink :: MyBatis

MyBatis ORM Framework

License

License

Categories

Categories

MyBatis Data ORM
GroupId

GroupId

com.github.springlink
ArtifactId

ArtifactId

springlink-mybatis
Last Version

Last Version

1.0.3
Release Date

Release Date

Type

Type

jar
Description

Description

SpringLink :: MyBatis
MyBatis ORM Framework
Project URL

Project URL

https://github.com/springlink/springlink-mybatis
Source Code Management

Source Code Management

https://github.com/springlink/springlink-mybatis

Download springlink-mybatis

How to add to project

<!-- https://jarcasting.com/artifacts/com.github.springlink/springlink-mybatis/ -->
<dependency>
    <groupId>com.github.springlink</groupId>
    <artifactId>springlink-mybatis</artifactId>
    <version>1.0.3</version>
</dependency>
// https://jarcasting.com/artifacts/com.github.springlink/springlink-mybatis/
implementation 'com.github.springlink:springlink-mybatis:1.0.3'
// https://jarcasting.com/artifacts/com.github.springlink/springlink-mybatis/
implementation ("com.github.springlink:springlink-mybatis:1.0.3")
'com.github.springlink:springlink-mybatis:jar:1.0.3'
<dependency org="com.github.springlink" name="springlink-mybatis" rev="1.0.3">
  <artifact name="springlink-mybatis" type="jar" />
</dependency>
@Grapes(
@Grab(group='com.github.springlink', module='springlink-mybatis', version='1.0.3')
)
libraryDependencies += "com.github.springlink" % "springlink-mybatis" % "1.0.3"
[com.github.springlink/springlink-mybatis "1.0.3"]

Dependencies

compile (2)

Group / Artifact Type Version
org.mybatis : mybatis jar 3.5.1
com.google.guava : guava jar 27.1-jre

test (7)

Group / Artifact Type Version
junit : junit jar 4.12
org.assertj : assertj-core jar 3.9.1
org.mockito : mockito-core jar 2.16.0
ch.qos.logback : logback-classic jar 1.2.3
com.h2database : h2 jar 1.4.197
com.wix : wix-embedded-mysql jar 4.2.0
mysql : mysql-connector-java jar 5.1.47

Project Modules

There are no modules declared in this project.

springlink-mybatis

基于MyBatis的ORM框架

Maven Repo

<dependency>
  <groupId>com.github.springlink</groupId>
  <artifactId>springlink-mybatis</artifactId>
  <version>1.0.2</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

Spring JavaConfig 配置

@Bean
public SqlRegistry sqlRegistry(SqlSessionFactory sqlSessionFactory) throws IOException, ClassNotFoundException {
  SqlRegistry registry = new SqlRegistry(sqlSessionFactory.getConfiguration(), SqlDialect.get("mysql"));
  registry.addPackage("com.github.springlink.example.entity", getClass().getClassLoader());
  return registry;
}
@Bean
public SqlDao sqlDao(SqlSessionFactory sqlSessionFactory, SqlRegistry registry) {
  return new DefaultSqlDao(registry, new SqlSessionTemplate(sqlSessionFactory))
}

实体类注解

  • 在实体类上使用@SqlEntity注解,以便在SqlRegistry进行包扫描时发现这个实体类
    • value:数据库表名
    • schema:数据库Schema
    • catalog: 数据库Catalog
    • nameStrategy:名称转换策略,默认为下划线转驼峰
  • 在实体类上使用@SqlCache注解,相当于为该实体类配置<cache>,注解参数与<cache>完全一致,这里不再详细描述
  • 在实体类上使用@SqlCacheRef注解,相当于为该实体类配置 <cache-ref>
    • value:指定与哪个实体类共享缓存,不能与namespace同时指定
    • namespace:指定与哪个命名空间共享缓存,不能与value同时指定
  • 在实体类字段上使用@SqlProperty注解,为字段配置不同属性
    • aliases:字段别名,可通过#alias的形式引用该字段,在不同实体上引用功能相同但字段名不同的属性时非常有用,默认为空
    • column:数据库列名,默认采取@SqlEntitynameStrategy策略自动计算
    • jdbcType:MyBatis JDBCType,默认为UNDEFINED
    • typeHandler:MyBatis TypeHandler,默认为UnknownTypeHandler
    • reference:字段引用,设置后意味着该字段引用另一个字段的值,自身并非具体的数据库字段,常用于表连接的情况下,默认为空
    • id:是否为ID字段,加上此注解并不意味着该字段为数据库主键,而是用于MyBatis对于缓存的优化,官方文档描述为“一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能”,默认为false
    • generated:是否由数据库生成值,配置为true相当于MyBatis的<insert useGeneratedKeys>配置,默认为false
  • 在实体类字段上使用@SqlIgnore注解,标记该字段不出现在生成的SQL语句中,与JPA 的 @Trasient 注解类似
  • static final字段上使用@SqlJoin注解,声明一条实体连接,字段值为相应的连接条件,在该实体的SELECT操作时,会自动生成相应的表连接语句
    • value:指定与哪个实体连接
    • name:为该实体连接命名,默认为空,直接使用注解字段的名称
    • type:指定连接类型,支持Inner、Left Outer、Right Outer和Full Outer,默认为Left Outer

SqlDao 接口

这个接口是框架的核心接口,所有DAO操作都从这个接口发起

select

调用SqlDao.select(Class<T> entityType)方法获取Selector<T>对象,支持链式调用 Selector接口主要包含以下方法

  • Selector<T> where(@Nullable SqlCriterion criterion) 设置select条件,多次调用只保留最后一次的值
    // 查询id为123的Post
    dao.select(Post.class).where(SqlCriterion.eq("id", 123)).asOne();
    
    // Lambda版本
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123));
  • Selector<T> orderBy(@Nullable SqlOrderBy order) 设置select排序,多次调用只保留最后一次的值
    // 根据标题(升序)和创建时间(降序)排序
    dao.select(Post.class).orderBy(SqlOrderBy.create().asc("title").desc("createTime")).asList();
    
    // Lambda版本
    dao.select(Post.class).orderBy(o -> o.asc(Post::getTitle).desc(Post::getCreateTime)).asList();
  • Selector<T> forUpdate() 设置是否附带forUpdate
    // 查询id为123的Post,并加上FOR UPDATE
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123)).forUpdate().asOne();
    
    // 可以通过参数指定是否加上FOR UPDATE
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123)).forUpdate(false).asOne();
  • Optional<T> asOne() 执行查询,返回最多一条结果,与SqlSession.selectOne()执行效果相同
    // 查询id为123的Post,如果存在,打印创建时间
    // 这里使用了Java8的Optional接口简化写法,省去了额外的if条件判断
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123)).asOne()
        .ifPresent(post -> System.out.println(post.getCreateTime()));
  • <R> Optional<R> asOne(SqlProjections projections) 执行查询,返回一个或多个字段
    // 查询id为123的Post,打印createTime字段
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123))
        .asOne(SqlProjections.create().property("createTime")))
        .ifPresent(createTime -> System.out.println(createTime));
    
    // 查询id为123的Post,返回title和createTime字段
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123))
        .asOne(SqlProjections.create()
            .property("postTitle", "title")
            .property("postCreateTime", "createTime"))
        .ifPresent(post -> System.out.println(post.getTitle() + "\t" + post.getCreateTime()));
    
    // Lambda写法
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123))
        .asOne(p -> p.property(Post::getCreateTime))
        .ifPresent(createTime -> System.out.println(createTime));
    
    dao.select(Post.class).where(c -> c.eq(Post::getId, 123))
        .asOne(p -> p.property("postTitle", "title")
            .property("postCreateTime", "createTime"))
        .ifPresent(result -> System.out.println(result.get("postTitle") + "\t" + result.get("postCreateTime")));
  • List<T> asList() 执行查询,返回多条结果
    // 查询start大于等于100的Post
    dao.select(Post.class).where(c -> c.ge(Post::getStar, 100)).asList();
    
    // 查询start大于等于100的Post,并按照LIMIT 100, 50进行分页
    dao.select(Post.class).where(c -> c.ge(Post::getStar, 100))
        .asList(new RowBounds(100, 50));
        
    // 查询start大于等于100的Post,按照star字段降序,返回title和star字段,并按照LIMIT 100, 50进行分页
    dao.select(Post.class).where(c -> c.ge(Post::getStar, 100))
        .orderBy(o -> o.desc(Post::getStar))
        .asList(new RowBounds(100, 50), p -> p.property(Post::getTitle).property(Post::getStar));
  • BoundList<T> asBoundList(RowBounds rowBounds) 执行查询,返回多条结果,并统计总数
    // 查询start大于等于100的Post,并按照LIMIT 100, 50进行分页
    dao.select(Post.class).where(c -> c.ge(Post::getStar, 100))
        .asBoundList(new RowBounds(100, 50));
        
    // 查询start大于等于100的Post,按照star字段降序,返回title和star字段,并按照LIMIT 100, 50进行分页
    dao.select(Post.class).where(c -> c.ge(Post::getStar, 100))
        .orderBy(o -> o.desc(Post::getStar))
        .asBoundList(new RowBounds(100, 50), p -> p.property(Post::getTitle).property(Post::getStar));
  • <K> Map<K, T> asMap(String mapKey) 执行查询,返回多条结果,并以mapKey为关键字放置在map中,与SqlSession.selectMap()执行效果相同
    // 查询所有Post,并按照id放置在map中
    dao.select(Post.class).asMap("id");

select count

long count(Class<?> entityType, @Nullable SqlCriterion criterion)

  // 查询title包含“title”的Post数量
  dao.count(Post.class, c -> c.like(Post::getTitle, "%news%"));

select exists

boolean exists(Class<?> entityType, @Nullable SqlCriterion criterion)

  // 查询是否存在id为123的Post
  dao.exists(Post.class, c -> c.eq(Post::getId, 123));

update

  • <T> int update(Class<T> entityType, @Nullable SqlUpdate update, @Nullable SqlCriterion criterion) 更新指定字段
    // id为123的Post,title设置为“Big news”,star值增加1
    dao.update(Post.class,
        SqlUpdate.create().set("title", "Big news").add("star", 1),
        SqlCriterion.eq("id", 123));
    
    // Lambda写法
    dao.update(Post.class,
        u -> u.set(Post::getTitle, "Big news").add(Post::getStar, 1),
        c -> c.eq(Post::getId, 123));
  • <T> int updateEntity(Class<T> entityType, @Nullable T entity, boolean ignoreNulls, @Nullable SqlCriterion criterion) 更新整个实体
    Post post = new Post();
    post.setTitle("Big news");
    post.setStar(500);
    // 按照post中的非空字段更新id为123的Post
    dao.update(Post.class, post, true, c -> c.eq(Post::getId, 123));
    
    // 其中ignoreNulls可以省略,默认值为true
    dao.update(Post.class, post, c -> c.eq(Post::getId, 123));

delete

int delete(Class<?> entityType, @Nullable SqlCriterion criterion)

// 删除star小于10的Post
dao.delete(Post.class, c -> c.lt(Post::getStar, 10));

insert

<T> int insert(Class<T> entityType, @Nullable T value)

Post post = new Post();
post.setTitle("New post");
post.setCreateTime(new Date());
// 插入新的Post
dao.insert(Post.class, post);

SqlCriterion条件

SqlCriterion.eq("id", 123); // id = 123
SqlCriterion.ne("id", 123); // id <> 123
SqlCriterion.gt("star", 100); // star > 100
SqlCriterion.ge("star", 100); // star >= 100
SqlCriterion.lt("star", 200); // star < 200
SqlCriterion.le("star", 200); // star <= 200
SqlCriterion.isNull("postId"); // postId IS NULL
SqlCriterion.isNotNull("postId"); // postId IS NOT NULL
SqlCriterion.like("title", "%news%"); // title LIKE '%news%'
SqlCriterion.like("title", "%big^_news%", "^"); // title LIKE '%big^_news%' ESCAPE '^'
SqlCriterion.between("star", 20, 80); // star BETWEEN 20 AND 80
SqlCriterion.in("section", Arrays.asList("SPORTS", "LIVE", "ART")); // section IN('SPORTS', 'LIVE', 'ART')
SqlCriterion.in("section", "SPORTS", "LIVE", "ART"); // section IN('SPORTS', 'LIVE', 'ART')

SqlCriterion.and(SqlCriterion.lt("star", 800), SqlCriterion.gt("star", 100)); // star < 800 AND star > 100
SqlCriterion.and(Arrays.asList(SqlCriterion.lt("star", 800), SqlCriterion.gt("star", 100))); // star < 800 AND star > 100

SqlCriterion.or(SqlCriterion.gt("star", 50), SqlCriterion.lt("star", 40)); // star > 50 OR star < 40
SqlCriterion.or(Arrays.asList(SqlCriterion.gt("star", 50), SqlCriterion.lt("star", 40))); // star > 50 OR star < 40

SqlCriterion.not(SqlCriterion.eq("id", 123)); // NOT (id = 123)
SqlCriterion.not(SqlCriterion.or(SqlCriterion.gt("star", 50), SqlCriterion.lt("star", 40))); // NOT(star > 50 OR star < 40)
SqlCriterion.notAny(SqlCriterion.gt("star", 50), SqlCriterion.lt("star", 40)); // NOT(star > 50 OR star < 40)

SqlCriterion..none(); // 空条件
SqlCriterion.trueValue(); // (1 = 1)
SqlCriterion.falseValue(); // (1 = 0)

// Lambda版本
SqlCriterion.lambda(Post.class, c -> c.eq(Post::getId, 123));

SqlCriterion.lambda(Post.class, c -> SqlCriterion.and(
  c.gt(Post::getStar, 500),
  c.like(Post::getTitle, "%news%"),
  c.in(Post::getSection, "SPORTS", "ART")
));

SqlOrderBy排序

SqlOrderBy.create().desc("star").asc("title"); // ORDER BY star DESC, title ASC

// Lambda版本
SqlOrderBy.create(Post.class).desc(Post::getStar).asc(Post::getTitle);

SqlUpdate更新

SqlUpdate.create().set("title", "New title").add("star", 2); // SET title = 'New title', star = star + 2, 
SqlUpdate.create().subtract("star", 3).nullify("createTime"); // SET star = star - 3, createTime = NULL

// Lambda版本
SqlUpdate.create(Post.class).set(Post::getTitle, "New title").add(Post::getStar, 2);
SqlUpdate.create(Post.class).subtract(Post::getStar, 3).nullify(Post::getCreateTime);

使用@SqlJoin注解进行实体连接

为了便于解释此功能的应用场景,我们假手头有三个实体类

// 用户实体
public class User {
  private String id;

  private String username;

  private String password;

  private String groupId;

  // 此处省略getter和setter
}

// 用户组实体
public class Group {
  private String id;

  private String groupName;

  // 此处省略getter和setter
}

// 帖子实体
public class Post {
  private String id;

  private String title;

  private String userId;

  private String createDate;

  // 此处省略getter和setter
}

此时我们希望实现下列SQL语句

-- 查询帖子列表时,通过左连接将帖子对应的用户名username一并带出
SELECT t.id, t.title, t.userId, t.createDate, a.username FROM post t LEFT JOIN user a ON a.id = t.userId;

由于username存在于User实体而不是Post,这时候我们可以通过给Post实体增加User的实体连接,将username引入,作为Post的一个引用字段

public class Post {
  // 连接User实体,并命名为postUser,连接条件为postUser.id = userId,此处userId即指当前实体的userId字段
  // 请注意这里必须使用static final修饰,并且类型为SqlCriterion,否则实体扫描时会产生错误
  // 这里eq的值使用了SqlReference,意味着postUser.id与userId构成相等条件,而不是postUser.id = 'userId'
  @SqlJoin(User.class)
  private static final SqlCriterion postUser = SqlCriterion.eq("postUser.id", SqlReference.of("userId"));

  private String id;

  private String title;

  private String userId;

  private String createDate;

  // 引用postUser的username字段
  @SqlProperty(reference = "postUser.username")
  private String username;

  // 此处省略getter和setter
}

这样一来,关于Post实体的select操作将会附加一条join语句,与主表关联。连接可以定义多个,下面是更复杂的例子,我们进一步地将用户组Group一并引入

public class Post {
  @SqlJoin(User.class)
  private static final SqlCriterion postUser = SqlCriterion.eq("postUser.id", SqlReference.of("userId"));

  // 将用户所属组关联进来
  @SqlJoin(Group.class)
  private static final SqlCriterion postUserGroup = SqlCriterion.eq("postUserGroup.id", SqlReference.of("postUser.groupId"))

  private String id;

  private String title;

  private String userId;

  private String createDate;

  @SqlProperty(reference = "postUser.username")
  private String username;

  @SqlProperty(reference = "postUserGroup.groupName")
  private String userGroupName;

  // 此处省略getter和setter
}

引入的字段不仅仅用来展示,还可以作为查询条件

// 查询所有所属用户组为ADMIN的相关帖子
dao.select(Post.class).where(c -> c.eq(Post::getUserGroupName, "ADMIN")).asList();

上述操作将会生成下列SQL语句

SELECT t.id, t.title, t.userId, t.createDate, a.username, b.groupName
FROM post t
    LEFT JOIN user a ON a.id = t.userId
    LEFT JOIN group b ON b.id = a.groupId
WHERE b.groupName = 'ADMIN';

请注意,声明表连接时你不必担心先后顺序,因为框架会分析关联条件进行拓扑排序,总之,即使调换postUser和postUserGroup,也不会生成下面这种错误的SQL语句

SELECT t.id, t.title, t.userId, t.createDate, a.username, b.groupName
FROM post t
    LEFT JOIN group b ON b.id = a.groupId -- JOIN顺序错误,应先连接user a,此处才能引用a.groupId
    LEFT JOIN user a ON a.id = t.userId
WHERE b.groupName = 'ADMIN';

所有的实体类注解都是可继承的,因此,如果你出于性能考虑,不希望Post实体在所有查询中都进行JOIN,你可以使用实体类继承的方案进行折中 如果你的表连接或者表连接条件是动态的,那么此方法并不适用,请考虑使用手写SQL语句配合SqlContext在Mapper中实现灵活的动态SQL

public class Post {
  private String id;

  private String title;

  private String userId;

  private String createDate;

  // 此处省略getter和setter
}

public class PostWithUserInfo extends Post {
  @SqlJoin(User.class)
  private static final SqlCriterion postUser = SqlCriterion.eq("postUser.id", SqlReference.of("userId"));

  @SqlJoin(Group.class)
  private static final SqlCriterion postUserGroup = SqlCriterion.eq("postUserGroup.id", SqlReference.of("postUser.groupId"));

  @SqlProperty(reference = "postUser.username")
  private String username;

  @SqlProperty(reference = "postUserGroup.groupName")
  private String userGroupName;

  // 此处省略getter和setter
}

继承的优先级:属性注解 > Getter注解 > Setter注解 > 父类属性注解 > 父类Getter注解 > 父类Setter注解 实体连接一样会被子类继承下来,如果子类声明了同名的连接,则会覆盖父类的声明

请特别注意,实体连接目前仅作用于查询语句,在其他更新(包括删除和插入)语句中将不可用,如果你在更新语句中使用了引用字段,那么将会出现SQL错误,类似下列异常

org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: org.h2.jdbc.JdbcSQLException: Column "J2.USERNAME" not found; SQL statement:
UPDATE `post` t   SET t.`subject` = j2.`username`    WHERE t.`id` = ? [42122-197]
### The error may exist in SqlRegistry[com.github.springlink.mybatis.entity.Post]
### The error may involve com.github.springlink.mybatis.entity.Post.update
### The error occurred while executing an update
### SQL: UPDATE `post` t   SET t.`subject` = j2.`username`    WHERE t.`id` = ?
### Cause: org.h2.jdbc.JdbcSQLException: Column "J2.USERNAME" not found; SQL statement:
UPDATE `post` t   SET t.`subject` = j2.`username`    WHERE t.`id` = ? [42122-197]
...

Versions

Version
1.0.3
1.0.2
1.0.1
1.0.0