DevOps Zone is brought to you in partnership with:

I am the founder and CEO of Data Geekery GmbH, located in Zurich, Switzerland. With our company, we have been selling database products and services around Java and SQL since 2013. Ever since my Master's studies at EPFL in 2006, I have been fascinated by the interaction of Java and SQL. Most of this experience I have obtained in the Swiss E-Banking field through various variants (JDBC, Hibernate, mostly with Oracle). I am happy to share this knowledge at various conferences, JUGs, in-house presentations and on our blog. Lukas is a DZone MVB and is not an employee of DZone and has posted 244 posts at DZone. You can read more from them at their website. View Full User Profile

No More Need for ORMs

04.14.2014
| 9370 views |
  • submit to reddit

Debates about the usefulness of ORM (Object-Relational Mapping) have been going on for the last decade. While many people would agree that Hibernate and JPA solve a lot of problems very well (mostly the persistence of complex object graphs), others may claim that the mapping complexity is mostly overkill for data-centric applications.

JPA solves mapping problems by establishing standardised, declarative mapping rules through hard-wired annotations on the receiving target types. We claim that many data-centric problems should not be limited by the narrow scope of these annotations, but be solved in a much more functional way. Java 8, and the new Streams API finally allow us to do this in a very concise manner!

Let’s start with a simple example, where we’re using H2′sINFORMATION_SCHEMA to collect all tables and their columns. We’ll want to produce an ad-hoc data structure of the type Map<String, List<String>> to contain this information. For simplicity of SQL interaction, we’ll use jOOQ (as always, a shocker on this blog). Here’s how we prepare this:

public static void main(String[] args)
throws Exception {
    Class.forName("org.h2.Driver");
    try (Connection c = getConnection(
        "jdbc:h2:~/sql-goodies-with-mapping",
        "sa", "")) {

        // This SQL statement produces all table
        // names and column names in the H2 schema
        String sql =
            "select table_name, column_name " +
            "from information_schema.columns " +
            "order by " +
                "table_catalog, " +
                "table_schema, " +
                "table_name, " +
                "ordinal_position";

        // This is jOOQ's way of executing the above
        // statement. Result implements List, which
        // makes subsequent steps much easier
        Result<Record> result =
        DSL.using(c)
           .fetch(sql)
    }
}

Now that we’ve set up this query, let’s see how we can produce theMap<String, List<String>> from the jOOQ Result:

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)
   );

The above example produces the following output:

FUNCTION_COLUMNS: 
CONSTANTS: [CONSTANT_CATALOG, CONSTANT_SCHEMA, ...]
SEQUENCES: [SEQUENCE_CATALOG, SEQUENCE_SCHEMA, ...]

How does it work? Let’s go through it step-by-step

DSL.using(c)
   .fetch(sql)

   // Here, we transform a List into a Stream
   .stream()

   // We're collecting Stream elements into a new
   // collection type
   .collect(

       // The Collector is a grouping operation, producing
       // a Map
       groupingBy(

           // The grouping operation's group key is defined by
           // the jOOQ Record's TABLE_NAME value
           r -> r.getValue("TABLE_NAME"),

           // The grouping operation's group value is generated
           // by this mapping expression...
           mapping(

               // ... which is essentially mapping each grouped
               // jOOQ Record to the Record's COLUMN_NAME value
               r -> r.getValue("COLUMN_NAME"),

               // ... and then collecting all those values into a
               // java.util.List. Whew
               toList()
           )
   ))

   // Once we have this List<String, List<String>> we
   // can simply consume it with the following Consumer
   // lambda expression
   .forEach(
       (table, columns) ->
           System.out.println(table + ": " + columns)
   );

Got it? These things are certainly a bit tricky when playing around with it for the first time. The combination of new types, extensive generics, lambda expressions can be a bit confusing at first. The best thing is to simply practice with these things until you get a hang of it. After all, the whole Streams API is really a revolution compared to previous Java Collections APIs.

The good news is: This API is final and here to stay. Every minute you spend practicing it is an investment into your own future.

Note that the above programme used the following static import:

import static java.util.stream.Collectors.*;

Note also, that the output was no longer ordered as in the database. This is because the groupingBy collector returns a java.util.HashMap. In our case, we might prefer collecting things into a [a href="http://docs.oracle.com/javase/8/docs/api/java/util/LinkedHashMap.html" style="font-size: 14px; background-color: transparent; color: rgb(0, 0, 0); text-decoration: none;"]java.util.LinkedHashMap, which preserves insertion / collection order:

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(...);

We could go on with other means of transforming results. Let’s imagine, we would like to generate simplistic DDL from the above schema. It’s very simple. First, we’ll need to select column’s data type. We’ll simply add it to our SQL query:

String sql =
    "select " +
        "table_name, " +
        "column_name, " +
        "type_name " + // Add the column type
    "from information_schema.columns " +
    "order by " +
        "table_catalog, " +
        "table_schema, " +
        "table_name, " +
        "ordinal_position";

I have also introduced a new local class for the example, to wrap name and type attributes:

class Column {

    final String name;
    final String type;

    Column(String name, String type) {
        this.name = name;
        this.type = type;
    }
}

Now, let’s see how we’ll change our Streams API method calls:

result
   .stream()
   .collect(groupingBy(
       r -> r.getValue("TABLE_NAME"),
       LinkedHashMap::new,
       mapping(

           // We now collect this new wrapper type
           // instead of just the COLUMN_NAME
           r -> new Column(
               r.getValue("COLUMN_NAME", String.class),
               r.getValue("TYPE_NAME", String.class)
           ),
           toList()
       )
   ))
   .forEach(
       (table, columns) -> {

           // Just emit a CREATE TABLE statement
           System.out.println("CREATE TABLE " + table + " (");

           // Map each "Column" type into a String
           // containing the column specification,
           // and join them using comma and
           // newline. Done!
           System.out.println(
                columns.stream()
                       .map(col -> "  " + col.name +
                                    " " + col.type)
                       .collect(Collectors.joining(",\n"))
           );

           System.out.println(");");
       }
    );

The output couldn’t be more awesome!

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
);

Excited? The ORM era may have ended just now

This is a strong statement. The ORM era may have ended. Why? Because using functional expressions to transform data sets is one of the most powerful concepts in software engineering. Functional programming is very expressive and very versatile. It is at the core of data and data streams processing. We Java developers already know existing functional languages. Everyone has used SQL before, for instance. Think about it. With SQL, you declare table sources, project / transform them onto new tuple streams, and feed them either as derived tables to other, higher-level SQL statements, or to your Java program.

If you’re using XML, you can declare XML transformation using XSLT and feed results to other XML processing entities, e.g. another XSL stylesheet, using XProc pipelining.

Java 8′s Streams are nothing else. Using SQL and the Streams API is one of the most powerful concepts for data processing. If you add jOOQ to the stack, you can profit from typesafe access to your database records and query APIs. Imagine writing the previous statement using jOOQ’s fluent API, instead of using SQL strings.

jooq-the-best-way-to-write-sql-in-java

The whole method chain could be one single fluent data transformation chain as such:

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 ends here
   .stream() // Streams start here
   .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 + " (");

           // Map each "Column" type into a String
           // containing the column specification,
           // and join them using comma and
           // newline. Done!
           System.out.println(
               columns.stream()
                      .map(col -> "  " + col.name +
                                   " " + col.type)
                      .collect(Collectors.joining(",\n"))
           );

           System.out.println(");");
       }
   );

Java 8 is the future, and with jOOQ, Java 8, and the Streams API, you can write powerful data transformation APIs. I hope we got you as excited as we are! Stay tuned for more awesome Java 8 content on this blog.

Published at DZone with permission of Lukas Eder, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Jp Gordon replied on Wed, 2014/04/16 - 6:39am

Hmm... you only give readonly examples...

The strength of ORM, I think, is when updating complex graphs...
Pull an object-graph from the DB, update/add/delete some values in a child-list, for example, and persist the result...
Does that work, too?

 

Lukas Eder replied on Wed, 2014/04/16 - 7:12am in response to: Jp Gordon

Good point. The article outlines your point by stressing the fact that:

[...] many people would agree that Hibernate and JPA solve a lot of problems very well (mostly the persistence of complex object graphs)

This is JPA's strength as it aims to solve precisely this problem. The article really focuses on the other type of RDBMS interaction:

[...] many data-centric problems should not be limited by the narrow scope of these annotations, but be solved in a much more functional way. 

This also includes bulk-updates through UPDATE, MERGE and other types of SQL statements, which were out of scope here, as indeed Streams do not help much when updating data.

Dean Schulze replied on Wed, 2014/04/16 - 8:26am

One of JPA's strengths is that JPQL queries are usually valid SQL queries as well so you can test your JPA query in a SQL shell.  Usually a copy and paste is all that is required.  When I tried JOOQ that simplicity was missing (at least I didn't see it).

JOOQ seems like unneeded complexity for CRUD use cases.  Based on what you have above JOOQ's sweet spot may be data manipulation after a read.


Lukas Eder replied on Wed, 2014/04/16 - 8:43am in response to: Dean Schulze

One of JPA's strengths is that JPQL queries are usually valid SQL queries as well so you can test your JPA query in a SQL shell. 

Ehm... Let's have a look at the JPQL tutorial. First example won't work:

SELECT p
FROM Player p

The DISTINCT example won't work either:

SELECT DISTINCT
 p
FROM Player p
WHERE p.position = ?1

The next example uses a JPQL (non-SQL) IN keyword:

SELECT DISTINCT p
FROM Player p, IN(p.teams) t

Then, we have an implicit equi-join, which is missing the SQL ON clause:

SELECT DISTINCT p
FROM Player p JOIN p.teams t

Then, we have this nice IS NOT EMPTY predicate, which doesn't exist in SQL:

SELECT DISTINCT p
FROM Player p
WHERE p.team IS NOT EMPTY

So, yes. JPQL is usually valid SQL for a very very relaxed definition of usual :-)

When I tried JOOQ that simplicity was missing (at least I didn't see it).

That's true. Internal DSL (as opposed to external ones) will always inherit all the syntax of their host language, in this case Java. In that sense, jOOQ is to SQL what Criteria Query is to JPQL, in a way - although Criteria Query doesn't resemble JPQL that much, visually.

JOOQ seems like unneeded complexity for CRUD use cases.  Based on what you have above JOOQ's sweet spot may be data manipulation after a read.

That depends on whether you're interested in persisting object graphs (where JPA shines), or if you like to operate on active records (where jOOQ is appreciated by its users)

Nidhi Kapoor replied on Wed, 2014/04/16 - 8:56am

Thats awesome idea!!!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.