基於 mysql 非同步驅動的非阻塞 Mybatis 瞭解一下

twogoods發表於2018-08-29

雖然 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。

更多內容移步程式碼庫吧~


AsyncDao

asyncDao是一款非同步非阻塞模型下的資料訪問層工具。

  • 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);
複製程式碼
@Select
  • columns:預設 select *可以配置columns("username,age")選擇部分欄位;
  • SqlMode:有兩個選擇,SqlMode.SELECTIVE 和 SqlMode.COMMON,區別是selective會檢查查詢條件的欄位是否為null來實現動態的查詢,即值為null時不會成為查詢條件。並且@Select@Count@Update@Delete都有selective這個屬性。
@Condition
  • criterion:查詢條件,=,<,>,in等,具體見Criterions
  • column:與表欄位的對應,若與欄位名相同可不配置
  • attach:連線 and,or, 預設是and
  • test: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);
複製程式碼
@ModelCondition
  • field:必填,查詢條件中類對應的屬性
  • column:對應的表欄位
  • test:動態SQL的判斷表示式

@Page只能用在ModelConditions下的查詢,並且方法引數的那個類應該有offsetlimit這兩個屬性,或者 使用@Page(offsetField = "offset",limitField = "limit")指定具體欄位

統計

@Count
void count(DataHandler<Integer> handler);//返回Long型別
複製程式碼

插入

@Insert(useGeneratedKeys = true, keyProperty = "id")//返回自增id
void insert(User user, DataHandler<Long> handler);
複製程式碼

更新

@Update(columns = "username,age")//選擇更新某幾個列
void update(User user, DataHandler<Long> handler);//返回affectedRows
複製程式碼

刪除

@Delete
int 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 start

YAML配置檔案:

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
@EnableAsyncDao
public 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);
        });
    }
}
複製程式碼

相關文章