关于ORM(对象-关系映射)使用的争论持续十年之久。一方面,许多人认为Hibernate和JPA很好的解决了许多问题(尤其是复杂对象的持久化方面)。而另一部分人认为,复杂的映射几乎扼杀了以数据为中心的应用。JPA通过在目标类型上使用硬编码方式的注解来建立标准的、声明式映射规则解决映射问题。我们认为很多的以数据为中心的问题不应该被限制在注解的狭小范围内,而应该使用更好的方式。Java 8通过新的Streams API,可以用很简洁的方式来解决这个问题。
我们通过一个很简单的实例开始,这个例子中的我们使用H2的INFORMATION_SCHEMA来存储表及列。我们将建立一个 Map<String, List<String>>
类型的数据结构来包含这些信息。为了使SQL关联简单,我们使用 jOOQ:
public static void main(String[] args) throws Exception {
Class.forName("org.h2.Driver");
try (Connection c = getConnection(
"jdbc:h2:~/sql-goodies-with-mapping",
"sa", "")) {
// 这条SQL语句在H2 schema中产生表名及列名
String sql =
"select table_name, column_name " +
"from information_schema.columns " +
"order by " +
"table_catalog, " +
"table_schema, " +
"table_name, " +
"ordinal_position";
// 这是使用jOOQ的方式来执行以上的语句。
// Result类实现了List接口, 这样使得剩下的
// 步骤更加简单
Result<Record> result =
DSL.using(c)
.fetch(sql)
}
}
现在我们先建立这个查询,然后我们看如何通过jOOQ的Result生成一个Map<String, List<String>>
:
DSL.using(c)
.fetch(sql)
.stream()
.collect(groupingBy(
r -> r.getValue("TABLE_NAME"),
mapping(
r -> r.getValue("COLUMN_NAME"),
toList()
)
))
.forEach(
(table, columns) ->
System.out.println(table + ": " + columns)
);
```
上面的例子产生如下的输出:
FUNCTION_COLUMNS: [ALIAS_CATALOG, ALIAS_SCHEMA, …]
CONSTANTS: [CONSTANT_CATALOG, CONSTANT_SCHEMA, …]
SEQUENCES: [SEQUENCE_CATALOG, SEQUENCE_SCHEMA, …]
这是如何实现的?让我们一步一步的来看这段程序:
DSL.using(c)
.fetch(sql)
//这里,我们将一个List转换为一个Stream对象
.stream()
//我们将这个Stream元素放到一个新的集合类型中
.collect(
//这个集合是一个分组行成的Map
groupingBy(
//分组操作后的组的key是JOOQ记录的TABLE_NAME的值
r -> r.getValue(“TABLE_NAME”),
//分组的value是通过mapping表达式生成的
mapping(
//该表达式主要将每条分组得到jOOQ记录映射到对应COLUMN_NAME值上
r -> r.getValue(“COLUMN_NAME”),
//然后再将所有的值收集到一个java.util.List中,
toList()
)
))
//一旦得到了这个List
import static java.util.stream.Collectors.*;
同时也注意,输出不在是数据库的顺序。这是因为分组后的集合返回的是一个java.util.HashMap
。在某些情况下,我们可能更倾向于用java.util.LinkedHashMap
来存储这些数据,这样能保证插入集合的顺序:
DSL.using(c)
.fetch(sql)
.stream()
.collect(groupingBy(
r -> r.getValue("TABLE_NAME"),
// Add this Supplier to the groupingBy
// method call
LinkedHashMap::new,
mapping(
r -> r.getValue("COLUMN_NAME"),
toList()
)
))
.forEach(...);
我们继续进行其他有意义的结果转换。想象一下,我们可能需要从上面的方案中生成一条简单的DDL。非常简单。首先,我么需要选择列的数据类型,我们简单的增加一条SQL查询:
String sql =
"select " +
"table_name, " +
"column_name, " +
"type_name " + // 增加type列
"from information_schema.columns " +
"order by " +
"table_catalog, " +
"table_schema, " +
"table_name, " +
我也为这个例子引入了一个新的局部类,用以包装name和type属性:
class Column {
final String name;
final String type;
Column(String name, String type) {
this.name = name;
this.type = type;
}
}
现在,我们看一下是怎样改变Streams API的方法调用的:
result
.stream()
.collect(groupingBy(
r -> r.getValue("TABLE_NAME"),
LinkedHashMap::new,
mapping(
// 我们现在使用新的封装的类型来替代原来的COLUMN_NAME
r -> new Column(
r.getValue("COLUMN_NAME", String.class),
r.getValue("TYPE_NAME", String.class)
),
toList()
)
))
.forEach(
(table, columns) -> {
// 仅发出一条创建表格的语句
System.out.println(
"CREATE TABLE " + table + " (");
// 将"Column"类型的指定列转换为字符串,
// 通过逗号与换行符进行连接。
System.out.println(
columns.stream()
.map(col -> " " + col.name +
" " + col.type)
.collect(Collectors.joining(",\n"))
);
System.out.println(");");
}
);
输出结果好得不可能更好了!
CREATE TABLE CATALOGS(
CATALOG_NAME VARCHAR
);
CREATE TABLE COLLATIONS(
NAME VARCHAR,
KEY VARCHAR
);
CREATE TABLE COLUMNS(
TABLE_CATALOG VARCHAR,
TABLE_SCHEMA VARCHAR,
TABLE_NAME VARCHAR,
COLUMN_NAME VARCHAR,
ORDINAL_POSITION INTEGER,
COLUMN_DEFAULT VARCHAR,
IS_NULLABLE VARCHAR,
DATA_TYPE INTEGER,
CHARACTER_MAXIMUM_LENGTH INTEGER,
CHARACTER_OCTET_LENGTH INTEGER,
NUMERIC_PRECISION INTEGER,
NUMERIC_PRECISION_RADIX INTEGER,
NUMERIC_SCALE INTEGER,
CHARACTER_SET_NAME VARCHAR,
COLLATION_NAME VARCHAR,
TYPE_NAME VARCHAR,
NULLABLE INTEGER,
IS_COMPUTED BOOLEAN,
SELECTIVITY INTEGER,
CHECK_CONSTRAINT VARCHAR,
SEQUENCE_NAME VARCHAR,
REMARKS VARCHAR,
SOURCE_DATA_TYPE SMALLINT
);
激动吗?ORM的时代将要终结了
再次强烈声明:ORM的时代要终结了。为什么?因为使用函数表达式来转换数据集合是软件工程中最强大的概念之一。函数式编程是非常灵活,又极具表现力。它是数据和数据流处理的核心。Java开发者都知道存在函数式编程语言。例如,每个人都曾使用SQL。思考一下,声明一个表资源,然后投影或转换为一个数组流,然后将其作为派生表返回给更高级别的SQL语句或者直接返回给java程序。
如果使用XML的话,我们可以通过XProc pipelining声明一个XSLT来转换XML,然后将结果返回给其他XML实体,如其他的XSL样式表。
Java 8的Streams正式如此。使用SQL和Streams API是数据处理的最有力的概念之一。如果增加JOOQ到其中,你就可以方便的实现对数据库类型安全的访问及查询。想象一下使用jOOQ API写SQL来代替直接使用SQL字符串:
整个方法链可以用一个单独的数据流转换链,像这样:
DSL.using(c)
.select(
COLUMNS.TABLE_NAME,
COLUMNS.COLUMN_NAME,
COLUMNS.TYPE_NAME
)
.from(COLUMNS)
.orderBy(
COLUMNS.TABLE_CATALOG,
COLUMNS.TABLE_SCHEMA,
COLUMNS.TABLE_NAME,
COLUMNS.ORDINAL_POSITION
)
.fetch() // jOOQ结束
.stream() // Streams开始
.collect(groupingBy(
r -> r.getValue(COLUMNS.TABLE_NAME),
LinkedHashMap::new,
mapping(
r -> new Column(
r.getValue(COLUMNS.COLUMN_NAME),
r.getValue(COLUMNS.TYPE_NAME)
),
toList()
)
))
.forEach(
(table, columns) -> {
// Just emit a CREATE TABLE statement
System.out.println(
"CREATE TABLE " + table + " (");
System.out.println(
columns.stream()
.map(col -> " " + col.name +
" " + col.type)
.collect(Collectors.joining(",\n"))
);
System.out.println(");");
}
);
Java 8 是未来的趋势,使用jOOQ、Java 8及 Streams API,你可以写出强大的数据转换API。我希望你像我们一样兴奋,敬请期待更多的更好的关于Java 8内容的blog。