The last ten years I was using JDBC, EJB entity beans, Hibernate, GORM and finally JPA (in this order). For my current project I have returned to using plain JDBC, because the emphasis is on performance. Therefore I wanted

Full control on the generation of SQL statements: To be able to pass a statement to DB performance tuners, and put the optimized version back in the program

Full control on the number of SQL statements which are sent to the database

Stored procedures (triggers), stored functions (for complex calculations in SQL queries)

To be able to use all available SQL features without restrictions (recursive queries with CTEs, window aggregate functions, ...)

The data model is defined in a data dictionary; using a model driven approach a generator creates helper classes, DDL scripts etc. Most operations on the database are read-only; only few use cases write.

Question 1: Fetching children

The system is built on a use cases, and we have one dedicated SQL statement to get all data for a given use case/request. Some of the SQL statments are bigger than 20kb, they join, calculate using stored functions written in Java/Scala, sort, paginate etc. in a way that the result is directly mapped into a data transfer object which in turn is fed into the view (no further processing in the application layer). As a consequence the data transfer object is use case specific as well. It only contains the data for the given use case (nothing more, nothing less).

As the result set is already "fully joined" there is no need for lazy/eager fetching etc. The data transfer object is complete. A cache is not needed (the database cache is fine); the exception: If the result set is large (around 50 000 rows), the data transfer object is used as a cache value.

Question 2: Saving

After the controller has merged back the changes from the GUI, again there is a specific object which holds the data: Basically the rows with a state (new, deleted, modified, ...) in a hierarchy. It's a manual iteration to save the data down the hierarchy: New or modified data is persisted using some helper classes with generate SQL insert or update commands. As for deleted items, this is optimized into cascaded deletes (PostgreSql). If multiple rows are to be deleted, this is optimized into a single delete ... where id in ... statement as well.

Again this is use case specific, so it's not dealing with a general approch. It needs more lines of code, but these are the lines which contain the optimizations.

The experiences so far

One should not underestimate the effort to learn Hibernate or JPA. One should consider the time spent in configuring the caches, cache invalidation in a cluster, eager/lazy fetching, and tuning as well. Migrating to another Hibernate major version is not just a recompilation.

One should not overestimate the effort to build an application without ORM.

It's simpler to use SQL directly - being close to SQL (like HQL, JPQL) is not the same, especially if you talk to your DB performance tuner

SQL servers are incredibly fast when running long and complex queries, especially if combined with stored functions written in Scala

Even with the use case specific SQL statements, the code size is smaller: "Fully joined" result sets save lots of lines in the application layer.

Related information:

Update: Interfaces

If there is a Person entity in the logical data model, there is a class to persist a Person entity (for CRUD operations), just like a JPA entity.

But the clou is that there is no single Person entity from the use case / query / business logic perspective: Each service method defines its own notion of a Person , and it only contains exactly the values required by that use case. Nothing more, nothing less. We use Scala, so the definition and usage of many small classes is very efficient (no setter/getter boiler plate code required).

Example:

class GlobalPersonUtils { public X doSomeProcessingOnAPerson( Person person, PersonAddress personAddress, PersonJob personJob, Set<Person> personFriends, ...) }

is replaced by

class Person { List addresses = ... public X doSomeProcessingOnAPerson(...) } class Dto { List persons = ... public X doSomeProcessingOnAllPersons() public List getPersons() }

using use case specific Persons , Adresses etc: In this case, the Person already aggregates all relevant data. Requires more classes, but there is no need to pass around JPA entities.

Note that this processing is read-only, the results are used by the view. Example: Get distinct City instances from a person's list.

If data is changed, this is another use case: If the city of a person is changed, this is processed by a different service method, and the person is fetched again from the database.