Initializing application data

On this page, you’ll learn:

  • What installers are

  • When installers are executed

  • How to create default data

Installers

Installers are components that allow developers to perform additional initialization for their applications and are always marked by the @Installer annotation. They are automatically detected if located in the installers package relative to the package of the application descriptor.

Specification as to how and when an installer should be executed are specified through a variety of attributes on the @Installer annotation.

During the application bootstrap, installers are created and executed if necessary. Once the application bootstrap is complete, installer components will be destroyed. As such, installers are only available during the start-up of the application.

Installer execution

Installers are always executed during a specific phase of the application bootstrap. This phase defines which components will be available and must always be specified for every installer.

Whether an installer is executed depends on its run condition as well as the @Conditional annotations that are present. If all conditions are fulfilled, installers will be executed in the following predetermined order:

  1. in bootstrap phase order

  2. in module bootstrap order

  3. in installer order (according to @Order annotations on the installer class)

Installers without an @Order annotation behave as if they have an order index of 0. In that case the installer registration order will be used as a fallback, with installers specified through getInstallers() having a higher precedence.

Tracking and synchronization

Installer execution is tracked to ensure that installers are only executed if necessary, which is specified through the runCondition and version attributes. Tracking is done by the AcrossInstallerRepository, which can also be wired as a bean in installers. This could be helpful in migration trajectories for example to rename installers.

Aside of tracking, execution is also synchronized for when multiple applications would connect to the same database. Synchronization is done through the use of a DistributedLockRepository requires the core schema. The DistributedLockRepository provides a simple locking mechanism for RDBMS synchronization. A lock will be taken on the database as soon as a single installer wishes to execute, and will be released once that installer has finished executing. The lock owner will contain the hostname of the computer running the application, as well as the displayName of the AcrossContext.

Because of tracking and synchronization, a datasource is required in order to execute installers. This is because the DistributedLockRepository relies on tables created in the core schema.

Installing default or test data

One of the primary use cases for installers is to create default or test data during application startup. In this section, we’ll insert data into the database by using an installer, in combination with Spring Data JPA.

Before we insert data into the database, we’ll create a domain model and a corresponding repository. To use Spring Data JPA in combination with Hibernate, we’ll add a dependency on the Spring Boot JPA starter.

Maven - Spring Boot Data JPA dependency
<dependencies>
  <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId> (1)
    </dependency>
</dependencies>
A version does not have to be specified. Across transitively depends on the Spring Boot dependencies pom, which specifies versions for the Spring Boot Starters, through the Spring platform BOM.

Now that we have the required libraries, we can create our domain model.

Product.java
@Entity (1)
@Table(name = "table_product") (1)
@Data (2)
public class Product implements Persistable<Long> { (3)
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "name")
    private String name;

    public Product(String name){
        this.name = name;
    }

    @Override
    public boolean isNew() { (4)
        return id == null || id == 0L;
    }
}
1 @Table and @Entity are annotations used for persistence.
2 @Data provides a shortcut for @ToString, @EqualsAndHashCode, @Getter, @Setter and @RequiredArgsConstructor.
3 The Persistable interface is implemented to further complete the mapping to our database model. Persistable requires getId to be implemented to fetch the id of the entity and isNew() is used to check whether the entity has already been persisted once before.

Next to the domain model, a Repository component will be created. The repository is the main component we’ll use to perform database operations for the Product entity.

ProductRepository.java
public interface UserRepository extends JpaRepository<Product, Long> { (1)
}
1 By extending JpaRepository, which is a specification of the Repository marker interface, Spring is able to automatically detect and create repository beans. See Working with Spring Data repositories and JPA repositories to learn more about the possibilities they provide.

When using the dev profile development mode is activated. As such, if you do not specify a database configuration, an in memory database will be created.

Spring data JPA supports automatic DDL generation, which will create a database schema based on the provided annotations. For more information see the Spring docs on initializing a database To disable DDL generation explicitly, set the spring.jpa.hibernate.ddl-auto=none property.

Now that we’ve created a domain entity and a repository, let’s create an installer and insert data into the database.

ProductInstaller.java
@Profile("dev") (1)
@RequiredArgsConstructor
@Installer(description = "Creates the initial products", phase = InstallerPhase.AfterContextBootstrap) (2)
public class ProductInstaller {

    private final ProductRepository productRepository; (4)

    @InstallerMethod (3)
    public void createDefaultUser() {
        productRepository.save( new Product("Mars") ); (5)
        productRepository.save( new Product("Snickers") );
        productRepository.save( new Product("Leo") );
        productRepository.save( new Product("Oreo") );
    }
}
1 The installer is annotated with the dev application profile. This means that the installer will only execute if the dev profile is active.
2 The @Installer annotation is provided so that the installer can be found and executed by Across. We’ve provided two attributes to the installer, a description and a phase. The phase must always be present and allows the developer to define during which bootstrap phase the installer should be executed. The default phase is InstallerPhase.BeforeContextBootstrap which ensures that installers are executed before any modules are bootstrapped. This allows installers to be executed before beans are created and is usually used when creating database schemas. InstallerPhase.AfterContextBootstrap ensures that all modules have been fully bootstrapped and are ready to be used.
3 @InstallerMethod is added to denote which methods of the installer should be executed. By using the @Order annotation a specific ordering can be defined for the `@InstallerMethod`s that are present.
4 Beans are automatically wired into the installer.
5 Various Product instances are saved to the database.

In Connecting to a database, we’ve used h2-console to check up on the data present in a database. If we would navigate to the h2-console once more, we’ll see a table named TABLE_PRODUCT in which our data is present.

Listing of products created by the installer