javaspringspring-bootmongock

MongoCK ChangeUnit doesn't auto wire other spring beans as dependency


How do I make the ChangeUnit work with other spring beans in my app? I've tried adding dependencies to the ChangeUnit via construction injection and setter injection, and using post construct to build the data I want to seed. But I get a

Caused by: java.lang.IllegalArgumentException: documents can not be null

exception while running the change unit, at line

mongoDatabase.getCollection("form_layouts", FormLayout.class)
    .insertMany(clientSession, formLayouts)
    .subscribe(insertSubscriber);

here's the code

private List<FormLayout> formLayouts;

@Autowired
private Jackson2ObjectMapperBuilder objectMapperBuilder;

@Autowired
private AppProperties appProperties;

@Value("classpath:form_layouts.json")
private Resource layouts;

@PostConstruct
public void initializeLayoutsJson() throws IOException {
    ObjectMapper objectMapper = objectMapperBuilder.failOnUnknownProperties(Boolean.TRUE).build();
    this.formLayouts = objectMapper.readValue(Files.readString(Path.of(layouts.getFile().getAbsolutePath())), new TypeReference<>() {
    });
    for (FormLayout layout : formLayouts) {
        layout.setRealmId(appProperties.getRealmId());
    }
    log.debug("Loaded {} Layouts from file seeder JSON {}", formLayouts.size(), layouts.getFilename());
    int i = 0;
    for (FormLayout layout : formLayouts) {
        i++;
        log.debug("Layout-{} : {}", i, layout);
    }
}


@Execution
public void migrationMethod(ClientSession clientSession, MongoDatabase mongoDatabase) {
    final SubscriberSync<InsertManyResult> insertSubscriber = new MongoSubscriberSync<>();
    mongoDatabase.getCollection("form_layouts", FormLayout.class)
            .insertMany(clientSession, this.formLayouts)
            .subscribe(insertSubscriber);
    InsertManyResult result = insertSubscriber.getFirst();
    log.info("{}.execution() wasAcknowledged: {}", this.getClass().getSimpleName(), result.wasAcknowledged());
    result.getInsertedIds()
            .forEach((key, value) -> log.info("Added Object[{}] : {}", key, value));
}

I've also tried constructor injection

private final List<FormLayout> formLayouts;

public LayoutsDataInitializer(@Autowired Jackson2ObjectMapperBuilder objectMapperBuilder, @Autowired AppProperties appProperties,
                              @Value("classpath:form_layouts.json") Resource layouts) throws IOException {
    ObjectMapper objectMapper = objectMapperBuilder.failOnUnknownProperties(Boolean.TRUE).build();
    this.formLayouts = objectMapper.readValue(Files.readString(Path.of(layouts.getFile().getAbsolutePath())), new TypeReference<>() {
    });
    for (FormLayout layout : formLayouts) {
        layout.setRealmId(appProperties.getRealmId());
    }
}

@Execution
public void migrationMethod(ClientSession clientSession, MongoDatabase mongoDatabase) throws IOException {
    final SubscriberSync<InsertManyResult> insertSubscriber = new MongoSubscriberSync<>();
    mongoDatabase.getCollection("form_layouts", FormLayout.class)
            .insertMany(clientSession, formLayouts)
            .subscribe(insertSubscriber);
    InsertManyResult result = insertSubscriber.getFirst();
    log.info("{}.execution() wasAcknowledged: {}", this.getClass().getSimpleName(), result.wasAcknowledged());
    result.getInsertedIds()
            .forEach((key, value) -> log.info("Added Object[{}] : {}", key, value));
}

This gives a rather cryptic error about wrong Dependency Caused by: io.mongock.driver.api.common.DependencyInjectionException: Wrong parameter[Resource]. Dependency not found. in ChangeLogRuntimeImpl.

Moving the entire logic into execution method also does not help.

@Value("classpath:form_layouts.json") 
Resource layouts;

@Execution
public void migrationMethod(ClientSession clientSession, MongoDatabase mongoDatabase) throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, Boolean.TRUE);
    List<FormLayout> formLayouts = objectMapper.readValue(Files.readString(Path.of(layouts.getFile().getAbsolutePath())), new TypeReference<>() {
    });
    for (FormLayout layout : formLayouts) {
        layout.setRealmId(REALM_ID);
    }
    final SubscriberSync<InsertManyResult> insertSubscriber = new MongoSubscriberSync<>();
    mongoDatabase.getCollection("form_layouts", FormLayout.class)
            .insertMany(clientSession, formLayouts)
            .subscribe(insertSubscriber);
    InsertManyResult result = insertSubscriber.getFirst();
    log.info("{}.execution() wasAcknowledged: {}", this.getClass().getSimpleName(), result.wasAcknowledged());
    result.getInsertedIds()
            .forEach((key, value) -> log.info("Added Object[{}] : {}", key, value));
}

Fails as the layouts object is reported Null.


Solution

  • You have to take into account that the ChangeUnits aren't Spring beans. They are POJOs managed by Mongock and supports Spring dependencies as explain above, which are retrieved from the ApplicationContext.

    Currently SpEL is not supported and the ApplicationContext is not injected as it's where the beans are retrieved from.

    However, injecting the ApplicationContext is something that we have in or roadmap and it would be great if you want to contribute and make a pull request. We'll assist you in anything you need.

    Alternatively, as a workaround in your case, you can create a Bean containing your resource and access it via ApplicationContext.

    Something like this:

    1. Declare your resource bean in your configuration/context
    @Bean("myResource")
    public Resource myResource(@Value("classpath:form_layouts.json") Resource resource) {
      return resource;
    }
    
    1. And then inject it in the changeunit. Like this
    public LayoutsDataInitializer(
                  Jackson2ObjectMapperBuilder objectMapperBuilder,
                  AppProperties appProperties,
                  @Named("myResource") Resource layouts) throws IOException {
      //your constructor's code
    }
    

    I haven't tested this, but it should work.