Daniel Taub
Daniel Taub

Reputation: 5389

Validate schema programmatically using hibernate

In mose projects the way to run your java app with schema validation is with that configuration (when using spring):

spring.jpa.hibernate.ddl-auto=validate

I ran into a problem that I need to validate my schema at a specific times during running, is there any way to implement that?

I saw that hibernate managed it with the AbstractSchemaValidator, I'm using spring with hibernate, and I didn't found any information how to deal with it,
the only thing I found is How to validate database schema programmatically in hibernate with annotations? , but it was removed in the older versions of spring-boot

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.0.4.RELEASE</version>
</dependency>

any ideas?

Upvotes: 7

Views: 12784

Answers (3)

Geert Graat
Geert Graat

Reputation: 205

I came across this post when I needed to find a way to validate the schema by Hibernate in a testcase. Reason for this is that the schema is created in the test using database scripts. When I set the following property

hibernate.hbm2ddl.auto=validate

Hibernate will immediately report the tables are not there, before the create scripts are executed.

So I needed a way to validate the schema after it has been created. I found that in Hibernate 6.2 the SchemaManager interface was introduced, which you can obtain via the SessionFactory, which is exactly suited for this task.

Using the following code you can easily validate the current schema in a testcase:

@Autowired
SessionFactory sessionFactory;
   
@Test
void validateSchema() {
  sessionFactory.getSchemaManager().validateMappedObjects();
}

Upvotes: 5

Oleksii Zghurskyi
Oleksii Zghurskyi

Reputation: 4365

This is solution, if your use case requires:

  • granular & explicit control of which part of the schema should be validated
  • the need is to validate multiple schemas
  • the need is to validate schema that is not used by the service, on which scheduled validator is running
  • db connections used by application should not be influenced by validation in any way (meaning, you don't want to borrow connection from main connections pool)

If above applies for your needs, than this is example of how to do scheduled schema validation:

  1. Sources
@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(ScheamValidatorProperties.class)
public class SchemaValidatorApplication {
     public static void main(String[] args) {
       SpringApplication.run(SchemaValidatorApplication.class, args);
    }
}

@ConfigurationProperties("schema-validator")
class ScheamValidatorProperties {
    public Map<String, String> settings = new HashMap<>();

    public ScheamValidatorProperties() {
    }

    public Map<String, String> getSettings() { 
        return this.settings;
    }

    public void setSome(Map<String, String> settings) { 
        this.settings = settings;
    }
}

@Component
class ScheduledSchemaValidator {

    private ScheamValidatorProperties props;

    public ScheduledSchemaValidator(ScheamValidatorProperties props) {
        this.props = props;
    }

    @Scheduled(cron = "0 0/1 * * * ?")
    public void validateSchema() {
        StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
            .applySettings(props.getSettings())
            .build();

        Metadata metadata = new MetadataSources(serviceRegistry)
            .addAnnotatedClass(Entity1.class)
            .addAnnotatedClass(Entity2.class)
            .buildMetadata();

        try {
            new SchemaValidator().validate(metadata, serviceRegistry);
        } catch (Exception e) {
            System.out.println("Validation failed: " + e.getMessage());
        } finally {
            StandardServiceRegistryBuilder.destroy(serviceRegistry);
        }
    }
}

@Entity
@Table(name = "table1")
class Entity1 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    Entity1() {}

    public Long getId() {
        return id;
    }

}

@Entity
@Table(name = "table2")
class Entity2 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    Entity2() {}

    public Long getId() {
        return id;
    }
}
  1. schema.sql
CREATE DATABASE IF NOT EXISTS testdb;

CREATE TABLE IF NOT EXISTS `table1` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `table2` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
);

  1. application.yml
spring:
  cache:
    type: none
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3309/testdb?useSSL=false&nullNamePatternMatchesAll=true&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: test_user
    password: test_password
    testWhileIdle: true
    validationQuery: SELECT 1
  jpa:
    show-sql: false
    database-platform: org.hibernate.dialect.MySQL8Dialect
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
    properties:
      hibernate.dialect: org.hibernate.dialect.MySQL8Dialect
      hibernate.cache.use_second_level_cache: false
      hibernate.cache.use_query_cache: false
      hibernate.generate_statistics: false
      hibernate.hbm2ddl.auto: validate

schema-validator:
    settings:
        connection.driver_class: com.mysql.cj.jdbc.Driver
        hibernate.dialect: org.hibernate.dialect.MySQL8Dialect
        hibernate.connection.url: jdbc:mysql://localhost:3309/testdb?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true
        hibernate.connection.username: test_user
        hibernate.connection.password: test_password
        hibernate.default_schema: testdb

  1. docker-compose.yml
version: '3.0'

services:
  db:
    image: mysql:8.0.14
    restart: always
    ports:
     - 3309:3306
    environment:
      MYSQL_ROOT_PASSWORD: test_password
      MYSQL_DATABASE: testdb
      MYSQL_USER: test_user
      MYSQL_PASSWORD: test_password

Upvotes: 5

Ken Chan
Ken Chan

Reputation: 90517

If you want to let the SchemaValidator to reuse the connection configuration and the mapping information that are already configured in the project rather then defining them once again for schema validation, you should consider my solution such that you are DRY and don't need to maintain these configurations in two separate places.

Actually , what SchemaValidator requires is the Metadata instance which is only available during bootstrapping Hibernate . But we can use Hibernate Integrator API (as described in here) to capture it such that we can validate them later.

(1) Create SchemaValidateService which implements Hibernate Integrator API to capture Metadata. Also setup a @Scheduled method to validate the schema at desired time.

@Component
public class SchemaValidateService implements Integrator {

    private Metadata metadata;

    @Override
    public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {
        this.metadata = metadata;
    }

    @Override
    public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
    }

    //Adjust the scheduled time here
    @Scheduled(cron = "0 0/1 * * * ?")
    public void validate() {
        try {
            System.out.println("Start validating schema");
            new SchemaValidator().validate(metadata);
        } catch (Exception e) {
            //log the validation error here.
        }
        System.out.println("Finish validating schema....");
    }
}

(2) Register SchemaValidateService to Hibernate

@SpringBootApplication
@EnableScheduling
public class App {

    @Bean
    public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(SchemaValidateService schemaValidateService) {
        return (prop -> {
            List<Integrator> integrators = new ArrayList<>();
            integrators.add(schemaValidateService);
            prop.put("hibernate.integrator_provider", (IntegratorProvider) () -> integrators);
        });
    }
}

Also, this solution should has better performance as it does not need to create a new database connection for validating schema each time as it can just grab the connection from the existing connection pool.

Upvotes: 3

Related Questions