javaspringspring-boothibernatejpa

Create Spring JPA Entities at runtime


I would like to create JPA Entities at runtime, based on a given database scheme, that will be polled through a REST-API and I need to register them in the Spring Context.

There are two approaches that comes to my mind:

  1. Use a code generator, similiar to hbm2java (hibernate-tools), generate the entities at startup. However I have to restart everytime a table will be added.

  2. Generate code for the entity as well but compile the java code at runtime and register the entity in the spring context manually.

It has to be Spring Data JPA, because I want to setup Spring Data (or OData with Spring support) on top.

Is there an approach to register entities at runtime without creating a POJO/Entity class (as a file)?


Solution

  • Yes, you can generate Spring JPA entities programmatically at runtime.

    You have to use HBM2Java in a tricky way. Let me show you how.

    I'm using MacOS, JDK 1.8, MySQL 8.0.30 Server, Spring Boot 1.5.3.RELEASE and HBM2Java to demonstrate the programmatic approach.

    Step - By - Step Process :

    The complete project structure:

    .
    ├── HELP.md
    ├── mvnw
    ├── mvnw.cmd
    ├── pom.xml
    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── com
    │   │   │       └── example
    │   │   │           └── hbm2java
    │   │   │               └── springboot
    │   │   │                   └── hbm2javaspringbootruntime
    │   │   │                       ├── Hbm2javaSpringBootRuntimeApplication.java
    │   │   │                       ├── controllers
    │   │   │                       │   └── EntityGeneratorController.java
    │   │   │                       ├── service
    │   │   │                       │   └── GenerateEntityFromDBService.java
    │   │   │                       ├── strategy
    │   │   │                       │   └── TestDatabaseReverseEngineeringStrategy.java
    │   │   │                       └── utils
    │   │   │                           └── Hbm2JavaUtils.java
    │   │   └── resources
    │   │       ├── application.properties
    │   │       ├── hbm
    │   │       │   └── test-db.cfg.xml
    │   │       ├── static
    │   │       └── templates
    │   └── test
    │       └── java
    │           └── com
    │               └── example
    │                   └── hbm2java
    │                       └── springboot
    │                           └── hbm2javaspringbootruntime
    

    You will need to dependencies like spring-dev-tools (for live reload), hibernate-tools, hibernate-entitymanager and spring-data-jpa in the pom.xml.

    Yes, you can use both JPA and explicit Hibernate approach in the same project.

    Note: hibernate-tools comes with HBM2Java features and we have to leverage those features in our spring boot application to generate the entities on runtime.

    pom.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.3.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example.hbm2java.spring-boot</groupId>
        <artifactId>hbm2java-spring-boot-runtime</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>hbm2java-spring-boot-runtime</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-core</artifactId>
                <version>5.2.10.Final</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-entitymanager</artifactId>
                <version>5.2.3.Final</version>
            </dependency>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-tools</artifactId>
                <version>5.2.3.Final</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
                <version>8.0.13</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    Now, I assume you already have schema with tables in it.

    I have created a schema 'TEST' in DB and it has tables 'User' and 'Role'.

    Schema Structure:

    enter image description here

    Table Structure:

    User:

    enter image description here

    Role: enter image description here

    Startup Class:

    @SpringBootApplication
    public class Hbm2javaSpringBootRuntimeApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(Hbm2javaSpringBootRuntimeApplication.class, args);
        }
        
    }
    

    EntityGeneratorController (REST-API):

    @RestController
    @RequestMapping("entity")
    public class EntityGeneratorController {
    
        @Autowired
        private GenerateEntityFromDBService service;
    
        @PostMapping("generate-from-db")
        public void generateEntitiesFromDatabase(@RequestParam String basePackageName,
                                                 @RequestParam String hibernateCfgFileName,
                                                 @RequestParam String reverseStrategyClassName) {
            try {
                service.generateEntities(basePackageName, hibernateCfgFileName, reverseStrategyClassName);
                System.out.println("Entities are generated successfully and now restarting the server....");
                // Reload/Restart the server after generating the entities.
                Restarter.getInstance().restart();
            } catch (Exception e) {
                System.out.println("Entities are failed to generate. Reason: " + e.getMessage());
            }
    
        }
    }
    

    Note: You can see the line Restarter.getInstance().restart();. This will take the responsibility of restarting the server.

    I have created a POST REST Endpoint to generate the entities on runtime, restart the server and spring context will be updated with JPA entities.

    This endpoint takes the three parameters:

    Business Logic/Service Layer:

    @Service
    public class GenerateEntityFromDBService {
    
        @Autowired
        private Hbm2JavaUtils utils;
    
        public void generateEntities(String basePackageName, String hibernateCfgFileName, String reverseStrategyClassName) {
            HibernateToolTask toolTask = utils.createHibernateToolTaskInstance();
            JDBCConfigurationTask jdbcConf = utils.createJDBCConfigurationTaskInstance(toolTask, basePackageName,
                    hibernateCfgFileName, reverseStrategyClassName);
            jdbcConf.execute();
            Hbm2JavaExporterTask hbm2Java = utils.createEntityExporterTaskInstance(toolTask);
            hbm2Java.execute();
        }
    
    }
    

    Reverse Engineering Strategy for TEST DB:

    /**
     * This class will be used to override the Default Reverse Engineering Strategy
     * and control the reverse engineering only for 'TEST' DB
     */
    public class TestDatabaseReverseEngineeringStrategy extends DelegatingReverseEngineeringStrategy {
    
        private ReverseEngineeringStrategy delegate;
    
        public TestDatabaseReverseEngineeringStrategy(ReverseEngineeringStrategy delegate) {
            super(delegate);
            this.delegate = delegate;
        }
    
        /**
         * This method be called to tell HBM to generate entities from tables present only in 'TEST' schema only.
         * <p>
         * Otherwise, by default all the schemas are selected.
         *
         * @return the selected list of schema on which the HBM has to run.
         */
        public List<SchemaSelection> getSchemaSelections() {
            return delegate == null ? null : Collections.singletonList(new SchemaSelection("TEST", "TEST"));
        }
    
    
    }
    

    Custom Utility to handle HBM2Java:

    @Component
    public class Hbm2JavaUtils {
    
        private static final File DESTINATION_DIR = new File("src/main/java");
    
        public HibernateToolTask createHibernateToolTaskInstance() {
            HibernateToolTask toolTask = new HibernateToolTask();
            toolTask.setDestDir(DESTINATION_DIR);
            return toolTask;
        }
    
        public JDBCConfigurationTask createJDBCConfigurationTaskInstance(HibernateToolTask toolTask,
                                                                         String basePackageName,
                                                                         String hibernateCfgFileName,
                                                                         String reverseStrategyClassName) {
            JDBCConfigurationTask jdbcConfTask = toolTask.createJDBCConfiguration();
            jdbcConfTask.setConfigurationFile(new File("src/main/resources/hbm/" + hibernateCfgFileName));
            jdbcConfTask.setReverseStrategy(basePackageName + ".strategy." + reverseStrategyClassName);
            jdbcConfTask.setDetectManyToMany(true);
            jdbcConfTask.setDetectOneToOne(true);
            jdbcConfTask.setPackageName(basePackageName + ".persistence");
            return jdbcConfTask;
        }
    
        public Hbm2JavaExporterTask createEntityExporterTaskInstance(HibernateToolTask toolTask) {
            Hbm2JavaExporterTask hbm2Java = (Hbm2JavaExporterTask) toolTask.createHbm2Java();
            hbm2Java.setJdk5(Boolean.TRUE);
            hbm2Java.setEjb3(Boolean.TRUE);
            hbm2Java.setDestdir(DESTINATION_DIR);
            hbm2Java.validateParameters();
            return hbm2Java;
        }
    
    
    }
    

    Note: The entities will in the basePackageName.persistence package. You can see the line jdbcConfTask.setPackageName(basePackageName + ".persistence");

    CFG file (test-db.cfg.xml):

    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE hibernate-configuration SYSTEM
            "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
    
    <hibernate-configuration>
        <session-factory>
            <property name="hibernate.dialect">
                org.hibernate.dialect.MySQLDialect
            </property>
            <property name="hibernate.connection.driver_class">
                com.mysql.cj.jdbc.Driver
            </property>
            <property name="hibernate.connection.url">
                jdbc:mysql://localhost:3306/TEST?autoReconnect=true&amp;useSSL=false
            </property>
            <property name="hibernate.default_schema">TEST</property>
            <property name="hibernate.connection.username">
                root
            </property>
            <property name="hibernate.connection.password">Anish@123</property>
            <property name="show_sql">true</property>
        </session-factory>
    </hibernate-configuration>
    

    application.properties (for Spring Data JPA) :

    spring.datasource.url=jdbc:mysql://localhost:3306/TEST
    spring.datasource.username=root
    spring.datasource.password=Anish@123
    

    After the application starts successfully, we will need to hit the POST EndPoint.

    enter image description here

    It will successfully create the Role and User JPA entities and restart the server too.

    enter image description here

    User:

    // Generated 01-Oct-2023 17:37:04 by Hibernate Tools 5.2.3.Final
    
    
    import javax.persistence.*;
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * User generated by hbm2java
     */
    @Entity
    @Table(name = "USER"
            , catalog = "TEST"
    )
    public class User implements java.io.Serializable {
    
    
        private int id;
        private String name;
        private Set<Role> roles = new HashSet<Role>(0);
    
        public User() {
        }
    
    
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
    
        public User(int id, String name, Set<Role> roles) {
            this.id = id;
            this.name = name;
            this.roles = roles;
        }
    
        @Id 
        @Column(name = "ID", unique = true, nullable = false)
        public int getId() {
            return this.id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
    
        @Column(name = "NAME", nullable = false, length = 50)
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
        public Set<Role> getRoles() {
            return this.roles;
        }
    
        public void setRoles(Set<Role> roles) {
            this.roles = roles;
        }
    
    
    }
    

    Role:

    // Generated 01-Oct-2023 17:37:04 by Hibernate Tools 5.2.3.Final
    
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.Id;
    import javax.persistence.JoinColumn;
    import javax.persistence.ManyToOne;
    import javax.persistence.Table;
    
    /**
     * Role generated by hbm2java
     */
    @Entity
    @Table(name = "ROLE"
            , catalog = "TEST"
    )
    public class Role implements java.io.Serializable {
    
    
        private int id;
        private User user;
        private String name;
    
        public Role() {
        }
    
        public Role(int id, User user, String name) {
            this.id = id;
            this.user = user;
            this.name = name;
        }
    
        @Id
        @Column(name = "ID", unique = true, nullable = false)
        public int getId() {
            return this.id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "FK_USER_ID", nullable = false)
        public User getUser() {
            return this.user;
        }
    
        public void setUser(User user) {
            this.user = user;
        }
    
    
        @Column(name = "NAME", nullable = false, length = 50)
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
    
    }
    

    Now to test whether or not I'm able to do CRUD operation via Spring-Data-JPA, I created an User CrudRepository interface.

    package com.example.hbm2java.springboot.hbm2javaspringbootruntime.persistence.repository;
    
    import com.example.hbm2java.springboot.hbm2javaspringbootruntime.persistence.User;
    import org.springframework.data.repository.CrudRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface UserRepository extends CrudRepository<User, Integer> {
    }
    

    Also, I updated the existing controller with new endpoint to verify.

    @RestController
    @RequestMapping("entity")
    public class EntityGeneratorController {
    
        @Autowired
        private UserRepository repo;
    
        @PostMapping("saveUser")
        public User saveUser() {
            User u = new User();
            u.setId(1);
            u.setName("Anish");
            return repo.save(u);
        }
    }
    

    Output:

    enter image description here

    DB:

    enter image description here

    This proves that Spring-Data-JPA is working fine.

    Restart logs:

    enter image description here

    Credits to the blogs that helped me to reach to the solution: