6

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?

Daniel Taub
  • 5,133
  • 7
  • 42
  • 72

2 Answers2

5

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
Oleksii Zghurskyi
  • 3,967
  • 1
  • 16
  • 17
  • Great job ! schema-validator properties could be build directly from spring.datasource and spring.jpa.database-platform properties in order to avoid duplicates. – robynico Nov 16 '21 at 09:15
3

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.

Ken Chan
  • 84,777
  • 26
  • 143
  • 172
  • I want to get the metadata with my own configurations, but when I'm creating my own one I'm getting: more then one table found in namespace – Daniel Taub Feb 05 '19 at 12:33
  • You mean you want to create `Metadata` again by yourself rather than reuse the existing one that is already built by hibernate ? – Ken Chan Feb 05 '19 at 13:09
  • Any concerns that you cannot configure your own packages that you want to validate as the usual hibernate entities package such that hibernate already built that metadata for you ? If you want to built the metadata by yourself , you should refer to other 's proposed solutions. The spirit of this solution is to propose something that that are different from the already proposed , so it is not intended to fiddle with the Metadata by ourself . – Ken Chan Feb 05 '19 at 13:24