or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

exception-translation.mdhibernate-configuration.mdhibernate-transaction-management.mdindex.mdjpa-configuration.mdjpa-transaction-management.mdjpa-vendor-adapters.mdpersistence-unit-management.mdshared-resources.mdutility-classes.mdweb-integration.md
tile.json

jpa-configuration.mddocs/

JPA EntityManagerFactory Configuration

Configuration for creating JPA EntityManagerFactory instances with persistence unit scanning, DataSource injection, and vendor-specific configuration.

Quick Navigation

  • Basic Configuration - Start here for most projects
  • Production Configuration - Production-ready setup
  • Multiple Databases - Multi-database setup
  • Testing Configuration - Test-specific setup
  • Troubleshooting - Common problems and solutions

Decision: Which Factory Bean?

Need JPA EntityManagerFactory?
  │
  ├─ YES (99% of cases)
  │    → LocalContainerEntityManagerFactoryBean
  │       ✅ Full container bootstrap with classpath scanning
  │       ✅ DataSource injection and management
  │       ✅ No persistence.xml required
  │       ✅ Spring Boot compatible
  │       ✅ Supports multiple databases
  │
  └─ Legacy migration only
       → LocalEntityManagerFactoryBean
          ⚠️  Requires persistence.xml
          ⚠️  Minimal Spring integration
          ❌ Use only for legacy apps

Template: Basic Configuration

COPY-PASTE READY for most projects:

package com.example.config;

import jakarta.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class JpaConfig {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.domain");  // Scans recursively for @Entity

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(false);  // ALWAYS false in production
        vendorAdapter.setShowSql(false);      // ALWAYS false in production
        em.setJpaVendorAdapter(vendorAdapter);

        return em;
    }
}

Template: Production-Grade with Properties

COPY-PASTE READY with externalized configuration:

package com.example.config;

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.orm.jpa.vendor.Database;

@Configuration
@EnableTransactionManagement
public class JpaConfig {

    @Value("${spring.jpa.show-sql:false}")
    private boolean showSql;

    @Value("${spring.jpa.hibernate.ddl-auto:validate}")
    private String ddlAuto;

    @Value("${spring.jpa.properties.hibernate.jdbc.batch_size:20}")
    private int batchSize;

    @Value("${spring.jpa.database:POSTGRESQL}")
    private Database database;

    @Value("${spring.jpa.properties.hibernate.dialect:}")
    private String dialect;

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.domain");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(database);
        vendorAdapter.setGenerateDdl(false);
        vendorAdapter.setShowSql(showSql);
        em.setJpaVendorAdapter(vendorAdapter);

        // Hibernate properties
        Map<String, Object> properties = buildHibernateProperties();
        em.setJpaPropertyMap(properties);

        return em;
    }

    private Map<String, Object> buildHibernateProperties() {
        Map<String, Object> properties = new HashMap<>();

        // Schema management
        properties.put("hibernate.hbm2ddl.auto", ddlAuto);

        // Dialect (if specified)
        if (dialect != null && !dialect.isEmpty()) {
            properties.put("hibernate.dialect", dialect);
        }

        // SQL logging
        properties.put("hibernate.show_sql", showSql);
        properties.put("hibernate.format_sql", showSql);
        properties.put("hibernate.use_sql_comments", showSql);

        // JDBC batch processing - critical for performance
        properties.put("hibernate.jdbc.batch_size", String.valueOf(batchSize));
        properties.put("hibernate.order_inserts", "true");
        properties.put("hibernate.order_updates", "true");
        properties.put("hibernate.jdbc.batch_versioned_data", "true");

        // Connection management
        properties.put("hibernate.connection.autocommit", "false");

        // Query plan cache
        properties.put("hibernate.query.plan_cache_max_size", "2048");
        properties.put("hibernate.query.plan_parameter_metadata_max_size", "128");

        // Default batch fetch size (helps with N+1)
        properties.put("hibernate.default_batch_fetch_size", "10");

        return properties;
    }
}

application.properties:

# Development
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.jdbc.batch_size=20
spring.jpa.database=POSTGRESQL
# spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# Production - override with:
# spring.jpa.show-sql=false
# spring.jpa.hibernate.ddl-auto=none

Template: Multiple Persistence Units (Multiple Databases)

COPY-PASTE READY - complete multi-database setup:

package com.example.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.orm.jpa.JpaTransactionManager;

@Configuration
@EnableTransactionManagement
public class MultiDbConfig {

    // ========== PRIMARY DATABASE ==========

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.primary.domain");
        em.setPersistenceUnitName("primaryPU");  // REQUIRED - must be unique

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.POSTGRESQL);
        vendorAdapter.setGenerateDdl(false);
        em.setJpaVendorAdapter(vendorAdapter);

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "validate");
        properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
        properties.put("hibernate.jdbc.batch_size", "20");
        properties.put("hibernate.order_inserts", "true");
        properties.put("hibernate.order_updates", "true");
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean
    @Primary
    public PlatformTransactionManager primaryTransactionManager(
            @Qualifier("primaryEntityManagerFactory") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }

    // ========== SECONDARY DATABASE ==========

    @Bean
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.secondary.domain");
        em.setPersistenceUnitName("secondaryPU");  // REQUIRED - must be unique

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.MYSQL);
        vendorAdapter.setGenerateDdl(false);
        em.setJpaVendorAdapter(vendorAdapter);

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "validate");
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
        properties.put("hibernate.jdbc.batch_size", "20");
        properties.put("hibernate.order_inserts", "true");
        properties.put("hibernate.order_updates", "true");
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean
    public PlatformTransactionManager secondaryTransactionManager(
            @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

application.properties:

# Primary Database (PostgreSQL)
spring.datasource.primary.jdbc-url=jdbc:postgresql://localhost:5432/primary_db
spring.datasource.primary.username=user1
spring.datasource.primary.password=pass1
spring.datasource.primary.driver-class-name=org.postgresql.Driver
spring.datasource.primary.hikari.maximum-pool-size=10

# Secondary Database (MySQL)
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/secondary_db
spring.datasource.secondary.username=user2
spring.datasource.secondary.password=pass2
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.secondary.hikari.maximum-pool-size=10

IMPORTANT: Use jdbc-url instead of url when using @ConfigurationProperties

Usage with multiple databases:

package com.example.service;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MultiDbService {

    @PersistenceContext(unitName = "primaryPU")
    private EntityManager primaryEM;

    @PersistenceContext(unitName = "secondaryPU")
    private EntityManager secondaryEM;

    // Uses primary transaction manager (default due to @Primary)
    @Transactional
    public void saveToPrimary(PrimaryEntity entity) {
        primaryEM.persist(entity);
    }

    // Explicitly specify secondary transaction manager
    @Transactional("secondaryTransactionManager")
    public void saveToSecondary(SecondaryEntity entity) {
        secondaryEM.persist(entity);
    }

    // CRITICAL ERROR: Cannot mix transaction managers in same method!
    // This will NOT work as expected:
    @Transactional("primaryTransactionManager")
    public void saveToBoth() {
        primaryEM.persist(new PrimaryEntity());   // OK - in primary transaction
        secondaryEM.persist(new SecondaryEntity()); // WRONG - not in secondary transaction!
    }

    // CORRECT: Use separate methods or ChainedTransactionManager (not recommended)
    @Transactional("primaryTransactionManager")
    public void saveToPrimaryOnly(PrimaryEntity entity) {
        primaryEM.persist(entity);
    }

    @Transactional("secondaryTransactionManager")
    public void saveToSecondaryOnly(SecondaryEntity entity) {
        secondaryEM.persist(entity);
    }

    // Call both methods from a service without @Transactional
    public void saveBoth(PrimaryEntity primary, SecondaryEntity secondary) {
        saveToPrimaryOnly(primary);
        saveToSecondaryOnly(secondary);
        // Note: Not atomic across both databases!
    }
}

Template: Second-Level Cache Configuration

import jakarta.persistence.SharedCacheMode;
import org.hibernate.cache.jcache.ConfigSettings;

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource);
    em.setPackagesToScan("com.example.domain");

    // Cache configuration
    em.setSharedCacheMode(SharedCacheMode.ENABLE_SELECTIVE);
    // Options: ALL, NONE, ENABLE_SELECTIVE (use @Cacheable), DISABLE_SELECTIVE

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);

    Map<String, Object> properties = new HashMap<>();
    properties.put("hibernate.cache.use_second_level_cache", "true");
    properties.put("hibernate.cache.use_query_cache", "true");
    properties.put("hibernate.cache.region.factory_class",
        "org.hibernate.cache.jcache.JCacheRegionFactory");
    properties.put("hibernate.javax.cache.provider",
        "org.ehcache.jsr107.EhcacheCachingProvider");

    // Cache configuration file
    properties.put("hibernate.javax.cache.uri",
        "classpath:ehcache.xml");

    em.setJpaPropertyMap(properties);

    return em;
}

Entity with cache:

import jakarta.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Cacheable  // Required when using ENABLE_SELECTIVE
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @OneToMany(mappedBy = "product")
    private List<Review> reviews;
}

ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.ehcache.org/v3">
    <cache alias="com.example.domain.Product">
        <expiry>
            <ttl unit="minutes">10</ttl>
        </expiry>
        <resources>
            <heap unit="entries">1000</heap>
            <offheap unit="MB">10</offheap>
        </resources>
    </cache>

    <cache alias="com.example.domain.Product.reviews">
        <expiry>
            <ttl unit="minutes">5</ttl>
        </expiry>
        <resources>
            <heap unit="entries">5000</heap>
        </resources>
    </cache>

    <!-- Default cache for query results -->
    <cache alias="default-query-results-region">
        <expiry>
            <ttl unit="minutes">5</ttl>
        </expiry>
        <resources>
            <heap unit="entries">1000</heap>
        </resources>
    </cache>
</config>

Template: Async Initialization (Faster Startup)

import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Bean
public AsyncTaskExecutor jpaBootstrapExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setMaxPoolSize(1);
    executor.setThreadNamePrefix("jpa-bootstrap-");
    executor.setWaitForTasksToCompleteOnShutdown(false);
    executor.initialize();
    return executor;
}

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
        DataSource dataSource,
        AsyncTaskExecutor jpaBootstrapExecutor) {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource);
    em.setPackagesToScan("com.example.domain");
    em.setBootstrapExecutor(jpaBootstrapExecutor);  // Async initialization

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);

    return em;
}

Testing Configuration

In-Memory H2 Database for Tests

package com.example.config;

import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

@Configuration
@EnableTransactionManagement
@Profile("test")
public class TestJpaConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .setName("testdb")
            .build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.domain");

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.H2);
        vendorAdapter.setGenerateDdl(true);  // OK for tests
        vendorAdapter.setShowSql(true);      // OK for tests
        em.setJpaVendorAdapter(vendorAdapter);

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "create-drop");  // OK for tests
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.put("hibernate.show_sql", "true");
        properties.put("hibernate.format_sql", "true");
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

Test Example

package com.example.service;

import com.example.domain.User;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@ActiveProfiles("test")
@Transactional  // Rollback after each test
class UserServiceTest {

    @Autowired
    private UserService userService;

    @PersistenceContext
    private EntityManager entityManager;

    @Test
    void testCreateUser() {
        User user = userService.create("John", "john@example.com");

        // Verify ID was generated
        assertThat(user.getId()).isNotNull();

        // Force write to DB and clear cache
        entityManager.flush();
        entityManager.clear();

        // Verify user was persisted
        User found = userService.findById(user.getId());
        assertThat(found.getName()).isEqualTo("John");
        assertThat(found.getEmail()).isEqualTo("john@example.com");
    }

    @Test
    void testUpdateUser() {
        User user = userService.create("Jane", "jane@example.com");
        entityManager.flush();
        entityManager.clear();

        userService.updateName(user.getId(), "Jane Doe");
        entityManager.flush();
        entityManager.clear();

        User updated = userService.findById(user.getId());
        assertThat(updated.getName()).isEqualTo("Jane Doe");
    }

    @Test
    void testDeleteUser() {
        User user = userService.create("Bob", "bob@example.com");
        Long userId = user.getId();
        entityManager.flush();
        entityManager.clear();

        userService.delete(userId);
        entityManager.flush();

        User found = entityManager.find(User.class, userId);
        assertThat(found).isNull();
    }
}

Testcontainers for Real Database Testing

package com.example.config;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;

@TestConfiguration
public class TestcontainersConfig {

    @Bean
    @ServiceConnection  // Spring Boot 3.1+
    public PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    }
}

Critical Property Reference

hibernate.hbm2ddl.auto (Schema Generation)

ValueBehaviorProduction Safe?Use CaseData Loss Risk
noneDo nothing✅ YESProduction (recommended) - use Flyway/Liquibase✅ No
validateValidate schema against entities✅ YESProduction fallback - ensures schema matches✅ No
updateUpdate schema (adds, doesn't drop)❌ NODevelopment only - risky, can cause issues⚠️ Possible
createDrop and create schema on startup❌ NOTesting only - loses all data❌ YES
create-dropDrop on startup and shutdown❌ NOUnit tests only - clean slate each run❌ YES

Validation Regex: ^(none|validate|update|create|create-drop)$

Configuration:

properties.put("hibernate.hbm2ddl.auto", "validate");

Database Dialects

DatabaseDialect ClassDatabase EnumMin Version
PostgreSQLorg.hibernate.dialect.PostgreSQLDialectDatabase.POSTGRESQL9.5+
MySQL 8.xorg.hibernate.dialect.MySQL8DialectDatabase.MYSQL8.0+
MySQL 5.7org.hibernate.dialect.MySQL57DialectDatabase.MYSQL5.7+
Oracle 12c+org.hibernate.dialect.Oracle12cDialectDatabase.ORACLE12c+
Oracle 21c+org.hibernate.dialect.Oracle21cDialectDatabase.ORACLE21c+
SQL Serverorg.hibernate.dialect.SQLServerDialectDatabase.SQL_SERVER2012+
H2org.hibernate.dialect.H2DialectDatabase.H22.x
MariaDBorg.hibernate.dialect.MariaDBDialectDatabase.MYSQL10.3+

Set dialect explicitly:

// Option 1: Via vendor adapter (recommended)
vendorAdapter.setDatabase(Database.POSTGRESQL);

// Option 2: Via properties (more specific)
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");

Performance Properties

Map<String, Object> properties = new HashMap<>();

// === JDBC Batch Processing - CRITICAL for write performance ===
properties.put("hibernate.jdbc.batch_size", "20");  // Recommended: 20-50
properties.put("hibernate.order_inserts", "true");  // REQUIRED to enable batching
properties.put("hibernate.order_updates", "true");  // REQUIRED to enable batching
properties.put("hibernate.jdbc.batch_versioned_data", "true");  // Batch @Version entities

// === Default Fetch Size - reduces round trips ===
properties.put("hibernate.default_batch_fetch_size", "10");
properties.put("hibernate.jdbc.fetch_size", "50");  // JDBC ResultSet fetch size

// === Query Plan Cache - improves query performance ===
properties.put("hibernate.query.plan_cache_max_size", "2048");
properties.put("hibernate.query.plan_parameter_metadata_max_size", "128");

// === Statistics - enable in development to detect N+1 ===
properties.put("hibernate.generate_statistics", "false");  // true in dev only
properties.put("hibernate.session.events.log", "false");  // true for detailed logging

// === SQL Logging - ALWAYS false in production ===
properties.put("hibernate.show_sql", "false");
properties.put("hibernate.format_sql", "false");
properties.put("hibernate.use_sql_comments", "false");
properties.put("hibernate.highlight_sql", "false");  // ANSI color highlighting in logs

// === Connection Provider ===
properties.put("hibernate.connection.provider_disables_autocommit", "true");  // HikariCP optimization

Common Mistakes → Solutions

Mistake 1: Wrong Transaction Manager Parameter Type

ERROR: java.lang.NullPointerException at JpaTransactionManager.<init>

// WRONG - receives factory bean, can return null during init
@Bean
public PlatformTransactionManager transactionManager(
        LocalContainerEntityManagerFactoryBean em) {
    return new JpaTransactionManager(em.getObject());  // NPE risk!
}

// CORRECT - receives actual EntityManagerFactory
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
}

Mistake 2: Missing Persistence Unit Name for Multiple Databases

ERROR:

org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'jakarta.persistence.EntityManager' available:
expected single matching bean but found 2
// WRONG - ambiguous which persistence unit
@PersistenceContext
private EntityManager entityManager;  // Which one?

// CORRECT - specify unit name
@PersistenceContext(unitName = "primaryPU")
private EntityManager primaryEntityManager;

@PersistenceContext(unitName = "secondaryPU")
private EntityManager secondaryEntityManager;

Mistake 3: Mixing JPA and Hibernate Transaction Managers

// WRONG - using Hibernate TXN manager with JPA factory
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(...) { ... }

@Bean
public HibernateTransactionManager transactionManager(SessionFactory sf) {
    return new HibernateTransactionManager(sf);  // Type mismatch!
}

// CORRECT - use matching transaction manager
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
}

Mistake 4: Not Setting Database Dialect

ISSUE: Hibernate may guess wrong dialect, causing SQL errors

// WRONG - Hibernate guesses dialect (unreliable)
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
// No dialect set - may use wrong SQL syntax

// CORRECT - explicit dialect (recommended)
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setDatabase(Database.POSTGRESQL);

// OR via properties (more specific)
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");

Mistake 5: Using "update" in Production

RISK: Can corrupt schema, drop columns, lose data

// WRONG - dangerous in production, can corrupt data
properties.put("hibernate.hbm2ddl.auto", "update");

// CORRECT - use schema migration tool
properties.put("hibernate.hbm2ddl.auto", "validate");
// Use Flyway or Liquibase for schema changes

Example Flyway migration:

-- V1__create_user_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Mistake 6: Incorrect Package Scanning

ERROR: No entities found for package com.example.domain

// WRONG - typo or wrong package
em.setPackagesToScan("com.exmaple.domain");  // Typo!

// WRONG - not recursive
em.setPackagesToScan("com.example");  // Misses com.example.domain.user.User

// CORRECT - exact package (scans recursively)
em.setPackagesToScan("com.example.domain");  // Finds all @Entity in subpackages

// CORRECT - multiple packages
em.setPackagesToScan("com.example.domain", "com.example.entities");

Mistake 7: Not Configuring Connection Pool

ISSUE: Poor performance, connection exhaustion

// WRONG - using default connection pool settings
// (Usually fine with Spring Boot, but not standalone Spring)

// CORRECT - configure HikariCP properly
@Bean
public DataSource dataSource() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
    config.setUsername("user");
    config.setPassword("pass");

    // Pool sizing
    config.setMaximumPoolSize(10);  // Based on available connections
    config.setMinimumIdle(5);       // Minimum idle connections

    // Timeouts
    config.setConnectionTimeout(30000);  // 30 seconds
    config.setIdleTimeout(600000);       // 10 minutes
    config.setMaxLifetime(1800000);      // 30 minutes

    // Health check
    config.setConnectionTestQuery("SELECT 1");

    return new HikariDataSource(config);
}

Spring Boot Integration

When using Spring Boot, configuration is simplified:

application.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: pass
    driver-class-name: org.postgresql.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    open-in-view: false  # Recommended: disable
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
        use_sql_comments: true
        jdbc:
          batch_size: 20
          fetch_size: 50
        order_inserts: true
        order_updates: true
        jdbc.batch_versioned_data: true
        default_batch_fetch_size: 10
        query.plan_cache_max_size: 2048
        query.plan_parameter_metadata_max_size: 128

Spring Boot Auto-Configures:

  • EntityManagerFactory bean
  • JpaTransactionManager bean
  • @EnableTransactionManagement
  • ✅ Exception translation
  • ✅ @PersistenceContext support
  • ✅ DataSource pooling (HikariCP by default)

Custom Configuration Still Needed For:

  • Multiple databases
  • Advanced Hibernate features (interceptors, custom types, multi-tenancy)
  • Custom vendor adapter settings
  • Non-standard entity scanning

API Reference: LocalContainerEntityManagerFactoryBean

class LocalContainerEntityManagerFactoryBean
    extends AbstractEntityManagerFactoryBean
    implements ResourceLoaderAware, LoadTimeWeaverAware {

    // ========== REQUIRED CONFIGURATION ==========

    // DataSource (REQUIRED)
    void setDataSource(DataSource dataSource);
    DataSource getDataSource();
    void setJtaDataSource(DataSource jtaDataSource);  // For JTA transactions

    // Entity Scanning (choose ONE approach)
    void setPackagesToScan(String... packagesToScan);  // Scan for @Entity (RECOMMENDED)
    void setManagedTypes(PersistenceManagedTypes managedTypes);  // Pre-scanned (AOT/GraalVM)
    void setManagedClassNameFilter(ManagedClassNameFilter filter);  // Filter scanned classes

    // ========== PERSISTENCE UNIT ==========

    // Unit name (REQUIRED for multiple databases)
    void setPersistenceUnitName(String name);
    String getPersistenceUnitName();

    // Optional persistence.xml location
    void setPersistenceXmlLocation(String location);  // Default: META-INF/persistence.xml

    // ========== VENDOR ADAPTER ==========

    // Vendor adapter (HibernateJpaVendorAdapter, EclipseLinkJpaVendorAdapter)
    void setJpaVendorAdapter(JpaVendorAdapter adapter);
    JpaVendorAdapter getJpaVendorAdapter();

    // Vendor properties
    void setJpaProperties(Properties properties);
    void setJpaPropertyMap(Map<String, ?> properties);  // PREFERRED over Properties
    Map<String, Object> getJpaPropertyMap();

    // ========== CACHE AND VALIDATION ==========

    // Cache mode
    void setSharedCacheMode(SharedCacheMode mode);
    // Options: ENABLE_SELECTIVE, DISABLE_SELECTIVE, ALL, NONE

    // Validation mode
    void setValidationMode(ValidationMode mode);
    // Options: AUTO, CALLBACK, NONE

    // ========== ADVANCED FEATURES ==========

    // Load-time weaving for entity enhancement
    void setLoadTimeWeaver(LoadTimeWeaver weaver);

    // Async initialization (faster startup)
    void setBootstrapExecutor(AsyncTaskExecutor bootstrapExecutor);

    // Post-processing
    void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... processors);

    // Persistence unit manager
    void setPersistenceUnitManager(PersistenceUnitManager pum);

    // ========== ACCESS ==========

    // Get created factory (returns null until afterPropertiesSet() completes)
    EntityManagerFactory getObject();
    Class<? extends EntityManagerFactory> getObjectType();
    boolean isSingleton();  // Returns true
}

Troubleshooting

Problem: EntityManagerFactory Not Created

ERROR: Error creating bean with name 'entityManagerFactory'

COMMON CAUSESSOLUTIONS:

CauseSolutionVerification
Missing DataSource beanCreate DataSource beanCheck bean exists in context
Invalid database URL/credentialsCheck connection stringTest with SQL client
Missing JDBC driver dependencyAdd driver to pom.xml/build.gradleCheck Maven/Gradle dependencies
Wrong dialect for databaseSet correct Database enum or dialect classMatch database version
Missing @EnableTransactionManagementAdd annotation to @ConfigurationCheck configuration class
Entity scanning path incorrectVerify package namesCheck @Entity classes exist

Verification:

@Bean
public DataSource dataSource() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
    config.setUsername("user");
    config.setPassword("pass");
    config.setDriverClassName("org.postgresql.Driver");

    // Test connection on startup
    config.setConnectionTestQuery("SELECT 1");
    config.setConnectionTimeout(5000);

    return new HikariDataSource(config);
}

Problem: Schema Validation Errors on Startup

ERROR: Schema-validation: missing table [user_table]

CAUSE: Entity doesn't match database schema

SOLUTIONS:

// Option 1: Fix entity to match database
@Entity
@Table(name = "user_table")  // Match exact table name (case-sensitive!)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")  // Match exact column name
    private Long id;

    @Column(name = "user_name")  // Match exact column name
    private String name;
}

// Option 2: Disable validation temporarily (development only)
properties.put("hibernate.hbm2ddl.auto", "update");  // OR "none"

// Option 3: Use naming strategy for automatic mapping
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource);
    em.setPackagesToScan("com.example.domain");

    // Spring naming strategy: camelCase -> snake_case
    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);

    Map<String, Object> properties = new HashMap<>();
    properties.put("hibernate.physical_naming_strategy",
        SpringPhysicalNamingStrategy.class.getName());
    em.setJpaPropertyMap(properties);

    return em;
}

Problem: Slow Startup Time

CAUSE: Entity scanning of large classpath

SOLUTIONS:

// Solution 1: Narrow package scanning
em.setPackagesToScan(
    "com.example.domain",  // Only scan specific packages
    "com.example.entities"
);

// Solution 2: Use async initialization
em.setBootstrapExecutor(jpaBootstrapExecutor);

// Solution 3: Explicitly list entity classes (fastest)
em.setManagedTypes(PersistenceManagedTypes.of(
    User.class,
    Order.class,
    Product.class
));

Problem: Multiple EntityManagerFactory Beans Conflict

ERROR: No qualifying bean of type 'EntityManagerFactory' available: expected single matching bean but found 2

CAUSE: Multiple EntityManagerFactory beans without @Primary

SOLUTION:

// Mark one as @Primary
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(...) { ... }

// Use @Qualifier for injection
@Autowired
@Qualifier("primaryEntityManagerFactory")
private EntityManagerFactory primaryEMF;

@Autowired
@Qualifier("secondaryEntityManagerFactory")
private EntityManagerFactory secondaryEMF;

Related Topics

  • JPA Transaction Management - Transaction configuration and patterns
  • JPA Vendor Adapters - Hibernate and EclipseLink adapter details
  • Persistence Unit Management - Advanced entity scanning