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!