ORM “杀器”之 JOOQ

ChrMagnus 5年前
   <h2>摘要</h2>    <p>介绍JOOQ简单实用,以及相对于传统ORM框架的不同点。</p>    <p><img src="https://simg.open-open.com/show/10d842c687c40f72a7b1878cddabfd36.jpg"></p>    <p> </p>    <h2>正文</h2>    <p>JOOQ是啥?</p>    <p>JOOQ 是基于Java访问关系型数据库的工具包,轻量,简单,并且足够灵活,可以轻松的使用Java面向对象语法来实现各种复杂的sql。对于写Java的码农来说ORMS再也熟悉不过了,不管是Hibernate或者Mybatis,都能简单的使用实体映射来访问数据库。但有时候这些 ‘智能’的对象关系映射又显得笨拙,没有直接使用原生sql来的灵活和简单,而且对于一些如:joins,union, nested selects等复杂的操作支持的不友好。JOOQ 既吸取了传统ORM操作数据的简单性和安全性,又保留了原生sql的灵活性,它更像是介于 ORMS和JDBC的中间层。对于喜欢写sql的码农来说,JOOQ可以完全满足你控制欲,可以是用Java代码写出sql的感觉来。就像官网说的那样 :</p>    <p>get back in control of your sql</p>    <p>这货有啥优点</p>    <p>JOOQ 目前在国内还是很小众,第一次听说这玩意还是通过 <a href="/misc/goto?guid=4959713456523672378" rel="nofollow,noindex">stream 大神</a> 的推荐。对于从 <strong>SSH</strong> 成长起来的猿类来说,心里也会质疑 “这玩意用的人那么少,靠不靠谱” ,“会不会有很多坑要踩”。通过对着官方文档写了几个demo,顿时心生敬畏,一个念头冲到脑袋 " 这东西一定会火",于是果断在项目中使用。在使用过程中也会遇到各种小问题,通过帮助手册和DEMO都能最终解决。相对于Hibernate或者其他ORMS的,JOOQ的编程模式有很大不同,强大的Fluent API使用起来非常方便和流畅。现在我们的项目( <a href="/misc/goto?guid=4959713456612097592" rel="nofollow,noindex">MaxWon</a> )使用JOOQ已经在生产环境运行了很长的一段时间,从来没花太多时间折腾在数据访问层上面。对于开发来说感受最深的就是这货真的很简单很灵活,正如文章标题那样,这是一个‘杀器’。下面是我总结的几点,个人愚见。</p>    <ul>     <li> <p>DSL(Domain Specific Language )风格,代码够简单和清晰。遇到不会写的sql可以充分利用IDEA代码提示功能轻松完成。</p> </li>     <li> <p>保留了传统ORM 的优点,简单操作性,安全性,类型安全等。不需要复杂的配置,并且可以利用Java 8 Stream API 做更加复杂的数据转换。</p> </li>     <li> <p>支持主流的RDMS和更多的特性,如self-joins,union,存储过程,复杂的子查询等等。</p> </li>     <li> <p>丰富的Fluent API和完善文档。</p> </li>     <li> <p><a href="/misc/goto?guid=4959713456702060203" rel="nofollow,noindex">runtime schema mapping</a> 可以支持多个数据库schema访问。简单来说使用一个连接池可以访问N个DB schema,使用比较多的就是SaaS应用的多租户场景。</p> </li>    </ul>    <p>如何使用</p>    <p>具体怎么使用 <a href="/misc/goto?guid=4958333388726067523" rel="nofollow,noindex">官网</a> 文档说的其实已经很详细了,爱学习的同学可以参阅一下。下面我根据实际项目中使用的过程讲述JOOQ的入门使用方法。</p>    <p>环境</p>    <table>     <thead>      <tr>       <th>描述</th>       <th>名称</th>      </tr>     </thead>     <tbody>      <tr>       <td>平台</td>       <td>JDK 1.8</td>      </tr>      <tr>       <td>maven</td>       <td>3.3.9</td>      </tr>      <tr>       <td>JOOQ</td>       <td>3.7.3</td>      </tr>      <tr>       <td>RDS</td>       <td>Mysql 5.7</td>      </tr>      <tr>       <td>mysql-connector</td>       <td>5.1.39</td>      </tr>     </tbody>    </table>    <p>maven依赖配置如下:</p>    <p style="text-align:center"> </p>    <p style="text-align:center"> </p>    <pre>  <code class="language-java"><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></code></pre>    <p>代码生成</p>    <p>目前官方提供了通过 java org.jooq.util.GenerationTool 来生成映射代码,但过程还是有点繁琐,这里就不演示了。还好万能的maven插件帮助我们解决了这个问题。</p>    <pre>  <code class="language-java"><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></code></pre>    <p>配置目标数据库schema信息后运行</p>    <pre>  <code class="language-java">$ mvn clean install -Djooq</code></pre>    <p>如果一切顺利的话,在项目目录下会看到JOOQ自动生成的代码</p>    <p><img src="https://simg.open-open.com/show/3a05618ae7f8a47dd1e489b52cbe1b39.jpg"></p>    <p>使用数据库的schema信息,JOOQ会自动生成对应的Java Record,这样就可以使用Record来操作对应的数据库和表,不需任何其他的关系映射配置。</p>    <p>下面展示使用JOOQ 增删改查的例子</p>    <pre>  <code class="language-java">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>    <p>首先根据mysql connection 信息构造DSLContext,然后使用它来对数据库进行增删改查操作。对于具体方法我就不解释了,懂一点sql我相信都应该能看懂。</p>    <p>上面例子可以窥探出JOOQ DSL 语法风格以及JOOQ的基本使用方法,通过代码可以so easy 的在脑子里映射出对应的sql语句,感觉就像直接写sql一样。但JOOQ和sql不同之处在于它保证了你写的sql语法正确性和类型安全,如果配上IDEA代码提示功能,那就更加完美了,再难写的sql只要 <strong>.</strong> 一下就会有完整的代码提示。</p>    <p>查看DSL类源码看以看到里面大概有14000多行代码,都是静态方法,里面包含JOOQ支持的各种DB操作。对于常用的的场景使用DSLContext一般都能满足需求,但是对于是一些复杂的需求,如创建一个临时表,column别名,table别名,schema 动态设置,就必须使用DSL来进行操作。</p>    <p>JOOQ最令人满意的就是在实际使用过程中解决问题的灵活性。下面将展示获取商品(prodcut)和商品评论(comment)总量逻辑。product 和comment 是通过product_id 关联。</p>    <p>直接上码</p>    <pre>  <code class="language-java">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;        });</code></pre>    <p>下面是原生sql的版本</p>    <pre>  <code class="language-java">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`=?;</code></pre>    <p>通过上面代码的对比可以看出JOOQ既享受了Java封装带来的便捷又保留了原生sql的灵活。</p>    <p>集成数据源</p>    <p>目前流行的数据源DHCP和c3p0大家都很熟悉了,没啥讲的。我们的项目使用的是阿里的 <a href="/misc/goto?guid=4958839583621930371" rel="nofollow,noindex">Druid</a> ,它是一个用于实时查询和分析的高容错、高性能开源分布式系统,旨在快速处理大规模的数据,并能够实现快速查询和分析。下面就以Druid为例演示把数据源绑定到JOOQ中</p>    <p>添加maven依赖</p>    <pre>  <code class="language-java"><dependency>        <groupId>com.alibaba</groupId>        <artifactId>druid</artifactId>        <version>1.0.20</version>    </dependency></code></pre>    <p>还是上面的JOOQTest demo,只需要重写 <strong>getDSLContext</strong> 方法</p>    <pre>  <code class="language-java">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);    }</code></pre>    <p>具体 <a href="/misc/goto?guid=4958858395578274471" rel="nofollow,noindex">Druid</a> 配置可以参考官方文档。</p>    <p>事务</p>    <p>JOOQ 官方提供了 <strong>TransactionProvider</strong> 对事务的支持,只需要在创建DSLContext的时候设置一下。代码如下:</p>    <pre>  <code class="language-java">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);</code></pre>    <p>下面展示事务的使用</p>    <pre>  <code class="language-java">@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;      });    }</code></pre>    <p>没错就这么简单,只需要把需要用事务的代码包在transaction里面,假如有异常发生,业务会自动回滚。需要注意一点的是必须使用configuration 重新构建context,要不然不会生效,这也是我为什么没有使用官方提供的事务管理器。正常的项目中一个业务需要组合若干个service 方法来完成,而官方提供的默认事务管理器就需要把所有业务写在一个方法中,这在实际应用中显然是不合理的。幸好JOOQ抽象了事务管理,这样我们就可以集成第三方的事务管理器。</p>    <p>以大家都熟悉的Spring事务管理器为例。添加依赖</p>    <pre>  <code class="language-java"><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></code></pre>    <pre>  <code class="language-java">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);</code></pre>    <pre>  <code class="language-java">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;      }    }</code></pre>    <p>集成完后 transaction 测试方法就可以这样写了</p>    <pre>  <code class="language-java">@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;      });    }</code></pre>    <p>其他特性</p>    <p>JOOQ还有很多其他有意思的特性 如对其他语言的支持,数据导出,存储过程,JPA支持等等,感兴趣的可以参阅一下文档。说到文档,不得不说开发者对JOOQ的用心,简单、详细、美观是最直接的感受,并且还有丰富的demo示例,对于编程新手来说上手使用也是手到擒来。</p>    <p>下面我就抱砖引玉,通过demo简单介绍一下ExecuteListener 的使用。ExecuteListener 可以看作是一个JOOQ执行的观察者,它可以监控SQL执行的整个生命周期。并且可以通过执行上下文,做一些个性化的操作。下面SlowQueryListener类的作用就是收集sql执行过程的慢查询日志。</p>    <pre>  <code class="language-java">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>    <p>在初始化DSLContext 的时候把SlowQueryListener配置进去 代码如下:</p>    <pre>  <code class="language-java">Configuration configuration = new DefaultConfiguration()        .set(new DataSourceConnectionProvider(proxy))        .set(new SpringTransactionProvider(txMgr))        .set(SQLDialect.MYSQL)          .set(DefaultExecuteListenerProvider.providers(new SlowQueryListener()));//配置执行监听器</code></pre>    <p>执行时间超过1s的sql,会打印如下日志</p>    <pre>  <code class="language-java">Slow SQL query meta executed : [ call ama_procedure.ama_app('57a013edaa150a000101ffca') ]: Total: 3.644s</code></pre>    <h3>写在最后</h3>    <p>对于在国内占了大半边天的Hibernate/Mybatis,JOOQ还是一个小清新,很多人对它都还陌生。通过上面的简单介绍,也许对你有一点帮助。无论是强大的数据转换能力还是处理业务的灵活性,简洁性,都会带来一些不一样的体验。如果你已经厌倦了ORMS的开发模式,正好又接手一个新的项目,JOOQ也许是一个不错的选择。</p>    <p> </p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000006748584</p>    <p> </p>