一款基於Mybatis的編譯期SQL生成器

twogoods發表於2017-12-06

介紹

TgDao是一款基於Mybatis的編譯期SQL生成器,利用註解來表達SQL,能根據你的方法簽名生成對應的Mapper.xml檔案。 它能減少你日常開發中大量簡單SQL的編寫,由於它只是生成Mapper.xml檔案,因此對於複雜的查詢場景, 你同樣可以自己編寫來完成一些工具所無法生成的SQL。

@Table(name = "T_User")
public class User {
    @Id("id")
    private int id;
    private String username;
    private int age;
}

複製程式碼

上面的model定義了模型和資料庫表的關係,看到下面這些方法的簽名,聰明的你肯定能猜出每個方法的sql吧,這就是這個庫要做的工作。

@DaoGen(model = User.class)
public interface UserDao {
    @Select
    @OrderBy("id desc")
    List<User> queryUser(@Condition(criterion = Criterions.EQUAL, column = "username") String name,
                         @Condition(criterion = Criterions.GREATER, attach = Attach.OR) int age,
                         @Limit int limit, @OffSet int offset);

    @Select
    List<User> queryUser2(@Condition(criterion = Criterions.GREATER, column = "age") int min,
                          @Condition(criterion = Criterions.LESS, column = "age") int max);

    @Select
    List<User> queryUser3(@Condition(criterion = Criterions.EQUAL, column = "username") String name,
                          @Condition(attach = Attach.OR, column = "id", criterion = Criterions.IN) String[] ids);

    @Insert(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    @BatchInsert(columns = "username,age")
    int batchInsert(List<User> users);

    @Update
    @ModelConditions({
            @ModelCondition(field = "id")
    })
    int update(User user);

    @Delete
    int delete(@Condition(criterion = Criterions.GREATER, column = "age") int min,
               @Condition(criterion = Criterions.LESS, column = "age") int max);
}
複製程式碼

專案地址 github.com/twogoods/Tg…


文件

引入如下依賴:

<dependency>
  <groupId>com.github.twogoods</groupId>
  <artifactId>tgdao-core</artifactId>
  <version>0.1.3</version>
</dependency>
複製程式碼

Table與Model關聯

@Table記錄資料表的名字 @Id記錄主鍵資訊 @Column對映了表欄位和屬性的關係,如果表欄位和類屬性同名,那麼可以省略這個註解 @Ingore忽略這個類屬性,沒有哪個表欄位與它關聯

@Table(name = "T_User")
public class User {
    @Id("id")
    private int id;

    private String username;
    private String password;
    private int age;

    @Column("old_address")
    private String oldAddress;
    @Column("now_address")
    private String nowAddress;

    private int state;

    @Column("created_at")
    private Timestamp createdAt;
    @Column("updated_at")
    private Timestamp updatedAt;

    @Ignore
    private String remrk;
複製程式碼

查詢

@Select
@OrderBy("id desc")
List<User> queryUser(@Condition(criterion = Criterions.EQUAL, column = "username") String name,
                    @Condition(criterion = Criterions.GREATER, attach = Attach.OR) int age,
                    @Condition(column = "id", criterion = Criterions.IN) String[] ids,
                    @Limit int limit, @OffSet int offset);
複製程式碼
@Select
  • columns:預設 select *可以配置columns("username,age")選擇部分欄位;
  • SqlMode:有兩個選擇,SqlMode.SELECTIVE 和 SqlMode.COMMON,區別是selective會檢查查詢條件的欄位是否為null來實現動態的查詢, 即<if test="name != null">username = #{name}</if>
@Condition
  • criterion:查詢條件,=,<,>,in等,具體見Criterions
  • column:與表欄位的對應,若與欄位名相同可不配置
  • attach:連線 and,or, 預設是and
  • test:selective下的判斷表示式,即<if test="username != null">裡的test屬性

@Limit@OffSet為分頁欄位。 方法的引數不加任何註解一樣會被當做查詢條件,如下面兩個函式效果是一樣的:

@Select()
List<User> queryUser(Integer age);

@Select()
List<User> queryUser(@Condition(criterion = Criterions.EQUAL, column = "age") Integer age);
複製程式碼

查詢Model

上面的例子在查詢條件比較多時方法引數會比較多,我們可以把查詢條件封裝到一個類裡,使用@ModelConditions來註解查詢條件,注意被@ModelConditions只能有一個引數。

@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),
       @ModelCondition(field = "idArr", column = "id", criterion = Criterions.IN, paramType = InType.ARRAY)
})
List<User> queryUser5(UserSearch userSearch);
複製程式碼
@ModelCondition
  • field:必填,查詢條件中類對應的屬性
  • column:對應的表欄位
  • paramType:in 查詢下才需要配置,陣列為array,List為collection型別
  • test:selective下的判斷表示式,即<if test="username != null">裡的test屬性

@Page只能用在ModelConditions下的查詢,並且方法引數的那個類應該有offsetlimit這兩個屬性。

注:

@Select(columns = "username,age")
List<User> queryUser(Integer age);

@Select(columns = "username,age")
List<User> queryUser2param(Integer age, String username);

<select id="queryUser" resultMap="XXX">select username,age from T_User
    <where>
      <if test="age != null">AND age = #{age}</if>
    </where>
</select>

<select id="queryUser2param" resultMap="XXX">select username,age from T_User
    <where>
      <if test="age != null">AND age = #{age}</if>
      <if test="username != null">AND username = #{username}</if>
    </where>
</select>
複製程式碼

兩個函式生成的sql如上,@Select的屬性SqlMode預設是Selective,所以兩個都有條件判斷,但是這裡第一個函式的sql, Mybatis不支援,執行會報錯,類似no age getter in java.lang.Interger,Mybatis會把這唯一的一個引數當做物件來取裡面的值。 解決方法:函式簽名裡強加@Param()註解,或者@Select裡使用sqlMode = SqlMode.COMMON去掉生成sql裡的if判斷。 這個問題只會在方法只有一個引數的情況下發生,第二個函式生成的sql是ok的。

分頁

查詢引數裡@Limit@OffSet或查詢model裡@Page的分頁功能都比較原始,TgDao只是一款SQL生成器而已,因此你可以使用各種外掛, 或者與其他框架整合。對於分頁,可以無縫與PageHelper整合。

@Select
List<User> queryUser2(@Condition(criterion = Criterions.GREATER, column = "age") int min,
                @Condition(criterion = Criterions.LESS, column = "age") int max);


@Test
public void testQueryUser2() throws Exception {
   PageHelper.offsetPage(1, 10);
   List<User> users = mapper.queryUser2(12, 30);
   PageInfo page = new PageInfo<>(users);
   System.out.println(page.getTotal());
   Assert.assertTrue(page.getList().size() > 0);
}
複製程式碼

插入

@Insert(useGeneratedKeys = true, keyProperty = "id")//獲取自增id
int insert(User user);

@BatchInsert(columns = "username,age")//插入的列
int batchInsert(List<User> users);
複製程式碼

BatchInsert強烈建議寫columns,因為生成的語句並不會過濾null欄位,資料庫中插入null易報錯。


更新

@Update(columns = "username,age")//選擇更新某幾個列
@ModelConditions({
       @ModelCondition(field = "id")
})
int update(User user);
複製程式碼

刪除

@Delete
int delete(@Condition(criterion = Criterions.GREATER, column = "age") int min,
          @Condition(criterion = Criterions.LESS, column = "age") int max);

@Delete
@ModelConditions({
       @ModelCondition(attach = Attach.AND, field = "minAge", column = "age", criterion = Criterions.GREATER),
       @ModelCondition(attach = Attach.AND, field = "maxAge", column = "age", criterion = Criterions.LESS)
})
int delete2(UserSearch userSearch);
複製程式碼

selective

@Select@Count@Update@Delete都有selective這個屬性,這個屬性有兩個值,分別是SqlMode.COMMONSqlMode.SELECTIVE。 它們的區別在下面這段生成的xml裡顯示的很清楚,SqlMode.SELECTIVE引入了Mybatis的動態SQL能力。

  <!-- SELECTIVE -->
  <select id="queryUser" resultMap="BaseResultMap">select username,age from T_User 
    <where>
      <if test="name!=null and name!=''">AND username = #{name}</if>
      <if test="age != null">OR age = #{age}</if>
    </where>
  </select>
  
  <!-- COMMON -->
  <select id="queryUser" resultMap="BaseResultMap">select username,age from T_User 
    <where>
      AND username = #{name} OR age = #{age}
    </where>
  </select>
複製程式碼

@Select@Count預設的selective屬性是SqlMode.SELECTIVE,這樣查詢語句可以充分利用Mybatis的動態SQL能力。 而@Update@Delete預設是SqlMode.COMMON,這樣做的原因是:selective模式下如果引數全是null會使得where語句裡沒有任何條件, 最終變成全表的更新和刪除,這是一個極其危險的動作。所以@Update@Delete慎用SqlMode.SELECTIVE模式。

@Params

在介紹這個註解時要先介紹一下Mybatis自己的@Param註解,@Param註解在方法的引數上,給引數定義了一個名字, 這樣可以在xml的sql裡使用這個名字來取得引數所對應的值。如下:

    List<User> queryUser(@Param("name") String name);
    
    <select id="queryUser">select * from T_User where username=#{name} </select>
複製程式碼

明明引數就叫name,為什麼還要@Param註解一個名字name呢?這是因為Java編譯完,會丟掉引數名,以至於執行期mybatis不知道這個引數叫什麼,所以需要註解一個名字。 在執行時看到mybatis報錯如:Parameter 'XXX' not found. Available parameters are... 這就是沒有這個註解導致的問題。 但是在Java8裡我們已經可以通過給javac 新增-parameters引數來保留引數名字資訊,這樣mybatis會利用這個資訊,這樣就不需要加@Param註解了。 maven可以通過如下方式設定:

  <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.1</version>
      <configuration>
          <compilerArgs>
              <arg>-parameters</arg>
          </compilerArgs>
      </configuration>
  </plugin>
複製程式碼

然而有一種情況-parameters也無能為力,List<User> queryUser4(List ids);當引數是collection或者陣列型別時,mybatis依舊無法認出ids這個引數,只認collectionarray。 而@Params註解是Mybatis自身註解@Param-parameters外的另外一種解決方案。@Params可以註解在類和方法上, 被它註解的類和方法會在編譯期自動給所有方法引數加上@Param註解,它借鑑了lombok的方式在編譯期修改抽象語法樹從而改變編譯生成的位元組碼檔案。

    @Select(columns = "username,age")
    @Params
    List<User> queryUser(Integer age, String username);
    
    //編譯後
    List<User> queryUser(@Param("age") Integer var1, @Param("username") String var2);
複製程式碼

更多請看example


說明

  • 編譯生成的XML檔案與Mapper介面在同一個包下
  • 只支援Java8和MySql
  • 修改了原始碼中方法的定義或者model裡和資料表的對映關係,發現編譯出來的xml卻沒有改變,這是增量編譯的原因。生成一個xml同時需要model和mapper interface兩個部分, 如果你只修改了其中一個的程式碼,那麼另一個未修改的程式碼編譯器就不做處理,這樣這一次編譯就無法得到全部的資訊,所以TgDao無法生成最新版本的xml。 解決方法是每次mvn clean compile先清除一下編譯目錄,更好的方案正在尋找...

相關文章