Spring Integration

เชื่อมโยงการแมพปิ้ง, การตั้งชื่อคอลเลกชัน และวงจรชีวิตเชิงปฏิกิริยาของ ReactiveMongoTemplate กับ MongoExecutionContext adapter โดยไม่ดึง Spring Data MongoDB มาเป็นการพึ่งพาหลัก


Spring Data MongoDB จะถูกดูแลในแอปพลิเคชัน

build.gradle
text
dependencies {
  implementation("com.byeolnaerim:reactive-mongo-dsl:1.0.0")
  implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
}

ไลบรารีหลักไม่พึ่งพา Spring Data MongoDB โดยตรง หากต้องการใช้การตั้งค่า ReactiveMongoTemplate ของแอปพลิเคชัน Spring ที่มีอยู่ จะต้องมี MongoExecutionContext adapter ในเลเยอร์แอปพลิเคชัน

SpringReactiveMongoExecutionContext adapter

SpringReactiveMongoExecutionContext.java
java
public class SpringReactiveMongoExecutionContext implements MongoExecutionContext {

  private final ReactiveMongoTemplate reactiveMongoTemplate;
  private final MongoClient mongoClient;
  private final MongoConverter mongoConverter;
  private final ReactiveEntityCallbacks entityCallbacks;

  public SpringReactiveMongoExecutionContext(
      ReactiveMongoTemplate reactiveMongoTemplate,
      MongoClient mongoClient,
      ReactiveEntityCallbacks entityCallbacks
  ) {
    this.reactiveMongoTemplate = Objects.requireNonNull(reactiveMongoTemplate);
    this.mongoClient = Objects.requireNonNull(mongoClient);
    this.mongoConverter = reactiveMongoTemplate.getConverter();
    this.entityCallbacks = Objects.requireNonNull(entityCallbacks);
  }

  @Override
  public Mono<MongoDatabase> getDatabase() {
    return reactiveMongoTemplate.getMongoDatabase();
  }

  @Override
  public Mono<ClientSession> startSession() {
    return Mono.from(mongoClient.startSession());
  }

  @Override
  public String getCollectionName(Class<?> entityClass) {
    return reactiveMongoTemplate.getCollectionName(entityClass);
  }

  @Override
  public Document write(Object source) {
    Document document = new Document();
    mongoConverter.write(source, document);
    return document;
  }

  @Override
  public <T> T read(Class<T> targetType, Document source) {
    return mongoConverter.read(targetType, source);
  }

  @Override
  public Object getId(Object entity) {
    MongoPersistentEntity<?> persistentEntity =
        mongoConverter.getMappingContext().getPersistentEntity(entity.getClass());

    if (persistentEntity == null || !persistentEntity.hasIdProperty()) {
      return null;
    }

    Object id = persistentEntity.getIdentifierAccessor(entity).getIdentifier();
    if (id == null) return null;

    Document document = new Document();
    mongoConverter.write(entity, document);
    return document.get("_id");
  }

  @Override
  @SuppressWarnings({ "rawtypes", "unchecked" })
  public void setId(Object entity, Object id) {
    MongoPersistentEntity persistentEntity =
        mongoConverter.getMappingContext().getPersistentEntity(entity.getClass());

    if (persistentEntity == null || !persistentEntity.hasIdProperty()) return;

    MongoPersistentProperty idProperty =
        (MongoPersistentProperty) persistentEntity.getRequiredIdProperty();

    Object targetId = id;
    if (id instanceof ObjectId objectId && idProperty.getType() == String.class) {
      targetId = objectId.toHexString();
    } else if (
        id != null
            && !idProperty.getType().isInstance(id)
            && mongoConverter.getConversionService()
                .canConvert(id.getClass(), idProperty.getType())
    ) {
      targetId = mongoConverter.getConversionService()
          .convert(id, idProperty.getType());
    }

    PersistentPropertyAccessor accessor = persistentEntity.getPropertyAccessor(entity);
    accessor.setProperty(idProperty, targetId);
  }

  @Override
  public <T> Mono<T> beforePersist(T entity, String collectionName) {
    return entityCallbacks.callback(
        ReactiveBeforeConvertCallback.class,
        entity,
        collectionName
    );
  }

  @Override
  public <T> Mono<T> afterPersist(
      T entity,
      Document document,
      String collectionName
  ) {
    return entityCallbacks.callback(
        ReactiveAfterSaveCallback.class,
        entity,
        document,
        collectionName
    );
  }

  @Override
  public Object getSessionScope() {
    return mongoClient;
  }

  @Override
  public Object getNative() {
    return reactiveMongoTemplate;
  }

  @Override
  public <T> T getNative(Class<T> nativeType) {
    if (nativeType.isInstance(reactiveMongoTemplate)) {
      return nativeType.cast(reactiveMongoTemplate);
    }
    if (nativeType.isInstance(mongoClient)) {
      return nativeType.cast(mongoClient);
    }
    return MongoExecutionContext.super.getNative(nativeType);
  }
}

ในการใช้งานจริงจะต้องเพิ่มการนำเข้า Spring/MongoDB ที่จำเป็นในคลาสข้างต้น จุดสำคัญคือการเชื่อมโยงการอ่าน/เขียน entity กับ MongoConverter, ชื่อคอลเลกชันกับ ReactiveMongoTemplate, และการเริ่มต้นเซสชันกับ MongoClient ที่เกี่ยวข้อง

การลงทะเบียน Resolver และ DSL Bean

MongoTemplateNameResolver.java
java
@Component
public class MongoTemplateNameResolver
    implements MongoTemplateResolver<MongoTemplateName> {

  private final MongoExecutionContext frontExecutionContext;
  private final MongoExecutionContext backExecutionContext;

  public MongoTemplateNameResolver(
      @Qualifier("frontMongoTemplate") ReactiveMongoTemplate frontTemplate,
      @Qualifier("frontMongoClient") MongoClient frontMongoClient,
      @Qualifier("backMongoTemplate") ReactiveMongoTemplate backTemplate,
      @Qualifier("backMongoClient") MongoClient backMongoClient,
      ApplicationContext applicationContext
  ) {
    ReactiveEntityCallbacks callbacks =
        ReactiveEntityCallbacks.create(applicationContext);

    this.frontExecutionContext = new SpringReactiveMongoExecutionContext(
        frontTemplate, frontMongoClient, callbacks
    );
    this.backExecutionContext = new SpringReactiveMongoExecutionContext(
        backTemplate, backMongoClient, callbacks
    );
  }

  @Override
  public MongoExecutionContext getTemplate(MongoTemplateName key) {
    return switch (key) {
      case FRONT -> frontExecutionContext;
      case BACK -> backExecutionContext;
    };
  }
}
ReactiveMongoDslConfiguration.java
java
@Bean
public ReactiveMongoDsl<MongoTemplateName> reactiveMongoDsl(
    MongoTemplateResolver<MongoTemplateName> resolver
) {
  return new ReactiveMongoDsl<>(resolver);
}

ขอบเขตที่เชื่อมต่อใน Adapter

@Document / ชื่อคอลเลกชัน

ใช้ ReactiveMongoTemplate#getCollectionName(...)

@Id / @Field / การแปลงแบบกำหนดเอง

ใช้ MongoConverter ในการแปลง entity ↔ Document

การตรวจสอบเชิงปฏิกิริยา

เชื่อมโยง beforePersist(...) กับ ReactiveBeforeConvertCallback จะทำให้มีการใช้ callback การตรวจสอบเชิงปฏิกิริยาในกลุ่ม save()

ReactiveAfterSaveCallback

สามารถเรียกใช้ได้ใน afterPersist(...) หลังจากการเขียนสำเร็จ

Spring QueryMapper

ไม่ถูกนำไปใช้ เส้นทางฟิลด์ DSL String จะอิงตามชื่อฟิลด์เอกสาร MongoDB จริง

Spring DataAccessException

ไม่ทำการแปลงอัตโนมัติ การดำเนินการจริงจะดำเนินการโดย Reactive Streams Driver

เมื่อใช้ @Field

java
@Field("user_name")
private String userName;

// MongoDB document field 이름을 사용
.fields(pair("user_name", "kim"))

การแปลงการบันทึก/ค้นหา entity จะใช้การแมพปิ้งของ Spring แต่เส้นทาง String ของเงื่อนไข DSL จะไม่ผ่าน QueryMapper ของ Spring อย่างไรก็ตาม ส่วนที่แน่นอนของเส้นทาง id จะถูกทำให้เป็นมาตรฐานเป็น _id ตามกฎของ DSL เอง

ธุรกรรมของ Spring และธุรกรรมของ DSL แยกจากกัน