Update spring-data-testapp for transactions. (#1577)

Closed #1576.
This commit is contained in:
Michael Reiche
2022-10-05 09:29:46 -10:00
committed by GitHub
parent aa09729b5c
commit d86e3b90cf
8 changed files with 258 additions and 298 deletions

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.index.QueryIndexed;
import org.springframework.data.couchbase.core.mapping.Document;
@Document
/**
* @author Michael Reiche
*/
public class AirlineGates {
@Id String id;
@Version Long version;
@QueryIndexed String name;
String iata;
Long gates;
@PersistenceConstructor
public AirlineGates(String id, String name, String iata, Long gates) {
this.id = id;
this.name = name;
this.iata = iata;
this.gates = gates;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getIata() {
return iata;
}
public Long getGates() {
return gates;
}
public String toString(){
StringBuffer sb=new StringBuffer();
sb.append("{");
sb.append("\"id\":"+id);
sb.append(", \"name\":"+name);
sb.append(", \"iata\":"+iata);
sb.append(", \"gates\":"+gates);
sb.append("}");
return sb.toString();
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2017-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.DynamicProxyable;
import org.springframework.stereotype.Repository;
/**
* @author Michael Reiche
*/
@Repository
public interface AirlineGatesRepository
extends CouchbaseRepository<AirlineGates, String>, DynamicProxyable<AirlineGatesRepository> {
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.TransactionalSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
/**
* @author Michael Reiche
*/
@Service
public class AirlineGatesService {
CouchbaseTemplate template;
ReactiveCouchbaseTemplate reactiveTemplate;
public AirlineGatesService(CouchbaseTemplate template) {
this.template = template;
this.reactiveTemplate = template.reactive();
}
// The @Transactional annotation results in the method of the proxy for the service executing this in a transaction
@Transactional
public void transferGates(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) {
// May wish to include this check to confirm this is actually in a transaction.
TransactionalSupport.checkForTransactionInThreadLocalStorage().map((h) -> {
if (!h.isPresent())
throw new RuntimeException("not in transaction!");
return h;
});
AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId);
AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId);
toAirlineGates.gates += gatesToTransfer;
fromAirlineGates.gates -= gatesToTransfer;
template.save(fromAirlineGates);
if(exceptionToThrow != null){
throw exceptionToThrow;
}
template.save(toAirlineGates);
}
// The @Transactional annotation results in the method of the proxy for the service executing this in a transaction
@Transactional
public Mono<Void> transferGatesReactive(String fromId, String toId, int gatesToTransfer, RuntimeException exceptionToThrow) {
return Mono.deferContextual(ctx -> {
// May wish to include this check to confirm this is actually in a transaction.
TransactionalSupport.checkForTransactionInThreadLocalStorage().map((h) -> {
if (!h.isPresent())
throw new RuntimeException("not in transaction!");
return h;
});
AirlineGates fromAirlineGates = template.findById(AirlineGates.class).one(fromId);
AirlineGates toAirlineGates = template.findById(AirlineGates.class).one(toId);
toAirlineGates.gates += gatesToTransfer;
fromAirlineGates.gates -= gatesToTransfer;
template.save(fromAirlineGates);
if(exceptionToThrow != null){
throw exceptionToThrow;
}
return reactiveTemplate.save(toAirlineGates).then();
});
}
// This does not have the @Transactional annotation therefore is not executed in a transaction
public AirlineGates save(AirlineGates airlineGates) {
return template.save(airlineGates);
}
// This does not have the @Transactional annotation therefore is not executed in a transaction
public AirlineGates findById(String id) {
return template.findById(AirlineGates.class).one(id);
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* Airport entity
*
* @author Michael Reiche
*/
@Document
@TypeAlias("airport")
public class Airport {
@Id String id;
String iata;
String icao;
LocalDateTime openDate;
Long gates;
String _class;
@Version Number version;
@CreatedBy private String createdBy;
public Airport() {}
//@PersistenceConstructor
public Airport(String id, String iata, String icao) {
this.id = id;
this.iata = iata;
this.icao = icao;
this.openDate = null; // LocalDateTime.now();
this.gates = Long.valueOf(200);
}
public String getId() {
return id;
}
public String getIata() {
return iata;
}
public String getIcao() {
return icao;
}
public Long getGates() {
return gates;
}
public String get_class() {
return _class;
}
public LocalDateTime getOpenDate() {
return openDate;
}
public Airport clearVersion() {
version = Long.valueOf(0);
return this;
}
public String getCreatedBy() {
return createdBy;
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("{ ");
sb.append(" id: " + id);
sb.append(" iata: " + iata);
sb.append(" icao: " + icao);
sb.append(" gates: " + gates);
sb.append(" open: "+openDate);
// sb.append(" date: "+openDate);
sb.append(" }");
return sb.toString();
}
}

View File

@@ -1,96 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import com.couchbase.client.java.query.QueryScanConsistency;
import org.springframework.data.couchbase.core.RemoveResult;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.couchbase.repository.ScanConsistency;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* Airport repository for testing <br>
*
* @author Michael Reiche
*/
@Repository
@Document
public interface AirportRepository extends CouchbaseRepository<Airport, String> {
// override an annotate with REQUEST_PLUS
@Override
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> findAll();
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> findAllByIata(String iata);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Airport findByIata(String iata);
@ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED)
Airport iata(String iata);
@Query("#{#n1ql.selectEntity} where iata = $1")
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> getAllByIata(String iata);
@Query("#{#n1ql.delete} WHERE #{#n1ql.filter} and iata = $1 #{#n1ql.returning}")
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<RemoveResult> deleteByIata(String iata);
@Query("SELECT __cas, * from `#{#n1ql.bucket}` where iata = $1")
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> getAllByIataNoID(String iata);
@Query("SELECT __id, * from `#{#n1ql.bucket}` where iata = $1")
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<Airport> getAllByIataNoCAS(String iata);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
long countByIataIn(String... iata);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
long countByIcaoAndIataIn(String icao, String... iata);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
long countByIcaoOrIataIn(String icao, String... iata);
@Override
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
long count();
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} #{#projectIds != null ? 'AND iata IN $1' : ''} "
+ " #{#planIds != null ? 'AND icao IN $2' : ''} #{#active != null ? 'AND false = $3' : ''} ")
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Long countFancyExpression(@Param("projectIds") List<String> projectIds, @Param("planIds") List<String> planIds,
@Param("active") Boolean active);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Page<Airport> findAllByIataNot(String iata, Pageable pageable);
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Optional<Airport> findByIdAndIata(String id, String iata);
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.demo;
import com.couchbase.client.java.transactions.TransactionResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.TransactionalSupport;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
/**
*
*
* @author Michael Reiche
*/
@Service
@Transactional
public class AirportService {
public AirportService(CouchbaseTemplate template){
this.template = template;
}
CouchbaseTemplate template;
public void transferGates(String fromId, String toId, int gatesToTransfer) {
TransactionalSupport.checkForTransactionInThreadLocalStorage()
.map( (h) -> { if ( ! h.isPresent() ) throw new RuntimeException("not in transaction!"); return h; } );
TransactionalSupport.checkForTransactionInThreadLocalStorage().map(stat -> {
Assert.isTrue(stat.isPresent(), "Not in transaction");
return stat;
});
Airport fromAirport = template.findById(Airport.class).one(fromId);
Airport toAirport = template.findById(Airport.class).one(toId);
toAirport.gates += gatesToTransfer;
fromAirport.gates -= gatesToTransfer;
template.save(fromAirport);
template.save(toAirport);
}
}

View File

@@ -15,12 +15,12 @@
*/
package com.example.demo;
import com.couchbase.client.java.transactions.TransactionResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Components of the type CommandLineRunner are called right after the application start up. So the method *run* is
@@ -31,52 +31,70 @@ import org.springframework.stereotype.Component;
@Component
public class CmdRunner implements CommandLineRunner {
@Autowired AirportRepository airportRepository;
@Autowired CouchbaseTemplate template;
@Autowired CouchbaseClientFactory couchbaseClientFactory;
@Autowired AirportService airportService;
@Autowired AirlineGatesService airlineGatesService;
@Override
public void run(String... strings) throws Exception {
public void run(String... strings) {
try { // remove leftovers from previous run
template.removeById(AirlineGates.class).one("1");
} catch (Exception e) {}
try {
template.removeById(Airport.class).one("1");
} catch (Exception e){}
template.removeById(AirlineGates.class).one("2");
} catch (Exception e) {}
AirlineGates airlineGates1 = new AirlineGates("1", "JFK", "American Airlines", Long.valueOf(200)); //1
AirlineGates airlineGates2 = new AirlineGates("2", "JFK", "Lufthansa", Long.valueOf(200));
AirlineGates saved1 = airlineGatesService.save(airlineGates1);
AirlineGates saved2 = airlineGatesService.save(airlineGates2);
AirlineGates found1 = airlineGatesService.findById(saved1.getId()); //2
AirlineGates found2 = airlineGatesService.findById(saved2.getId());
System.err.println("initialized airlines");
System.err.println(" found before transferGates: " + found1);
System.err.println(" found before transferGates: " + found2);
System.err.println("this transferGates attempt will succeed");
// move 50 gates from airline1 to airline2
int gatesToTransfer=50;
airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), gatesToTransfer, null); //3
found1 = airlineGatesService.findById(saved1.getId());
found2 = airlineGatesService.findById(saved2.getId());
System.err.println(" found after transferGates: " + found1); //4
System.err.println(" found after transferGates: " + found2);
Assert.isTrue(found1.getGates().equals(airlineGates1.getGates()-gatesToTransfer), "should have transferred");
Assert.isTrue(found2.getGates().equals(airlineGates1.getGates()+gatesToTransfer), "should have transferred");
System.err.println("this transferGates attempt will fail");
// attempt to move 44 gates from airline1 to airline2, but it fails.
try {
template.removeById(Airport.class).one("2");
} catch (Exception e){}
Airport new_1 = new Airport("1", "JFK", "jfk");
Airport saved_1 = airportRepository.save(new_1);
Airport found_1 = airportRepository.findById(saved_1.getId()).get();
System.out.println("found using repository by id: " + found_1);
Airport new_2 = new Airport("2", "LGA", "lga");
Airport saved_2 = airportRepository.save(new_2);
Airport found_2 = airportRepository.findById(saved_2.getId()).get();
System.out.println("found using repository by id: " + found_2);
Airport found_3 = airportRepository.findByIata("JFK");
System.out.println("founding using repository findByIata: "+found_3);
airportService.transferGates(new_1.getId(), new_2.getId(), 50);
System.out.println("found after transferGates: "+airportRepository.findById(saved_1.getId()).get());
System.out.println("found after transferGates: "+airportRepository.findById(saved_2.getId()).get());
}
public void transferGatesDeprecated(String fromId, String toId, int gatesToTransfer) {
TransactionResult txResult = template.getCouchbaseClientFactory().getCluster().transactions().run(ctx -> {
Airport fromAirport = template.findById(Airport.class).one(fromId);
Airport toAirport = template.findById(Airport.class).one(toId);
toAirport.gates += gatesToTransfer;
fromAirport.gates -= gatesToTransfer;
template.save(fromAirport);
template.save(toAirport);
});
// 5
airlineGatesService.transferGates(airlineGates1.getId(), airlineGates2.getId(), 44, new SimulateErrorException());
} catch (RuntimeException rte) {
if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null
&& rte.getCause() instanceof SimulateErrorException) {
throw rte;
}
System.err.println(" got exception "+rte);
}
System.err.println(" found after transferGates: " + airlineGatesService.findById(airlineGates1.getId()));
System.err.println(" found after transferGates: " + airlineGatesService.findById(airlineGates2.getId()));
Assert.isTrue(found1.getGates().equals(airlineGates1.getGates()-gatesToTransfer), "should be same as previous");
Assert.isTrue(found2.getGates().equals(airlineGates1.getGates()+gatesToTransfer), "should be same as previous");
System.err.println("this transferGates attempt will succeed");
try {
// 5
airlineGatesService.transferGatesReactive(airlineGates1.getId(), airlineGates2.getId(), 44, null).block();
} catch (RuntimeException rte) {
if (!(rte instanceof TransactionSystemUnambiguousException) && rte != null
&& rte.getCause() instanceof SimulateErrorException) {
throw rte;
}
System.err.println(" got exception "+rte);
}
System.err.println(" found after transferGates: " + airlineGatesService.findById(airlineGates1.getId()));
System.err.println(" found after transferGates: " + airlineGatesService.findById(airlineGates2.getId()));
Assert.isTrue(found1.getGates().equals(airlineGates1.getGates()-gatesToTransfer), "should have transferred");
Assert.isTrue(found2.getGates().equals(airlineGates1.getGates()+gatesToTransfer), "should have transferred");
}
static class SimulateErrorException extends RuntimeException {}
}

View File

@@ -15,6 +15,7 @@
*/
package com.example.demo;
import com.couchbase.client.core.msg.kv.DurabilityLevel;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.transactions.config.TransactionsConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -22,6 +23,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
*
@@ -29,7 +31,8 @@ import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepos
* @author Michael Reiche
*/
@Configuration
@EnableCouchbaseRepositories({"com.example.demo", "com.wu.onep.ordnrt.cbviewsrch.repository"})
@EnableCouchbaseRepositories({"com.example.demo"})
@EnableTransactionManagement
public class Config extends AbstractCouchbaseConfiguration {
@Override
public String getConnectionString() {
@@ -53,7 +56,7 @@ public class Config extends AbstractCouchbaseConfiguration {
@Override
public void configureEnvironment(ClusterEnvironment.Builder builder){
// builder.transactionsConfig(TransactionsConfig.durabilityLevel(DurabilityLevel.NONE));
builder.transactionsConfig(TransactionsConfig.durabilityLevel(DurabilityLevel.NONE));
}