雖然 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下的查詢,並且方法引數的那個類應該有offset
,limit
這兩個屬性,或者 使用@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);
});
}
}
複製程式碼