Getting started with jOOQ: A Tutorial
Introduction
I accidentally stumbled onto jOOQ a few days ago while doing a lot of research on Hibernate. Funny how things work, isn’t it? For those of you that aren’t familiar with it, jOOQ is a different approach to the over-ORMing of Java persistence. Rather than try to map database tables to Java classes and abstract away the SQL underneath, jOOQ assumes you want low level control over the SQL queries you execute, and provides a mostly typesafe interface for executing queries. I don’t have anything against simple ORMs, but it’s good to have the right tool for the right job. From the jOOQ homepage:</p?
Instead of this SQL query:
SELECT * FROM BOOK WHERE PUBLISHED_IN = 2011 ORDER BY TITLE
You would execute this Java code:
create.selectFrom(BOOK)
.where(PUBLISHED_IN.equal(2011))
.orderBy(TITLE)
Why a Java interface? Type safety, for one. Programmatically using jOOQ’s DSL has some advantages over writing SQL queries by hand, such as IDE support and compile time checking of some things.
The idea interested me and I dug in. Unfortunately, the jOOQ site’s documentation, while fairly comprehensive, DO NOT PROVIDE AN END TO END “GETTING STARTED” PAGE!!! This means that if you want to learn jOOQ, you’ll have to jump to the chapter about Meta model code generation, then jump to the DSL, then jump to jOOQ classes section. It’s a bit of a mess for new users. Google search also didn’t turn up many useful results, so I figured I’d whip up a quick “Getting started” guide. We’re going to go over the following steps:
Preparation: Download jOOQ and your SQL driver
Step 1: Create a SQL database and a table
Step 2: Generate classes
Step 3. Write a main class and establish MySQL connection
Step 4: Write a query using jOOQ’s DSL
Step 5: Iterate over results
Step 6: Profit!
Ready? Let’s get started.
Getting our hands dirty
Preparation: Download jOOQ and your SQL driver
If you haven’t already downloaded them, download jOOQ:
http://sourceforge.net/projects/jooq/files/
For this example, we’ll be using MySQL. If you haven’t already downloaded MySQL Connector/J, download it here:
http://dev.mysql.com/downloads/connector/j/
Stash these somewhere where you can get to them later.
Step 1: Create a SQL database and a table
We’re going to create a database called “guestbook” and a corresponding “posts” table. Connect to MySQL via your command line client and type the following:
create database guestbook; CREATE TABLE `posts` ( `id` bigint(20) NOT NULL, `body` varchar(255) DEFAULT NULL, `timestamp` datetime DEFAULT NULL, `title` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) );
(I copied and pasted the create table statement from a “show create table” command)
Step 2: Generate classes
In this step, we’re going to use jOOQ’s command line tools to generate classes that map to the Posts table we just created. The official docs are here.
I’m going to augment the command line steps a bit. The easiest way to generate a schema is to copy the jOOQ jar files (there should be 3) and the MySQL Connector jar file to a temporary directory. Create a properties file. I’ve created a file called guestbook.properties that looks like this:
#Configure the database connection here jdbc.Driver=com.mysql.jdbc.Driver jdbc.URL=jdbc:mysql://localhost:3306/guestbook jdbc.Schema=guestbook jdbc.User=ikai jdbc.Password= #The default code generator. You can override this one, to generate your own code style #Defaults to org.jooq.util.DefaultGenerator generator=org.jooq.util.DefaultGenerator #The database type. The format here is: #generator.database=org.util.[database].[database]Database generator.database=org.jooq.util.mysql.MySQLDatabase #All elements that are generated from your schema (several Java regular expressions, separated by comma) #Watch out for case-sensitivity. Depending on your database, this might be important! generator.database.includes=.* #All elements that are excluded from your schema (several Java regular expressions, separated by comma). Excludes match before includes generator.database.excludes= #Primary key / foreign key relations should be generated and used. #This will be a prerequisite for various advanced features #Defaults to false generator.generate.relations=true #Generate deprecated code for backwards compatibility #Defaults to true generator.generate.deprecated=false #The destination package of your generated classes (within the destination directory) generator.target.package=test.generated #The destination directory of your generated classes generator.target.directory=/Users/ikai/workspace/MySQLTest/src
One thing that wasn’t clear from jOOQ’s docs is the value of jdbc.Schema: it should be your database name. Since our database name is “guestbook”, that’s what we put. Replace the username with whatever user has the appropriate privileges: in my local dev database, my user has what is effectively root access to everything without a password. You’ll want to look at the other values and replace as necessary. Here are the two interesting properties:
generator.target.package – set this to the parent package you want to create for the generated classes. My setting of test.generated will cause the test.generated.Posts and test.generated.PostsRecord to be created
generator.target.directory – the directory to output to. Worst case scenario you can just copy the files to the package.
Once you have the JAR files and guestbook.properties in your temp directory, type this:
java -classpath jooq-1.6.8.jar:jooq-meta-1.6.8.jar:jooq-codegen-1.6.8.jar:mysql-connector-java-5.1.18-bin.jar:. org.jooq.util.GenerationTool /jooq.properties
Note the prefix slash before jooq.properies. Even though it’s in our working directory, we need to prepend a slash.
Replace the filenames with your filenames. In this example, I’m using jOOQ 1.6.8. If everything has worked, you should see this in your console output:
Nov 1, 2011 7:25:06 PM org.jooq.impl.JooqLogger info INFO: Initialising properties : /jooq.properties Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Database parameters Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: ---------------------------------------------------------- Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: dialect : MYSQL Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: schema : guestbook Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: target dir : /Users/ikai/Documents/workspace/MySQLTest/src Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: target package : test.generated Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: ---------------------------------------------------------- Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Emptying : /Users/ikai/workspace/MySQLTest/src/test/generated Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Generating classes in : /Users/ikai/workspace/MySQLTest/src/test/generated Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Generating schema : Guestbook.java Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Generating factory : GuestbookFactory.java Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Schema generated : Total: 122.18ms Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Sequences fetched : 0 (0 included, 0 excluded) Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Masterdata tables fetched: 0 (0 included, 0 excluded) Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Tables fetched : 5 (5 included, 0 excluded) Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Generating tables : /Users/ikai/workspace/MySQLTest/src/test/generated/tables Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: ARRAYs fetched : 0 (0 included, 0 excluded) Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Enums fetched : 0 (0 included, 0 excluded) Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: UDTs fetched : 0 (0 included, 0 excluded) Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Generating table : Posts.java Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Tables generated : Total: 680.464ms, +558.284ms Nov 1, 2011 7:25:07 PM org.jooq.impl.JooqLogger info INFO: Generating Keys : /Users/ikai/workspace/MySQLTest/src/test/generated/tables Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: Keys generated : Total: 718.621ms, +38.157ms Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: Generating records : /Users/ikai/workspace/MySQLTest/src/test/generated/tables/records Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: Generating record : PostsRecord.java Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: Table records generated : Total: 782.545ms, +63.924ms Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: Routines fetched : 0 (0 included, 0 excluded) Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: Packages fetched : 0 (0 included, 0 excluded) Nov 1, 2011 7:25:08 PM org.jooq.impl.JooqLogger info INFO: GENERATION FINISHED! : Total: 791.688ms, +9.143ms
Step 3. Write a main class and establish MySQL connection
Let’s just write a vanilla main class in the project containing the generated classes:
public class Main {
public static void main(String[] args) {
Connection conn = null;
String userName = "ikai";
String password = "";
String url = "jdbc:mysql://localhost:3306/guestbook";
try {
Class.forName("com.mysql.jdbc.Driver").newInstance();
conn = DriverManager.getConnection(url, userName, password);
conn.close();
} catch (Exception e) {
// You'll probably want to handle the exceptions in a real app
// Don't ever do this silence catch(Exception e) thing. I've seen this in
// live code and it is horrendous.
e.printStackTrace();
}
}
}
This is pretty standard code for establishing a MySQL connection.
Step 4: Write a query using jOOQ’s DSL
Let’s add a simple query:
GuestbookFactory create = new GuestbookFactory(conn); Result result = create.select().from(Posts.POSTS).fetch();
We need to first get an instance of GuestbookFactory so we can write a simple SELECT query. We pass an instance of the MySQL connection to GuestbookFactory. Note that factory doesn’t close the connection. We’ll have to do that ourselves.
We then use jOOQ’s DSL to return an instance of Result. We’ll be using this result in the next step.
Step 5: Iterate over results
After the line where we retrieve the results, let’s iterate over the results and print out the data:
for (Record r : result) {
Long id = r.getValueAsLong(Posts.ID);
String title = r.getValueAsString(Posts.TITLE);
String description = r.getValueAsString(Posts.BODY);
System.out.println("ID: " + id + " title: " + title + " desciption: " + description);
}
The full program should now look like this:
package test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import org.jooq.Record;
import org.jooq.Result;
import test.generated.GuestbookFactory;
import test.generated.tables.Posts;
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
Connection conn = null;
String userName = "ikai";
String password = "";
String url = "jdbc:mysql://localhost:3306/guestbook";
try {
Class.forName("com.mysql.jdbc.Driver").newInstance();
conn = DriverManager.getConnection(url, userName, password);
GuestbookFactory create = new GuestbookFactory(conn);
Result result = create.select().from(Posts.POSTS).fetch();
for (Record r : result) {
Long id = r.getValueAsLong(Posts.ID);
String title = r.getValueAsString(Posts.TITLE);
String description = r.getValueAsString(Posts.BODY);
System.out.println("ID: " + id + " title: " + title + " desciption: " + description);
}
conn.close();
} catch (Exception e) {
// You'll probably want to handle the exceptions in a real app
// Don't ever do this silence catch(Exception e) thing. I've seen this in
// live code and it is horrendous.
e.printStackTrace();
}
}
}
Step 6: Profit!
Get a job and go to work like the rest of us.
Conclusion
I haven’t explored the more advanced bits of jOOQ, but, at least judging from the docs, it looks like there’s a lot of meat there. I’m hoping this guide makes it easier for new users to dive in.
- ikai
Currently listening: Sweat – Snoop Dogg vs David Guetta
You still have to iterate over the Result and “rebuild” the objects with the different parts (columns) the same way you have to do with a ResultSet…Still looks interesting, I’ll have to take a closer look! Thanks for sharing!
Sergio
November 1, 2011 at 7:30 pm
Sergio,
You have various means of fetching results as documented here:
http://www.jooq.org/manual/JOOQ/ResultQuery
This includes Spring-like rowmappers, cursors, asynchronous fetching, and other transformations
Cheers
Lukas
lukaseder
November 2, 2011 at 2:04 pm
^^^ (Library creator, woot!)
Yeah, Sergio, there are other things you can do to get a direct instantiated Object result, but I couldn’t figure out how to do it.
Ikai Lan
November 2, 2011 at 2:06 pm
That’s pretty cool lib.
branflake2267
November 2, 2011 at 6:24 am
Hello Ikai,
Thank you very much for this nice tutorial. Indeed, this is an important part of the documentation that’s still missing. I get a lot of hits on my blog and on http://www.jooq.org from Google searches for a decent jOOQ tutorial.
With your permission, I’d like to adapt this tutorial a little bit and make it part of the official documentation.
Cheers
Lukas
lukaseder
November 2, 2011 at 2:09 pm
Sure, go for it!
Ikai Lan
November 2, 2011 at 2:11 pm
Finally found the time to do it. I gave you credit on the bottom of that page:
http://www.jooq.org/tutorial.php
Thanks again for this nice tutorial!
lukaseder
November 12, 2011 at 8:35 am
[...] http://ikaisays.com/2011/11/01/getting-started-with-jooq-a-tutorial [...]
jOOQ Tutorial by Ikai Lan « Java, SQL, and jOOQ
November 2, 2011 at 2:16 pm
helpful, thanks :)
m
November 4, 2011 at 12:09 pm
It’s a good way of solving the n+1 selects problem.
It’s still ORMy though.
I’m still using calls to SQL (usually some parameterized form, i.e. stored proc, view, cursor, SQL with :params) because that way I’m taking advantage of the DB cache. This assumes your DB has a cache of previous execution plans. I need to avoid the overhead of passing a brand new SQL code block and having
the database build a new execution plan each time.
It would be really, really nice if the Google App Engine had a cache of previous execution plans for datastore SQL so it would not need to re-calculate on each call.
Dragos Toader (@dtoader)
March 30, 2012 at 9:27 am