├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── bash.lnk ├── mvnw ├── pom.xml ├── roach-data-2x-parent └── pom.xml ├── roach-data-3x-parent └── pom.xml ├── roach-data-jdbc-plain ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jdbc │ │ └── plain │ │ ├── Account.java │ │ ├── ConnectionCallback.java │ │ ├── ConnectionTemplate.java │ │ ├── DataAccessException.java │ │ ├── PlainJdbcApplication.java │ │ ├── SchemaSupport.java │ │ ├── TransactionCallback.java │ │ └── TransactionTemplate.java │ └── resources │ ├── db │ └── create.sql │ └── logback.xml ├── roach-data-jdbc ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jdbc │ │ ├── Account.java │ │ ├── AccountController.java │ │ ├── AccountModel.java │ │ ├── AccountRepository.java │ │ ├── AccountType.java │ │ ├── JdbcApplication.java │ │ ├── NegativeBalanceException.java │ │ ├── PagedAccountHelper.java │ │ ├── PagedAccountRepository.java │ │ ├── PagedAccountRepositoryImpl.java │ │ ├── RetryableTransactionAspect.java │ │ └── TransactionHintsAspect.java │ └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ ├── changelog-master.xml │ └── create.sql │ └── logback.xml ├── roach-data-jdbi ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jdbi │ │ ├── Account.java │ │ ├── DataAccessException.java │ │ ├── JdbiApplication.java │ │ └── SchemaSupport.java │ └── resources │ ├── db │ └── create.sql │ └── logback.xml ├── roach-data-jooq ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jooq │ │ ├── AccountController.java │ │ ├── AccountModel.java │ │ ├── AccountRepository.java │ │ ├── AccountType.java │ │ ├── JooQApplication.java │ │ ├── JooqAccountRepository.java │ │ ├── NegativeBalanceException.java │ │ ├── RetryableTransactionAspect.java │ │ └── model │ │ ├── DefaultCatalog.java │ │ ├── Keys.java │ │ ├── Public.java │ │ ├── Tables.java │ │ └── tables │ │ ├── Account.java │ │ ├── Databasechangelog.java │ │ ├── Databasechangeloglock.java │ │ └── records │ │ ├── AccountRecord.java │ │ ├── DatabasechangelogRecord.java │ │ └── DatabasechangeloglockRecord.java │ └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ ├── changelog-master.xml │ └── create.sql │ ├── jooq_config.properties │ └── logback.xml ├── roach-data-jpa-orders ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jpa │ │ ├── Main.java │ │ ├── OrderSystemClient.java │ │ ├── config │ │ └── JpaConfig.java │ │ ├── domain │ │ ├── AbstractEntity.java │ │ ├── Customer.java │ │ ├── Order.java │ │ ├── OrderItem.java │ │ └── Product.java │ │ ├── experimental │ │ └── QueryInterceptor.java │ │ ├── repository │ │ ├── CustomerRepository.java │ │ ├── OrderRepository.java │ │ └── ProductRepository.java │ │ └── service │ │ ├── OrderSystem.java │ │ └── OrderSystemImpl.java │ └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ └── migration │ │ └── V1_1__create.sql │ └── logback-spring.xml ├── roach-data-jpa ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jpa │ │ ├── Account.java │ │ ├── AccountController.java │ │ ├── AccountModel.java │ │ ├── AccountRepository.java │ │ ├── AccountType.java │ │ ├── JpaApplication.java │ │ ├── NegativeBalanceException.java │ │ └── RetryableTransactionAspect.java │ └── resources │ ├── application-dev.yml │ ├── application.yml │ ├── banner.txt │ ├── db │ ├── changelog-master.xml │ └── create.sql │ └── logback.xml ├── roach-data-json ├── README.md ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── roach │ │ │ └── data │ │ │ └── json │ │ │ ├── DataSourceConfig.java │ │ │ ├── JsonApplication.java │ │ │ ├── chat │ │ │ ├── ChatHistory.java │ │ │ ├── ChatHistoryController.java │ │ │ └── ChatHistoryRepository.java │ │ │ ├── department │ │ │ ├── Address.java │ │ │ ├── Department.java │ │ │ ├── DepartmentRepository.java │ │ │ └── User.java │ │ │ ├── journal │ │ │ ├── Account.java │ │ │ ├── AccountJournal.java │ │ │ ├── AccountJournalRepository.java │ │ │ ├── AccountRepository.java │ │ │ ├── Journal.java │ │ │ ├── Transaction.java │ │ │ ├── TransactionItem.java │ │ │ ├── TransactionJournal.java │ │ │ ├── TransactionJournalRepository.java │ │ │ └── TransactionRepository.java │ │ │ └── support │ │ │ ├── AbstractJsonDataType.java │ │ │ ├── LocalDateDeserializer.java │ │ │ ├── LocalDateSerializer.java │ │ │ └── SchemaExporter.java │ └── resources │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── db │ │ ├── changelog-master.xml │ │ └── create.sql │ │ └── logback.xml │ └── test │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── jpa │ │ ├── AbstractIntegrationTest.java │ │ ├── JacksonConfig.java │ │ ├── chat │ │ └── ChatHistoryRepositoryTest.java │ │ ├── department │ │ └── DepartmentRepositoryTest.java │ │ └── journal │ │ ├── AccountJournalTest.java │ │ ├── AccountRepositoryTest.java │ │ ├── ObjectMapperFormatTest.java │ │ ├── TransactionJournalTest.java │ │ └── TransactionRepositoryTest.java │ └── resources │ └── application.yml ├── roach-data-mybatis ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── mybatis │ │ ├── Account.java │ │ ├── AccountController.java │ │ ├── AccountModel.java │ │ ├── AccountRepository.java │ │ ├── AccountType.java │ │ ├── IndexModel.java │ │ ├── MyBatisApplication.java │ │ ├── NegativeBalanceException.java │ │ ├── PagedAccountMapper.java │ │ ├── PagedAccountRepository.java │ │ ├── PagedAccountRepositoryImpl.java │ │ └── RetryableTransactionAspect.java │ └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ ├── changelog-master.xml │ └── create.sql │ └── logback.xml ├── roach-data-parallel ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── roach │ │ │ └── data │ │ │ └── parallel │ │ │ ├── ConcurrencyUtils.java │ │ │ └── Main.java │ └── resources │ │ └── logback.xml │ └── test │ └── java │ └── io │ └── roach │ └── data │ └── parallel │ └── ConcurrencyUtilsTest.java ├── roach-data-reactive-2x ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── reactive │ │ ├── Account.java │ │ ├── AccountController.java │ │ ├── AccountRepository.java │ │ ├── AccountType.java │ │ ├── NegativeBalanceException.java │ │ ├── ReactiveApplication.java │ │ └── RetryableTransactionAspect.java │ └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ └── create.sql │ └── logback.xml ├── roach-data-reactive ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── io │ │ └── roach │ │ └── data │ │ └── reactive │ │ ├── Account.java │ │ ├── AccountController.java │ │ ├── AccountRepository.java │ │ ├── AccountType.java │ │ ├── NegativeBalanceException.java │ │ ├── ReactiveApplication.java │ │ └── RetryableTransactionAspect.java │ └── resources │ ├── application.yml │ ├── banner.txt │ ├── db │ └── create.sql │ └── logback.xml └── roach-data-relational ├── README.md ├── pom.xml └── src └── main ├── java └── io │ └── roach │ └── data │ └── relational │ ├── JdbcConfiguration.java │ ├── OrderSystemClient.java │ ├── RelationalApplication.java │ ├── domain │ ├── AbstractEntity.java │ ├── Customer.java │ ├── Order.java │ ├── OrderItem.java │ └── Product.java │ ├── repository │ ├── CustomerRepository.java │ ├── OrderRepository.java │ └── ProductRepository.java │ └── service │ ├── DefaultOrderSystem.java │ ├── OrderService.java │ └── OrderSystem.java └── resources ├── application-dev.yml ├── application.yml ├── banner.txt ├── db └── migration │ └── V1_1__create.sql └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .DS_Store 7 | .DS_Store? 8 | Thumbs.db 9 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockroachlabs/roach-data/234f5ecaa637deeb3360ea053b12f1e007488852/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar 3 | -------------------------------------------------------------------------------- /bash.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockroachlabs/roach-data/234f5ecaa637deeb3360ea053b12f1e007488852/bash.lnk -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-project 8 | 1.0.0-SNAPSHOT 9 | pom 10 | 11 | 12 | Roach Data Project Aggregator 13 | 14 | 15 | 16 | roach-data-2x-parent 17 | roach-data-3x-parent 18 | roach-data-jdbc 19 | roach-data-jdbc-plain 20 | roach-data-relational 21 | roach-data-jpa 22 | roach-data-jpa-orders 23 | roach-data-json 24 | roach-data-mybatis 25 | roach-data-reactive 26 | roach-data-reactive-2x 27 | roach-data-jdbi 28 | roach-data-parallel 29 | 30 | 31 | 32 | 33 | jdk17+ 34 | 35 | (1.4, 1.8] 36 | 37 | 38 | roach-data-jooq 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /roach-data-2x-parent/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.10 9 | 10 | 11 | 12 | io.roach.data 13 | roach-data-2x-parent 14 | 1.0.0-SNAPSHOT 15 | pom 16 | 17 | 18 | true 19 | 1.8 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 2.7.10 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-tomcat 33 | 34 | 35 | 36 | 37 | org.liquibase 38 | liquibase-core 39 | 3.8.9 40 | runtime 41 | 42 | 43 | net.ttddyy 44 | datasource-proxy 45 | 1.7 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | repackage 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /roach-data-3x-parent/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.5 9 | 10 | 11 | 12 | io.roach.data 13 | roach-data-3x-parent 14 | 1.0.0-SNAPSHOT 15 | pom 16 | 17 | 18 | true 19 | 17 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 3.5.5 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-tomcat 34 | 35 | 36 | 37 | 38 | org.liquibase 39 | liquibase-core 40 | 4.29.1 41 | runtime 42 | 43 | 44 | org.flywaydb 45 | flyway-core 46 | 47 | 9.22.3 48 | runtime 49 | 50 | 51 | net.ttddyy 52 | datasource-proxy 53 | 1.11.0 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-maven-plugin 64 | 65 | 66 | 67 | repackage 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: Plain JDBC 2 | 3 | A CockroachDB demo using only JDBC for data access. 4 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-3x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-3x-parent 10 | 11 | 12 | roach-data-jdbc-plain 13 | 14 | 15 | 16 | org.slf4j 17 | slf4j-api 18 | 19 | 20 | ch.qos.logback 21 | logback-core 22 | 23 | 24 | ch.qos.logback 25 | logback-classic 26 | 27 | 28 | 29 | com.zaxxer 30 | HikariCP 31 | 32 | 33 | org.postgresql 34 | postgresql 35 | 36 | 37 | net.ttddyy 38 | datasource-proxy 39 | 1.9 40 | 41 | 42 | 43 | 44 | roach-data-jdbc-plain 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/java/io/roach/data/jdbc/plain/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc.plain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class Account { 6 | String name; 7 | 8 | BigDecimal amount; 9 | 10 | Account(String name, BigDecimal amount) { 11 | this.name = name; 12 | this.amount = amount; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/java/io/roach/data/jdbc/plain/ConnectionCallback.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc.plain; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | 6 | interface ConnectionCallback { 7 | T doInConnection(Connection conn) throws SQLException; 8 | } 9 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/java/io/roach/data/jdbc/plain/ConnectionTemplate.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc.plain; 2 | 3 | import java.lang.reflect.UndeclaredThrowableException; 4 | import java.sql.Connection; 5 | import java.sql.SQLException; 6 | 7 | import javax.sql.DataSource; 8 | 9 | public abstract class ConnectionTemplate { 10 | public static T execute(DataSource ds, 11 | ConnectionCallback action) { 12 | try (Connection conn = ds.getConnection()) { 13 | T result; 14 | try { 15 | result = action.doInConnection(conn); 16 | } catch (RuntimeException | Error ex) { 17 | throw ex; 18 | } catch (Throwable ex) { 19 | throw new UndeclaredThrowableException(ex, 20 | "TransactionCallback threw undeclared checked exception"); 21 | } 22 | return result; 23 | } catch (SQLException e) { 24 | throw new DataAccessException(e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/java/io/roach/data/jdbc/plain/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc.plain; 2 | 3 | public class DataAccessException extends RuntimeException { 4 | public DataAccessException(String message) { 5 | super(message); 6 | } 7 | 8 | public DataAccessException(Throwable cause) { 9 | super(cause); 10 | } 11 | 12 | public DataAccessException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/java/io/roach/data/jdbc/plain/SchemaSupport.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc.plain; 2 | 3 | import java.net.URL; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | import java.sql.Statement; 7 | 8 | import javax.sql.DataSource; 9 | 10 | public abstract class SchemaSupport { 11 | private SchemaSupport() { 12 | } 13 | 14 | public static void setupSchema(DataSource ds) throws Exception { 15 | URL sql = PlainJdbcApplication.class.getResource("/db/create.sql"); 16 | 17 | StringBuilder buffer = new StringBuilder(); 18 | 19 | Files.readAllLines(Paths.get(sql.toURI())).forEach(line -> { 20 | if (!line.startsWith("--") && !line.isEmpty()) { 21 | buffer.append(line); 22 | } 23 | if (line.endsWith(";") && buffer.length() > 0) { 24 | ConnectionTemplate.execute(ds, conn -> { 25 | try (Statement statement = conn.createStatement()) { 26 | statement.execute(buffer.toString()); 27 | } 28 | buffer.setLength(0); 29 | return null; 30 | }); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/java/io/roach/data/jdbc/plain/TransactionCallback.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc.plain; 2 | 3 | import java.sql.Connection; 4 | import java.sql.SQLException; 5 | 6 | public interface TransactionCallback { 7 | T doInTransaction(Connection conn) throws SQLException; 8 | } 9 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | -- drop table if exists account; 2 | 3 | create table if not exists account 4 | ( 5 | id int not null primary key, 6 | balance numeric(19, 2) not null, 7 | name varchar(128) not null, 8 | updated timestamptz not null default clock_timestamp() 9 | ); 10 | 11 | -- alter table account 12 | -- add constraint check_account_positive_balance check (balance >= 0); 13 | -- 14 | -- insert into account (id, balance, name) 15 | -- select i, 16 | -- 5000.00, 17 | -- concat('customer:', (i::varchar)) 18 | -- from generate_series(1, 100) as i; 19 | -------------------------------------------------------------------------------- /roach-data-jdbc-plain/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /roach-data-jdbc/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: JDBC 2 | 3 | A CockroachDB Spring Boot Demo using [Spring Data JDBC](https://spring.io/projects/spring-data-jdbc) 4 | for data access. 5 | 6 | Spring Data JDBC provides a simpler, yet powerful model than JPA or any other ORM frameworks. It's considered a good 7 | fit for data access logic that doesn't involve large, complex domain model mapping or any of the advanced features 8 | of JPA, such as lazy loading, second level caching, auto-batching and transparent persistence. 9 | 10 | The JDBC demo has most code comments and also provides a few extra features compared to the other modules. 11 | -------------------------------------------------------------------------------- /roach-data-jdbc/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-2x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-2x-parent 10 | 11 | 12 | roach-data-jdbc 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-jetty 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-hateoas 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-aop 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jdbc 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-actuator 38 | 39 | 40 | org.liquibase 41 | liquibase-core 42 | runtime 43 | 44 | 45 | org.postgresql 46 | postgresql 47 | 48 | 49 | 50 | 51 | roach-data-jdbc 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.annotation.Id; 6 | 7 | /** 8 | * Domain entity mapped to the account table. 9 | */ 10 | public class Account { 11 | @Id 12 | private Long id; 13 | 14 | private String name; 15 | 16 | private AccountType type; 17 | 18 | private BigDecimal balance; 19 | 20 | public Long getId() { 21 | return id; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public AccountType getType() { 29 | return type; 30 | } 31 | 32 | public BigDecimal getBalance() { 33 | return balance; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/AccountModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.hateoas.RepresentationModel; 6 | import org.springframework.hateoas.server.core.Relation; 7 | 8 | /** 9 | * Account resource represented in HAL+JSON via REST API. 10 | */ 11 | @Relation(value = "account", collectionRelation = "accounts") 12 | public class AccountModel extends RepresentationModel { 13 | private String name; 14 | 15 | private AccountType type; 16 | 17 | private BigDecimal balance; 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | 27 | public AccountType getType() { 28 | return type; 29 | } 30 | 31 | public void setType(AccountType type) { 32 | this.type = type; 33 | } 34 | 35 | public BigDecimal getBalance() { 36 | return balance; 37 | } 38 | 39 | public void setBalance(BigDecimal balance) { 40 | this.balance = balance; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.jdbc.repository.query.Modifying; 6 | import org.springframework.data.jdbc.repository.query.Query; 7 | import org.springframework.data.repository.CrudRepository; 8 | import org.springframework.data.repository.query.Param; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 13 | 14 | /** 15 | * The main account repository, notice there's no implementation needed since its auto-proxied by 16 | * spring-data. 17 | */ 18 | @Repository 19 | @Transactional(propagation = MANDATORY) 20 | public interface AccountRepository extends CrudRepository, PagedAccountRepository { 21 | @Query(value = "SELECT balance FROM account WHERE id=:id FOR UPDATE") 22 | BigDecimal getBalance(@Param("id") Long id); 23 | 24 | @Modifying 25 | @Query("UPDATE account SET balance = balance + :balance WHERE id=:id") 26 | void updateBalance(@Param("id") Long id, @Param("balance") BigDecimal balance); 27 | } 28 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | public enum AccountType { 4 | asset, 5 | expense 6 | } 7 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import org.springframework.dao.DataIntegrityViolationException; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | /** 8 | * Business exception that maps to a given HTTP status code. 9 | */ 10 | @ResponseStatus(value = HttpStatus.EXPECTATION_FAILED, reason = "Negative balance") 11 | public class NegativeBalanceException extends DataIntegrityViolationException { 12 | public NegativeBalanceException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/PagedAccountHelper.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.jdbc.repository.query.Query; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface PagedAccountHelper extends org.springframework.data.repository.Repository { 11 | @Query("SELECT * FROM account LIMIT :pageSize OFFSET :offset") 12 | List findAll(@Param("pageSize") int pageSize, @Param("offset") long offset); 13 | 14 | @Query("SELECT count(id) FROM account") 15 | long countAll(); 16 | } 17 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/PagedAccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | 6 | /** 7 | * Pagination is not available in spring-data-jdbc (yet) so we create a separate 8 | * repository to provide basic limit+offset pagination queries for accounts. 9 | */ 10 | public interface PagedAccountRepository { 11 | Page findAll(Pageable pageable); 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/PagedAccountRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.PageImpl; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.stereotype.Repository; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 11 | 12 | @Repository 13 | // @Transactional annotation here to emphasise that repositories should always be called within an existing transaction context 14 | @Transactional(propagation = MANDATORY) 15 | public class PagedAccountRepositoryImpl implements PagedAccountRepository { 16 | @Autowired 17 | private PagedAccountHelper pagedAccountHelper; 18 | 19 | @Override 20 | public Page findAll(Pageable pageable) { 21 | return new PageImpl<>(pagedAccountHelper.findAll(pageable.getPageSize(), pageable.getOffset()), 22 | pageable, 23 | pagedAccountHelper.countAll()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/java/io/roach/data/jdbc/TransactionHintsAspect.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbc; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.Around; 5 | import org.aspectj.lang.annotation.Aspect; 6 | import org.aspectj.lang.annotation.Pointcut; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.core.Ordered; 11 | import org.springframework.core.annotation.Order; 12 | import org.springframework.jdbc.core.JdbcTemplate; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.transaction.TransactionDefinition; 15 | import org.springframework.transaction.annotation.Transactional; 16 | import org.springframework.transaction.support.TransactionSynchronizationManager; 17 | import org.springframework.util.Assert; 18 | 19 | /** 20 | * Aspect with an around advice that intercepts and sets transaction attributes. 21 | *

22 | * This advice needs to runs in a transactional context, which is after the underlying 23 | * transaction advisor. 24 | */ 25 | @Component 26 | @Aspect 27 | // After TX advisor 28 | @Order(Ordered.LOWEST_PRECEDENCE) 29 | public class TransactionHintsAspect { 30 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 31 | 32 | @Autowired 33 | private JdbcTemplate jdbcTemplate; 34 | 35 | private String applicationName = "roach-data"; 36 | 37 | @Pointcut("execution(* io.roach..*(..)) && @annotation(transactional)") 38 | public void anyTransactionBoundaryOperation(Transactional transactional) { 39 | } 40 | 41 | @Around(value = "anyTransactionBoundaryOperation(transactional)", 42 | argNames = "pjp,transactional") 43 | public Object setTransactionAttributes(ProceedingJoinPoint pjp, Transactional transactional) 44 | throws Throwable { 45 | Assert.isTrue(TransactionSynchronizationManager.isActualTransactionActive(), "TX not active"); 46 | 47 | // https://www.cockroachlabs.com/docs/v19.2/set-vars.html 48 | jdbcTemplate.update("SET application_name=?", applicationName); 49 | 50 | if (transactional.timeout() != TransactionDefinition.TIMEOUT_DEFAULT) { 51 | logger.info("Setting statement time {} for {}", transactional.timeout(), 52 | pjp.getSignature().toShortString()); 53 | jdbcTemplate.update("SET statement_timeout=?", transactional.timeout() * 1000); 54 | } 55 | 56 | if (transactional.readOnly()) { 57 | logger.info("Setting transaction read only for {}", pjp.getSignature().toShortString()); 58 | jdbcTemplate.execute("SET transaction_read_only=true"); 59 | } 60 | 61 | return pjp.proceed(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | output: 8 | ansi: 9 | enabled: ALWAYS 10 | 11 | liquibase: 12 | change-log: classpath:db/changelog-master.xml 13 | default-schema: 14 | drop-first: false 15 | contexts: crdb 16 | enabled: true 17 | 18 | datasource: 19 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 20 | driver-class-name: org.postgresql.Driver 21 | username: root 22 | password: 23 | hikari: 24 | maximum-pool-size: 4 25 | connection-init-sql: SELECT 1 26 | 27 | jpa: 28 | open-in-view: false 29 | 30 | management: 31 | endpoints: 32 | web: 33 | exposure: 34 | include: conditions,env,info,health,httptrace,metrics,threaddump,shutdown,configprops,liquibase 35 | endpoint: 36 | health: 37 | show-details: always 38 | health: 39 | defaults: 40 | enabled: true 41 | 42 | server: 43 | port: 9090 44 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB on Spring Data / JDBC ${application.formatted-version} 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/resources/db/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | ANY 12 | 13 | 14 | 15 | 16 | 17 | 1 18 | Alice 19 | 20 | asset 21 | 22 | 23 | 2 24 | Bob 25 | 26 | expense 27 | 28 | 29 | 3 30 | Bobby Tables 31 | 32 | asset 33 | 34 | 35 | 4 36 | Doris 37 | 38 | expense 39 | 40 | 41 | -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | create table account 2 | ( 3 | id int not null primary key default unique_rowid(), 4 | balance numeric(19, 2) not null, 5 | name varchar(128) not null, 6 | type varchar(25) not null, 7 | updated timestamptz not null default clock_timestamp() 8 | ); 9 | 10 | -- insert into account (id,balance,name,type) 11 | -- values 12 | -- (1,100.50,'a','expense'), 13 | -- (2,100.50,'b','expense'), 14 | -- (3,100.50,'c','expense'), 15 | -- (4,100.50,'d','expense'), 16 | -- (5,100.50,'e','expense'); 17 | 18 | -- select * from account AS OF SYSTEM TIME '-5s'; -------------------------------------------------------------------------------- /roach-data-jdbc/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /roach-data-jdbi/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: JDBI 2 | 3 | A CockroachDB demo using JDBI for data access. 4 | -------------------------------------------------------------------------------- /roach-data-jdbi/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-2x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-2x-parent 10 | 11 | 12 | roach-data-jdbi 13 | 14 | 15 | 16 | org.jdbi 17 | jdbi3-core 18 | 3.37.1 19 | 20 | 21 | org.slf4j 22 | slf4j-api 23 | 1.7.31 24 | 25 | 26 | ch.qos.logback 27 | logback-core 28 | 1.2.3 29 | 30 | 31 | ch.qos.logback 32 | logback-classic 33 | 1.2.3 34 | 35 | 36 | 37 | com.zaxxer 38 | HikariCP 39 | 4.0.3 40 | 41 | 42 | org.postgresql 43 | postgresql 44 | 45 | 46 | net.ttddyy 47 | datasource-proxy 48 | 1.7 49 | 50 | 51 | 52 | 53 | roach-data-jdbc-plain 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-maven-plugin 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /roach-data-jdbi/src/main/java/io/roach/data/jdbi/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbi; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class Account { 6 | String name; 7 | 8 | BigDecimal amount; 9 | 10 | Account(String name, BigDecimal amount) { 11 | this.name = name; 12 | this.amount = amount; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /roach-data-jdbi/src/main/java/io/roach/data/jdbi/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbi; 2 | 3 | public class DataAccessException extends RuntimeException { 4 | public DataAccessException(String message) { 5 | super(message); 6 | } 7 | 8 | public DataAccessException(Throwable cause) { 9 | super(cause); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /roach-data-jdbi/src/main/java/io/roach/data/jdbi/SchemaSupport.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jdbi; 2 | 3 | import org.jdbi.v3.core.Jdbi; 4 | 5 | import java.io.IOException; 6 | import java.net.URISyntaxException; 7 | import java.net.URL; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | 11 | public abstract class SchemaSupport { 12 | private SchemaSupport() { 13 | } 14 | 15 | public static void setupSchema(Jdbi jdbi) { 16 | StringBuilder buffer = new StringBuilder(); 17 | 18 | try { 19 | URL sql = JdbiApplication.class.getResource("/db/create.sql"); 20 | Files.readAllLines(Paths.get(sql.toURI())).forEach(line -> { 21 | if (!line.startsWith("--") && !line.isEmpty()) { 22 | buffer.append(line); 23 | } 24 | if (line.endsWith(";") && buffer.length() > 0) { 25 | jdbi.useHandle(handle -> { 26 | handle.execute(buffer.toString()); 27 | }); 28 | buffer.setLength(0); 29 | } 30 | }); 31 | } catch (IOException e) { 32 | throw new RuntimeException(e); 33 | } catch (URISyntaxException e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /roach-data-jdbi/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | drop table if exists account; 2 | 3 | create table if not exists account 4 | ( 5 | id int not null primary key, 6 | balance numeric(19, 2) not null, 7 | name varchar(128) not null, 8 | updated timestamptz not null default clock_timestamp() 9 | ); 10 | 11 | alter table account 12 | add constraint if not exists check_account_positive_balance check (balance >= 0); 13 | 14 | truncate table account; 15 | 16 | insert into account (id, balance, name) 17 | select i, 18 | 5000.00, 19 | concat('customer:', (i::varchar)) 20 | from generate_series(1, 100) as i; 21 | -------------------------------------------------------------------------------- /roach-data-jdbi/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /roach-data-jooq/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: jOOQ 2 | 3 | A CockroachDB Spring Boot Demo using [jOOQ](https://www.jooq.org/) for data access. 4 | 5 | ## Generate jOOQ classes 6 | 7 | First create the DB schema: 8 | 9 | create table account 10 | ( 11 | id int not null primary key, 12 | balance numeric(19, 2) not null, 13 | name varchar(128) not null, 14 | type varchar(25) not null 15 | ); 16 | 17 | Then generate code by activating a Maven profile called _generate_: 18 | 19 | mvn -P generate clean install 20 | 21 | (Note: this will fail with an error when using CRDB even if classes are generated correctly) 22 | 23 | Finally drop the table 24 | 25 | drop table account cascade; 26 | 27 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/AccountModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jooq; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.hateoas.RepresentationModel; 6 | import org.springframework.hateoas.server.core.Relation; 7 | 8 | @Relation(value = "account", collectionRelation = "accounts") 9 | public class AccountModel extends RepresentationModel { 10 | private String name; 11 | 12 | private AccountType type; 13 | 14 | private BigDecimal balance; 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public AccountType getType() { 25 | return type; 26 | } 27 | 28 | public void setType(AccountType type) { 29 | this.type = type; 30 | } 31 | 32 | public BigDecimal getBalance() { 33 | return balance; 34 | } 35 | 36 | public void setBalance(BigDecimal balance) { 37 | this.balance = balance; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jooq; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | import io.roach.data.jooq.model.tables.records.AccountRecord; 9 | 10 | public interface AccountRepository { 11 | Page findAll(Pageable pageable); 12 | 13 | AccountRecord getOne(Long id); 14 | 15 | BigDecimal getBalance(Long id); 16 | 17 | void updateBalance(Long id, BigDecimal balance); 18 | } 19 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jooq; 2 | 3 | public enum AccountType { 4 | asset, 5 | expense 6 | } 7 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/JooqAccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jooq; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | import org.jooq.DSLContext; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.domain.Page; 9 | import org.springframework.data.domain.PageImpl; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.stereotype.Repository; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import io.roach.data.jooq.model.tables.records.AccountRecord; 15 | 16 | import static io.roach.data.jooq.model.Tables.ACCOUNT; 17 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 18 | 19 | @Repository 20 | @Transactional(propagation = MANDATORY) 21 | public class JooqAccountRepository implements AccountRepository { 22 | @Autowired 23 | private DSLContext dsl; 24 | 25 | @Override 26 | public Page findAll(Pageable pageable) { 27 | List accountRecords = dsl.selectFrom(ACCOUNT).limit(pageable.getPageSize()) 28 | .offset(pageable.getOffset()) 29 | .fetchInto(AccountRecord.class); 30 | long totalRecords = dsl.fetchCount(dsl.select().from(ACCOUNT)); 31 | return new PageImpl<>(accountRecords, pageable, totalRecords); 32 | } 33 | 34 | @Override 35 | public AccountRecord getOne(Long id) { 36 | return dsl.selectFrom(ACCOUNT) 37 | .where(ACCOUNT.ID.eq(id)) 38 | .fetchOne(); 39 | } 40 | 41 | @Override 42 | public BigDecimal getBalance(Long id) { 43 | return dsl.select(ACCOUNT.BALANCE) 44 | .from(ACCOUNT) 45 | .where(ACCOUNT.ID.eq(id)) 46 | .forUpdate() 47 | .fetchOne() 48 | .value1(); 49 | } 50 | 51 | @Override 52 | public void updateBalance(Long id, BigDecimal balance) { 53 | dsl.update(ACCOUNT) 54 | .set(ACCOUNT.BALANCE, ACCOUNT.BALANCE.plus(balance)) 55 | .where(ACCOUNT.ID.eq(id)) 56 | .execute(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jooq; 2 | 3 | import org.springframework.dao.DataIntegrityViolationException; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Negative balance") 8 | public class NegativeBalanceException extends DataIntegrityViolationException { 9 | public NegativeBalanceException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/model/DefaultCatalog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package io.roach.data.jooq.model; 5 | 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import org.jooq.Schema; 11 | import org.jooq.impl.CatalogImpl; 12 | 13 | 14 | /** 15 | * This class is generated by jOOQ. 16 | */ 17 | @SuppressWarnings({"all", "unchecked", "rawtypes"}) 18 | public class DefaultCatalog extends CatalogImpl { 19 | 20 | /** 21 | * The reference instance of DEFAULT_CATALOG 22 | */ 23 | public static final DefaultCatalog DEFAULT_CATALOG = new DefaultCatalog(); 24 | 25 | private static final long serialVersionUID = 671484435; 26 | 27 | /** 28 | * The schema public. 29 | */ 30 | public final Public PUBLIC = Public.PUBLIC; 31 | 32 | /** 33 | * No further instances allowed 34 | */ 35 | private DefaultCatalog() { 36 | super(""); 37 | } 38 | 39 | @Override 40 | public final List getSchemas() { 41 | return Arrays.asList( 42 | Public.PUBLIC); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/model/Keys.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package io.roach.data.jooq.model; 5 | 6 | 7 | import org.jooq.TableField; 8 | import org.jooq.UniqueKey; 9 | import org.jooq.impl.Internal; 10 | 11 | import io.roach.data.jooq.model.tables.Account; 12 | import io.roach.data.jooq.model.tables.Databasechangeloglock; 13 | import io.roach.data.jooq.model.tables.records.AccountRecord; 14 | import io.roach.data.jooq.model.tables.records.DatabasechangeloglockRecord; 15 | 16 | 17 | /** 18 | * A class modelling foreign key relationships and constraints of tables of 19 | * the public schema. 20 | */ 21 | @SuppressWarnings({"all", "unchecked", "rawtypes"}) 22 | public class Keys { 23 | 24 | // ------------------------------------------------------------------------- 25 | // IDENTITY definitions 26 | // ------------------------------------------------------------------------- 27 | 28 | 29 | // ------------------------------------------------------------------------- 30 | // UNIQUE and PRIMARY KEY definitions 31 | // ------------------------------------------------------------------------- 32 | 33 | public static final UniqueKey PRIMARY = UniqueKeys0.PRIMARY; 34 | 35 | public static final UniqueKey DATABASECHANGELOGLOCK_PKEY = UniqueKeys0.DATABASECHANGELOGLOCK_PKEY; 36 | 37 | // ------------------------------------------------------------------------- 38 | // FOREIGN KEY definitions 39 | // ------------------------------------------------------------------------- 40 | 41 | 42 | // ------------------------------------------------------------------------- 43 | // [#1459] distribute members to avoid static initialisers > 64kb 44 | // ------------------------------------------------------------------------- 45 | 46 | private static class UniqueKeys0 { 47 | public static final UniqueKey PRIMARY = Internal 48 | .createUniqueKey(Account.ACCOUNT, "primary", new TableField[] {Account.ACCOUNT.ID}, true); 49 | 50 | public static final UniqueKey DATABASECHANGELOGLOCK_PKEY = Internal 51 | .createUniqueKey(Databasechangeloglock.DATABASECHANGELOGLOCK, "databasechangeloglock_pkey", 52 | new TableField[] {Databasechangeloglock.DATABASECHANGELOGLOCK.ID}, true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/model/Public.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package io.roach.data.jooq.model; 5 | 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import org.jooq.Catalog; 11 | import org.jooq.Table; 12 | import org.jooq.impl.SchemaImpl; 13 | 14 | import io.roach.data.jooq.model.tables.Account; 15 | import io.roach.data.jooq.model.tables.Databasechangelog; 16 | import io.roach.data.jooq.model.tables.Databasechangeloglock; 17 | 18 | 19 | /** 20 | * This class is generated by jOOQ. 21 | */ 22 | @SuppressWarnings({"all", "unchecked", "rawtypes"}) 23 | public class Public extends SchemaImpl { 24 | 25 | /** 26 | * The reference instance of public 27 | */ 28 | public static final Public PUBLIC = new Public(); 29 | 30 | private static final long serialVersionUID = 243892754; 31 | 32 | /** 33 | * The table public.account. 34 | */ 35 | public final Account ACCOUNT = Account.ACCOUNT; 36 | 37 | /** 38 | * The table public.databasechangelog. 39 | */ 40 | public final Databasechangelog DATABASECHANGELOG = Databasechangelog.DATABASECHANGELOG; 41 | 42 | /** 43 | * The table public.databasechangeloglock. 44 | */ 45 | public final Databasechangeloglock DATABASECHANGELOGLOCK = Databasechangeloglock.DATABASECHANGELOGLOCK; 46 | 47 | /** 48 | * No further instances allowed 49 | */ 50 | private Public() { 51 | super("public", null); 52 | } 53 | 54 | 55 | @Override 56 | public Catalog getCatalog() { 57 | return DefaultCatalog.DEFAULT_CATALOG; 58 | } 59 | 60 | @Override 61 | public final List> getTables() { 62 | return Arrays.>asList( 63 | Account.ACCOUNT, 64 | Databasechangelog.DATABASECHANGELOG, 65 | Databasechangeloglock.DATABASECHANGELOGLOCK); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/java/io/roach/data/jooq/model/Tables.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is generated by jOOQ. 3 | */ 4 | package io.roach.data.jooq.model; 5 | 6 | 7 | import io.roach.data.jooq.model.tables.Account; 8 | import io.roach.data.jooq.model.tables.Databasechangelog; 9 | import io.roach.data.jooq.model.tables.Databasechangeloglock; 10 | 11 | 12 | /** 13 | * Convenience access to all tables in public 14 | */ 15 | @SuppressWarnings({"all", "unchecked", "rawtypes"}) 16 | public class Tables { 17 | 18 | /** 19 | * The table public.account. 20 | */ 21 | public static final Account ACCOUNT = Account.ACCOUNT; 22 | 23 | /** 24 | * The table public.databasechangelog. 25 | */ 26 | public static final Databasechangelog DATABASECHANGELOG = Databasechangelog.DATABASECHANGELOG; 27 | 28 | /** 29 | * The table public.databasechangeloglock. 30 | */ 31 | public static final Databasechangeloglock DATABASECHANGELOGLOCK = Databasechangeloglock.DATABASECHANGELOGLOCK; 32 | } 33 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | output: 8 | ansi: 9 | enabled: ALWAYS 10 | 11 | liquibase: 12 | change-log: classpath:db/changelog-master.xml 13 | default-schema: 14 | drop-first: false 15 | contexts: crdb 16 | enabled: true 17 | 18 | datasource: 19 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 20 | driver-class-name: org.postgresql.Driver 21 | username: root 22 | password: 23 | hikari: 24 | connection-test-query: SELECT 1 25 | 26 | jpa: 27 | open-in-view: false 28 | 29 | server: 30 | port: 9090 31 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB on Spring Data / JooQ ${application.formatted-version} 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/resources/db/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | ANY 12 | 13 | 14 | 15 | 16 | 17 | 1 18 | Alice 19 | 20 | asset 21 | 22 | 23 | 2 24 | Bob 25 | 26 | expense 27 | 28 | 29 | 3 30 | Bobby Tables 31 | 32 | asset 33 | 34 | 35 | 4 36 | Doris 37 | 38 | expense 39 | 40 | 41 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE IF EXISTS account cascade; 2 | -- DROP TABLE IF EXISTS databasechangelog cascade; 3 | -- DROP TABLE IF EXISTS databasechangeloglock cascade; 4 | 5 | create table account 6 | ( 7 | id int not null primary key, 8 | balance numeric(19, 2) not null, 9 | name varchar(128) not null, 10 | type varchar(25) not null 11 | ); 12 | 13 | -- insert into account (id,balance,name,type) values 14 | -- (1, 500.00,'Alice','asset'), 15 | -- (2, 500.00,'Bob','expense'), 16 | -- (3, 500.00,'Bobby Tables','asset'), 17 | -- (4, 500.00,'Doris','expense'); 18 | 19 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/resources/jooq_config.properties: -------------------------------------------------------------------------------- 1 | db.driver=org.postgresql.Driver 2 | db.url=jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 3 | db.username=root 4 | db.password= 5 | -------------------------------------------------------------------------------- /roach-data-jooq/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/README.md: -------------------------------------------------------------------------------- 1 | # JPA/Hibernate Demo for CockroachDB 2 | 3 | A CockroachDB Spring Boot Demo using [Spring Data JPA](https://spring.io/projects/spring-data-jpa) 4 | with Hibernate for data access. 5 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-3x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-3x-parent 10 | 11 | 12 | roach-data-jpa-orders 13 | jar 14 | 15 | 16 | true 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-jdbc 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-jpa 27 | 28 | 29 | org.springframework.data 30 | spring-data-commons 31 | 32 | 33 | net.ttddyy 34 | datasource-proxy 35 | 1.11.0 36 | 37 | 38 | org.fusesource.jansi 39 | jansi 40 | 2.4.1 41 | runtime 42 | 43 | 44 | org.flywaydb 45 | flyway-core 46 | 47 | 9.22.3 48 | runtime 49 | 50 | 51 | org.postgresql 52 | postgresql 53 | runtime 54 | 55 | 56 | 57 | 58 | roach-data-jpa-orders 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | repackage 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/Main.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | import org.springframework.boot.WebApplicationType; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.builder.SpringApplicationBuilder; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | @EnableAutoConfiguration(exclude = { 12 | DataSourceAutoConfiguration.class 13 | }) 14 | @ComponentScan(basePackageClasses = Main.class) 15 | public class Main { 16 | public static void main(String[] args) { 17 | new SpringApplicationBuilder(Main.class) 18 | .web(WebApplicationType.NONE) 19 | .run(args); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/OrderSystemClient.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.stereotype.Component; 11 | 12 | import io.roach.data.jpa.domain.Customer; 13 | import io.roach.data.jpa.domain.Order; 14 | import io.roach.data.jpa.domain.Product; 15 | import io.roach.data.jpa.service.OrderSystem; 16 | 17 | @Component 18 | public class OrderSystemClient implements CommandLineRunner { 19 | private final Logger logger = LoggerFactory.getLogger(getClass()); 20 | 21 | @Autowired 22 | private OrderSystem orderSystem; 23 | 24 | @Override 25 | public void run(String... args) { 26 | orderSystem.clearAll(); 27 | orderSystem.createProductInventory(); 28 | orderSystem.createCustomers(); 29 | List ids = orderSystem.createOrders(); 30 | 31 | ids.forEach(id -> { 32 | Order o = orderSystem.findOrderById(id); 33 | print(o); 34 | }); 35 | 36 | orderSystem.listAllOrders().forEach(this::print); 37 | orderSystem.listAllOrderDetails().forEach(this::print); 38 | 39 | logger.info(">> Find by sku: {}", orderSystem.findProductBySku("CRDB-UL-ED1")); 40 | logger.info(">> Total order price: {}", orderSystem.getTotalOrderPrice()); 41 | 42 | orderSystem.removeOrders(); 43 | } 44 | 45 | private void print(Order order) { 46 | Customer c = order.getCustomer(); 47 | logger.info(""" 48 | Order placed by: %s 49 | Total cost: %s 50 | """.formatted(c.getUserName(), order.getTotalPrice())); 51 | } 52 | 53 | private void printDetails(Order order) { 54 | Customer c = order.getCustomer(); 55 | 56 | logger.info(""" 57 | Order placed by: %s 58 | Total cost: %s 59 | """.formatted(c.getUserName(), order.getTotalPrice())); 60 | 61 | order.getOrderItems().forEach(orderItem -> { 62 | Product p = orderItem.getProduct(); 63 | 64 | logger.info(""" 65 | Product name: %s 66 | Product price: %s 67 | Product sku: %s 68 | Item qty: %s 69 | Unit price: %s 70 | Total cost: %s 71 | """.formatted( 72 | p.getName(), 73 | p.getPrice(), 74 | p.getSku(), 75 | orderItem.getQuantity(), 76 | orderItem.getUnitPrice(), 77 | orderItem.totalCost())); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/config/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.config; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.hibernate.engine.jdbc.internal.FormatStyle; 6 | import org.hibernate.engine.jdbc.internal.Formatter; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 10 | import org.springframework.boot.context.properties.ConfigurationProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 14 | import org.springframework.context.annotation.Primary; 15 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 16 | import org.springframework.transaction.annotation.EnableTransactionManagement; 17 | 18 | import com.zaxxer.hikari.HikariDataSource; 19 | 20 | import net.ttddyy.dsproxy.listener.logging.DefaultQueryLogEntryCreator; 21 | import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; 22 | import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; 23 | import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; 24 | 25 | import io.roach.data.jpa.repository.CustomerRepository; 26 | 27 | @Configuration 28 | @EnableTransactionManagement(proxyTargetClass = true) 29 | @EnableAspectJAutoProxy(proxyTargetClass = true) 30 | @EnableJpaRepositories(basePackageClasses = CustomerRepository.class, enableDefaultTransactions = false) 31 | public class JpaConfig { 32 | private final Logger logger = LoggerFactory.getLogger("io.roach.data.jpa.SQL_TRACE"); 33 | 34 | @Bean 35 | @ConfigurationProperties("spring.datasource") 36 | public DataSourceProperties dataSourceProperties() { 37 | return new DataSourceProperties(); 38 | } 39 | 40 | @Bean 41 | @Primary 42 | public DataSource primaryDataSource() { 43 | return logger.isTraceEnabled() ? loggingProxy(targetDataSource()) : targetDataSource(); 44 | } 45 | 46 | @Bean 47 | @ConfigurationProperties("spring.datasource.hikari") 48 | public HikariDataSource targetDataSource() { 49 | HikariDataSource ds = dataSourceProperties() 50 | .initializeDataSourceBuilder() 51 | .type(HikariDataSource.class) 52 | .build(); 53 | ds.addDataSourceProperty("reWriteBatchedInserts", true); 54 | ds.addDataSourceProperty("ApplicationName", "jdbc-test"); 55 | return ds; 56 | } 57 | 58 | private DataSource loggingProxy(DataSource dataSource) { 59 | final Formatter formatterBasic = FormatStyle.BASIC.getFormatter(); 60 | final Formatter formatterHighlight = FormatStyle.HIGHLIGHT.getFormatter(); 61 | 62 | DefaultQueryLogEntryCreator creator = new DefaultQueryLogEntryCreator() { 63 | @Override 64 | protected String formatQuery(String query) { 65 | return formatterHighlight.format(formatterBasic.format(query)); 66 | } 67 | }; 68 | creator.setMultiline(true); 69 | 70 | SLF4JQueryLoggingListener listener = new SLF4JQueryLoggingListener(); 71 | listener.setLogger(logger); 72 | listener.setLogLevel(SLF4JLogLevel.TRACE); 73 | listener.setWriteConnectionId(true); 74 | listener.setWriteIsolation(true); 75 | listener.setQueryLogEntryCreator(creator); 76 | 77 | return ProxyDataSourceBuilder 78 | .create(dataSource) 79 | .name("SQL-Trace") 80 | .asJson() 81 | .listener(listener) 82 | .build(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/domain/AbstractEntity.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import jakarta.persistence.MappedSuperclass; 6 | import jakarta.persistence.PostLoad; 7 | import jakarta.persistence.PostPersist; 8 | import jakarta.persistence.Transient; 9 | 10 | import org.springframework.data.domain.Persistable; 11 | 12 | @MappedSuperclass 13 | public abstract class AbstractEntity implements Persistable { 14 | @Transient 15 | private boolean isNew = true; 16 | 17 | @PostPersist 18 | @PostLoad 19 | void markNotNew() { 20 | this.isNew = false; 21 | } 22 | 23 | @Override 24 | public boolean isNew() { 25 | return isNew; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/domain/Customer.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.domain; 2 | 3 | import java.util.UUID; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.NamedQueries; 11 | import jakarta.persistence.NamedQuery; 12 | import jakarta.persistence.Table; 13 | 14 | @Entity 15 | @Table(name = "customers") 16 | @NamedQueries({ 17 | @NamedQuery( 18 | name = "Customer.findByUserName", 19 | query = "from Customer u where u.userName = :userName" 20 | ) 21 | }) 22 | public class Customer extends AbstractEntity { 23 | public static Builder builder() { 24 | return new Builder(); 25 | } 26 | 27 | public static final class Builder { 28 | private String userName; 29 | 30 | private String firstName; 31 | 32 | private String lastName; 33 | 34 | private Builder() { 35 | } 36 | 37 | public Builder withUserName(String userName) { 38 | this.userName = userName; 39 | return this; 40 | } 41 | 42 | public Builder withFirstName(String firstName) { 43 | this.firstName = firstName; 44 | return this; 45 | } 46 | 47 | public Builder withLastName(String lastName) { 48 | this.lastName = lastName; 49 | return this; 50 | } 51 | 52 | public Customer build() { 53 | Customer customer = new Customer(); 54 | customer.userName = this.userName; 55 | customer.firstName = this.firstName; 56 | customer.lastName = this.lastName; 57 | return customer; 58 | } 59 | } 60 | 61 | @Id 62 | @GeneratedValue(strategy = GenerationType.AUTO) 63 | private UUID id; 64 | 65 | @Column(name = "user_name", length = 15, nullable = false, unique = true) 66 | private String userName; 67 | 68 | @Column(name = "first_name", length = 45) 69 | private String firstName; 70 | 71 | @Column(name = "last_name", length = 45) 72 | private String lastName; 73 | 74 | @Override 75 | public UUID getId() { 76 | return id; 77 | } 78 | 79 | public String getUserName() { 80 | return userName; 81 | } 82 | 83 | public String getFirstName() { 84 | return firstName; 85 | } 86 | 87 | public String getLastName() { 88 | return lastName; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/domain/Order.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.UUID; 8 | 9 | import org.hibernate.annotations.Fetch; 10 | import org.hibernate.annotations.FetchMode; 11 | 12 | import jakarta.persistence.Column; 13 | import jakarta.persistence.ElementCollection; 14 | import jakarta.persistence.Entity; 15 | import jakarta.persistence.FetchType; 16 | import jakarta.persistence.GeneratedValue; 17 | import jakarta.persistence.GenerationType; 18 | import jakarta.persistence.Id; 19 | import jakarta.persistence.JoinColumn; 20 | import jakarta.persistence.JoinTable; 21 | import jakarta.persistence.ManyToOne; 22 | import jakarta.persistence.OrderColumn; 23 | import jakarta.persistence.Table; 24 | 25 | @Entity 26 | @Table(name = "orders") 27 | public class Order extends AbstractEntity { 28 | public static Builder builder() { 29 | return new Builder(); 30 | } 31 | 32 | public static final class Builder { 33 | private Customer customer; 34 | 35 | private final List orderItems = new ArrayList<>(); 36 | 37 | private Builder() { 38 | } 39 | 40 | public Builder withCustomer(Customer customer) { 41 | this.customer = customer; 42 | return this; 43 | } 44 | 45 | public OrderItem.Builder andOrderItem() { 46 | return new OrderItem.Builder(this, orderItems::add); 47 | } 48 | 49 | public Order build() { 50 | if (this.customer == null) { 51 | throw new IllegalStateException("Missing customer"); 52 | } 53 | if (this.orderItems.isEmpty()) { 54 | throw new IllegalStateException("Empty order"); 55 | } 56 | Order order = new Order(); 57 | order.customer = this.customer; 58 | order.orderItems.addAll(this.orderItems); 59 | order.totalPrice = order.subTotal(); 60 | return order; 61 | } 62 | } 63 | 64 | @Id 65 | @GeneratedValue(strategy = GenerationType.AUTO) 66 | private UUID id; 67 | 68 | @Column(name = "total_price", nullable = false, updatable = false) 69 | private BigDecimal totalPrice; 70 | 71 | @ManyToOne(fetch = FetchType.LAZY) 72 | @JoinColumn(name = "customer_id", nullable = false, updatable = false) 73 | private Customer customer; 74 | 75 | @ElementCollection(fetch = FetchType.LAZY) 76 | @JoinTable(name = "order_items", 77 | joinColumns = @JoinColumn(name = "order_id")) 78 | @OrderColumn(name = "item_pos") 79 | @Fetch(FetchMode.SUBSELECT) 80 | private List orderItems = new ArrayList<>(); 81 | 82 | @Override 83 | public UUID getId() { 84 | return id; 85 | } 86 | 87 | public Order setId(UUID id) { 88 | this.id = id; 89 | return this; 90 | } 91 | 92 | public BigDecimal getTotalPrice() { 93 | return totalPrice; 94 | } 95 | 96 | public Customer getCustomer() { 97 | return customer; 98 | } 99 | 100 | public List getOrderItems() { 101 | return Collections.unmodifiableList(orderItems); 102 | } 103 | 104 | public BigDecimal subTotal() { 105 | BigDecimal subTotal = BigDecimal.ZERO; 106 | for (OrderItem oi : orderItems) { 107 | subTotal = subTotal.add(oi.totalCost()); 108 | } 109 | return subTotal; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/domain/OrderItem.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.function.Consumer; 5 | 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Embeddable; 8 | import jakarta.persistence.FetchType; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | 12 | @Embeddable 13 | public class OrderItem { 14 | public static final class Builder { 15 | private final Order.Builder parentBuilder; 16 | 17 | private final Consumer callback; 18 | 19 | private int quantity; 20 | 21 | private BigDecimal unitPrice; 22 | 23 | private Product product; 24 | 25 | Builder(Order.Builder parentBuilder, Consumer callback) { 26 | this.parentBuilder = parentBuilder; 27 | this.callback = callback; 28 | } 29 | 30 | public Builder withQuantity(int quantity) { 31 | this.quantity = quantity; 32 | return this; 33 | } 34 | 35 | public Builder withUnitPrice(BigDecimal unitPrice) { 36 | this.unitPrice = unitPrice; 37 | return this; 38 | } 39 | 40 | public Builder withProduct(Product product) { 41 | this.product = product; 42 | return this; 43 | } 44 | 45 | public Order.Builder then() { 46 | if (this.unitPrice == null) { 47 | this.unitPrice = product.getPrice(); 48 | } 49 | 50 | OrderItem orderItem = new OrderItem(); 51 | orderItem.product = this.product; 52 | orderItem.unitPrice = this.unitPrice; 53 | orderItem.quantity = this.quantity; 54 | 55 | callback.accept(orderItem); 56 | 57 | return parentBuilder; 58 | } 59 | } 60 | 61 | @Column(nullable = false, updatable = false) 62 | private int quantity; 63 | 64 | @Column(name = "unit_price", nullable = false, updatable = false) 65 | private BigDecimal unitPrice; 66 | 67 | @ManyToOne(fetch = FetchType.LAZY) // Default fetch type is EAGER for @ManyToOne 68 | @JoinColumn(name = "product_id", updatable = false) 69 | private Product product; 70 | 71 | public int getQuantity() { 72 | return quantity; 73 | } 74 | 75 | public BigDecimal getUnitPrice() { 76 | return unitPrice; 77 | } 78 | 79 | public Product getProduct() { 80 | return product; 81 | } 82 | 83 | public BigDecimal totalCost() { 84 | if (unitPrice == null) { 85 | throw new IllegalStateException("unitPrice is null"); 86 | } 87 | return unitPrice.multiply(new BigDecimal(quantity)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/domain/Product.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.Table; 12 | 13 | @Entity 14 | @Table(name = "products") 15 | public class Product extends AbstractEntity { 16 | public static Builder builder() { 17 | return new Builder(); 18 | } 19 | 20 | public static final class Builder { 21 | private String name; 22 | 23 | private String sku; 24 | 25 | private BigDecimal price; 26 | 27 | private int quantity; 28 | 29 | private Builder() { 30 | } 31 | 32 | public Builder withName(String name) { 33 | this.name = name; 34 | return this; 35 | } 36 | 37 | public Builder withSku(String sku) { 38 | this.sku = sku; 39 | return this; 40 | } 41 | 42 | public Builder withPrice(BigDecimal price) { 43 | this.price = price; 44 | return this; 45 | } 46 | 47 | public Builder withQuantity(int quantity) { 48 | this.quantity = quantity; 49 | return this; 50 | } 51 | 52 | 53 | public Product build() { 54 | Product product = new Product(); 55 | product.name = this.name; 56 | product.sku = this.sku; 57 | product.price = this.price; 58 | product.inventory = this.quantity; 59 | return product; 60 | } 61 | } 62 | 63 | @Id 64 | @GeneratedValue(strategy = GenerationType.AUTO) 65 | private UUID id; 66 | 67 | @Column(length = 128, nullable = false) 68 | private String name; 69 | 70 | @Column(length = 128, nullable = false, unique = true) 71 | private String sku; 72 | 73 | @Column(length = 25, nullable = false) 74 | private BigDecimal price; 75 | 76 | @Column(nullable = false) 77 | private int inventory; 78 | 79 | @Override 80 | public UUID getId() { 81 | return id; 82 | } 83 | 84 | public String getName() { 85 | return name; 86 | } 87 | 88 | public BigDecimal getPrice() { 89 | return price; 90 | } 91 | 92 | public String getSku() { 93 | return sku; 94 | } 95 | 96 | public void setPrice(BigDecimal price) { 97 | this.price = price; 98 | } 99 | 100 | public int addInventoryQuantity(int qty) { 101 | this.inventory += qty; 102 | return inventory; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/experimental/QueryInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.experimental; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Arrays; 5 | import java.util.Deque; 6 | import java.util.Map; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import org.hibernate.engine.jdbc.internal.FormatStyle; 11 | import org.hibernate.engine.jdbc.internal.Formatter; 12 | import org.hibernate.resource.jdbc.spi.StatementInspector; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | /** 17 | * A statement interceptor that applies various transformations on Hibernate generated SQL 18 | * before passed to the DB. Interception is triggered by semantic tokens detected in SQL 19 | * hint comment prefixes. The comment itself names the token and the transformation to 20 | * apply, or many transformations in a deterministic order. 21 | *

22 | * This is to showcase how JPA/Hibernate SQL rewrites can be done to inject things like 23 | * add optimizer join hints and other elements without changing any entity mappings or 24 | * JPQL queries. 25 | * 26 | * @author Kai Niemi 27 | */ 28 | public class QueryInterceptor implements StatementInspector { 29 | @FunctionalInterface 30 | private interface Transformation { 31 | String transform(String sql); 32 | } 33 | 34 | private static final Map transformationMap 35 | = Map.of("replaceJoinWithInnerJoin", 36 | sql -> { 37 | return sql.replaceAll("join", "inner join"); 38 | }, "appendForUpdate", sql -> { 39 | return sql + " FOR UPDATE"; 40 | }, "appendForShare", sql -> { 41 | return sql + " FOR SHARE"; 42 | }); 43 | 44 | private static final Logger logger = LoggerFactory.getLogger(QueryInterceptor.class); 45 | 46 | private static final Pattern SQL_COMMENT = Pattern.compile("/\\*(.*?)\\*/\\s*"); 47 | 48 | private static final Formatter FORMATTER_BASIC = FormatStyle.BASIC.getFormatter(); 49 | 50 | private static final Formatter FORMATTER_HIGHLIGHT = FormatStyle.HIGHLIGHT.getFormatter(); 51 | 52 | @Override 53 | public String inspect(String sql) { 54 | Matcher m = SQL_COMMENT.matcher(sql); 55 | if (m.find()) { 56 | // Extract semantic token 57 | String comment = m.group(1).trim(); 58 | // Strip the comment prefix 59 | sql = m.replaceAll(""); 60 | // Stack of transformations 61 | Deque stack = new ArrayDeque<>(); 62 | stack.push(sql); 63 | // Split by comma and transform in-order 64 | Arrays.stream(comment.split(",")) 65 | .forEachOrdered(token -> { 66 | if (transformationMap.containsKey(token)) { 67 | String result = transformationMap.get(token).transform(stack.pop()); 68 | stack.push(result); 69 | } 70 | }); 71 | // Last item is the final SQL 72 | sql = stack.pop(); 73 | } 74 | 75 | if (logger.isTraceEnabled()) { 76 | logger.trace(FORMATTER_HIGHLIGHT.format(FORMATTER_BASIC.format(sql))); 77 | } 78 | 79 | return sql; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.repository; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import io.roach.data.jpa.domain.Customer; 10 | 11 | @Repository 12 | public interface CustomerRepository extends JpaRepository { 13 | Optional findByUserName(String userName); 14 | } 15 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Modifying; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.jpa.repository.QueryHints; 11 | import org.springframework.data.repository.query.Param; 12 | import org.springframework.stereotype.Repository; 13 | 14 | import jakarta.persistence.QueryHint; 15 | 16 | import io.roach.data.jpa.domain.Order; 17 | 18 | @Repository 19 | public interface OrderRepository extends JpaRepository { 20 | @Modifying 21 | @Query(value = "delete from order_items where 1=1", nativeQuery = true) 22 | void deleteAllOrderItems(); 23 | 24 | @Query(value = "from Order o " 25 | + "join fetch o.customer c where o.id=:id") 26 | Optional findOrderById(@Param("id") UUID id); 27 | 28 | @Query(value = "from Order o " 29 | + "join fetch o.customer c") 30 | List findAllOrders(); 31 | 32 | @Query(value = "from Order o " 33 | + "join fetch o.customer " 34 | + "join fetch o.orderItems oi " 35 | + "join fetch oi.product") 36 | @QueryHints({ 37 | @QueryHint(name = "org.hibernate.comment", value = "replaceJoinWithInnerJoin,appendForShare") 38 | }) 39 | List findAllOrderDetails(); 40 | 41 | @Query(value = "from Order o " 42 | + "join fetch o.customer c " 43 | + "where c.userName=:userName") 44 | List findOrdersByUserName(@Param("userName") String userName); 45 | } 46 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.repository; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Lock; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | import jakarta.persistence.LockModeType; 13 | 14 | import io.roach.data.jpa.domain.Product; 15 | 16 | @Repository 17 | public interface ProductRepository extends JpaRepository { 18 | @Lock(LockModeType.PESSIMISTIC_WRITE) 19 | Optional findProductBySku(String sku); 20 | 21 | @Query("select p from Product p where p.sku=:sku") 22 | Optional findProductBySkuNoLock(@Param("sku") String sku); 23 | } 24 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/java/io/roach/data/jpa/service/OrderSystem.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa.service; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | import io.roach.data.jpa.domain.Order; 8 | import io.roach.data.jpa.domain.Product; 9 | 10 | public interface OrderSystem { 11 | void clearAll(); 12 | 13 | void createProductInventory(); 14 | 15 | void createCustomers(); 16 | 17 | List createOrders(); 18 | 19 | List listAllOrders(); 20 | 21 | List listAllOrderDetails(); 22 | 23 | Order findOrderById(UUID id); 24 | 25 | Product findProductBySku(String sku); 26 | 27 | BigDecimal getTotalOrderPrice(); 28 | 29 | void removeOrders(); 30 | } 31 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ############################# 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ############################# 5 | spring: 6 | profiles: 7 | active: 8 | output: 9 | ansi: 10 | enabled: ALWAYS 11 | flyway: 12 | enabled: true 13 | connect-retries: 15 14 | baseline-on-migrate: true 15 | clean-disabled: false 16 | jpa: 17 | open-in-view: false 18 | properties: 19 | hibernate: 20 | use_sql_comments: true 21 | connection: 22 | provider_disables_autocommit: false 23 | jdbc: 24 | lob: 25 | non_contextual_creation: true 26 | batch_size: 128 27 | fetch_size: 128 28 | batch_versioned_data: true 29 | order_inserts: true 30 | order_updates: true 31 | hql: 32 | bulk_id_strategy: org.hibernate.hql.spi.id.inline.CteValuesListBulkIdStrategy 33 | cache: 34 | use_minimal_puts: true 35 | use_second_level_cache: false 36 | session_factory: 37 | statement_inspector: io.roach.data.jpa.experimental.QueryInterceptor 38 | datasource: 39 | url: jdbc:postgresql://localhost:26257/defaultdb?sslmode=disable 40 | driver-class-name: org.postgresql.Driver 41 | username: root 42 | password: 43 | hikari: 44 | connection-test-query: SELECT 1 45 | maximum-pool-size: 20 46 | minimum-idle: 0 47 | auto-commit: true 48 | transaction-isolation: TRANSACTION_SERIALIZABLE 49 | initialization-fail-timeout: -1 50 | ############################# 51 | logging: 52 | pattern: 53 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr([%logger{39}]){cyan} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 54 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB JPA Demo Sandbox ${application.formatted-version} 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/resources/db/migration/V1_1__create.sql: -------------------------------------------------------------------------------- 1 | create table customers 2 | ( 3 | id uuid not null default gen_random_uuid(), 4 | first_name varchar(45), 5 | last_name varchar(45), 6 | user_name varchar(15) not null, 7 | primary key (id) 8 | ); 9 | 10 | create table order_items 11 | ( 12 | order_id uuid not null, 13 | product_id uuid not null, 14 | quantity int not null, 15 | unit_price numeric(19, 2) not null, 16 | item_pos int not null, 17 | primary key (order_id, item_pos) 18 | ); 19 | 20 | create table orders 21 | ( 22 | id uuid not null default gen_random_uuid(), 23 | customer_id uuid not null, 24 | total_price numeric(19, 2) not null, 25 | tags string(128) null, 26 | 27 | primary key (id) 28 | ); 29 | 30 | create table products 31 | ( 32 | id uuid not null default gen_random_uuid(), 33 | inventory int not null, 34 | name string(128) not null, 35 | price numeric(19, 2) not null, 36 | sku string(128) not null, 37 | primary key (id) 38 | ); 39 | 40 | alter table products 41 | add constraint check_product_positive_inventory check (products.inventory >= 0); 42 | 43 | alter table if exists customers 44 | add constraint uc_user_name unique (user_name); 45 | 46 | alter table if exists product 47 | add constraint uc_product_sku unique (sku); 48 | 49 | alter table if exists order_items 50 | add constraint fk_order_item_ref_product 51 | foreign key (product_id) 52 | references products; 53 | 54 | alter table if exists order_items 55 | add constraint fk_order_item_ref_order 56 | foreign key (order_id) 57 | references orders; 58 | 59 | alter table if exists orders 60 | add constraint fk_order_ref_customer 61 | foreign key (customer_id) 62 | references customers; 63 | 64 | -- Foreign key indexes 65 | create index fk_order_item_ref_product_idx on order_items (product_id); 66 | create index fk_order_ref_customer_idx on orders (customer_id); -------------------------------------------------------------------------------- /roach-data-jpa-orders/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | 8 | %d{HH:mm:ss.SSS} [%-15thread] %highlight(%-5level) %cyan(%logger{15}) %msg %n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /roach-data-jpa/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: JPA/Hibernate 2 | 3 | A CockroachDB Spring Boot Demo using [Spring Data JPA](https://spring.io/projects/spring-data-jpa) 4 | with Hibernate for data access. 5 | 6 | Spring Data JPA provides a powerful model for using ORM frameworks such as Hibernate. It's considered a good 7 | fit for data access logic that involve large, complex domain model mapping. Spring Data JPA essentially 8 | removes the need for application code to bind directly to Hibernate APIs. It also unlocks many advanced 9 | features of JPA. 10 | 11 | 12 | -------------------------------------------------------------------------------- /roach-data-jpa/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-2x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-2x-parent 10 | 11 | 12 | roach-data-jpa 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-jetty 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-hateoas 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-jdbc 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jdbc 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-jpa 38 | 39 | 40 | org.springframework.data 41 | spring-data-commons 42 | 43 | 44 | org.liquibase 45 | liquibase-core 46 | runtime 47 | 48 | 49 | org.postgresql 50 | postgresql 51 | provided 52 | 53 | 54 | 55 | 56 | roach-data-jpa 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/java/io/roach/data/jpa/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import javax.persistence.*; 6 | 7 | @Entity 8 | @Table(name = "account") 9 | public class Account { 10 | @Id 11 | @Column 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Long id; 14 | 15 | @Column(length = 128, nullable = false, unique = true) 16 | private String name; 17 | 18 | @Column(length = 25, nullable = false) 19 | @Enumerated(EnumType.STRING) 20 | private AccountType type; 21 | 22 | @Column(length = 25, nullable = false) 23 | private BigDecimal balance; 24 | 25 | public Long getId() { 26 | return id; 27 | } 28 | 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | public AccountType getType() { 34 | return type; 35 | } 36 | 37 | public BigDecimal getBalance() { 38 | return balance; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/java/io/roach/data/jpa/AccountModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.hateoas.RepresentationModel; 6 | import org.springframework.hateoas.server.core.Relation; 7 | 8 | @Relation(value = "account", collectionRelation = "accounts") 9 | public class AccountModel extends RepresentationModel { 10 | private String name; 11 | 12 | private AccountType type; 13 | 14 | private BigDecimal balance; 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public AccountType getType() { 25 | return type; 26 | } 27 | 28 | public void setType(AccountType type) { 29 | this.type = type; 30 | } 31 | 32 | public BigDecimal getBalance() { 33 | return balance; 34 | } 35 | 36 | public void setBalance(BigDecimal balance) { 37 | this.balance = balance; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/java/io/roach/data/jpa/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import javax.persistence.LockModeType; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 9 | import org.springframework.data.jpa.repository.Lock; 10 | import org.springframework.data.jpa.repository.Modifying; 11 | import org.springframework.data.jpa.repository.Query; 12 | import org.springframework.stereotype.Repository; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 16 | 17 | @Repository 18 | @Transactional(propagation = MANDATORY) 19 | public interface AccountRepository extends JpaRepository, JpaSpecificationExecutor { 20 | @Query(value = "select balance from Account where id=?1") 21 | @Lock(LockModeType.PESSIMISTIC_READ) 22 | BigDecimal getBalance(Long id); 23 | 24 | @Modifying 25 | @Query("update Account set balance = balance + ?2 where id=?1") 26 | void updateBalance(Long id, BigDecimal balance); 27 | } 28 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/java/io/roach/data/jpa/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | public enum AccountType { 4 | asset, 5 | expense 6 | } 7 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/java/io/roach/data/jpa/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.jpa; 2 | 3 | import org.springframework.dao.DataIntegrityViolationException; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Negative balance") 8 | public class NegativeBalanceException extends DataIntegrityViolationException { 9 | public NegativeBalanceException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:postgresql://192.168.1.99:26257/roach_data?sslmode=disable 4 | username: root 5 | password: 6 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | output: 8 | ansi: 9 | enabled: ALWAYS 10 | 11 | liquibase: 12 | change-log: classpath:db/changelog-master.xml 13 | default-schema: 14 | drop-first: false 15 | contexts: crdb 16 | enabled: true 17 | 18 | datasource: 19 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 20 | driver-class-name: org.postgresql.Driver 21 | username: root 22 | password: 23 | hikari: 24 | connection-test-query: SELECT 1 25 | maximum-pool-size: 50 26 | minimum-idle: 20 27 | 28 | jpa: 29 | open-in-view: false 30 | properties: 31 | hibernate: 32 | dialect: org.hibernate.dialect.CockroachDB201Dialect 33 | show-sql: false 34 | 35 | server: 36 | port: 9090 -------------------------------------------------------------------------------- /roach-data-jpa/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB on Spring Data / JPA ${application.formatted-version} 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/resources/db/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | ANY 12 | 13 | 14 | 15 | 16 | 17 | 1 18 | Alice 19 | 20 | asset 21 | 22 | 23 | 2 24 | Bob 25 | 26 | expense 27 | 28 | 29 | 3 30 | Bobby Tables 31 | 32 | asset 33 | 34 | 35 | 4 36 | Doris 37 | 38 | expense 39 | 40 | 41 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | create table account 2 | ( 3 | id int not null primary key default unique_rowid(), 4 | balance numeric(19, 2) not null, 5 | name varchar(128) not null, 6 | type varchar(25) not null 7 | ); 8 | -------------------------------------------------------------------------------- /roach-data-jpa/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /roach-data-json/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: JPA/Hibernate with JSONB 2 | 3 | A CockroachDB Spring Boot Demo using [Spring Data JPA](https://spring.io/projects/spring-data-jpa) 4 | with Hibernate for data access. Demonstrates various JSONB column type mappings with inverted 5 | and computed indexes. -------------------------------------------------------------------------------- /roach-data-json/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-2x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-2x-parent 10 | 11 | 12 | roach-data-json 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-jetty 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-hateoas 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-jdbc 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jdbc 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-jpa 38 | 39 | 40 | org.liquibase 41 | liquibase-core 42 | runtime 43 | 44 | 45 | org.springframework.data 46 | spring-data-commons 47 | 48 | 49 | org.postgresql 50 | postgresql 51 | provided 52 | 53 | 54 | net.ttddyy 55 | datasource-proxy 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-test 65 | test 66 | 67 | 68 | org.junit.jupiter 69 | junit-jupiter-engine 70 | test 71 | 72 | 73 | 74 | 75 | roach-data-jpa 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-maven-plugin 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Primary; 11 | 12 | import net.ttddyy.dsproxy.listener.ChainListener; 13 | import net.ttddyy.dsproxy.listener.DataSourceQueryCountListener; 14 | import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; 15 | import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; 16 | 17 | @Configuration 18 | public class DataSourceConfig { 19 | @Bean 20 | @Primary 21 | public DataSource dataSource(DataSourceProperties properties) { 22 | ChainListener listener = new ChainListener(); 23 | listener.addListener(new DataSourceQueryCountListener()); 24 | return ProxyDataSourceBuilder 25 | .create(properties.initializeDataSourceBuilder().build()) 26 | .name("SQL-Trace") 27 | .listener(listener) 28 | .asJson() 29 | .countQuery() 30 | .logQueryBySlf4j(SLF4JLogLevel.DEBUG, "io.roach.sql_trace") 31 | .logSlowQueryBySlf4j(50, TimeUnit.MILLISECONDS) 32 | // .multiline() 33 | .build(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/JsonApplication.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.WebApplicationType; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.builder.SpringApplicationBuilder; 8 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 9 | import org.springframework.core.Ordered; 10 | import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; 11 | import org.springframework.hateoas.config.EnableHypermediaSupport; 12 | import org.springframework.transaction.annotation.EnableTransactionManagement; 13 | 14 | @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) 15 | @EnableJdbcRepositories 16 | @EnableAspectJAutoProxy(proxyTargetClass = true) 17 | @EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 1) // Bump up one level to enable extra advisors 18 | @SpringBootApplication 19 | public class JsonApplication { 20 | protected static final Logger logger = LoggerFactory.getLogger(JsonApplication.class); 21 | 22 | public static void main(String[] args) { 23 | new SpringApplicationBuilder(JsonApplication.class) 24 | .web(WebApplicationType.SERVLET) 25 | .run(args); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/chat/ChatHistory.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.chat; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import javax.persistence.*; 7 | 8 | import org.hibernate.annotations.Type; 9 | import org.hibernate.annotations.TypeDef; 10 | 11 | import com.fasterxml.jackson.databind.JsonNode; 12 | 13 | import io.roach.data.json.support.AbstractJsonDataType; 14 | 15 | @Entity 16 | @Table(name = "chat_history") 17 | @TypeDef(name = "jsonb-message", typeClass = ChatHistory.StringCollectionJsonType.class) 18 | public class ChatHistory { 19 | public static class StringCollectionJsonType extends AbstractJsonDataType { 20 | @Override 21 | public Class returnedClass() { 22 | return JsonNode.class; 23 | } 24 | 25 | @Override 26 | public boolean isCollectionType() { 27 | return true; 28 | } 29 | } 30 | 31 | @Id 32 | @GeneratedValue(strategy = GenerationType.AUTO) 33 | private UUID id; 34 | 35 | @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) 36 | @JoinColumn(name = "parent_id") 37 | private ChatHistory parent; 38 | 39 | @Type(type = "jsonb-message") 40 | @Column(name = "messages") 41 | @Basic(fetch = FetchType.LAZY) 42 | private List messages; 43 | 44 | public UUID getId() { 45 | return id; 46 | } 47 | 48 | public ChatHistory getParent() { 49 | return parent; 50 | } 51 | 52 | public void setParent(ChatHistory parent) { 53 | this.parent = parent; 54 | } 55 | 56 | public List getMessages() { 57 | return messages; 58 | } 59 | 60 | public void setMessages(List messages) { 61 | this.messages = messages; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/chat/ChatHistoryController.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.chat; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.domain.PageRequest; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.domain.Sort; 7 | import org.springframework.data.web.PageableDefault; 8 | import org.springframework.data.web.PagedResourcesAssembler; 9 | import org.springframework.hateoas.CollectionModel; 10 | import org.springframework.hateoas.EntityModel; 11 | import org.springframework.hateoas.PagedModel; 12 | import org.springframework.hateoas.RepresentationModel; 13 | import org.springframework.hateoas.server.SimpleRepresentationModelAssembler; 14 | import org.springframework.http.HttpEntity; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.transaction.annotation.Transactional; 18 | import org.springframework.web.bind.annotation.GetMapping; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 22 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 23 | import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; 24 | 25 | @RestController 26 | public class ChatHistoryController { 27 | @Autowired 28 | private ChatHistoryRepository chatHistoryRepository; 29 | 30 | @Autowired 31 | private PagedResourcesAssembler pagedResourcesAssembler; 32 | 33 | @GetMapping 34 | public ResponseEntity> index() { 35 | RepresentationModel index = new RepresentationModel<>(); 36 | 37 | index.add(linkTo(methodOn(ChatHistoryController.class) 38 | .listChats(PageRequest.of(0, 5))) 39 | .withRel("chats")); 40 | 41 | return new ResponseEntity<>(index, HttpStatus.OK); 42 | } 43 | 44 | @GetMapping("/chathistory") 45 | @Transactional(propagation = REQUIRES_NEW) 46 | public HttpEntity>> listChats( 47 | @PageableDefault(size = 5, direction = Sort.Direction.ASC) Pageable page) { 48 | return ResponseEntity 49 | .ok(pagedResourcesAssembler.toModel(chatHistoryRepository.findAll(page), chatModelAssembler())); 50 | } 51 | 52 | private SimpleRepresentationModelAssembler chatModelAssembler() { 53 | return new SimpleRepresentationModelAssembler(){ 54 | @Override 55 | public void addLinks(EntityModel resource) { 56 | 57 | } 58 | 59 | @Override 60 | public void addLinks(CollectionModel> resources) { 61 | 62 | } 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/chat/ChatHistoryRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.chat; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface ChatHistoryRepository extends JpaRepository { 10 | } 11 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/department/Address.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.department; 2 | 3 | public class Address { 4 | public static class Builder { 5 | private final Address adddress = new Address(); 6 | 7 | public Builder setAddress1(String address1) { 8 | adddress.address1 = address1; 9 | return this; 10 | } 11 | 12 | public Builder setAddress2(String address2) { 13 | adddress.address2 = address2; 14 | return this; 15 | } 16 | 17 | public Builder setCity(String city) { 18 | adddress.city = city; 19 | return this; 20 | } 21 | 22 | public Builder setPostcode(String postcode) { 23 | adddress.postcode = postcode; 24 | return this; 25 | } 26 | 27 | public Builder setCountry(String country) { 28 | adddress.country = country; 29 | return this; 30 | } 31 | 32 | public Address build() { 33 | return adddress; 34 | } 35 | } 36 | 37 | private String address1; 38 | 39 | private String address2; 40 | 41 | private String city; 42 | 43 | private String postcode; 44 | 45 | private String country; 46 | 47 | protected Address() { 48 | } 49 | 50 | public Address(String address1, String address2, String city, String postcode, String country) { 51 | this.address1 = address1; 52 | this.address2 = address2; 53 | this.city = city; 54 | this.postcode = postcode; 55 | this.country = country; 56 | } 57 | 58 | public String getAddress1() { 59 | return address1; 60 | } 61 | 62 | public String getAddress2() { 63 | return address2; 64 | } 65 | 66 | public String getCity() { 67 | return city; 68 | } 69 | 70 | public String getPostcode() { 71 | return postcode; 72 | } 73 | 74 | public String getCountry() { 75 | return country; 76 | } 77 | 78 | public void setAddress1(String address1) { 79 | this.address1 = address1; 80 | } 81 | 82 | public void setAddress2(String address2) { 83 | this.address2 = address2; 84 | } 85 | 86 | public void setCity(String city) { 87 | this.city = city; 88 | } 89 | 90 | public void setPostcode(String postcode) { 91 | this.postcode = postcode; 92 | } 93 | 94 | public void setCountry(String country) { 95 | this.country = country; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/department/Department.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.department; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import javax.persistence.*; 7 | 8 | import org.hibernate.annotations.Type; 9 | import org.hibernate.annotations.TypeDef; 10 | 11 | import io.roach.data.json.support.AbstractJsonDataType; 12 | 13 | @Entity 14 | @Table(name = "department") 15 | @TypeDef(name = "jsonb-users", typeClass = Department.UserCollectionJsonType.class, defaultForType = User.class) 16 | public class Department { 17 | public static class UserCollectionJsonType extends AbstractJsonDataType { 18 | @Override 19 | public Class returnedClass() { 20 | return User.class; 21 | } 22 | 23 | @Override 24 | public boolean isCollectionType() { 25 | return true; 26 | } 27 | } 28 | 29 | @Id 30 | @GeneratedValue(strategy = GenerationType.AUTO) 31 | private UUID id; 32 | 33 | @Type(type = "jsonb-users") 34 | @Column(name = "users") 35 | @Basic(fetch = FetchType.LAZY) 36 | private List users; 37 | 38 | public UUID getId() { 39 | return id; 40 | } 41 | 42 | public List getUsers() { 43 | return users; 44 | } 45 | 46 | public void setUsers(List users) { 47 | this.users = users; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/department/DepartmentRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.department; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface DepartmentRepository extends JpaRepository { 10 | } 11 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/department/User.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.department; 2 | 3 | public class User { 4 | private String userName; 5 | 6 | private byte[] password; 7 | 8 | private String firstName; 9 | 10 | private String lastName; 11 | 12 | private String telephone; 13 | 14 | private String email; 15 | 16 | private Address address; 17 | 18 | public String getUserName() { 19 | return userName; 20 | } 21 | 22 | public void setUserName(String userName) { 23 | this.userName = userName; 24 | } 25 | 26 | public byte[] getPassword() { 27 | return password; 28 | } 29 | 30 | public void setPassword(byte[] password) { 31 | this.password = password; 32 | } 33 | 34 | public String getFirstName() { 35 | return firstName; 36 | } 37 | 38 | public void setFirstName(String firstName) { 39 | this.firstName = firstName; 40 | } 41 | 42 | public String getLastName() { 43 | return lastName; 44 | } 45 | 46 | public void setLastName(String lastName) { 47 | this.lastName = lastName; 48 | } 49 | 50 | public String getTelephone() { 51 | return telephone; 52 | } 53 | 54 | public void setTelephone(String telephone) { 55 | this.telephone = telephone; 56 | } 57 | 58 | public String getEmail() { 59 | return email; 60 | } 61 | 62 | public void setEmail(String email) { 63 | this.email = email; 64 | } 65 | 66 | public Address getAddress() { 67 | return address; 68 | } 69 | 70 | public void setAddress(Address address) { 71 | this.address = address; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/AccountJournal.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import javax.persistence.DiscriminatorValue; 4 | import javax.persistence.Entity; 5 | 6 | import org.hibernate.annotations.TypeDef; 7 | 8 | @Entity 9 | @DiscriminatorValue("ACCOUNT") 10 | @TypeDef(name = "jsonb", typeClass = Account.JsonType.class, defaultForType = Account.class) 11 | public class AccountJournal extends Journal { 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/AccountJournalRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | @Repository 13 | public interface AccountJournalRepository extends JpaRepository { 14 | @Query(value = "SELECT * FROM journal WHERE event_type='ACCOUNT'" 15 | + " AND (payload ->> 'balance')::::decimal BETWEEN :lowerBound AND :upperBound", 16 | nativeQuery = true) 17 | List findWithBalanceBetween( 18 | @Param("lowerBound") BigDecimal lowerBound, @Param("upperBound") BigDecimal upperBound); 19 | } 20 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 8 | import org.springframework.data.jpa.repository.Modifying; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.stereotype.Repository; 11 | 12 | @Repository 13 | public interface AccountRepository extends JpaRepository, JpaSpecificationExecutor { 14 | Account findByName(String name); 15 | 16 | @Query(value = "select balance from Account where id=?1") 17 | BigDecimal getBalance(UUID id); 18 | 19 | @Modifying 20 | @Query("update Account set balance = balance + ?2 where id=?1") 21 | void updateBalance(UUID id, BigDecimal balance); 22 | } 23 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/Journal.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import javax.persistence.*; 6 | 7 | import org.hibernate.annotations.Type; 8 | 9 | @Entity 10 | @Table(name = "journal") 11 | @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 12 | @DiscriminatorColumn( 13 | name = "event_type", 14 | discriminatorType = DiscriminatorType.STRING, 15 | length = 15 16 | ) 17 | public abstract class Journal { 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) // Computed column 20 | private String id; 21 | 22 | @Column(name = "tag", updatable = false) 23 | private String tag; 24 | 25 | @Basic 26 | @Column(name = "updated", updatable = false) 27 | private LocalDateTime updated; 28 | 29 | @Type(type = "jsonb") 30 | @Column(name = "payload", updatable = false) 31 | @Basic(fetch = FetchType.LAZY) 32 | private T event; 33 | 34 | @PrePersist 35 | protected void onCreate() { 36 | if (updated == null) { 37 | updated = LocalDateTime.now(); 38 | } 39 | } 40 | 41 | public String getId() { 42 | return id; 43 | } 44 | 45 | public T getEvent() { 46 | return event; 47 | } 48 | 49 | public void setEvent(T payload) { 50 | this.event = payload; 51 | } 52 | 53 | public String getTag() { 54 | return tag; 55 | } 56 | 57 | public void setTag(String origin) { 58 | this.tag = origin; 59 | } 60 | 61 | public LocalDateTime getUpdated() { 62 | return updated; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/TransactionJournal.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import javax.persistence.DiscriminatorValue; 4 | import javax.persistence.Entity; 5 | 6 | import org.hibernate.annotations.TypeDef; 7 | 8 | @Entity 9 | @DiscriminatorValue("TRANSACTION") 10 | @TypeDef(name = "jsonb", typeClass = Transaction.JsonType.class, defaultForType = Transaction.class) 11 | public class TransactionJournal extends Journal { 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/TransactionJournalRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | @Repository 13 | public interface TransactionJournalRepository extends JpaRepository { 14 | @Query(value = "SELECT j FROM Journal j WHERE j.tag=:tag") 15 | List findByTag(@Param("tag") String tag); 16 | 17 | @Query(value = "SELECT * FROM journal WHERE event_type='TRANSACTION'" 18 | + " AND payload ->> 'transferDate' BETWEEN :startDate AND :endDate", 19 | nativeQuery = true) 20 | List findBetweenTransferDates(@Param("startDate") String startDate, 21 | @Param("endDate") String endDate); 22 | 23 | @Query(value = 24 | "WITH x AS(SELECT payload from journal where event_type='TRANSACTION' AND tag=:tag)," 25 | + "items AS(SELECT json_array_elements(payload->'items') as y FROM x) " 26 | + "SELECT sum((y->>'amount')::::decimal) FROM items", 27 | nativeQuery = true) 28 | BigDecimal sumTransactionLegAmounts(@Param("tag") String tag); 29 | } 30 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/journal/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface TransactionRepository extends JpaRepository, JpaSpecificationExecutor { 11 | } 12 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/support/LocalDateDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.support; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDate; 5 | 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.databind.DeserializationContext; 8 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 9 | 10 | public class LocalDateDeserializer extends StdDeserializer { 11 | private static final long serialVersionUID = 1L; 12 | 13 | protected LocalDateDeserializer() { 14 | super(LocalDate.class); 15 | } 16 | 17 | 18 | @Override 19 | public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) 20 | throws IOException { 21 | return LocalDate.parse(jp.readValueAs(String.class)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /roach-data-json/src/main/java/io/roach/data/json/support/LocalDateSerializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.support; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDate; 5 | import java.time.format.DateTimeFormatter; 6 | 7 | import com.fasterxml.jackson.core.JsonGenerator; 8 | import com.fasterxml.jackson.databind.SerializerProvider; 9 | import com.fasterxml.jackson.databind.ser.std.StdSerializer; 10 | 11 | public class LocalDateSerializer extends StdSerializer { 12 | private static final long serialVersionUID = 1L; 13 | 14 | public LocalDateSerializer() { 15 | super(LocalDate.class); 16 | } 17 | 18 | @Override 19 | public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider sp) throws IOException { 20 | gen.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /roach-data-json/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | output: 8 | ansi: 9 | enabled: ALWAYS 10 | 11 | liquibase: 12 | change-log: classpath:db/changelog-master.xml 13 | default-schema: 14 | drop-first: false 15 | enabled: true 16 | contexts: crdb 17 | 18 | datasource: 19 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 20 | driver-class-name: org.postgresql.Driver 21 | username: root 22 | password: 23 | hikari: 24 | connection-test-query: SELECT 1 25 | auto-commit: true 26 | 27 | jpa: 28 | open-in-view: false 29 | properties: 30 | hibernate: 31 | dialect: org.hibernate.dialect.CockroachDB201Dialect 32 | connection: 33 | provider_disables_autocommit: false 34 | 35 | server: 36 | port: 9090 37 | -------------------------------------------------------------------------------- /roach-data-json/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB on Spring ${application.formatted-version} - Exploring JSONB and inverted indexes 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-json/src/main/resources/db/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ANY 10 | 11 | 12 | -------------------------------------------------------------------------------- /roach-data-json/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | -- drop table if exists databasechangelog cascade; 2 | -- drop table if exists databasechangeloglock cascade; 3 | -- drop table if exists account cascade; 4 | -- drop table if exists journal cascade; 5 | -- drop table if exists transaction cascade; 6 | -- drop table if exists transaction_item cascade; 7 | 8 | -- Stores immutable events in json format, mapped to entity types via JPA's single table inheritance model 9 | create table journal 10 | ( 11 | id STRING PRIMARY KEY AS (payload->>'id') STORED, -- computed primary index column 12 | event_type varchar(15) not null, -- entity type discriminator 13 | payload json, 14 | tag varchar(64), 15 | updated timestamptz default clock_timestamp(), 16 | INVERTED INDEX event_payload (payload) 17 | ); 18 | 19 | -- Event type always used 20 | create index idx_journal_main on journal (event_type, tag); 21 | 22 | create table account 23 | ( 24 | id uuid not null default gen_random_uuid(), 25 | account_type varchar(10) not null, 26 | balance numeric(19, 2) not null, 27 | name varchar(64), 28 | updated timestamptz default clock_timestamp(), 29 | primary key (id) 30 | ); 31 | 32 | create unique index uidx_account_name on account (name); 33 | 34 | create table transaction 35 | ( 36 | id uuid not null default gen_random_uuid(), 37 | booking_date date not null, 38 | transfer_date date not null, 39 | primary key (id) 40 | ); 41 | 42 | create table transaction_item 43 | ( 44 | id uuid not null default gen_random_uuid(), 45 | amount numeric(19, 2) not null, 46 | running_balance numeric(19, 2) not null, 47 | note varchar(128), 48 | account_id uuid not null, 49 | transaction_id uuid not null, 50 | primary key (id) 51 | ); 52 | 53 | alter table if exists transaction_item 54 | add constraint fk_id_ref_account 55 | foreign key (account_id) 56 | references account; 57 | 58 | alter table if exists transaction_item 59 | add constraint fk_id_ref_transaction 60 | foreign key (transaction_id) 61 | references transaction; 62 | 63 | create table department 64 | ( 65 | id uuid not null default gen_random_uuid(), 66 | users jsonb 67 | ); 68 | 69 | create table chat_history 70 | ( 71 | id uuid not null default gen_random_uuid(), 72 | parent_id uuid, 73 | messages jsonb, 74 | 75 | INVERTED INDEX message_payload (messages) 76 | ); 77 | 78 | INSERT INTO chat_history (messages) VALUES 79 | ('[ 80 | {"title": "Sleeping Beauties", "genres": ["Fiction", "Thriller", "Horror"], "published": false}, 81 | {"title": "The Dictator''s Handbook", "genres": ["Law", "Politics"], "authors": ["Bruce Bueno de Mesquita", "Alastair Smith"], "published": true} 82 | ]'); 83 | -------------------------------------------------------------------------------- /roach-data-json/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/AbstractIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json; 2 | 3 | import org.junit.jupiter.api.MethodOrderer; 4 | import org.junit.jupiter.api.TestInstance; 5 | import org.junit.jupiter.api.TestMethodOrder; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | 11 | @SpringBootTest 12 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 13 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 14 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 15 | public abstract class AbstractIntegrationTest { 16 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 17 | } 18 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/JacksonConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.databind.DeserializationFeature; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import com.fasterxml.jackson.databind.SerializationFeature; 10 | 11 | @Configuration 12 | public class JacksonConfig { 13 | @Bean 14 | public ObjectMapper objectMapper() { 15 | return new ObjectMapper() 16 | .enable(SerializationFeature.INDENT_OUTPUT) 17 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 18 | .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) 19 | .setSerializationInclusion(JsonInclude.Include.NON_NULL); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/chat/ChatHistoryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.chat; 2 | 3 | import java.io.IOException; 4 | import java.io.StringWriter; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import org.junit.jupiter.api.Order; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.annotation.Commit; 13 | import org.springframework.transaction.annotation.Propagation; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import com.fasterxml.jackson.databind.JsonNode; 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | 19 | import io.roach.data.json.AbstractIntegrationTest; 20 | 21 | public class ChatHistoryRepositoryTest extends AbstractIntegrationTest { 22 | @Autowired 23 | private ChatHistoryRepository chatHistoryRepository; 24 | 25 | @Autowired 26 | private ObjectMapper objectMapper; 27 | 28 | @Test 29 | @Transactional(propagation = Propagation.REQUIRES_NEW) 30 | @Commit 31 | @Order(1) 32 | public void whenCreatingChatHistory_thenSuccess() throws Exception { 33 | String data1 = "{\"title\": \"Sleeping Beauties\", \"genres\": [\"Fiction\", \"Thriller\", \"Horror\"], \"published\": false}"; 34 | String data2 = "{\"title\": \"The Dictator''s Handbook\", \"genres\": [\"Law\", \"Politics\"], \"authors\": [\"Bruce Bueno de Mesquita\", \"Alastair Smith\"], \"published\": true}"; 35 | String data3 = "{\"review\": \"A good book\", \"visitor\": \"alice\"}"; 36 | String data4 = "{\"review\": \"A really good book\", \"visitor\": \"bob\"}"; 37 | 38 | ChatHistory h1 = new ChatHistory(); 39 | h1.setMessages(Arrays.asList(objectMapper.readTree(data1), objectMapper.readTree(data2))); 40 | 41 | ChatHistory h2 = new ChatHistory(); 42 | h2.setParent(h1); 43 | h2.setMessages(Collections.singletonList(objectMapper.readTree(data3))); 44 | 45 | ChatHistory h3 = new ChatHistory(); 46 | h3.setParent(h1); 47 | h3.setMessages(Collections.singletonList(objectMapper.readTree(data4))); 48 | 49 | chatHistoryRepository.saveAll(Arrays.asList(h1, h2, h3)); 50 | } 51 | 52 | @Test 53 | @Transactional(propagation = Propagation.REQUIRES_NEW) 54 | @Commit 55 | @Order(2) 56 | public void whenRetrievingById_thenReturnStuff() { 57 | List chatHistory = chatHistoryRepository.findAll(); 58 | chatHistory.forEach(h -> { 59 | List result = h.getMessages(); 60 | result.forEach(jsonNode -> { 61 | StringWriter w = new StringWriter(); 62 | try { 63 | objectMapper.writeValue(w, jsonNode); 64 | System.out.println(w); 65 | } catch (IOException e) { 66 | e.printStackTrace(); 67 | } 68 | }); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/department/DepartmentRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.department; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.MessageDigest; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.security.SecureRandom; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Locale; 10 | import java.util.Objects; 11 | import java.util.UUID; 12 | 13 | import org.junit.jupiter.api.Order; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.test.annotation.Commit; 17 | import org.springframework.transaction.annotation.Propagation; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | import io.roach.data.json.AbstractIntegrationTest; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertNotNull; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | 25 | public class DepartmentRepositoryTest extends AbstractIntegrationTest { 26 | @Autowired 27 | private DepartmentRepository departmentRepository; 28 | 29 | private UUID id; 30 | 31 | public static byte[] hashSecret(String password) { 32 | try { 33 | byte[] salt = new byte[16]; 34 | SecureRandom random = new SecureRandom(); 35 | random.nextBytes(salt); 36 | MessageDigest md = MessageDigest.getInstance("SHA-512"); 37 | md.update(salt); 38 | return md.digest(password.getBytes(StandardCharsets.UTF_8)); 39 | } catch (NoSuchAlgorithmException e) { 40 | throw new IllegalStateException(e); 41 | } 42 | } 43 | 44 | @Test 45 | @Transactional(propagation = Propagation.REQUIRES_NEW) 46 | @Commit 47 | @Order(1) 48 | public void whenCreatingDepartmentUser_thenSuccess() { 49 | User customer = new User(); 50 | customer.setUserName("alice"); 51 | customer.setPassword(hashSecret("password")); 52 | customer.setFirstName("Alice"); 53 | customer.setLastName("Alison"); 54 | customer.setTelephone("555-5555"); 55 | customer.setEmail("alice@roaches.io"); 56 | 57 | Address address1 = new Address.Builder() 58 | .setAddress1("street1") 59 | .setAddress2("street2") 60 | .setCity("street2") 61 | .setCountry(Locale.US.getCountry()) 62 | .setPostcode("street2").build(); 63 | customer.setAddress(address1); 64 | 65 | Department department = new Department(); 66 | department.setUsers(Collections.singletonList(customer)); 67 | department = departmentRepository.save(department); 68 | 69 | assertNotNull(department); 70 | assertNotNull(department.getId()); 71 | 72 | id = department.getId(); 73 | } 74 | 75 | @Test 76 | @Transactional(propagation = Propagation.REQUIRES_NEW) 77 | @Commit 78 | @Order(2) 79 | public void whenRetrievingById_thenReturnAlice() { 80 | Department department = departmentRepository.getOne(id); 81 | List result = department.getUsers(); 82 | assertTrue(result.size() > 0); 83 | assertTrue(result.stream() 84 | .map(User::getUserName) 85 | .anyMatch(name -> Objects.equals("alice", name))); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/journal/AccountJournalTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | import java.sql.Connection; 5 | import java.util.List; 6 | 7 | import org.junit.jupiter.api.Order; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.annotation.Commit; 11 | import org.springframework.test.annotation.Repeat; 12 | import org.springframework.transaction.annotation.Propagation; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import io.roach.data.json.AbstractIntegrationTest; 16 | 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | public class AccountJournalTest extends AbstractIntegrationTest { 20 | @Autowired 21 | private AccountJournalRepository repository; 22 | 23 | @Test 24 | // @Transactional(propagation = Propagation.REQUIRES_NEW) 25 | // @Commit 26 | @Order(1) 27 | public void whenCreatingAccountEventInJournal_thenComputedKeyIsReturnedFromPayload() { 28 | Account account1 = Account.builder() 29 | .withGeneratedId() 30 | .withAccountType("asset") 31 | .withName("abc") 32 | .withBalance(BigDecimal.valueOf(250.00)) 33 | .build(); 34 | Account account2 = Account.builder() 35 | .withGeneratedId() 36 | .withAccountType("expense") 37 | .withName("def") 38 | .withBalance(BigDecimal.valueOf(500.00)) 39 | .build(); 40 | 41 | AccountJournal journal1 = new AccountJournal(); 42 | journal1.setTag("asset"); 43 | journal1.setEvent(account1); 44 | journal1 = repository.save(journal1); 45 | 46 | assertNotNull(journal1); 47 | assertEquals(account1.getId().toString(), journal1.getId()); 48 | 49 | AccountJournal journal2 = new AccountJournal(); 50 | journal2.setTag("expense"); 51 | journal2.setEvent(account2); 52 | journal2 = repository.save(journal2); 53 | 54 | assertNotNull(journal2); 55 | assertEquals(account2.getId().toString(), journal2.getId()); 56 | } 57 | 58 | @Test 59 | @Transactional(propagation = Propagation.REQUIRES_NEW) 60 | @Commit 61 | @Order(2) 62 | public void whenFindBalanceBetween_thenAtLeastOneJournalIsReturned() { 63 | List result = repository.findWithBalanceBetween( 64 | BigDecimal.valueOf(250.00), BigDecimal.valueOf(500.00)); 65 | assertTrue(result.size() > 0); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/journal/AccountRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.MethodOrderer; 7 | import org.junit.jupiter.api.Order; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.TestMethodOrder; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.test.annotation.Commit; 12 | import org.springframework.transaction.annotation.Propagation; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import io.roach.data.json.AbstractIntegrationTest; 16 | 17 | public class AccountRepositoryTest extends AbstractIntegrationTest { 18 | @Autowired 19 | private AccountRepository accountRepository; 20 | 21 | @Test 22 | @Transactional(propagation = Propagation.REQUIRES_NEW) 23 | @Commit 24 | @Order(3) 25 | public void testCreateAccount() { 26 | Account a = accountRepository.findByName("A"); 27 | if (a != null) { 28 | accountRepository.delete(a); 29 | } 30 | accountRepository.flush(); 31 | 32 | a = Account.builder() 33 | .withGeneratedId() 34 | .withName("A") 35 | .withBalance(BigDecimal.TEN) 36 | .withAccountType("X") 37 | .build(); 38 | accountRepository.save(a); 39 | logger.info("Created {}", a); 40 | } 41 | 42 | @Test 43 | @Transactional(propagation = Propagation.REQUIRES_NEW) 44 | @Commit 45 | @Order(4) 46 | public void testRetrieveAccount() { 47 | Account a = accountRepository.findByName("A"); 48 | Assertions.assertNotNull(a); 49 | Assertions.assertEquals(BigDecimal.TEN.setScale(2), a.getBalance()); 50 | } 51 | 52 | @Test 53 | @Transactional(propagation = Propagation.REQUIRES_NEW) 54 | @Commit 55 | @Order(5) 56 | public void testUpdateAccount() { 57 | Account a = accountRepository.findByName("A"); 58 | Assertions.assertNotNull(a); 59 | a.setBalance(BigDecimal.ZERO); 60 | } 61 | 62 | @Test 63 | @Transactional(propagation = Propagation.REQUIRES_NEW) 64 | @Commit 65 | @Order(6) 66 | public void testVerifyUpdate() { 67 | Account a = accountRepository.findByName("A"); 68 | Assertions.assertEquals(BigDecimal.ZERO.setScale(2), a.getBalance()); 69 | } 70 | 71 | @Test 72 | @Transactional(propagation = Propagation.REQUIRES_NEW) 73 | @Commit 74 | @Order(1) 75 | public void testDeleteAccount() { 76 | Account a = accountRepository.findByName("A"); 77 | Assertions.assertNotNull(a); 78 | accountRepository.delete(a); 79 | } 80 | 81 | @Test 82 | @Transactional(propagation = Propagation.REQUIRES_NEW) 83 | @Commit 84 | @Order(2) 85 | public void testVerifyDeleted() { 86 | Account a = accountRepository.findByName("A"); 87 | Assertions.assertNull(a); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/journal/ObjectMapperFormatTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDate; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import com.fasterxml.jackson.annotation.JsonInclude; 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.DeserializationFeature; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.databind.SerializationFeature; 13 | 14 | public class ObjectMapperFormatTest { 15 | @Test 16 | public void testSerialisation() { 17 | Transaction transaction = Transaction.builder() 18 | .withGeneratedId() 19 | .withBookingDate(LocalDate.now().minusDays(2)) 20 | .withTransferDate(LocalDate.now()) 21 | .andItem() 22 | .withAccount(Account.builder() 23 | .withGeneratedId() 24 | .build()) 25 | .withAmount(BigDecimal.valueOf(-50.00)) 26 | .withNote("debit A") 27 | .then() 28 | .andItem() 29 | .withAccount(Account.builder() 30 | .withGeneratedId() 31 | .build()) 32 | .withAmount(BigDecimal.valueOf(50.00)) 33 | .withNote("credit A") 34 | .then() 35 | .build(); 36 | 37 | ObjectMapper mapper = new ObjectMapper() 38 | .enable(SerializationFeature.INDENT_OUTPUT) 39 | .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 40 | .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) 41 | .setSerializationInclusion(JsonInclude.Include.NON_NULL); 42 | try { 43 | String s = mapper.writeValueAsString(transaction); 44 | System.out.println(s); 45 | } catch (JsonProcessingException e) { 46 | e.printStackTrace(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /roach-data-json/src/test/java/io/roach/data/jpa/journal/TransactionRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.json.journal; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDate; 5 | 6 | import org.junit.jupiter.api.Order; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.test.annotation.Commit; 10 | import org.springframework.transaction.annotation.Propagation; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import io.roach.data.json.AbstractIntegrationTest; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertNotNull; 16 | 17 | public class TransactionRepositoryTest extends AbstractIntegrationTest { 18 | @Autowired 19 | private AccountRepository accountRepository; 20 | 21 | @Autowired 22 | private TransactionRepository transactionRepository; 23 | 24 | @Test 25 | @Transactional(propagation = Propagation.REQUIRES_NEW) 26 | @Commit 27 | @Order(1) 28 | public void cleanUp() { 29 | transactionRepository.deleteAll(); 30 | accountRepository.deleteAll(); 31 | } 32 | 33 | @Test 34 | @Transactional(propagation = Propagation.REQUIRES_NEW) 35 | @Commit 36 | @Order(2) 37 | public void whenCreatingTransaction_thenSuccess() { 38 | Account a = Account.builder() 39 | .withGeneratedId() 40 | .withName("A") 41 | .withBalance(BigDecimal.valueOf(50.00)) 42 | .withAccountType("X") 43 | .build(); 44 | a = accountRepository.save(a); 45 | 46 | Account b = Account.builder() 47 | .withGeneratedId() 48 | .withName("B") 49 | .withBalance(BigDecimal.valueOf(0.00)) 50 | .withAccountType("X") 51 | .build(); 52 | b = accountRepository.save(b); 53 | 54 | Transaction transaction = Transaction.builder() 55 | .withGeneratedId() 56 | .withBookingDate(LocalDate.now().minusDays(1)) 57 | .withTransferDate(LocalDate.now()) 58 | .andItem() 59 | .withAccount(a) 60 | .withAmount(BigDecimal.valueOf(-50.00)) 61 | .withRunningBalance(BigDecimal.valueOf(-0.00)) 62 | .withNote("debit A") 63 | .then() 64 | .andItem() 65 | .withAccount(b) 66 | .withAmount(BigDecimal.valueOf(50.00)) 67 | .withRunningBalance(BigDecimal.valueOf(-0.00)) 68 | .withNote("credit A") 69 | .then() 70 | .build(); 71 | 72 | transaction = transactionRepository.save(transaction); 73 | 74 | assertNotNull(transaction); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /roach-data-json/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | output: 8 | ansi: 9 | enabled: ALWAYS 10 | 11 | liquibase: 12 | change-log: classpath:db/changelog-master.xml 13 | default-schema: 14 | drop-first: false 15 | enabled: true 16 | 17 | datasource: 18 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 19 | driver-class-name: org.postgresql.Driver 20 | username: root 21 | password: 22 | hikari: 23 | connection-test-query: SELECT 1 24 | 25 | jpa: 26 | open-in-view: false 27 | properties: 28 | hibernate: 29 | dialect: org.hibernate.dialect.CockroachDB201Dialect 30 | -------------------------------------------------------------------------------- /roach-data-mybatis/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: MyBatis 2 | 3 | A CockroachDB Spring Boot Demo using [MyBatis](https://mybatis.org/mybatis-3/) 4 | integrated with [Spring Data JDBC](https://docs.spring.io/spring-data/jdbc/docs/1.1.6.RELEASE/reference/html/#jdbc.mybatis) 5 | for data access. 6 | -------------------------------------------------------------------------------- /roach-data-mybatis/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-2x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-2x-parent 10 | 11 | 12 | roach-data-mybatis 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-web 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-jetty 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-hateoas 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-aop 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jdbc 34 | 35 | 36 | org.mybatis.spring.boot 37 | mybatis-spring-boot-starter 38 | 2.1.0 39 | 40 | 41 | org.springframework.data 42 | spring-data-commons 43 | 44 | 45 | org.liquibase 46 | liquibase-core 47 | runtime 48 | 49 | 50 | org.postgresql 51 | postgresql 52 | provided 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | provided 58 | 59 | 60 | 61 | 62 | roach-data-mybatis 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-maven-plugin 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.annotation.Id; 6 | 7 | import lombok.Data; 8 | 9 | @Data 10 | public class Account { 11 | @Id 12 | private Long id; 13 | 14 | private String name; 15 | 16 | private AccountType type; 17 | 18 | private BigDecimal balance; 19 | 20 | public Long getId() { 21 | return id; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public AccountType getType() { 29 | return type; 30 | } 31 | 32 | public BigDecimal getBalance() { 33 | return balance; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/AccountModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.hateoas.RepresentationModel; 6 | import org.springframework.hateoas.server.core.Relation; 7 | 8 | @Relation(value = "account", collectionRelation = "accounts") 9 | public class AccountModel extends RepresentationModel { 10 | private String name; 11 | 12 | private AccountType type; 13 | 14 | private BigDecimal balance; 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public AccountType getType() { 25 | return type; 26 | } 27 | 28 | public void setType(AccountType type) { 29 | this.type = type; 30 | } 31 | 32 | public BigDecimal getBalance() { 33 | return balance; 34 | } 35 | 36 | public void setBalance(BigDecimal balance) { 37 | this.balance = balance; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.apache.ibatis.annotations.Param; 6 | import org.springframework.data.jdbc.repository.query.Modifying; 7 | import org.springframework.data.jdbc.repository.query.Query; 8 | import org.springframework.data.repository.CrudRepository; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 13 | 14 | @Repository 15 | @Transactional(propagation = MANDATORY) 16 | interface AccountRepository extends CrudRepository, PagedAccountRepository { 17 | @Query("SELECT balance FROM account WHERE id = :id FOR UPDATE") 18 | BigDecimal getBalance(@Param("id") Long id); 19 | 20 | @Query("UPDATE account SET balance = balance + :balance WHERE id=:id") 21 | @Modifying 22 | void updateBalance(@Param("id") Long id, @Param("balance") BigDecimal balance); 23 | } 24 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | public enum AccountType { 4 | asset, 5 | expense 6 | } 7 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/IndexModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import org.springframework.hateoas.RepresentationModel; 4 | 5 | public class IndexModel extends RepresentationModel { 6 | private String message; 7 | 8 | public IndexModel(String message) { 9 | this.message = message; 10 | } 11 | 12 | public String getMessage() { 13 | return message; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import org.springframework.dao.DataIntegrityViolationException; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Negative balance") 8 | public class NegativeBalanceException extends DataIntegrityViolationException { 9 | public NegativeBalanceException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/PagedAccountMapper.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import java.util.List; 4 | 5 | import org.apache.ibatis.annotations.Mapper; 6 | import org.apache.ibatis.annotations.Param; 7 | import org.apache.ibatis.annotations.Select; 8 | import org.springframework.data.domain.Pageable; 9 | 10 | @Mapper 11 | public interface PagedAccountMapper { 12 | @Select("SELECT * FROM account LIMIT #{pageable.pageSize} OFFSET #{pageable.offset}") 13 | List findAll(@Param("pageable") Pageable pageable); 14 | 15 | @Select("SELECT count(id) from account") 16 | long countAll(); 17 | } 18 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/PagedAccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | 6 | public interface PagedAccountRepository { 7 | Page findAll(Pageable pageable); 8 | } 9 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/java/io/roach/data/mybatis/PagedAccountRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.mybatis; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageImpl; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 13 | 14 | @Repository 15 | @Transactional(propagation = MANDATORY) 16 | public class PagedAccountRepositoryImpl implements PagedAccountRepository { 17 | @Autowired 18 | private PagedAccountMapper pagedAccountMapper; 19 | 20 | @Override 21 | public Page findAll(Pageable pageable) { 22 | List accounts = pagedAccountMapper.findAll(pageable); 23 | long totalRecords = pagedAccountMapper.countAll(); 24 | return new PageImpl<>(accounts, pageable, totalRecords); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | output: 8 | ansi: 9 | enabled: ALWAYS 10 | 11 | liquibase: 12 | change-log: classpath:db/changelog-master.xml 13 | default-schema: 14 | drop-first: false 15 | contexts: crdb 16 | enabled: true 17 | 18 | datasource: 19 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 20 | driver-class-name: org.postgresql.Driver 21 | username: root 22 | password: 23 | hikari: 24 | connection-test-query: SELECT 1 25 | 26 | jpa: 27 | open-in-view: false 28 | 29 | server: 30 | port: 9090 31 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB on Spring Data / MyBatis ${application.formatted-version} 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/resources/db/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | ANY 12 | 13 | 14 | 15 | 16 | 17 | 1 18 | Alice 19 | 20 | asset 21 | 22 | 23 | 2 24 | Bob 25 | 26 | expense 27 | 28 | 29 | 3 30 | Bobby Tables 31 | 32 | asset 33 | 34 | 35 | 4 36 | Doris 37 | 38 | expense 39 | 40 | 41 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE IF EXISTS account cascade; 2 | -- DROP TABLE IF EXISTS databasechangelog cascade; 3 | -- DROP TABLE IF EXISTS databasechangeloglock cascade; 4 | 5 | create table account 6 | ( 7 | id int not null primary key default unique_rowid(), 8 | balance numeric(19, 2) not null, 9 | name varchar(128) not null, 10 | type varchar(25) not null 11 | ); 12 | 13 | -- insert into account (id,balance,name,type) values 14 | -- (1, 500.00,'Alice','asset'), 15 | -- (2, 500.00,'Bob','expense'), 16 | -- (3, 500.00,'Bobby Tables','asset'), 17 | -- (4, 500.00,'Doris','expense'); 18 | -------------------------------------------------------------------------------- /roach-data-mybatis/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /roach-data-parallel/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-2x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-2x-parent 10 | 11 | 12 | roach-data-parallel 13 | 14 | 15 | true 16 | 17 | 18 | 19 | 20 | 21 | org.slf4j 22 | slf4j-api 23 | 24 | 25 | ch.qos.logback 26 | logback-core 27 | 28 | 29 | ch.qos.logback 30 | logback-classic 31 | 32 | 33 | 34 | com.zaxxer 35 | HikariCP 36 | 37 | 38 | org.postgresql 39 | postgresql 40 | 41 | 42 | net.ttddyy 43 | datasource-proxy 44 | 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter-engine 49 | test 50 | 51 | 52 | org.junit.jupiter 53 | junit-jupiter-params 54 | test 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /roach-data-parallel/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: Reactive 2x 2 | 3 | A CockroachDB Spring Boot 2.x Demo using [Spring Data R2DBC](https://spring.io/projects/spring-data-r2dbc) 4 | for data access combined with WebFlux. 5 | 6 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | 7 | io.roach.data 8 | roach-data-2x-parent 9 | 1.0.0-SNAPSHOT 10 | ../roach-data-2x-parent 11 | 12 | 13 | roach-data-reactive-2x 14 | jar 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-webflux 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-jetty 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-hateoas 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-aop 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-r2dbc 44 | 45 | 46 | io.r2dbc 47 | r2dbc-postgresql 48 | 0.8.13.RELEASE 49 | 50 | 51 | 52 | 53 | roach-data-reactive 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-maven-plugin 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/java/io/roach/data/reactive/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.relational.core.mapping.Table; 7 | 8 | @Table("account") 9 | public class Account { 10 | @Id 11 | private Long id; 12 | 13 | private String name; 14 | 15 | private AccountType type; 16 | 17 | private BigDecimal balance; 18 | 19 | public Long getId() { 20 | return id; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public AccountType getType() { 28 | return type; 29 | } 30 | 31 | public BigDecimal getBalance() { 32 | return balance; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/java/io/roach/data/reactive/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.r2dbc.repository.Modifying; 7 | import org.springframework.data.r2dbc.repository.Query; 8 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 16 | 17 | @Repository 18 | @Transactional(propagation = MANDATORY) 19 | public interface AccountRepository extends ReactiveCrudRepository { 20 | Flux findAllBy(Pageable pageable); 21 | 22 | @Query(value = "select balance from Account where id=:id") 23 | Mono getBalance(Long id); 24 | 25 | @Modifying 26 | @Query("update Account set balance = balance + :balance where id=:id") 27 | Mono updateBalance(Long id, BigDecimal balance); 28 | } 29 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/java/io/roach/data/reactive/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | public enum AccountType { 4 | asset, 5 | expense 6 | } 7 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/java/io/roach/data/reactive/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | import org.springframework.dao.DataIntegrityViolationException; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Negative balance") 8 | public class NegativeBalanceException extends DataIntegrityViolationException { 9 | public NegativeBalanceException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | spring: 6 | output: 7 | ansi: 8 | enabled: ALWAYS 9 | r2dbc: 10 | url: r2dbc:postgresql://kai-odin-11257.8nj.cockroachlabs.cloud:26257/kai-odin-11257.defaultdb?sslmode=verify-full 11 | username: guest 12 | password: 13 | pool: 14 | initial-size: 1 15 | max-size: 10 16 | max-idle-time: 30m 17 | server: 18 | port: 9090 -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ___ 2 | /() \ CockroachDB on Spring Data / Reactive ${application.formatted-version} 3 | _|_____|_ powered by Spring Boot ${spring-boot.formatted-version} 4 | | | === | | 5 | |_| 0 |_| 6 | || 0 || 7 | ||__*__|| 8 | |~ \___/ ~| 9 | [_]_[_]_[_] 10 | -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | drop table if exists account; 2 | 3 | create table if not exists account 4 | ( 5 | id int not null primary key default unique_rowid(), 6 | balance numeric(19, 2) not null, 7 | name varchar(128) not null, 8 | type varchar(25) not null default 'expense' 9 | ); 10 | 11 | insert into account (id, balance, name) 12 | select id::float8, 13 | 5000.00::decimal, 14 | md5(random()::text) 15 | from generate_series(1, 10000) as id; -------------------------------------------------------------------------------- /roach-data-reactive-2x/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /roach-data-reactive/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: Reactive 2 | 3 | A CockroachDB Spring Boot 3.x Demo using [Spring Data R2DBC](https://spring.io/projects/spring-data-r2dbc) 4 | for data access combined with WebFlux. 5 | 6 | -------------------------------------------------------------------------------- /roach-data-reactive/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | 7 | io.roach.data 8 | roach-data-3x-parent 9 | 1.0.0-SNAPSHOT 10 | ../roach-data-3x-parent 11 | 12 | 13 | roach-data-reactive 14 | jar 15 | 16 | 17 | true 18 | 5.0.0 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-webflux 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-jetty 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-hateoas 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-aop 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-data-r2dbc 45 | 46 | 47 | org.postgresql 48 | r2dbc-postgresql 49 | 1.0.2.RELEASE 50 | 51 | 52 | 53 | 54 | roach-data-reactive 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-maven-plugin 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /roach-data-reactive/src/main/java/io/roach/data/reactive/Account.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.relational.core.mapping.Table; 7 | 8 | @Table("account") 9 | public class Account { 10 | @Id 11 | private Long id; 12 | 13 | private String name; 14 | 15 | private AccountType type; 16 | 17 | private BigDecimal balance; 18 | 19 | public Long getId() { 20 | return id; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public AccountType getType() { 28 | return type; 29 | } 30 | 31 | public BigDecimal getBalance() { 32 | return balance; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /roach-data-reactive/src/main/java/io/roach/data/reactive/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.r2dbc.repository.Modifying; 7 | import org.springframework.data.r2dbc.repository.Query; 8 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | import static org.springframework.transaction.annotation.Propagation.MANDATORY; 16 | 17 | @Repository 18 | @Transactional(propagation = MANDATORY) 19 | public interface AccountRepository extends ReactiveCrudRepository { 20 | Flux findAllBy(Pageable pageable); 21 | 22 | @Query(value = "select balance from Account where id=:id") 23 | Mono getBalance(Long id); 24 | 25 | @Modifying 26 | @Query("update Account set balance = balance + :balance where id=:id") 27 | Mono updateBalance(Long id, BigDecimal balance); 28 | } 29 | -------------------------------------------------------------------------------- /roach-data-reactive/src/main/java/io/roach/data/reactive/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | public enum AccountType { 4 | asset, 5 | expense 6 | } 7 | -------------------------------------------------------------------------------- /roach-data-reactive/src/main/java/io/roach/data/reactive/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.reactive; 2 | 3 | import org.springframework.dao.DataIntegrityViolationException; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Negative balance") 8 | public class NegativeBalanceException extends DataIntegrityViolationException { 9 | public NegativeBalanceException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /roach-data-reactive/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | spring: 6 | output: 7 | ansi: 8 | enabled: ALWAYS 9 | r2dbc: 10 | url: r2dbc:postgresql://kai-odin-11257.8nj.cockroachlabs.cloud:26257/kai-odin-11257.defaultdb?sslmode=verify-full 11 | username: guest 12 | password: 13 | pool: 14 | initial-size: 1 15 | max-size: 10 16 | max-idle-time: 30m 17 | server: 18 | port: 9090 -------------------------------------------------------------------------------- /roach-data-reactive/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ___ 2 | /() \ CockroachDB on Spring Data / Reactive ${application.formatted-version} 3 | _|_____|_ powered by Spring Boot ${spring-boot.formatted-version} 4 | | | === | | 5 | |_| 0 |_| 6 | || 0 || 7 | ||__*__|| 8 | |~ \___/ ~| 9 | [_]_[_]_[_] 10 | -------------------------------------------------------------------------------- /roach-data-reactive/src/main/resources/db/create.sql: -------------------------------------------------------------------------------- 1 | drop table if exists account; 2 | 3 | create table if not exists account 4 | ( 5 | id int not null primary key default unique_rowid(), 6 | balance numeric(19, 2) not null, 7 | name varchar(128) not null, 8 | type varchar(25) not null default 'expense' 9 | ); 10 | 11 | insert into account (id, balance, name) 12 | select id::float8, 13 | 5000.00::decimal, 14 | md5(random()::text) 15 | from generate_series(1, 10000) as id; -------------------------------------------------------------------------------- /roach-data-reactive/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /roach-data-relational/README.md: -------------------------------------------------------------------------------- 1 | # Roach Demo Data :: JDBC / Relational 2 | 3 | A CockroachDB Spring Boot Demo using [Spring Data JDBC](https://spring.io/projects/spring-data-jdbc) for data access. 4 | 5 | 6 | -------------------------------------------------------------------------------- /roach-data-relational/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.roach.data 7 | roach-data-3x-parent 8 | 1.0.0-SNAPSHOT 9 | ../roach-data-3x-parent 10 | 11 | 12 | roach-data-relational 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-data-jdbc 18 | 19 | 20 | 21 | org.flywaydb 22 | flyway-core 23 | runtime 24 | 25 | 26 | net.ttddyy 27 | datasource-proxy 28 | 1.9 29 | 30 | 31 | org.postgresql 32 | postgresql 33 | compile 34 | 35 | 36 | 37 | 38 | roach-data-jdbc-orders 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-maven-plugin 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/JdbcConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational; 2 | 3 | import com.zaxxer.hikari.HikariDataSource; 4 | import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; 5 | import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Primary; 11 | import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; 12 | import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; 13 | import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; 14 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 15 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 16 | import org.springframework.transaction.PlatformTransactionManager; 17 | import org.springframework.transaction.annotation.EnableTransactionManagement; 18 | 19 | import javax.sql.DataSource; 20 | 21 | @Configuration 22 | @EnableTransactionManagement 23 | @EnableJdbcRepositories(basePackages = {"io.roach"}) 24 | public class JdbcConfiguration extends AbstractJdbcConfiguration { 25 | public static final String SQL_TRACE_LOGGER = "io.roach.SQL_TRACE"; 26 | 27 | @Bean 28 | @Primary 29 | @ConfigurationProperties("spring.datasource") 30 | public DataSourceProperties dataSourceProperties() { 31 | return new DataSourceProperties(); 32 | } 33 | 34 | @Bean 35 | @Primary 36 | public DataSource primaryDataSource() { 37 | return loggingProxy(targetDataSource()); 38 | } 39 | 40 | @Bean 41 | @ConfigurationProperties("spring.datasource.hikari") 42 | public HikariDataSource targetDataSource() { 43 | HikariDataSource ds = dataSourceProperties() 44 | .initializeDataSourceBuilder() 45 | .type(HikariDataSource.class) 46 | .build(); 47 | ds.addDataSourceProperty("reWriteBatchedInserts", true); 48 | ds.addDataSourceProperty("ApplicationName", "jdbc-test"); 49 | return ds; 50 | } 51 | 52 | private DataSource loggingProxy(DataSource dataSource) { 53 | return ProxyDataSourceBuilder 54 | .create(dataSource) 55 | .name("SQL-Trace") 56 | .asJson() 57 | .countQuery() 58 | .logQueryBySlf4j(SLF4JLogLevel.TRACE, SQL_TRACE_LOGGER) 59 | .multiline() 60 | .build(); 61 | } 62 | 63 | @Bean 64 | public NamedParameterJdbcTemplate namedParameterJdbcTemplate() { 65 | return new NamedParameterJdbcTemplate(primaryDataSource()); 66 | } 67 | 68 | @Bean 69 | public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() { 70 | return new PersistenceExceptionTranslationPostProcessor(); 71 | } 72 | 73 | @Bean 74 | public PlatformTransactionManager transactionManager() { 75 | return new DataSourceTransactionManager(primaryDataSource()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/OrderSystemClient.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational; 2 | 3 | import io.roach.data.relational.domain.Customer; 4 | import io.roach.data.relational.domain.Product; 5 | import io.roach.data.relational.service.OrderSystem; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class OrderSystemClient implements CommandLineRunner { 14 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 15 | 16 | @Autowired 17 | private OrderSystem orderSystem; 18 | 19 | @Override 20 | public void run(String... args) { 21 | logger.info("Removing everything..."); 22 | orderSystem.clearAll(); 23 | 24 | logger.info("Creating products..."); 25 | orderSystem.createProductInventory(); 26 | 27 | logger.info("Creating customers..."); 28 | orderSystem.createCustomers(); 29 | 30 | logger.info("Creating orders..."); 31 | orderSystem.createOrders(); 32 | 33 | logger.info("Listing orders..."); 34 | orderSystem.findOrders().forEach(order -> { 35 | Customer c = orderSystem.findCustomerById(order.getCustomerRef().getId()); 36 | logger.info("Order placed by {}", c.getUserName()); 37 | logger.info("\tOrder total price: {}", order.getTotalPrice()); 38 | logger.info("\tOrder items:"); 39 | 40 | order.getOrderItems().forEach(orderItem -> { 41 | Product p = orderSystem.findProductById(orderItem.getProductRef().getId()); 42 | logger.info("\t\t{} price: {} sku: {} qty: {} unit price: {} cost: {}", 43 | p.getName(), 44 | p.getPrice(), 45 | p.getSku(), 46 | orderItem.getQuantity(), 47 | orderItem.getUnitPrice(), 48 | orderItem.totalCost() 49 | ); 50 | }); 51 | }); 52 | 53 | 54 | logger.info("Find by sku: {}", orderSystem.findProductBySku("CRDB-UL-ED1")); 55 | 56 | logger.info("Listing customer orders..."); 57 | orderSystem.findCustomers().forEach(customer -> { 58 | logger.info("Customer {} orders:", customer.getId()); 59 | orderSystem.findOrders(customer).forEach(order -> { 60 | logger.info("\t{}:", order.toString()); 61 | }); 62 | }); 63 | 64 | orderSystem.getTotalOrderCost((iterated, aggregated) -> { 65 | logger.info("Total order cost iterated: {}", iterated); 66 | logger.info("Total order cost aggregated: {}", aggregated); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/RelationalApplication.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational; 2 | 3 | import org.springframework.boot.WebApplicationType; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; 8 | import org.springframework.boot.builder.SpringApplicationBuilder; 9 | import org.springframework.context.annotation.ComponentScan; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | @EnableAutoConfiguration(exclude = { 14 | TransactionAutoConfiguration.class, 15 | DataSourceTransactionManagerAutoConfiguration.class, 16 | DataSourceAutoConfiguration.class 17 | }) 18 | @ComponentScan(basePackages = "io.roach.data.relational") 19 | public class RelationalApplication { 20 | public static void main(String[] args) { 21 | new SpringApplicationBuilder(RelationalApplication.class) 22 | .web(WebApplicationType.NONE) 23 | .run(args); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/domain/AbstractEntity.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | 6 | import org.springframework.data.annotation.Transient; 7 | import org.springframework.data.domain.Persistable; 8 | 9 | public abstract class AbstractEntity implements Persistable { 10 | @Transient 11 | private boolean isNew = true; 12 | 13 | protected void markNotNew() { 14 | this.isNew = false; 15 | } 16 | 17 | @Override 18 | public boolean isNew() { 19 | return isNew; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/domain/Customer.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.relational.core.mapping.Column; 5 | import org.springframework.data.relational.core.mapping.Table; 6 | 7 | import java.util.UUID; 8 | 9 | @Table(name = "customer") 10 | public class Customer extends AbstractEntity { 11 | public static Builder builder() { 12 | return new Builder(); 13 | } 14 | 15 | public static final class Builder { 16 | private String userName; 17 | 18 | private String firstName; 19 | 20 | private String lastName; 21 | 22 | private Builder() { 23 | } 24 | 25 | public Builder withUserName(String userName) { 26 | this.userName = userName; 27 | return this; 28 | } 29 | 30 | public Builder withFirstName(String firstName) { 31 | this.firstName = firstName; 32 | return this; 33 | } 34 | 35 | public Builder withLastName(String lastName) { 36 | this.lastName = lastName; 37 | return this; 38 | } 39 | 40 | public Customer build() { 41 | Customer customer = new Customer(); 42 | customer.userName = this.userName; 43 | customer.firstName = this.firstName; 44 | customer.lastName = this.lastName; 45 | return customer; 46 | } 47 | } 48 | 49 | @Id 50 | private UUID id; 51 | 52 | @Column(value = "user_name") 53 | private String userName; 54 | 55 | @Column(value = "first_name") 56 | private String firstName; 57 | 58 | @Column(value = "last_name") 59 | private String lastName; 60 | 61 | @Override 62 | public UUID getId() { 63 | return id; 64 | } 65 | 66 | public String getUserName() { 67 | return userName; 68 | } 69 | 70 | public String getFirstName() { 71 | return firstName; 72 | } 73 | 74 | public String getLastName() { 75 | return lastName; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/domain/Order.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.jdbc.core.mapping.AggregateReference; 5 | import org.springframework.data.relational.core.mapping.Column; 6 | import org.springframework.data.relational.core.mapping.MappedCollection; 7 | import org.springframework.data.relational.core.mapping.Table; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.*; 11 | 12 | @Table(name = "orders") 13 | public class Order extends AbstractEntity { 14 | public static Builder builder() { 15 | return new Builder(); 16 | } 17 | 18 | public static final class Builder { 19 | private Customer customer; 20 | 21 | private final List orderItems = new ArrayList<>(); 22 | 23 | private Builder() { 24 | } 25 | 26 | public Builder withCustomer(Customer customer) { 27 | this.customer = customer; 28 | return this; 29 | } 30 | 31 | public OrderItem.Builder andOrderItem() { 32 | return new OrderItem.Builder(this, orderItems::add); 33 | } 34 | 35 | public Order build() { 36 | if (this.customer == null) { 37 | throw new IllegalStateException("Missing customer"); 38 | } 39 | if (this.orderItems.isEmpty()) { 40 | throw new IllegalStateException("Empty order"); 41 | } 42 | Order order = new Order(); 43 | order.customer = AggregateReference.to(this.customer.getId()); 44 | order.orderItems.addAll(this.orderItems); 45 | order.totalPrice = order.subTotal(); 46 | return order; 47 | } 48 | } 49 | 50 | @Id 51 | private UUID id; 52 | 53 | @Column(value = "total_price") 54 | private BigDecimal totalPrice; 55 | 56 | @Column(value = "customer_id") 57 | private AggregateReference customer; 58 | 59 | @MappedCollection(idColumn = "order_id", keyColumn = "order_id") 60 | private Set orderItems = new HashSet<>(); 61 | 62 | @Override 63 | public UUID getId() { 64 | return id; 65 | } 66 | 67 | public Order setId(UUID id) { 68 | this.id = id; 69 | return this; 70 | } 71 | 72 | public BigDecimal getTotalPrice() { 73 | return totalPrice; 74 | } 75 | 76 | public AggregateReference getCustomerRef() { 77 | return customer; 78 | } 79 | 80 | public Collection getOrderItems() { 81 | return Collections.unmodifiableSet(orderItems); 82 | } 83 | 84 | public BigDecimal subTotal() { 85 | BigDecimal subTotal = BigDecimal.ZERO; 86 | for (OrderItem oi : orderItems) { 87 | subTotal = subTotal.add(oi.totalCost()); 88 | } 89 | return subTotal; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/domain/OrderItem.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.domain; 2 | 3 | import org.springframework.data.jdbc.core.mapping.AggregateReference; 4 | import org.springframework.data.relational.core.mapping.Column; 5 | import org.springframework.data.relational.core.mapping.Table; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.UUID; 9 | import java.util.function.Consumer; 10 | 11 | @Table("order_item") 12 | public class OrderItem { 13 | public static final class Builder { 14 | private final Order.Builder parentBuilder; 15 | 16 | private final Consumer callback; 17 | 18 | private int quantity; 19 | 20 | private BigDecimal unitPrice; 21 | 22 | private Product product; 23 | 24 | Builder(Order.Builder parentBuilder, Consumer callback) { 25 | this.parentBuilder = parentBuilder; 26 | this.callback = callback; 27 | } 28 | 29 | public Builder withQuantity(int quantity) { 30 | this.quantity = quantity; 31 | return this; 32 | } 33 | 34 | public Builder withUnitPrice(BigDecimal unitPrice) { 35 | this.unitPrice = unitPrice; 36 | return this; 37 | } 38 | 39 | public Builder withProduct(Product product) { 40 | this.product = product; 41 | return this; 42 | } 43 | 44 | public Order.Builder then() { 45 | if (this.unitPrice == null) { 46 | this.unitPrice = product.getPrice(); 47 | } 48 | 49 | OrderItem orderItem = new OrderItem(); 50 | orderItem.unitPrice = this.unitPrice; 51 | orderItem.quantity = this.quantity; 52 | orderItem.product = AggregateReference.to(this.product.getId()); 53 | orderItem.order = AggregateReference.to(this.product.getId()); 54 | 55 | callback.accept(orderItem); 56 | 57 | return parentBuilder; 58 | } 59 | 60 | } 61 | 62 | @Column(value = "product_id") 63 | private AggregateReference product; 64 | 65 | @Column(value = "order_id") 66 | private AggregateReference order; 67 | 68 | @Column 69 | private int quantity; 70 | 71 | @Column(value = "unit_price") 72 | private BigDecimal unitPrice; 73 | 74 | public int getQuantity() { 75 | return quantity; 76 | } 77 | 78 | public BigDecimal getUnitPrice() { 79 | return unitPrice; 80 | } 81 | 82 | public AggregateReference getProductRef() { 83 | return product; 84 | } 85 | 86 | public BigDecimal totalCost() { 87 | if (unitPrice == null) { 88 | throw new IllegalStateException("unitPrice is null"); 89 | } 90 | return unitPrice.multiply(new BigDecimal(quantity)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/domain/Product.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.relational.core.mapping.Column; 5 | import org.springframework.data.relational.core.mapping.Table; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.UUID; 9 | 10 | @Table(name = "product") 11 | public class Product extends AbstractEntity { 12 | public static Builder builder() { 13 | return new Builder(); 14 | } 15 | 16 | public static final class Builder { 17 | private String name; 18 | 19 | private String sku; 20 | 21 | private BigDecimal price; 22 | 23 | private int quantity; 24 | 25 | private Builder() { 26 | } 27 | 28 | public Builder withName(String name) { 29 | this.name = name; 30 | return this; 31 | } 32 | 33 | public Builder withSku(String sku) { 34 | this.sku = sku; 35 | return this; 36 | } 37 | 38 | public Builder withPrice(BigDecimal price) { 39 | this.price = price; 40 | return this; 41 | } 42 | 43 | public Builder withQuantity(int quantity) { 44 | this.quantity = quantity; 45 | return this; 46 | } 47 | 48 | 49 | public Product build() { 50 | Product product = new Product(); 51 | product.name = this.name; 52 | product.sku = this.sku; 53 | product.price = this.price; 54 | product.inventory = this.quantity; 55 | return product; 56 | } 57 | } 58 | 59 | @Id 60 | private UUID id; 61 | 62 | @Column 63 | private String name; 64 | 65 | @Column 66 | private String sku; 67 | 68 | @Column 69 | private BigDecimal price; 70 | 71 | @Column 72 | private int inventory; 73 | 74 | @Override 75 | public UUID getId() { 76 | return id; 77 | } 78 | 79 | public String getName() { 80 | return name; 81 | } 82 | 83 | public BigDecimal getPrice() { 84 | return price; 85 | } 86 | 87 | public String getSku() { 88 | return sku; 89 | } 90 | 91 | public void setPrice(BigDecimal price) { 92 | this.price = price; 93 | } 94 | 95 | public int addInventoryQuantity(int qty) { 96 | this.inventory += qty; 97 | return inventory; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.repository; 2 | 3 | import io.roach.data.relational.domain.Customer; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | @Repository 11 | public interface CustomerRepository extends CrudRepository { 12 | Optional findByUserName(String userName); 13 | } 14 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.repository; 2 | 3 | import io.roach.data.relational.domain.Order; 4 | import org.springframework.data.jdbc.repository.query.Query; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.UUID; 11 | 12 | @Repository 13 | public interface OrderRepository extends CrudRepository { 14 | @Query(value = "select * from orders") 15 | Iterable findOrders(); 16 | 17 | @Query(value = "select o.* from orders o " 18 | + "join customer c on o.customer_id = c.id " 19 | + "where c.id=:customerId") 20 | Iterable findOrdersByCustomerId(@Param("customerId") UUID customerId); 21 | 22 | @Query(value = "select sum(o.total_price) from orders o") 23 | BigDecimal totalOrderCost(); 24 | } 25 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.repository; 2 | 3 | import io.roach.data.relational.domain.Product; 4 | import org.springframework.data.jdbc.repository.query.Query; 5 | import org.springframework.data.relational.core.sql.LockMode; 6 | import org.springframework.data.relational.repository.Lock; 7 | import org.springframework.data.repository.CrudRepository; 8 | import org.springframework.data.repository.query.Param; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.Optional; 12 | import java.util.UUID; 13 | 14 | @Repository 15 | public interface ProductRepository extends CrudRepository { 16 | @Lock(LockMode.PESSIMISTIC_READ) 17 | Optional findProductBySku(String sku); 18 | 19 | @Query("select p from product p where p.sku=:sku") 20 | Optional findProductBySkuNoLock(@Param("sku") String sku); 21 | } 22 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.service; 2 | 3 | import io.roach.data.relational.domain.Order; 4 | import io.roach.data.relational.repository.OrderRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Propagation; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import java.math.BigDecimal; 11 | 12 | @Service 13 | public class OrderService { 14 | @Autowired 15 | private OrderRepository orderRepository; 16 | 17 | @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) 18 | public BigDecimal getTotalOrderCost() { 19 | BigDecimal price = BigDecimal.ZERO; 20 | // N+1 query 21 | Iterable orders = orderRepository.findOrders(); 22 | for (Order order : orders) { 23 | price = price.add(order.getTotalPrice()); 24 | } 25 | return price; 26 | } 27 | 28 | @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) 29 | public BigDecimal getTotalOrderCostAggregate() { 30 | return orderRepository.totalOrderCost(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/java/io/roach/data/relational/service/OrderSystem.java: -------------------------------------------------------------------------------- 1 | package io.roach.data.relational.service; 2 | 3 | import io.roach.data.relational.domain.Customer; 4 | import io.roach.data.relational.domain.Order; 5 | import io.roach.data.relational.domain.Product; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.List; 9 | import java.util.UUID; 10 | import java.util.function.BiConsumer; 11 | 12 | /** 13 | * Main order system business facade. 14 | */ 15 | public interface OrderSystem { 16 | List createProductInventory(); 17 | 18 | List createCustomers(); 19 | 20 | List createOrders(); 21 | 22 | Iterable findOrders(); 23 | 24 | Iterable findOrders(Customer customer); 25 | 26 | Iterable findCustomers(); 27 | 28 | Product findProductBySku(String sku); 29 | 30 | Product findProductById(UUID id); 31 | 32 | Customer findCustomerById(UUID id); 33 | 34 | void getTotalOrderCost(BiConsumer result); 35 | 36 | void clearAll(); 37 | } 38 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:postgresql://192.168.1.99:26257/roach_data?sslmode=disable 4 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Spring boot properties 3 | # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 4 | ######################## 5 | 6 | spring: 7 | profiles: 8 | active: 9 | output: 10 | ansi: 11 | enabled: ALWAYS 12 | flyway: 13 | enabled: true 14 | clean-on-validation-error: true 15 | baseline-on-migrate: true 16 | clean-disabled: false 17 | datasource: 18 | url: jdbc:postgresql://localhost:26257/roach_data?sslmode=disable 19 | driver-class-name: org.postgresql.Driver 20 | username: root 21 | password: 22 | hikari: 23 | connection-test-query: SELECT 1 24 | maximum-pool-size: 20 25 | minimum-idle: 20 26 | auto-commit: false 27 | ############################# 28 | logging: 29 | pattern: 30 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr([%logger{39}]){cyan} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 31 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ^__^ 2 | (oo)\_______ 3 | (__)\ )\/\ CockroachDB on Spring Data JDBC / Relational ${application.formatted-version} 4 | ||----w | powered by Spring Boot ${spring-boot.formatted-version} 5 | || || 6 | -------------------------------------------------------------------------------- /roach-data-relational/src/main/resources/db/migration/V1_1__create.sql: -------------------------------------------------------------------------------- 1 | create table customer 2 | ( 3 | id uuid not null default gen_random_uuid(), 4 | first_name varchar(45), 5 | last_name varchar(45), 6 | user_name varchar(15) not null, 7 | 8 | primary key (id) 9 | ); 10 | 11 | create table orders 12 | ( 13 | id uuid not null default gen_random_uuid(), 14 | customer_id uuid not null, 15 | total_price numeric(19, 2) not null, 16 | tags string null, 17 | 18 | primary key (id) 19 | ); 20 | 21 | create table product 22 | ( 23 | id uuid not null default gen_random_uuid(), 24 | inventory int not null, 25 | name string not null, 26 | description jsonb null, 27 | price numeric(19, 2) not null, 28 | sku string not null, 29 | 30 | primary key (id) 31 | ); 32 | 33 | create table order_item 34 | ( 35 | order_id uuid not null, 36 | product_id uuid not null, 37 | quantity int not null, 38 | unit_price numeric(19, 2) not null, 39 | 40 | primary key (order_id, product_id) 41 | ); 42 | 43 | alter table product 44 | add constraint check_product_positive_inventory check (product.inventory >= 0); 45 | 46 | alter table if exists customer 47 | add constraint uc_user_name unique (user_name); 48 | 49 | alter table if exists product 50 | add constraint uc_product_sku unique (sku); 51 | 52 | alter table if exists order_item 53 | add constraint fk_order_item_ref_product 54 | foreign key (product_id) 55 | references product; 56 | 57 | alter table if exists order_item 58 | add constraint fk_order_item_ref_order 59 | foreign key (order_id) 60 | references orders; 61 | 62 | alter table if exists orders 63 | add constraint fk_order_ref_customer 64 | foreign key (customer_id) 65 | references customer; 66 | 67 | create index fk_order_item_ref_product_idx on order_item (product_id); 68 | 69 | create index fk_order_ref_customer_idx on orders (customer_id); -------------------------------------------------------------------------------- /roach-data-relational/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------