O CZYM PISZEMY?

Populate database in Spring

Wysłano dnia: 14.05.2018 | Komentarzy: 0
Populate database in Spring

Once upon a time there was BootStrap class. The class was a very friendly class, therefore it befriended many more classes. With more classes inside, BootStrap grew bigger and bigger, expanding itself at the rate of the entire universe. This is not a fairy tale.

 

This is not a fairy tale, because this is exactly what happened. But first things first, you may wonder what the BootStrap is. It’s a mechanism coming from Grails Framework, which executes code on application startup; I’m using it mostly to populate database. Just put the BootStrap.groovy in grails-app/init folder and add some goodies to the init closure. Having a background in Grails, this is something I missed in Spring, especially because as I mentioned before, the code grew fairly big. I wanted to rewrite the whole BootStrap logic in Java, because its older Groovy version somehow reminded me of poorly written tests you may see here and there: verbose and ugly. It just wasn’t a first-class citizen of the production code.

@Log4j @AllArgsConstructor
@Component
public class BootStrap {

    private final BootStrapService bootStrapService;

    @EventListener(ApplicationReadyEvent.class)
    private void init() {
        try {
            log.info("BootStrap start");
            bootStrapService.boot();
            log.info("BootStrap success");
        } catch (Exception e) {
            log.error("BootStrap failed," + e);
            e.printStackTrace();
            throw e;
        }
    }

}

Surprise, surprise! There’s an EventListener annotation that you can put on a method in order to track the ApplicationReadyEvent and run some code on application startup. Job’s done, right? Well, not really, you CAN do that, but do you WANT TO do that? I prefer to keep the business logic in a service, therefore I created and injected BootStrapService, that just leaves logging and error handling here and Lombok’s annotations make the whole thing even neater.

public abstract class BootStrapService {

    @Autowired
    protected BootStrapEntryService entryService;

    @Autowired
    protected MovieService movieService;

    @Transactional
    public void boot() {
        writeDefaults();
    }

    protected void writeDefaults() {
        entryService.createIfNotExists(BootStrapLabel.CREATE_MOVIE, this::createMovie);
    }

    private void createMovie() {
        movieService.create("Movie");
    }

}

I made the boot method @Transactional, because it’s our starting point to populate database, later in the project you might want to add here some data migration as well. The REAL rocket science begins in writeDefaults method! The example createMovie method reference is passed as a parameter, with double colon as a syntactic sugar, to the createIfNotExists method of the injected BootStrapEntryService. The other parameter is a simple BootStrapLabel enum, with a value used as a description for a given operation. I prefer to add a verb as a prefix, just not to be confused later, when a possibility of other operations comes up.

@Log4j @AllArgsConstructor
@Transactional
@Service
public class BootStrapEntryService {

    private final BootStrapEntryRepository bootStrapEntryRepository;

    public void createIfNotExists(BootStrapLabel label, Runnable runnable) {
        String entryStatus = "already in db";
        boolean entryExists = existsByLabel(label);

        if(!entryExists) {
            runnable.run();
            create(label);
            entryStatus = "creating";
        }

        log(label, entryStatus);
    }

    public boolean existsByLabel(BootStrapLabel label) {
        return bootStrapEntryRepository.existsByLabel(label);
    }

    public BootStrapEntry create(BootStrapLabel label) {
        BootStrapEntry bootStrapEntry = new BootStrapEntry();
        bootStrapEntry.setLabel(label);
        return bootStrapEntryRepository.save(bootStrapEntry);
    }

    private void log(BootStrapLabel label, String entryStatus) {
        String entryMessage = "processing " + label + " -> " + entryStatus;
        log.info(entryMessage);
    }

}

Finally, createIfNotExists method is the place to call the actual methods to populate database, however, in a generic way. Method passed as a reference may be called, however, we don’t want to write the data that was written to the database before, at least considering a not in-memory database, so we check if an entry for a given label already exists. We have to create an entity, a pretty simple entity, in this case the BootStrapEntry, with just a label field to keep the labels in database. existsByLabel and create are simple generic methods responsible for basic database operations on the labels.

@Profile("development")
@Service
public class DevelopmentBootStrapService extends BootStrapService {

    @Autowired
    private BookService bookService;

    @Override
    protected void writeDefaults() {
        super.writeDefaults();
        entryService.createIfNotExists(BootStrapLabel.CREATE_BOOK, this::createBook);
    }

    private void createBook() {
        bookService.create("Book");
    }

}

Now we’re getting somewhere! If you wondered why I made the BootStrapService abstract, now is the answer. I wanted to make it possible to run some code only in a given environment, like development. The Profile annotation with environment name as a parameter comes in hand. Overriding the writeDefaults method provides a way to initialize some data only in development.

@Profile("production")
@Service
public class ProductionBootStrapService extends BootStrapService {

}

If there is the development, there may be the production as well. In the given example, I just wanted to run the default data from the parent class, without filling the environment with some random data.
 

That’s all, my little guide to populate database in Spring. Hopefully, it wasn’t THAT bad and even if it was, feel free to leave the feedback nonetheless!

Dodaj komentarz