Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion _includes/sidebar-data-v20.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,8 @@
"urls": [
"/${VERSION}/build-a-java-app-with-cockroachdb.html",
"/${VERSION}/build-a-java-app-with-cockroachdb-hibernate.html",
"/${VERSION}/build-a-java-app-with-cockroachdb-jooq.html"
"/${VERSION}/build-a-java-app-with-cockroachdb-jooq.html",
"/${VERSION}/build-a-spring-app-with-cockroachdb-mybatis.html"
]
},
{
Expand Down
3 changes: 2 additions & 1 deletion _includes/sidebar-data-v20.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,8 @@
"urls": [
"/${VERSION}/build-a-java-app-with-cockroachdb.html",
"/${VERSION}/build-a-java-app-with-cockroachdb-hibernate.html",
"/${VERSION}/build-a-java-app-with-cockroachdb-jooq.html"
"/${VERSION}/build-a-java-app-with-cockroachdb-jooq.html",
"/${VERSION}/build-a-spring-app-with-cockroachdb-mybatis.html"
]
},
{
Expand Down
77 changes: 77 additions & 0 deletions _includes/v20.1/app/spring-mybatis/BasicExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.example.cockroachdemo;

import java.time.LocalTime;

import com.example.cockroachdemo.model.Account;
import com.example.cockroachdemo.model.BatchResults;
import com.example.cockroachdemo.service.AccountService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!test")
public class BasicExample implements CommandLineRunner {
@Autowired
private AccountService accountService;

@Override
public void run(String... args) throws Exception {
accountService.createAccountsTable();
deleteAllAccounts();
insertAccounts();
printNumberOfAccounts();
printBalances();
transferFunds();
printBalances();
bulkInsertRandomAccountData();
printNumberOfAccounts();
}

private void deleteAllAccounts() {
int numDeleted = accountService.deleteAllAccounts();
System.out.printf("deleteAllAccounts:\n => %s total deleted accounts\n", numDeleted);
}

private void insertAccounts() {
Account account1 = new Account();
account1.setId(1);
account1.setBalance(1000);

Account account2 = new Account();
account2.setId(2);
account2.setBalance(250);
BatchResults results = accountService.addAccounts(account1, account2);
System.out.printf("insertAccounts:\n => %s total new accounts in %s batches\n", results.getTotalRowsAffected(), results.getNumberOfBatches());
}

private void printBalances() {
int balance1 = accountService.getAccount(1).map(Account::getBalance).orElse(-1);
int balance2 = accountService.getAccount(2).map(Account::getBalance).orElse(-1);

System.out.printf("printBalances:\n => Account balances at time '%s':\n ID %s => $%s\n ID %s => $%s\n",
LocalTime.now(), 1, balance1, 2, balance2);
}

private void printNumberOfAccounts() {
System.out.printf("printNumberOfAccounts:\n => Number of accounts at time '%s':\n => %s total accounts\n",
LocalTime.now(), accountService.findCountOfAccounts());
}

private void transferFunds() {
int fromAccount = 1;
int toAccount = 2;
int transferAmount = 100;
int transferredAccounts = accountService.transferFunds(fromAccount, toAccount, transferAmount);
System.out.printf("transferFunds:\n => $%s transferred between accounts %s and %s, %s rows updated\n",
transferAmount, fromAccount, toAccount, transferredAccounts);
}

private void bulkInsertRandomAccountData() {
BatchResults results = accountService.bulkInsertRandomAccountData(500);
System.out.printf("bulkInsertRandomAccountData:\n => finished, %s total rows inserted in %s batches\n",
results.getTotalRowsAffected(), results.getNumberOfBatches());
}
}
11 changes: 11 additions & 0 deletions _includes/v20.1/app/spring-mybatis/CockroachDemoApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.cockroachdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CockroachDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CockroachDemoApplication.class, args);
}
}
51 changes: 51 additions & 0 deletions _includes/v20.1/app/spring-mybatis/MyBatisConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example.cockroachdemo;

import javax.sql.DataSource;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
* This class configures MyBatis and sets up mappers for injection.
*
* When using the Spring Boot Starter, using a class like this is completely optional unless you need to
* have some mappers use the BATCH executor (as we do in this demo). If you don't have that requirement,
* then you can remove this class. By Default, the MyBatis Spring Boot Starter will find all mappers
* annotated with @Mapper and will automatically wire your Datasource to the underlying MyBatis
* infrastructure.
*/
@Configuration
@MapperScan(basePackages = "com.example.cockroachdemo.mapper", annotationClass = Mapper.class)
@MapperScan(basePackages = "com.example.cockroachdemo.batchmapper", annotationClass = Mapper.class,
sqlSessionTemplateRef = "batchSqlSessionTemplate")
public class MyBatisConfiguration {

@Autowired
private DataSource dataSource;

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory.getObject();
}

@Bean
@Primary
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}

@Bean(name = "batchSqlSessionTemplate")
public SqlSessionTemplate batchSqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory(), ExecutorType.BATCH);
}
}
87 changes: 87 additions & 0 deletions _includes/v20.1/app/spring-mybatis/RetryableTransactionAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.example.cockroachdemo;

import java.lang.reflect.UndeclaredThrowableException;
import java.util.concurrent.atomic.AtomicLong;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.TransientDataAccessException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

/**
* Aspect with an around advice that intercepts and retries transient concurrency exceptions.
* Methods matching the pointcut expression (annotated with @Transactional) are retried.
* <p>
* This advice needs to runs in a non-transactional context, which is before the underlying
* transaction advisor (@Order ensures that).
*/
@Component
@Aspect
// Before TX advisor
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class RetryableTransactionAspect {
protected final Logger logger = LoggerFactory.getLogger(getClass());

@Pointcut("@annotation(transactional)")
public void anyTransactionBoundaryOperation(Transactional transactional) {
}

@Around(value = "anyTransactionBoundaryOperation(transactional)",
argNames = "pjp,transactional")
public Object retryableOperation(ProceedingJoinPoint pjp, Transactional transactional)
throws Throwable {
final int totalRetries = 30;
int numAttempts = 0;
AtomicLong backoffMillis = new AtomicLong(150);

Assert.isTrue(!TransactionSynchronizationManager.isActualTransactionActive(), "TX active");

do {
try {
numAttempts++;
return pjp.proceed();
} catch (TransientDataAccessException | TransactionSystemException ex) {
handleTransientException(ex, numAttempts, totalRetries, pjp, backoffMillis);
} catch (UndeclaredThrowableException ex) {
Throwable t = ex.getUndeclaredThrowable();
if (t instanceof TransientDataAccessException) {
handleTransientException(t, numAttempts, totalRetries, pjp, backoffMillis);
} else {
throw ex;
}
}
} while (numAttempts < totalRetries);

throw new ConcurrencyFailureException("Too many transient errors (" + numAttempts + ") for method ["
+ pjp.getSignature().toLongString() + "]. Giving up!");
}

private void handleTransientException(Throwable ex, int numAttempts, int totalAttempts,
ProceedingJoinPoint pjp, AtomicLong backoffMillis) {
if (logger.isWarnEnabled()) {
logger.warn("Transient data access exception (" + numAttempts + " of max " + totalAttempts + ") "
+ "detected (retry in " + backoffMillis + " ms) "
+ "in method '" + pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName()
+ "': " + ex.getMessage());
}
if (backoffMillis.get() >= 0) {
try {
Thread.sleep(backoffMillis.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
backoffMillis.set(Math.min((long) (backoffMillis.get() * 1.5), 1500));
}
}
}
5 changes: 5 additions & 0 deletions _includes/v20.1/app/spring-mybatis/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:26257/bank?ssl=true&sslmode=require&sslrootcert=/certs/ca.crt&sslkey=/certs/client.maxroach.key.pk8&sslcert=/certs/client.maxroach.crt
username: maxroach
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.cockroachdemo.batchmapper;

import java.util.List;

import com.example.cockroachdemo.model.Account;

import org.apache.ibatis.annotations.Flush;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.executor.BatchResult;

@Mapper
public interface BatchAccountMapper {
@Insert("upsert into accounts(id, balance) values(#{id}, #{balance})")
void insertAccount(Account account);

@Flush
List<BatchResult> flush();
}
40 changes: 40 additions & 0 deletions _includes/v20.1/app/spring-mybatis/mapper/AccountMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.cockroachdemo.mapper;

import java.util.List;
import java.util.Optional;

import com.example.cockroachdemo.model.Account;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface AccountMapper {
@Delete("delete from accounts")
int deleteAllAccounts();

@Update("update accounts set balance=#{balance} where id=${id}")
void updateAccount(Account account);

@Select("select id, balance from accounts where id=#{id}")
Optional<Account> findAccountById(int id);

@Select("select id, balance from accounts order by id")
List<Account> findAllAccounts();

@Update({
"upsert into accounts (id, balance) values",
"(#{fromId}, ((select balance from accounts where id = #{fromId}) - #{amount})),",
"(#{toId}, ((select balance from accounts where id = #{toId}) + #{amount}))",
})
int transfer(@Param("fromId") int fromId, @Param("toId") int toId, @Param("amount") int amount);

@Update("CREATE TABLE IF NOT EXISTS accounts (id INT PRIMARY KEY, balance INT, CONSTRAINT balance_gt_0 CHECK (balance >= 0))")
void createAccountsTable();

@Select("select count(*) from accounts")
Long findCountOfAccounts();
}
22 changes: 22 additions & 0 deletions _includes/v20.1/app/spring-mybatis/model/Account.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.cockroachdemo.model;

public class Account {
private int id;
private int balance;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getBalance() {
return balance;
}

public void setBalance(int balance) {
this.balance = balance;
}
}
19 changes: 19 additions & 0 deletions _includes/v20.1/app/spring-mybatis/model/BatchResults.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.cockroachdemo.model;

public class BatchResults {
private int numberOfBatches;
private int totalRowsAffected;

public BatchResults(int numberOfBatches, int totalRowsAffected) {
this.numberOfBatches = numberOfBatches;
this.totalRowsAffected = totalRowsAffected;
}

public int getNumberOfBatches() {
return numberOfBatches;
}

public int getTotalRowsAffected() {
return totalRowsAffected;
}
}
16 changes: 16 additions & 0 deletions _includes/v20.1/app/spring-mybatis/service/AccountService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.cockroachdemo.service;

import java.util.Optional;

import com.example.cockroachdemo.model.Account;
import com.example.cockroachdemo.model.BatchResults;

public interface AccountService {
void createAccountsTable();
Optional<Account> getAccount(int id);
BatchResults bulkInsertRandomAccountData(int numberToInsert);
BatchResults addAccounts(Account...accounts);
int transferFunds(int fromAccount, int toAccount, int amount);
long findCountOfAccounts();
int deleteAllAccounts();
}
Loading