ORM “杀器”之 JOOQ

IN 后端编程,JAVA,敏捷开发,数据库

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

(图片来自http://www.jooq.org/)

这货有啥优点

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依赖配置如下:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    <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插件帮助我们解决了这个问题。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<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信息后运行

 
1
$ mvn clean install -Djooq

如果一切顺利的话,在项目目录下会看到JOOQ自动生成的代码

使用数据库的schema信息,JOOQ会自动生成对应的Java Record,这样就可以使用Record来操作对应的数据库和表,不需任何其他的关系映射配置。

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

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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 关联。

直接上码

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的版本

 
1
2
3
4
5
6
7
8
9
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依赖

 
1
2
3
4
5
  <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.0.20</version>
  </dependency>

还是上面的JOOQTest demo,只需要重写getDSLContext 方法

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  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的时候设置一下。代码如下:

 
1
2
3
4
5
6
7
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);

下面展示事务的使用

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @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事务管理器为例。添加依赖

 
1
2
3
4
5
6
7
8
9
10
<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>
 
1
2
3
4
5
6
7
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);
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 测试方法就可以这样写了

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @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执行过程的慢查询日志。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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配置进去 代码如下:

 
1
2
3
4
5
Configuration configuration = new DefaultConfiguration()
      .set(new DataSourceConnectionProvider(proxy))
      .set(new SpringTransactionProvider(txMgr))
      .set(SQLDialect.MYSQL)  
      .set(DefaultExecuteListenerProvider.providers(new SlowQueryListener()));//配置执行监听器

执行时间超过1s的sql,会打印如下日志

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

写在最后

对于在国内占了大半边天的Hibernate/Mybatis,JOOQ还是一个小清新,很多人对它都还陌生。通过上面的简单介绍,也许对你有一点帮助。无论是强大的数据转换能力还是处理业务的灵活性,简洁性,都会带来一些不一样的体验。如果你已经厌倦了ORMS的开发模式,正好又接手一个新的项目,JOOQ也许是一个不错的选择。

作者信息

本文系力谱宿云 LeapCloud旗下MaxLeap团队_数据服务组 成员:马传林【原创】

– 力谱宿云 LeapCloud 首发 –

马传林,从事开发工作已经有多年。当前在MaxLeap数据服务组担任开发工程师,主要负责MaxWon服务器开发。

ORM “杀器”之 JOOQ的更多相关文章

  1. [NewLife.XCode]反向工程(自动建表建库大杀器)

    NewLife.XCode是一个有10多年历史的开源数据中间件,支持nfx/netstandard,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode. 整个系列教程会大量结合示 ...

  2. 使用docker-compose 大杀器来部署服务 上

    使用docker-compose 大杀器来部署服务 上 我们都听过或者用过 docker,然而使用方式却是仅仅用手动的方式,这样去操作 docker 还是很原始. 好吧,可能在小白的眼中噼里啪啦的对着 ...

  3. 使用docker-compose 大杀器来部署服务 上(转)

    使用docker-compose 大杀器来部署服务 上 我们都听过或者用过 docker,然而使用方式却是仅仅用手动的方式,这样去操作 docker 还是很原始. 好吧,可能在小白的眼中噼里啪啦的对着 ...

  4. Postgresql-模糊匹配大杀器

    # Postgresql-模糊匹配大杀器 ## 问题背景 随着pg越来越强大,abase目前已经升级到5.0(postgresql10.4),目前abase5.0继承了全文检索插件(zhparser) ...

  5. 一文读懂机器学习大杀器XGBoost原理

    http://blog.itpub.net/31542119/viewspace-2199549/ XGBoost是boosting算法的其中一种.Boosting算法的思想是将许多弱分类器集成在一起 ...

  6. 利用pentestbox打造ms17-010移动"杀器"

    本文首发Freebuf,属原创奖励计划,未经许可禁止转载. 链接:http://www.freebuf.com/articles/system/132274.html 一. 前言 前段时间Shadow ...

  7. [转]使用docker-compose 大杀器来部署服务 上

    本文转自:https://www.cnblogs.com/neptunemoon/p/6512121.html 使用docker-compose 大杀器来部署服务 上 我们都听过或者用过 docker ...

  8. 离群点检测与序列数据异常检测以及异常检测大杀器-iForest

    1. 异常检测简介 异常检测,它的任务是发现与大部分其他对象不同的对象,我们称为异常对象.异常检测算法已经广泛应用于电信.互联网和信用卡的诈骗检测.贷款审批.电子商务.网络入侵和天气预报等领域.这些异 ...

  9. 使用docker-compose 大杀器来部署服务

    使用docker-compose 大杀器来部署服务 上 我们都听过或者用过 docker,然而使用方式却是仅仅用手动的方式,这样去操作 docker 还是很原始. 好吧,可能在小白的眼中噼里啪啦的对着 ...

随机推荐

  1. asp.net mvc 5 关闭xss过滤

    在控制器方法的头部添加        [ValidateInput(false)] 如果向mvc服务端提交带html标签的内容就会导致校验失败异常,从而得不到想要的结果,关闭的方法是在相应方法头部添加 ...

  2. 初步谈谈 C# 多线程、异步编程与并发服务器

    多线程与异步编程可以达到避免调用线程异步阻塞作用,但是两者还是有点不同. 多线程与异步编程的异同: 1.线程是cpu 调度资源和分配的基本单位,本质上是进程中的一段并发执行的代码. 2.线程编程的思维 ...

  3. epoll 实现回射服务器

    epoll是I/O复用模型中相对epoll和select更高效的实现对套接字管理的函数. epoll有两种模式 LT 和 ET 二者的差异在于 level-trigger 模式下只要某个 socket ...

  4. 设计模式——外观模式(C++实现)

    #include <string> #include <iostream> using namespace std; class STSystemA { public: voi ...

  5. es6使用技巧

    ##1.通过参数默认值实现强制参数 ES6 的参数默认值只有在真正使用时才会求值.这可以让你强制确保提供参数: /** * Called if a parameter is missing and * ...

  6. Mycat 读写分离详解

    Mycat 的读写分离是依赖数据库级别的数据主从同步的基础上来实现的(Mysql 的主从配置链接),Mycat 的读写分离是在 schema.xml 配置的 dataHost 节点的 balance ...

  7. 前端的UI设计与交互之文案篇

    在界面中,我们需要通过对话的方式与用户产生共鸣.精准.清晰的语言会更容易让用户理解,合适的语气更容易让用户建立信任感.因此在界面设计时,文案也应当被重视. 在使用和书写文案时有以下几点需要注意:从用户 ...

  8. 前端的UI设计与交互之图标篇

    图标是具有指代意义的图形,也是一种标识.通过使用图标表达命令,强调状态,表示产品或类别.为了系统及跨平台之间图形认知保持一致, 图标在设计和使用时有以下两个原则点需要注意:简单的图形语言以及高辨识度. ...

  9. c++编码及读写文件

    写文件 #include <fstream> #include <iostream> using namespace std; int main() { ofstream ou ...

  10. MYSQL数据库学习十一 多表数据记录查询

    11.1 关系数据操作 并(UNION):把具有相同字段数目和字段类型的表合并到一起. 笛卡尔积(CARTESIAN PRODUCT):没有连接条件表关系的返回结果.字段数=table1字段数+tab ...