ORM “殺器”之 JOOQ

ChrMagnus 8年前發布 | 98K 次閱讀 數據持久化 SQL 持久層框架

摘要

介紹JOOQ簡單實用,以及相對于傳統ORM框架的不同點。

 

正文

JOOQ是啥?

JOOQ 是基于Java訪問關系型數據庫的工具包,輕量,簡單,并且足夠靈活,可以輕松的使用Java面向對象語法來實現各種復雜的sql。對于寫Java的碼農來說ORMS再也熟悉不過了,不管是Hibernate或者Mybatis,都能簡單的使用實體映射來訪問數據庫。但有時候這些 ‘智能’的對象關系映射又顯得笨拙,沒有直接使用原生sql來的靈活和簡單,而且對于一些如:joins,union, nested selects等復雜的操作支持的不友好。JOOQ 既吸取了傳統ORM操作數據的簡單性和安全性,又保留了原生sql的靈活性,它更像是介于 ORMS和JDBC的中間層。對于喜歡寫sql的碼農來說,JOOQ可以完全滿足你控制欲,可以是用Java代碼寫出sql的感覺來。就像官網說的那樣 :

get back in control of your sql

這貨有啥優點

JOOQ 目前在國內還是很小眾,第一次聽說這玩意還是通過 stream 大神 的推薦。對于從 SSH 成長起來的猿類來說,心里也會質疑 “這玩意用的人那么少,靠不靠譜” ,“會不會有很多坑要踩”。通過對著官方文檔寫了幾個demo,頓時心生敬畏,一個念頭沖到腦袋 " 這東西一定會火",于是果斷在項目中使用。在使用過程中也會遇到各種小問題,通過幫助手冊和DEMO都能最終解決。相對于Hibernate或者其他ORMS的,JOOQ的編程模式有很大不同,強大的Fluent API使用起來非常方便和流暢。現在我們的項目( MaxWon )使用JOOQ已經在生產環境運行了很長的一段時間,從來沒花太多時間折騰在數據訪問層上面。對于開發來說感受最深的就是這貨真的很簡單很靈活,正如文章標題那樣,這是一個‘殺器’。下面是我總結的幾點,個人愚見。

  • DSL(Domain Specific Language )風格,代碼夠簡單和清晰。遇到不會寫的sql可以充分利用IDEA代碼提示功能輕松完成。

  • 保留了傳統ORM 的優點,簡單操作性,安全性,類型安全等。不需要復雜的配置,并且可以利用Java 8 Stream API 做更加復雜的數據轉換。

  • 支持主流的RDMS和更多的特性,如self-joins,union,存儲過程,復雜的子查詢等等。

  • 豐富的Fluent API和完善文檔。

  • runtime schema mapping 可以支持多個數據庫schema訪問。簡單來說使用一個連接池可以訪問N個DB schema,使用比較多的就是SaaS應用的多租戶場景。

如何使用

具體怎么使用 官網 文檔說的其實已經很詳細了,愛學習的同學可以參閱一下。下面我根據實際項目中使用的過程講述JOOQ的入門使用方法。

環境

描述 名稱
平臺 JDK 1.8
maven 3.3.9
JOOQ 3.7.3
RDS Mysql 5.7
mysql-connector 5.1.39

maven依賴配置如下:

 

 

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jooq</groupId>
            <artifactId>jooq</artifactId>
            <version>${jooq.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jooq</groupId>
            <artifactId>jooq-meta</artifactId>
            <version>${jooq.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jooq</groupId>
            <artifactId>jooq-codegen</artifactId>
            <version>${jooq.version}</version>
        </dependency>

代碼生成

目前官方提供了通過 java org.jooq.util.GenerationTool 來生成映射代碼,但過程還是有點繁瑣,這里就不演示了。還好萬能的maven插件幫助我們解決了這個問題。

<profiles>
   <profile>
      <id>jooq</id>
      <properties />
      <activation>
         <property>
            <name>jooq</name>
         </property>
      </activation>
      <build>
         <plugins>
            <plugin>
               <groupId>org.jooq</groupId>
               <artifactId>jooq-codegen-maven</artifactId>
               <version>${jooq.version}</version>
               <executions>
                  <execution>
                     <goals>
                        <goal>generate</goal>
                     </goals>
                  </execution>
               </executions>
               <dependencies>
                  <dependency>
                     <groupId>mysql</groupId>
                     <artifactId>mysql-connector-java</artifactId>
                     <version>${mysql.version}</version>
                  </dependency>
               </dependencies>
               <configuration>
                  <jdbc>
                     <driver>${jdbc.driver}</driver>
                     <url>${jdbc.url}</url>
                     <user>${jdbc.user}</user>
                     <password>${jdbc.password}</password>
                  </jdbc>
                  <generator>
                     <database>
                        <name>org.jooq.util.mysql.MySQLDatabase</name>
                        <includes>.*</includes>
                        <excludes />
                        <inputSchema>${jdbc.database.name}</inputSchema>
                        <forcedTypes>
                           <forcedType>
                              <name>BOOLEAN</name>
                              <types>(?i:TINYINT(\s*\(\d+\))?(\s*UNSIGNED)?)</types>
                           </forcedType>
                        </forcedTypes>
                     </database>
                     <generate>
                        <deprecated>false</deprecated>
                     </generate>
                     <target>
                        <packageName>com.maxleap.jooq.data.jooq</packageName>
                        <directory>src/main/java</directory>
                     </target>
                     <generate>
                        <pojos>false</pojos>
                        <daos>false</daos>
                     </generate>
                  </generator>
               </configuration>
            </plugin>
         </plugins>
      </build>
   </profile>
</profiles>

配置目標數據庫schema信息后運行

$ mvn clean install -Djooq

如果一切順利的話,在項目目錄下會看到JOOQ自動生成的代碼

使用數據庫的schema信息,JOOQ會自動生成對應的Java Record,這樣就可以使用Record來操作對應的數據庫和表,不需任何其他的關系映射配置。

下面展示使用JOOQ 增刪改查的例子

public class JOOQTest {
  private DSLContext dslContext;

@Before public void before() { this.dslContext = getDSLContext(); }

@Test public void insert() { MyStore store = new MyStore(); store.setName("foo"); store.setAddress("mars No. 1989"); StoreRecord storeRecord = dslContext.newRecord(Tables.STORE, store); storeRecord.insert();

dslContext.insertInto(Tables.STORE)
  .set(Store.STORE.NAME, "bar")
  .set(Store.STORE.ADDRESS, "eclipse No.1891")
  .execute();

}

@Test public void find() { dslContext.selectFrom(Tables.STORE) .where(Store.STORE.NAME.eq("foo")) .fetchInto(MyStore.class) .stream() .forEach(myStore -> System.out.println(myStore.getName())); }

@Test public void update() {

dslContext.update(Tables.STORE)
  .set(Store.STORE.ADDRESS, "sun No.1988")
  .where(Store.STORE.ID.eq(UInteger.valueOf(1)))
  .execute();

}

@After public void after() { dslContext.delete(Tables.STORE); }

private DSLContext getDSLContext() { try { Connection connection = DriverManager.getConnection("jdbc:mysql://2.mysql.myself:3306/app_maker", "mars","mars"); return DSL.using(connection, SQLDialect.MYSQL) } catch (Exception e) { e.printStackTrace(); } return null; }

public static class MyStore { private String name; private String address;

public String getName() {
  return name;
}

public void setName(String name) {
  this.name = name;
}

public String getAddress() {
  return address;
}

public void setAddress(String address) {
  this.address = address;
}

} }</code></pre>

首先根據mysql connection 信息構造DSLContext,然后使用它來對數據庫進行增刪改查操作。對于具體方法我就不解釋了,懂一點sql我相信都應該能看懂。

上面例子可以窺探出JOOQ DSL 語法風格以及JOOQ的基本使用方法,通過代碼可以so easy 的在腦子里映射出對應的sql語句,感覺就像直接寫sql一樣。但JOOQ和sql不同之處在于它保證了你寫的sql語法正確性和類型安全,如果配上IDEA代碼提示功能,那就更加完美了,再難寫的sql只要 . 一下就會有完整的代碼提示。

查看DSL類源碼看以看到里面大概有14000多行代碼,都是靜態方法,里面包含JOOQ支持的各種DB操作。對于常用的的場景使用DSLContext一般都能滿足需求,但是對于是一些復雜的需求,如創建一個臨時表,column別名,table別名,schema 動態設置,就必須使用DSL來進行操作。

JOOQ最令人滿意的就是在實際使用過程中解決問題的靈活性。下面將展示獲取商品(prodcut)和商品評論(comment)總量邏輯。product 和comment 是通過product_id 關聯。

直接上碼

List<MyProduct> products = dslContext.select()
      .from(Tables.PRODUCT)
      .leftJoin(DSL.table(
          DSL.select(Comment.COMMENT.PRODUCT_ID, DSL.count().as("comment_num"))
            .from(Tables.COMMENT) 
            .where(Comment.COMMENT.PRODUCT_ID.in(ids))
            .groupBy(Comment.COMMENT.PRODUCT_ID)
        ).as("c1")
      )
      .on(Product.PRODUCT.ID.eq(DSL.field(DSL.name("c1",  
          Comment.COMMENT.PRODUCT_ID.getName()),UInteger.class))) 
      .where(Product.PRODUCT.ID.in(ids))
      .fetch()
      .map(record -> {        
        MyProduct product = record.into(MyProduct.class); 
        return product;
      });

下面是原生sql的版本

select * from `product` as `prod` 
left outer join
  (select  `comment`.`product_id`,count(*) as `comment_num` from `comment` 
   where `commment`.`product_id`=?
   group by `comment`.`product_id`
  ) 
as `c1`
on `prod`.`id`=`c1`.`product_id`
where `prod`.`id`=?;

通過上面代碼的對比可以看出JOOQ既享受了Java封裝帶來的便捷又保留了原生sql的靈活。

集成數據源

目前流行的數據源DHCP和c3p0大家都很熟悉了,沒啥講的。我們的項目使用的是阿里的 Druid ,它是一個用于實時查詢和分析的高容錯、高性能開源分布式系統,旨在快速處理大規模的數據,并能夠實現快速查詢和分析。下面就以Druid為例演示把數據源綁定到JOOQ中

添加maven依賴

<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.0.20</version>
  </dependency>

還是上面的JOOQTest demo,只需要重寫 getDSLContext 方法

private DSLContext getDSLContext() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl("jdbc:mysql://localhost:3306/app_maker");
    dataSource.setUsername("mars");
    dataSource.setPassword("mars");
    dataSource.setMaxActive(20);
    dataSource.setMaxWait(20_000);
    dataSource.setMinIdle(0);
    dataSource.setTestOnBorrow(true);
    dataSource.setTestWhileIdle(true);
    dataSource.setInitialSize(1);
    dataSource.setMinEvictableIdleTimeMillis(1000*60*10);
    dataSource.setTimeBetweenEvictionRunsMillis(60*1000);
    dataSource.setPoolPreparedStatements(true);
    dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
    dataSource.setValidConnectionChecker(new MySqlValidConnectionChecker());
    ConnectionProvider connectionProvider =  new DataSourceConnectionProvider(dataSource)
    Configuration configuration = new DefaultConfiguration()
      .set(connectionProvider)
      .set(SQLDialect.MYSQL);
    return DSL.using(configuration);
  }

具體 Druid 配置可以參考官方文檔。

事務

JOOQ 官方提供了 TransactionProvider 對事務的支持,只需要在創建DSLContext的時候設置一下。代碼如下:

ConnectionProvider connectionProvider =  new DataSourceConnectionProvider(dataSource)
TransactionProvider transactionProvider = new DefaultTransactionProvider(connectionProvider, false);
Configuration configuration = new DefaultConfiguration()
      .set(connectionProvider)
      .set(transactionProvider)
      .set(SQLDialect.MYSQL);
return DSL.using(configuration);

下面展示事務的使用

@Test
  public void transaction() {
    dslContext.transaction(configuration -> {
      DSL.using(configuration).update(Tables.STORE)
        .set(Store.STORE.ADDRESS, "transaction test1")
        .where(Store.STORE.ID.eq(UInteger.valueOf(1)))
        .execute();
      DSL.using(configuration).update(Tables.STORE)
        .set(Store.STORE.ADDRESS, "transaction test1")
        .where(Store.STORE.ID.eq(UInteger.valueOf(2)))
        .execute();
      int i = 1/0;
    });
  }

沒錯就這么簡單,只需要把需要用事務的代碼包在transaction里面,假如有異常發生,業務會自動回滾。需要注意一點的是必須使用configuration 重新構建context,要不然不會生效,這也是我為什么沒有使用官方提供的事務管理器。正常的項目中一個業務需要組合若干個service 方法來完成,而官方提供的默認事務管理器就需要把所有業務寫在一個方法中,這在實際應用中顯然是不合理的。幸好JOOQ抽象了事務管理,這樣我們就可以集成第三方的事務管理器。

以大家都熟悉的Spring事務管理器為例。添加依賴

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>4.1.2.RELEASE</version>
 </dependency>
 <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>4.1.2.RELEASE</version>
 </dependency>
TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(druidDataSource);
 DataSourceTransactionManager txMgr =  new DataSourceTransactionManager(druidDataSource);
 Configuration configuration = new DefaultConfiguration()
      .set(new DataSourceConnectionProvider(proxy))
      .set(new SpringTransactionProvider(txMgr))
      .set(SQLDialect.MYSQL);
 return DSL.using(configuration);
public class SpringTransactionProvider implements TransactionProvider {
    private static final JooqLogger log = JooqLogger.getLogger(SpringTransactionProvider.class);   
    DataSourceTransactionManager txMgr;
    public SpringTransactionProvider(DataSourceTransactionManager txMgr){
        this.txMgr = txMgr;
    }
    @Override
    public void begin(TransactionContext ctx) {
        log.debug("Begin transaction");
        TransactionStatus tx = txMgr.getTransaction(new DefaultTransactionDefinition());
        ctx.transaction(new SpringTransaction(tx));
    }
    @Override
    public void commit(TransactionContext ctx) {
        log.debug("commit transaction");
        txMgr.commit(((SpringTransaction) ctx.transaction()).tx);
    }
    @Override
    public void rollback(TransactionContext ctx) {
        log.debug("rollback transaction");
        txMgr.rollback(((SpringTransaction) ctx.transaction()).tx);
    }
}
public class SpringTransaction implements Transaction {
    final TransactionStatus tx;
    SpringTransaction(TransactionStatus tx) {
      this.tx = tx;
    }
  }

集成完后 transaction 測試方法就可以這樣寫了

@Test
  public void transaction(){
    dslContext.transaction(configuration -> {
     dslContext.update(Tables.STORE) //共用同一個context
        .set(Store.STORE.ADDRESS, "transaction test3")
        .where(Store.STORE.ID.eq(UInteger.valueOf(1)))
        .execute();
      dslContext.update(Tables.STORE)
        .set(Store.STORE.ADDRESS, "transaction test4")
        .where(Store.STORE.ID.eq(UInteger.valueOf(2)))
        .execute();
      int i = 1/0;
    });
  }

其他特性

JOOQ還有很多其他有意思的特性 如對其他語言的支持,數據導出,存儲過程,JPA支持等等,感興趣的可以參閱一下文檔。說到文檔,不得不說開發者對JOOQ的用心,簡單、詳細、美觀是最直接的感受,并且還有豐富的demo示例,對于編程新手來說上手使用也是手到擒來。

下面我就抱磚引玉,通過demo簡單介紹一下ExecuteListener 的使用。ExecuteListener 可以看作是一個JOOQ執行的觀察者,它可以監控SQL執行的整個生命周期。并且可以通過執行上下文,做一些個性化的操作。下面SlowQueryListener類的作用就是收集sql執行過程的慢查詢日志。

class SlowQueryListener extends DefaultExecuteListener {
  private Logger logger = LoggerFactory.getLogger(SlowQueryListener.class);
  StopWatch watch;

@Override public void executeStart(ExecuteContext ctx) { super.executeStart(ctx); watch = new StopWatch(); }

@Override public void executeEnd(ExecuteContext ctx) { try{ super.executeEnd(ctx); if (watch.split() > 1_000_000_000L) {//記錄執行時間超過1s的操作 ExecuteType type = ctx.type(); StringBuffer sqlBuffer = new StringBuffer(); if(type == ExecuteType.BATCH) { for(Query query:ctx.batchQueries()) { sqlBuffer.append(query.toString()).append("\n"); } }else { sqlBuffer.append(ctx.query() == null ? "blank query ":ctx.query().toString()); } watch.splitInfo(String.format("Slow SQL query meta executed : [ %s ]", sqlBuffer.toString() )); } }catch (Exception e) { logger.error(" SlowQueryListener has occur,fix bug ",e); } } }</code></pre>

在初始化DSLContext 的時候把SlowQueryListener配置進去 代碼如下:

Configuration configuration = new DefaultConfiguration()
      .set(new DataSourceConnectionProvider(proxy))
      .set(new SpringTransactionProvider(txMgr))
      .set(SQLDialect.MYSQL)  
      .set(DefaultExecuteListenerProvider.providers(new SlowQueryListener()));//配置執行監聽器

執行時間超過1s的sql,會打印如下日志

Slow SQL query meta executed : [ call ama_procedure.ama_app('57a013edaa150a000101ffca') ]: Total: 3.644s

寫在最后

對于在國內占了大半邊天的Hibernate/Mybatis,JOOQ還是一個小清新,很多人對它都還陌生。通過上面的簡單介紹,也許對你有一點幫助。無論是強大的數據轉換能力還是處理業務的靈活性,簡潔性,都會帶來一些不一樣的體驗。如果你已經厭倦了ORMS的開發模式,正好又接手一個新的項目,JOOQ也許是一個不錯的選擇。

 

 

來自:https://segmentfault.com/a/1190000006748584

 

 本文由用戶 ChrMagnus 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!