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

190 Views Asked by At

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.

2

There are 2 best solutions below

1
Mongock team On BEST ANSWER

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.

3
Mongock team On

Mongock supports spring dependency injections. As explained in the documentation, there are two ways, in the constructor and in the method itself. In case you have more than one constructor, you need to indicate to Mongock which one you want to use by annotating it with ChangeUnitConstructor.

Note that you don't need to annotate the injection with @Autowired. If it's injected into the spring context, Mongock is smart enough to infer it.

However, what Mongock does support is the annotation @Named, which would make Mongock to look the dependency by type and bean name.