Learn how to use JOOQ (Java Object Oriented Querying) for type-safe SQL construction and execution in Java applications.
Table of Contents
- JOOQ Overview & Setup
- Code Generation
- Basic CRUD Operations
- Advanced Querying
- Transactions & Batch Operations
- Custom Types & Converters
- Performance Optimization
- Integration with Spring Boot
JOOQ Overview & Setup
What is JOOQ?
JOOQ is a popular Java library that provides:
- Type-safe SQL query construction
- Code generation from database schema
- Fluent API for SQL building
- Support for complex SQL features
Maven Configuration
<properties> <jooq.version>3.19.0</jooq.version> <jooq-meta.version>3.19.0</jooq-meta.version> <jooq-codegen.version>3.19.0</jooq-codegen.version> </properties> <dependencies> <!-- JOOQ Runtime --> <dependency> <groupId>org.jooq</groupId> <artifactId>jooq</artifactId> <version>${jooq.version}</version> </dependency> <!-- Database Driver (PostgreSQL example) --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <!-- Spring Boot Starter (optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jooq</artifactId> </dependency> </dependencies> <build> <plugins> <!-- JOOQ Code Generation Plugin --> <plugin> <groupId>org.jooq</groupId> <artifactId>jooq-codegen-maven</artifactId> <version>${jooq-codegen.version}</version> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <configurationFile>src/main/resources/jooq-config.xml</configurationFile> </configuration> </plugin> </plugins> </build> Gradle Configuration
dependencies { implementation 'org.jooq:jooq:3.19.0' implementation 'org.postgresql:postgresql:42.6.0' implementation 'org.springframework.boot:spring-boot-starter-jooq' } // JOOQ Code Generation Task task generateJooqClasses { doLast { Configuration configuration = new Configuration() .withJdbc(new Jdbc() .withDriver('org.postgresql.Driver') .withUrl('jdbc:postgresql://localhost:5432/mydb') .withUser('username') .withPassword('password')) .withGenerator(new Generator() .withDatabase(new Database() .withName('org.jooq.meta.postgres.PostgresDatabase') .withIncludes('.*') .withExcludes('') .withInputSchema('public')) .withTarget(new Target() .withPackageName('com.example.jooq') .withDirectory('src/main/java'))) GenerationTool.generate(configuration) } } Code Generation
1. JOOQ Configuration File
<!-- src/main/resources/jooq-config.xml --> <configuration> <jdbc> <driver>org.postgresql.Driver</driver> <url>jdbc:postgresql://localhost:5432/ecommerce</url> <user>admin</user> <password>secret</password> </jdbc> <generator> <name>org.jooq.codegen.JavaGenerator</name> <database> <name>org.jooq.meta.postgres.PostgresDatabase</name> <includes>.*</includes> <excludes> schema_version | flyway_schema_history | .*\\$.* </excludes> <inputSchema>public</inputSchema> <!-- Custom types --> <forcedTypes> <forcedType> <name>varchar</name> <includeTypes>JSONB?|JSON</includeTypes> </forcedType> <forcedType> <name>boolean</name> <includeTypes>BOOL</includeTypes> </forcedType> </forcedTypes> </database> <generate> <pojos>true</pojos> <immutablePojos>false</immutablePojos> <daos>true</daos> <javaTimeTypes>true</javaTimeTypes> <fluentSetters>true</fluentSetters> <springAnnotations>true</springAnnotations> </generate> <target> <packageName>com.example.ecommerce.jooq</packageName> <directory>src/main/java</directory> </target> </generator> </configuration>
2. Database Schema Example
-- Sample database schema for e-commerce application CREATE TABLE customers ( id BIGSERIAL PRIMARY KEY, uuid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, phone VARCHAR(20), created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(), status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' ); CREATE TABLE addresses ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL REFERENCES customers(id), type VARCHAR(20) NOT NULL, -- 'SHIPPING', 'BILLING' street VARCHAR(255) NOT NULL, city VARCHAR(100) NOT NULL, state VARCHAR(100) NOT NULL, zip_code VARCHAR(20) NOT NULL, country VARCHAR(100) NOT NULL DEFAULT 'US', is_primary BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, sku VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, stock_quantity INTEGER NOT NULL DEFAULT 0, category VARCHAR(100) NOT NULL, tags JSONB, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(), is_active BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL REFERENCES customers(id), order_number VARCHAR(50) NOT NULL UNIQUE, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', total_amount DECIMAL(10,2) NOT NULL, shipping_address_id BIGINT NOT NULL REFERENCES addresses(id), billing_address_id BIGINT NOT NULL REFERENCES addresses(id), created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE TABLE order_items ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id), product_id BIGINT NOT NULL REFERENCES products(id), quantity INTEGER NOT NULL, unit_price DECIMAL(10,2) NOT NULL, subtotal DECIMAL(10,2) NOT NULL ); CREATE INDEX idx_customers_email ON customers(email); CREATE INDEX idx_products_category ON products(category); CREATE INDEX idx_orders_customer_id ON orders(customer_id); CREATE INDEX idx_orders_status ON orders(status);
Basic CRUD Operations
3. JOOQ Configuration Service
@Configuration public class JooqConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Bean public DataSourceConnectionProvider connectionProvider(DataSource dataSource) { return new DataSourceConnectionProvider(dataSource); } @Bean public DefaultConfigurationCustomizer configurationCustomizer() { return configuration -> { // Configure JOOQ settings configuration.set(SQLDialect.POSTGRES); configuration.set(ExecuteListenerProvider.providers( new JooqQueryListener() )); configuration.settings() .withExecuteLogging(true) .withRenderSchema(false); }; } @Bean public DefaultDSLContext dsl(org.jooq.Configuration configuration) { return new DefaultDSLContext(configuration); } } // Query listener for logging and monitoring @Component public class JooqQueryListener extends DefaultExecuteListener { private static final Logger logger = LoggerFactory.getLogger(JooqQueryListener.class); @Override public void executeStart(ExecuteContext ctx) { super.executeStart(ctx); // Log the SQL query with parameters if (logger.isDebugEnabled()) { String sql = ctx.sql(); logger.debug("Executing SQL: {}", sql); } } @Override public void executeEnd(ExecuteContext ctx) { super.executeEnd(ctx); // Log execution time and results long time = System.currentTimeMillis() - ctx.queryCreated(); logger.debug("Query executed in {} ms", time); } } 4. Customer Repository
@Repository @Transactional public class CustomerRepository { private static final Logger logger = LoggerFactory.getLogger(CustomerRepository.class); private final DSLContext dsl; // Generated JOOQ tables private static final Customers CUSTOMERS = Customers.CUSTOMERS; private static final Addresses ADDRESSES = Addresses.ADDRESSES; public CustomerRepository(DSLContext dsl) { this.dsl = dsl; } // CREATE operations public Long createCustomer(CustomerRecord customer) { return dsl.insertInto(CUSTOMERS) .set(CUSTOMERS.EMAIL, customer.getEmail()) .set(CUSTOMERS.FIRST_NAME, customer.getFirstName()) .set(CUSTOMERS.LAST_NAME, customer.getLastName()) .set(CUSTOMERS.PHONE, customer.getPhone()) .returning(CUSTOMERS.ID) .fetchOne() .getId(); } public void createCustomerWithAddress(CustomerRecord customer, AddressRecord address) { dsl.transaction(configuration -> { DSLContext transactionDsl = DSL.using(configuration); // Insert customer Long customerId = transactionDsl.insertInto(CUSTOMERS) .set(CUSTOMERS.EMAIL, customer.getEmail()) .set(CUSTOMERS.FIRST_NAME, customer.getFirstName()) .set(CUSTOMERS.LAST_NAME, customer.getLastName()) .set(CUSTOMERS.PHONE, customer.getPhone()) .returning(CUSTOMERS.ID) .fetchOne() .getId(); // Insert address transactionDsl.insertInto(ADDRESSES) .set(ADDRESSES.CUSTOMER_ID, customerId) .set(ADDRESSES.TYPE, address.getType()) .set(ADDRESSES.STREET, address.getStreet()) .set(ADDRESSES.CITY, address.getCity()) .set(ADDRESSES.STATE, address.getState()) .set(ADDRESSES.ZIP_CODE, address.getZipCode()) .set(ADDRESSES.COUNTRY, address.getCountry()) .set(ADDRESSES.IS_PRIMARY, address.getIsPrimary()) .execute(); }); } // READ operations public Optional<CustomerRecord> findById(Long id) { return dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.ID.eq(id)) .fetchOptional(); } public Optional<CustomerRecord> findByEmail(String email) { return dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.EMAIL.eq(email)) .fetchOptional(); } public List<CustomerRecord> findAllActiveCustomers() { return dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.STATUS.eq("ACTIVE")) .orderBy(CUSTOMERS.LAST_NAME.asc(), CUSTOMERS.FIRST_NAME.asc()) .fetch(); } public List<CustomerRecord> findCustomersByStatus(String status) { return dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.STATUS.eq(status)) .fetch(); } public Page<CustomerRecord> findCustomersPaginated(Pageable pageable) { // Get total count int total = dsl.fetchCount(CUSTOMERS); // Get paginated results List<CustomerRecord> customers = dsl.selectFrom(CUSTOMERS) .orderBy(getSortFields(pageable.getSort())) .limit(pageable.getPageSize()) .offset(pageable.getOffset()) .fetch(); return new PageImpl<>(customers, pageable, total); } // UPDATE operations public int updateCustomerStatus(Long customerId, String status) { return dsl.update(CUSTOMERS) .set(CUSTOMERS.STATUS, status) .set(CUSTOMERS.UPDATED_AT, LocalDateTime.now()) .where(CUSTOMERS.ID.eq(customerId)) .execute(); } public int updateCustomerEmail(Long customerId, String email) { return dsl.update(CUSTOMERS) .set(CUSTOMERS.EMAIL, email) .set(CUSTOMERS.UPDATED_AT, LocalDateTime.now()) .where(CUSTOMERS.ID.eq(customerId)) .execute(); } // DELETE operations (soft delete) public int deactivateCustomer(Long customerId) { return dsl.update(CUSTOMERS) .set(CUSTOMERS.STATUS, "INACTIVE") .set(CUSTOMERS.UPDATED_AT, LocalDateTime.now()) .where(CUSTOMERS.ID.eq(customerId)) .execute(); } public int deleteCustomerPermanently(Long customerId) { return dsl.deleteFrom(CUSTOMERS) .where(CUSTOMERS.ID.eq(customerId)) .execute(); } // Helper methods private Collection<SortField<?>> getSortFields(Sort sort) { if (sort == null || sort.isUnsorted()) { return List.of(CUSTOMERS.LAST_NAME.asc(), CUSTOMERS.FIRST_NAME.asc()); } return sort.stream() .map(order -> { SortField<?> field = switch (order.getProperty()) { case "firstName" -> CUSTOMERS.FIRST_NAME; case "lastName" -> CUSTOMERS.LAST_NAME; case "email" -> CUSTOMERS.EMAIL; case "createdAt" -> CUSTOMERS.CREATED_AT; default -> CUSTOMERS.LAST_NAME; }; return order.isAscending() ? field.asc() : field.desc(); }) .collect(Collectors.toList()); } } 5. Product Repository
@Repository @Transactional public class ProductRepository { private final DSLContext dsl; private static final Products PRODUCTS = Products.PRODUCTS; private static final OrderItems ORDER_ITEMS = OrderItems.ORDER_ITEMS; public ProductRepository(DSLContext dsl) { this.dsl = dsl; } // Complex queries with joins and aggregations public List<ProductSales> findTopSellingProducts(int limit) { return dsl.select( PRODUCTS.NAME, PRODUCTS.CATEGORY, ORDER_ITEMS.QUANTITY.sum().as("total_quantity"), ORDER_ITEMS.SUBTOTAL.sum().as("total_revenue") ) .from(PRODUCTS) .join(ORDER_ITEMS).on(PRODUCTS.ID.eq(ORDER_ITEMS.PRODUCT_ID)) .where(PRODUCTS.IS_ACTIVE.isTrue()) .groupBy(PRODUCTS.ID, PRODUCTS.NAME, PRODUCTS.CATEGORY) .orderBy(field("total_quantity").desc()) .limit(limit) .fetch() .map(record -> new ProductSales( record.get(PRODUCTS.NAME), record.get(PRODUCTS.CATEGORY), record.get("total_quantity", Integer.class), record.get("total_revenue", BigDecimal.class) )); } public List<ProductRecord> findProductsByCategory(String category, Pageable pageable) { return dsl.selectFrom(PRODUCTS) .where(PRODUCTS.CATEGORY.eq(category)) .and(PRODUCTS.IS_ACTIVE.isTrue()) .orderBy(PRODUCTS.NAME.asc()) .limit(pageable.getPageSize()) .offset(pageable.getOffset()) .fetch(); } public List<ProductRecord> searchProducts(String searchTerm) { return dsl.selectFrom(PRODUCTS) .where(PRODUCTS.NAME.likeIgnoreCase("%" + searchTerm + "%") .or(PRODUCTS.DESCRIPTION.likeIgnoreCase("%" + searchTerm + "%"))) .and(PRODUCTS.IS_ACTIVE.isTrue()) .orderBy(PRODUCTS.NAME.asc()) .fetch(); } public int updateProductStock(Long productId, int quantityChange) { return dsl.update(PRODUCTS) .set(PRODUCTS.STOCK_QUANTITY, PRODUCTS.STOCK_QUANTITY.add(quantityChange)) .set(PRODUCTS.UPDATED_AT, LocalDateTime.now()) .where(PRODUCTS.ID.eq(productId)) .and(PRODUCTS.STOCK_QUANTITY.add(quantityChange).ge(0)) .execute(); } public Map<String, Long> getProductCountByCategory() { return dsl.select(PRODUCTS.CATEGORY, count()) .from(PRODUCTS) .where(PRODUCTS.IS_ACTIVE.isTrue()) .groupBy(PRODUCTS.CATEGORY) .fetch() .stream() .collect(Collectors.toMap( record -> record.get(PRODUCTS.CATEGORY), record -> record.get(count()) )); } // Record class for custom result public record ProductSales( String productName, String category, Integer totalQuantity, BigDecimal totalRevenue ) {} } Advanced Querying
6. Complex Query Service
@Service @Transactional(readOnly = true) public class AdvancedQueryService { private final DSLContext dsl; // Generated tables private static final Customers CUSTOMERS = Customers.CUSTOMERS; private static final Orders ORDERS = Orders.ORDERS; private static final OrderItems ORDER_ITEMS = OrderItems.ORDER_ITEMS; private static final Products PRODUCTS = Products.PRODUCTS; private static final Addresses ADDRESSES = Addresses.ADDRESSES; public AdvancedQueryService(DSLContext dsl) { this.dsl = dsl; } // Complex join with multiple conditions public List<CustomerOrderSummary> getCustomerOrderSummaries() { return dsl.select( CUSTOMERS.FIRST_NAME, CUSTOMERS.LAST_NAME, CUSTOMERS.EMAIL, count(ORDERS.ID).as("order_count"), sum(ORDERS.TOTAL_AMOUNT).as("total_spent"), max(ORDERS.CREATED_AT).as("last_order_date") ) .from(CUSTOMERS) .leftJoin(ORDERS).on(CUSTOMERS.ID.eq(ORDERS.CUSTOMER_ID)) .where(CUSTOMERS.STATUS.eq("ACTIVE")) .groupBy(CUSTOMERS.ID, CUSTOMERS.FIRST_NAME, CUSTOMERS.LAST_NAME, CUSTOMERS.EMAIL) .having(count(ORDERS.ID).gt(0)) .orderBy(field("total_spent").desc()) .fetch() .map(record -> new CustomerOrderSummary( record.get(CUSTOMERS.FIRST_NAME) + " " + record.get(CUSTOMERS.LAST_NAME), record.get(CUSTOMERS.EMAIL), record.get("order_count", Long.class), record.get("total_spent", BigDecimal.class), record.get("last_order_date", LocalDateTime.class) )); } // Subquery example public List<ProductRecord> findProductsNeverOrdered() { var orderedProductIds = dsl.select(ORDER_ITEMS.PRODUCT_ID) .from(ORDER_ITEMS) .groupBy(ORDER_ITEMS.PRODUCT_ID); return dsl.selectFrom(PRODUCTS) .where(PRODUCTS.ID.notIn(orderedProductIds)) .and(PRODUCTS.IS_ACTIVE.isTrue()) .fetch(); } // Conditional queries with dynamic filters public List<OrderRecord> findOrdersWithFilters(OrderFilter filter) { var condition = DSL.noCondition(); if (filter.customerId() != null) { condition = condition.and(ORDERS.CUSTOMER_ID.eq(filter.customerId())); } if (filter.status() != null && !filter.status().isEmpty()) { condition = condition.and(ORDERS.STATUS.in(filter.status())); } if (filter.fromDate() != null) { condition = condition.and(ORDERS.CREATED_AT.ge(filter.fromDate())); } if (filter.toDate() != null) { condition = condition.and(ORDERS.CREATED_AT.le(filter.toDate())); } if (filter.minAmount() != null) { condition = condition.and(ORDERS.TOTAL_AMOUNT.ge(filter.minAmount())); } if (filter.maxAmount() != null) { condition = condition.and(ORDERS.TOTAL_AMOUNT.le(filter.maxAmount())); } return dsl.selectFrom(ORDERS) .where(condition) .orderBy(ORDERS.CREATED_AT.desc()) .fetch(); } // Window functions for advanced analytics public List<CustomerRanking> getCustomerSpendingRankings() { var totalSpent = sum(ORDERS.TOTAL_AMOUNT).over() .partitionBy(ORDERS.CUSTOMER_ID) .as("customer_total"); var spendingRank = rowNumber().over() .orderBy(field("customer_total").desc()) .as("rank"); return dsl.select( CUSTOMERS.FIRST_NAME, CUSTOMERS.LAST_NAME, totalSpent, spendingRank ) .from(CUSTOMERS) .join(ORDERS).on(CUSTOMERS.ID.eq(ORDERS.CUSTOMER_ID)) .where(CUSTOMERS.STATUS.eq("ACTIVE")) .groupBy(CUSTOMERS.ID, CUSTOMERS.FIRST_NAME, CUSTOMERS.LAST_NAME) .orderBy(field("rank").asc()) .fetch() .map(record -> new CustomerRanking( record.get(CUSTOMERS.FIRST_NAME) + " " + record.get(CUSTOMERS.LAST_NAME), record.get("customer_total", BigDecimal.class), record.get("rank", Integer.class) )); } // Common Table Expression (CTE) example public List<MonthlySales> getMonthlySalesReport(int year) { var monthlySales = name("monthly_sales").fields( "year_month", "total_sales", "order_count" ).as( dsl.select( dateTrunc("month", ORDERS.CREATED_AT).as("year_month"), sum(ORDERS.TOTAL_AMOUNT).as("total_sales"), count(ORDERS.ID).as("order_count") ) .from(ORDERS) .where(extract(ORDERS.CREATED_AT, DatePart.YEAR).eq(year)) .groupBy(dateTrunc("month", ORDERS.CREATED_AT)) ); return dsl.with(monthlySales) .select() .from(monthlySales) .orderBy(field("year_month").asc()) .fetch() .map(record -> new MonthlySales( record.get("year_month", LocalDateTime.class), record.get("total_sales", BigDecimal.class), record.get("order_count", Long.class) )); } // JSON querying (PostgreSQL JSONB) public List<ProductRecord> findProductsByTag(String tag) { return dsl.selectFrom(PRODUCTS) .where(PRODUCTS.TAGS.contains(tag)) .and(PRODUCTS.IS_ACTIVE.isTrue()) .fetch(); } // Record classes for complex results public record CustomerOrderSummary( String customerName, String email, Long orderCount, BigDecimal totalSpent, LocalDateTime lastOrderDate ) {} public record CustomerRanking( String customerName, BigDecimal totalSpent, Integer rank ) {} public record MonthlySales( LocalDateTime month, BigDecimal totalSales, Long orderCount ) {} public record OrderFilter( Long customerId, List<String> status, LocalDateTime fromDate, LocalDateTime toDate, BigDecimal minAmount, BigDecimal maxAmount ) {} } Transactions & Batch Operations
7. Transaction Management Service
@Service public class OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderService.class); private final DSLContext dsl; private final ProductRepository productRepository; private static final Orders ORDERS = Orders.ORDERS; private static final OrderItems ORDER_ITEMS = OrderItems.ORDER_ITEMS; private static final Products PRODUCTS = Products.PRODUCTS; public OrderService(DSLContext dsl, ProductRepository productRepository) { this.dsl = dsl; this.productRepository = productRepository; } @Transactional public OrderResult createOrder(CreateOrderRequest request) { return dsl.transactionResult(configuration -> { DSLContext transactionDsl = DSL.using(configuration); try { // 1. Validate stock availability validateStockAvailability(transactionDsl, request.items()); // 2. Create order record Long orderId = createOrderRecord(transactionDsl, request); // 3. Create order items and update stock createOrderItems(transactionDsl, orderId, request.items()); // 4. Calculate and update order total BigDecimal totalAmount = calculateOrderTotal(transactionDsl, orderId); updateOrderTotal(transactionDsl, orderId, totalAmount); logger.info("Order created successfully: {}", orderId); return new OrderResult(orderId, "SUCCESS", totalAmount); } catch (Exception e) { logger.error("Failed to create order", e); throw new OrderCreationException("Order creation failed", e); } }); } @Transactional public void bulkUpdateProductPrices(List<PriceUpdate> updates) { dsl.batch(updates.stream() .map(update -> dsl.update(PRODUCTS) .set(PRODUCTS.PRICE, update.newPrice()) .set(PRODUCTS.UPDATED_AT, LocalDateTime.now()) .where(PRODUCTS.ID.eq(update.productId())) ) .collect(Collectors.toList()) ).execute(); } @Transactional public void bulkInsertProducts(List<ProductRecord> products) { var batchInsert = dsl.batchInsert(products); batchInsert.execute(); } // Optimistic locking example @Transactional public boolean updateProductWithOptimisticLock(ProductRecord product, LocalDateTime currentVersion) { int updated = dsl.update(PRODUCTS) .set(PRODUCTS.NAME, product.getName()) .set(PRODUCTS.PRICE, product.getPrice()) .set(PRODUCTS.DESCRIPTION, product.getDescription()) .set(PRODUCTS.UPDATED_AT, LocalDateTime.now()) .where(PRODUCTS.ID.eq(product.getId())) .and(PRODUCTS.UPDATED_AT.eq(currentVersion)) .execute(); return updated > 0; } private void validateStockAvailability(DSLContext dsl, List<OrderItemRequest> items) { for (OrderItemRequest item : items) { int availableStock = dsl.select(PRODUCTS.STOCK_QUANTITY) .from(PRODUCTS) .where(PRODUCTS.ID.eq(item.productId())) .fetchOne(PRODUCTS.STOCK_QUANTITY); if (availableStock < item.quantity()) { throw new InsufficientStockException( "Insufficient stock for product: " + item.productId()); } } } private Long createOrderRecord(DSLContext dsl, CreateOrderRequest request) { return dsl.insertInto(ORDERS) .set(ORDERS.CUSTOMER_ID, request.customerId()) .set(ORDERS.ORDER_NUMBER, generateOrderNumber()) .set(ORDERS.STATUS, "PENDING") .set(ORDERS.SHIPPING_ADDRESS_ID, request.shippingAddressId()) .set(ORDERS.BILLING_ADDRESS_ID, request.billingAddressId()) .set(ORDERS.TOTAL_AMOUNT, BigDecimal.ZERO) // Temporary value .returning(ORDERS.ID) .fetchOne() .getId(); } private void createOrderItems(DSLContext dsl, Long orderId, List<OrderItemRequest> items) { var batchInserts = new ArrayList<InsertValuesStepN<OrderItemsRecord>>(); for (OrderItemRequest item : items) { // Get current product price BigDecimal unitPrice = dsl.select(PRODUCTS.PRICE) .from(PRODUCTS) .where(PRODUCTS.ID.eq(item.productId())) .fetchOne(PRODUCTS.PRICE); BigDecimal subtotal = unitPrice.multiply(BigDecimal.valueOf(item.quantity())); // Prepare insert var insert = dsl.insertInto(ORDER_ITEMS) .columns( ORDER_ITEMS.ORDER_ID, ORDER_ITEMS.PRODUCT_ID, ORDER_ITEMS.QUANTITY, ORDER_ITEMS.UNIT_PRICE, ORDER_ITEMS.SUBTOTAL ) .values( orderId, item.productId(), item.quantity(), unitPrice, subtotal ); batchInserts.add(insert); // Update product stock dsl.update(PRODUCTS) .set(PRODUCTS.STOCK_QUANTITY, PRODUCTS.STOCK_QUANTITY.sub(item.quantity())) .set(PRODUCTS.UPDATED_AT, LocalDateTime.now()) .where(PRODUCTS.ID.eq(item.productId())) .execute(); } // Execute all item inserts dsl.batch(batchInserts).execute(); } private BigDecimal calculateOrderTotal(DSLContext dsl, Long orderId) { return dsl.select(sum(ORDER_ITEMS.SUBTOTAL)) .from(ORDER_ITEMS) .where(ORDER_ITEMS.ORDER_ID.eq(orderId)) .fetchOneInto(BigDecimal.class); } private void updateOrderTotal(DSLContext dsl, Long orderId, BigDecimal totalAmount) { dsl.update(ORDERS) .set(ORDERS.TOTAL_AMOUNT, totalAmount) .set(ORDERS.STATUS, "CONFIRMED") .set(ORDERS.UPDATED_AT, LocalDateTime.now()) .where(ORDERS.ID.eq(orderId)) .execute(); } private String generateOrderNumber() { return "ORD-" + System.currentTimeMillis() + "-" + ThreadLocalRandom.current().nextInt(1000, 9999); } // DTO classes public record CreateOrderRequest( Long customerId, Long shippingAddressId, Long billingAddressId, List<OrderItemRequest> items ) {} public record OrderItemRequest(Long productId, Integer quantity) {} public record OrderResult(Long orderId, String status, BigDecimal totalAmount) {} public record PriceUpdate(Long productId, BigDecimal newPrice) {} } Custom Types & Converters
8. Custom Data Type Binding
// JSONB converter for PostgreSQL @Component public class JsonbConverter implements Binding<JSONB, String> { @Override public Converter<String, JSONB> converter() { return new Converter<String, JSONB>() { @Override public JSONB from(String databaseObject) { return databaseObject == null ? null : JSONB.valueOf(databaseObject); } @Override public String to(JSONB userObject) { return userObject == null ? null : userObject.data(); } @Override public Class<String> fromType() { return String.class; } @Override public Class<JSONB> toType() { return JSONB.class; } }; } @Override public void sql(BindingSQLContext<JSONB> ctx) throws SQLException { ctx.render().visit(DSL.val(ctx.convert(converter()).value())); } @Override public void register(BindingRegisterContext<JSONB> ctx) throws SQLException { ctx.statement().registerOutParameter(ctx.index(), Types.VARCHAR); } @Override public void set(BindingSetStatementContext<JSONB> ctx) throws SQLException { ctx.statement().setString(ctx.index(), Objects.toString(ctx.convert(converter()).value(), null)); } @Override public void get(BindingGetResultSetContext<JSONB> ctx) throws SQLException { ctx.convert(converter()).value(ctx.resultSet().getString(ctx.index())); } @Override public void get(BindingGetStatementContext<JSONB> ctx) throws SQLException { ctx.convert(converter()).value(ctx.statement().getString(ctx.index())); } @Override public void set(BindingSetSQLOutputContext<JSONB> ctx) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void get(BindingGetSQLInputContext<JSONB> ctx) throws SQLException { throw new SQLFeatureNotSupportedException(); } } // Custom enum converter public class StatusConverter implements Converter<String, CustomerStatus> { @Override public CustomerStatus from(String databaseObject) { return databaseObject == null ? null : CustomerStatus.valueOf(databaseObject); } @Override public String to(CustomerStatus userObject) { return userObject == null ? null : userObject.name(); } @Override public Class<String> fromType() { return String.class; } @Override public Class<CustomerStatus> toType() { return CustomerStatus.class; } } public enum CustomerStatus { ACTIVE, INACTIVE, SUSPENDED, PENDING_VERIFICATION } // Custom configuration for type binding @Configuration public class CustomJooqConfig { @Bean public DefaultConfigurationCustomizer jooqConfigCustomizer() { return config -> { // Register custom converters config.set(new DefaultBinding( SQLDataType.VARCHAR, new JsonbConverter() )); config.set(new DefaultBinding( SQLDataType.VARCHAR.asEnumDataType(CustomerStatus.class), new StatusConverter() )); }; } } Performance Optimization
9. JOOQ Performance Tips
@Service public class OptimizedQueryService { private final DSLContext dsl; public OptimizedQueryService(DSLContext dsl) { this.dsl = dsl; } // Use fetchGroups for one-to-many relationships public Map<CustomerRecord, Result<OrderRecord>> getCustomersWithOrders() { return dsl.select() .from(CUSTOMERS) .leftJoin(ORDERS).on(CUSTOMERS.ID.eq(ORDERS.CUSTOMER_ID)) .where(CUSTOMERS.STATUS.eq("ACTIVE")) .fetchGroups(CUSTOMERS, ORDERS); } // Use fetchLazy for large result sets public void processLargeCustomerSet() { try (Cursor<CustomerRecord> cursor = dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.STATUS.eq("ACTIVE")) .fetchLazy()) { for (CustomerRecord customer : cursor) { // Process each customer processCustomer(customer); // Manual memory management for very large datasets if (cursor.rownum() % 1000 == 0) { System.gc(); // Use cautiously } } } } // Use fetchInto for direct DTO mapping public List<CustomerDto> getCustomerDtos() { return dsl.select( CUSTOMERS.FIRST_NAME, CUSTOMERS.LAST_NAME, CUSTOMERS.EMAIL, count(ORDERS.ID).as("orderCount") ) .from(CUSTOMERS) .leftJoin(ORDERS).on(CUSTOMERS.ID.eq(ORDERS.CUSTOMER_ID)) .groupBy(CUSTOMERS.ID, CUSTOMERS.FIRST_NAME, CUSTOMERS.LAST_NAME, CUSTOMERS.EMAIL) .fetchInto(CustomerDto.class); } // Optimized bulk operations public void bulkCustomerStatusUpdate(List<Long> customerIds, String newStatus) { dsl.update(CUSTOMERS) .set(CUSTOMERS.STATUS, newStatus) .set(CUSTOMERS.UPDATED_AT, LocalDateTime.now()) .where(CUSTOMERS.ID.in(customerIds)) .execute(); } // Use exists for existence checks (more efficient than count) public boolean customerHasOrders(Long customerId) { return dsl.fetchExists( dsl.selectOne() .from(ORDERS) .where(ORDERS.CUSTOMER_ID.eq(customerId)) ); } // Optimized pagination with keyset pagination public List<CustomerRecord> getCustomersKeysetPagination(Long lastId, int limit) { return dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.ID.gt(lastId)) .orderBy(CUSTOMERS.ID.asc()) .limit(limit) .fetch(); } // Use explain to analyze query performance public void analyzeQueryPerformance() { String explain = dsl.explain( dsl.selectFrom(CUSTOMERS) .where(CUSTOMERS.STATUS.eq("ACTIVE")) .orderBy(CUSTOMERS.LAST_NAME.asc()) ); logger.debug("Query plan: {}", explain); } private void processCustomer(CustomerRecord customer) { // Process customer record } // DTO class public static class CustomerDto { private String firstName; private String lastName; private String email; private Long orderCount; // Getters and setters public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Long getOrderCount() { return orderCount; } public void setOrderCount(Long orderCount) { this.orderCount = orderCount; } } } Integration with Spring Boot
10. Spring Boot Configuration
# application.yml spring: datasource: url: jdbc:postgresql://localhost:5432/ecommerce username: admin password: secret hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 jooq: sql-dialect: POSTGRES logging: level: org.jooq: DEBUG org.jooq.tools.LoggerListener: DEBUG
11. Spring Service Integration
@Service @Transactional public class EcommerceService { private final DSLContext dsl; private final CustomerRepository customerRepository; private final ProductRepository productRepository; private final AdvancedQueryService advancedQueryService; public EcommerceService(DSLContext dsl, CustomerRepository customerRepository, ProductRepository productRepository, AdvancedQueryService advancedQueryService) { this.dsl = dsl; this.customerRepository = customerRepository; this.productRepository = productRepository; this.advancedQueryService = advancedQueryService; } public CustomerDashboard getCustomerDashboard(Long customerId) { // Get customer basic info var customer = customerRepository.findById(customerId) .orElseThrow(() -> new CustomerNotFoundException(customerId)); // Get recent orders var recentOrders = getRecentOrders(customerId); // Get order statistics var orderStats = getCustomerOrderStats(customerId); // Get recommended products var recommendations = getProductRecommendations(customerId); return new CustomerDashboard(customer, recentOrders, orderStats, recommendations); } @Transactional(readOnly = true) public SalesReport generateSalesReport(ReportCriteria criteria) { var monthlySales = advancedQueryService.getMonthlySalesReport(criteria.year()); var topProducts = productRepository.findTopSellingProducts(10); var customerRankings = advancedQueryService.getCustomerSpendingRankings(); return new SalesReport(monthlySales, topProducts, customerRankings, criteria); } @Async @Transactional public CompletableFuture<Void> processCustomerDataAsync(Long customerId) { return CompletableFuture.runAsync(() -> { // Process customer data asynchronously updateCustomerStatistics(customerId); generateCustomerInsights(customerId); }); } // Helper methods private List<OrderRecord> getRecentOrders(Long customerId) { return dsl.selectFrom(Orders.ORDERS) .where(Orders.ORDERS.CUSTOMER_ID.eq(customerId)) .orderBy(Orders.ORDERS.CREATED_AT.desc()) .limit(10) .fetch(); } private OrderStats getCustomerOrderStats(Long customerId) { var result = dsl.select( count(Orders.ORDERS.ID).as("total_orders"), sum(Orders.ORDERS.TOTAL_AMOUNT).as("total_spent"), avg(Orders.ORDERS.TOTAL_AMOUNT).as("avg_order_value") ) .from(Orders.ORDERS) .where(Orders.ORDERS.CUSTOMER_ID.eq(customerId)) .fetchOne(); return new OrderStats( result.get("total_orders", Long.class), result.get("total_spent", BigDecimal.class), result.get("avg_order_value", BigDecimal.class) ); } private List<ProductRecord> getProductRecommendations(Long customerId) { // Simple recommendation based on customer's previous purchases return dsl.selectDistinct(Products.PRODUCTS.fields()) .from(Products.PRODUCTS) .join(OrderItems.ORDER_ITEMS).on(Products.PRODUCTS.ID.eq(OrderItems.ORDER_ITEMS.PRODUCT_ID)) .join(Orders.ORDERS).on(OrderItems.ORDER_ITEMS.ORDER_ID.eq(Orders.ORDERS.ID)) .where(Orders.ORDERS.CUSTOMER_ID.eq(customerId)) .and(Products.PRODUCTS.IS_ACTIVE.isTrue()) .orderBy(Products.PRODUCTS.CREATED_AT.desc()) .limit(5) .fetchInto(Products.PRODUCTS); } private void updateCustomerStatistics(Long customerId) { // Update customer statistics asynchronously } private void generateCustomerInsights(Long customerId) { // Generate customer insights asynchronously } // DTO classes public record CustomerDashboard( CustomerRecord customer, List<OrderRecord> recentOrders, OrderStats orderStats, List<ProductRecord> recommendations ) {} public record OrderStats(Long totalOrders, BigDecimal totalSpent, BigDecimal avgOrderValue) {} public record SalesReport( List<AdvancedQueryService.MonthlySales> monthlySales, List<ProductRepository.ProductSales> topProducts, List<AdvancedQueryService.CustomerRanking> customerRankings, ReportCriteria criteria ) {} public record ReportCriteria(Integer year, String category, LocalDate fromDate, LocalDate toDate) {} } This comprehensive JOOQ guide covers everything from basic setup and code generation to advanced querying patterns, transaction management, and Spring Boot integration, providing a solid foundation for building type-safe, performant database applications in Java.