spring-data-couchbase icon indicating copy to clipboard operation
spring-data-couchbase copied to clipboard

Fix sample transaction code. was: Difference save entity between using repository and transaction

Open rbleuse opened this issue 3 years ago • 4 comments

Hello,

I was playing with transactions provided by couchbase and spring data following the documentation from here I noticed that there's a difference if I persist a document with a repository or with a transaction.

Here are details : spring-boot 2.7.2 couchbase-transactions 1.2.4

Simple entity

@Document
@Scope("dev")
@Collection("schedule")
data class Schedule(
    @field:Id
    val id: String,

    val nested: Nested,

    @field:Field
    val list: List<String>
)

data class Nested(
    @field:Field("isFree")
    val free: Boolean
)

Repository

interface ScheduleRepository : ReactiveCouchbaseRepository<Schedule, String>

Service

private val collection = clientFactory.withScope("dev").getCollection("schedule").reactive()

fun saveScheduleWithRepo(schedule: Schedule) = repository.save(schedule)

fun saveScheduleAsDocWithTransaction(schedule: Schedule): Mono<TransactionResult> {
    val target = CouchbaseDocument()
    converter.write(schedule, target)
    return transactions.reactive().run {
        it.insert(collection, target.id, target.content)
            .then()
    }
}
val schedule = Schedule("1", Nested(true), listOf("TEST"))
return service.saveScheduleWithRepo(schedule)
    .flatMap {
        service.saveScheduleAsDocWithTransaction(schedule.copy(id = "2"))
    }

Here are differences :

with repository as doc with transaction
{
  "_class": "com.rbleuse.spring.reactive.couchbase.model.Schedule",
  "list": [
    "TEST"
  ],
  "nested": {
    "isFree": true
  }
}
{
  "_class": "com.rbleuse.spring.reactive.couchbase.model.Schedule",
  "list": {
    "empty": false
  },
  "nested": {
    "content": {
      "isFree": true
    },
    "id": null,
    "expiration": 0
  }
}

As you can see, inserting the document with a transaction adds extra fields to the document : content, id and expiration (all in nested). Shouldn't it be in root object instead ? Furthermore, my simple list of string is not converted into an array with the actual String value. If I convert my List into an Array in my data class, result is same when using a transaction.

Did I miss something ?

rbleuse avatar Aug 02 '22 23:08 rbleuse

Before I go into investigating the differences - couchbase-transactions are directly supported in spring-data-couchbase in 5.0.0-M5 It's not necessary to do the conversion. [ the doc is staged at https://mikereiche.github.io/staged/ and will be in the next release which should be in the next couple weeks ] There are instructions on how to use a SNAPSHOT at https://docs.spring.io/spring-data/couchbase/docs/5.0.0-M5/reference/html/ (just use milestone instead of snapshot and use M5).

mikereiche avatar Aug 03 '22 18:08 mikereiche

you need to use target.export instead of target.content to get the correct serialization.

mikereiche avatar Aug 03 '22 19:08 mikereiche

Thanks a lot for your suggestion, it's indeed working fine with target.export()

I took this code from spring data couchbase documentation, which gives an example using the target.content instead. Perhaps the documentation needs to be updated while waiting for the 5.0.0 to be released

8.3. Object Conversions Since the transactions library itself has no knowledge of your spring data entity types, you need to convert it back and forth when reading/writing to interact properly. Fortunately, all you need to do is autowire the MappingCouchbaseConverter and utilize it:

Example 92. Transaction Conversion on Write

@Autowired
MappingCouchbaseConverter mappingCouchbaseConverter;

public void doSomething() {
  transactions.run(ctx -> {

   Airline airline = new Airline("demo-airline", "at");
   CouchbaseDocument target = new CouchbaseDocument();
   mappingCouchbaseConverter.write(airline, target);

   ctx.insert(couchbaseClientFactory.getDefaultCollection(), target.getId(), target.getContent());

   ctx.commit();
  });
}

I also tried to use the 5.0.0-M5 and I'm facing this exception (it's same with the snapshot one) :

@Service
class ScheduleService(
    private val reactiveOperations: ReactiveCouchbaseOperations
) {
    @Transactional
    fun saveScheduleWithTransaction(schedule: Schedule): Mono<Schedule> {
        return reactiveOperations.insertById(Schedule::class.java).one(schedule)
    }
}
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
	at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.5.0-M4.jar:3.5.0-M4]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP POST "/schedule" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.5.0-M4.jar:3.5.0-M4]
		at reactor.core.publisher.Mono.block(Mono.java:1675) ~[reactor-core-3.5.0-M4.jar:3.5.0-M4]
		at org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager.handlePropagation(CouchbaseCallbackTransactionManager.java:191) ~[spring-data-couchbase-5.0.0-M5.jar:5.0.0-M5]
		at org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager.execute(CouchbaseCallbackTransactionManager.java:76) ~[spring-data-couchbase-5.0.0-M5.jar:5.0.0-M5]
		at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:417) ~[spring-tx-6.0.0-M5.jar:6.0.0-M5]
		at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.0.0-M5.jar:6.0.0-M5]
		at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.0-M5.jar:6.0.0-M5]
		at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-6.0.0-M5.jar:6.0.0-M5]
		at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-6.0.0-M5.jar:6.0.0-M5]
		at com.rbleuse.spring.reactive.couchbase.service.ScheduleService$$EnhancerBySpringCGLIB$$f091fb23.saveScheduleWithTransaction(<generated>) ~[main/:na]
		at com.rbleuse.spring.reactive.couchbase.handler.ScheduleHandler.createSchedule(ScheduleHandler.kt:20) ~[main/:na]

rbleuse avatar Aug 03 '22 23:08 rbleuse

I also tried to use the 5.0.0-M5 and I'm facing this exception (it's same with the snapshot one)

I'll see if I can eliminate that block().

I'm curious how this code is being called as you had a similar issue with #1516 and I haven't been able to reproduce it. (I did provide a fix for #1516 which eliminates the block()).

I opened #1527 for the exception in M5.

mikereiche avatar Aug 04 '22 00:08 mikereiche