ORM 「殺器」之 JOOQ

摘要

介紹JOOQ簡單實用,以及相對於傳統ORM框架的不一樣點。java

maxwon_chuangtong

(圖片來自http://www.jooq.org/mysql

正文

JOOQ是啥?

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

get back in control of your sqlgithub

這貨有啥優勢

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

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

  • 保留了傳統ORM 的優勢,簡單操做性,安全性,類型安全等。不須要複雜的配置,而且能夠利用Java 8 Stream API 作更加複雜的數據轉換。sql

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

  • 豐富的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自動生成的代碼

jooqgenrator

使用數據庫的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;
    }
  }
}

首先根據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);
    } 
  }
}

在初始化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也許是一個不錯的選擇。

做者信息
本文系力譜宿雲 LeapCloud旗下MaxLeap團隊_數據服務組 成員:馬傳林【原創】
力譜宿雲首發:https://blog.maxleap.cn/archi...
馬傳林,從事開發工做已經有多年。當前在MaxLeap數據服務組擔任開發工程師,主要負責MaxWon服務器開發。

做者往期佳做
移動雲平臺的基礎架構之旅(一):雲應用

歡迎關注微信公衆號:MaxLeap_yidongyanfa


關於 MaxLeap
官網:https://maxleap.cn/簡介:MaxLeap 移動業務研發的雲服務平臺,爲企業提供包括應用開發所需的後端雲數據庫、雲數據源、雲代碼、雲容器、 IM、移動支付、應用內社交、第三方登陸、社交分享、數據分析、推送營銷,用戶支持等服務, MaxLeap 致力於讓移動應用開發更快速簡單。

相關文章
相關標籤/搜索