mongock / mongock

Lightweight Java based migration tool

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

anadimisra opened this issue · comments

Description

How do I make the ChangeUnit work with other spring beans in my app, ChangeUnits doesn't seem to be importing the dependencies before running the execution annotated method.

PRIORITY

[CRITICAL]

Version and environment

Mongock

  • Mongock version - 5.3.4
  • Modules involved(springboot, reactive-driver)
  • How Mongock is used(builder or annotation approach, etc.) builder approach.

Environment

  • Framework and libraries versions. Especially those that affect directly to Mongock(Spring, Spring data, MongoDB driver, etc.) - Spring Boot version 2.7.1, Mongo 4.4, MongoDB-driver-reactivestreams 4.6.1, spring-data-mongodb 3.4.14

Steps to Reproduce

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.

Behaviour

Expected behavior:
ChangeUnit to be applied successfully.

Actual behavior:
Application startup fails due to errors mentioned in the steps to reproduce section.

How often the bug happens:
100%

Hello @anadimisra , your question is answered in stackoverflow. Please take a look and it's useful, mark the answer as useful, that will help us a lot.

Thanks

Thanks @dieppa ! I'll detail my findings here

  1. It works if the fields autowired in the constructor are also private final in the bean and I set them. Now there can be a case where I'm autowiring constructor dependencies for initialising other private fields of this object and I don't need the autowired beans outside of the constructor - in such cases I get the wrong Parameter error which I should not
  2. While other setter injection based and method parameter based dependency injection works, @Value annotated parameters don't get autowired definitely. Like in my case it was @Value("classpath:form_templates") Resource templateFile

I have answered via stackoverflow. I explained the current limitations, offer a potential workaround and suggest to contribute 😄

It will help a lot if you can mark the answer as helpful.

Thanks