diff --git a/NOTES b/NOTES index 65017da..a2cb1f2 100644 --- a/NOTES +++ b/NOTES @@ -14,7 +14,7 @@ Operation/ | | `-- BoundOptionalOperation | `-- AbstractStreamOperation | |-- AbstractFilterStreamOperation -| | |-- SelectOperation + | | |-- SelectOperation | | `-- SelectTransformingOperation | `-- BoundStreamOperation |-- PreparedOperation @@ -22,6 +22,14 @@ Operation/ `-- PreparedStreamOperation +---- +@CompoundIndex() +create a new col in the same table called __idx_a_b_c that the hash of the concatenated values in that order is stored, create a normal index for that (CREATE INDEX ...) +if a query matches that set of columns then use that indexed col to fetch the desired results from that table +could also work with .in() query if materialized view exists +---- + + // TODO(gburd): create a statement that matches one that wasn't prepared //String key = // "use " + preparedStatement.getQueryKeyspace() + "; " + preparedStatement.getQueryString(); @@ -183,3 +191,12 @@ InsertOperation if (resultType == iface) { if (values.size() > 0 && includesNonIdentityValues) { boolean immutable = iface.isAssignableFrom(Drafted.class); +------------------- + + final Object value; + if (method.getParameterCount() == 1 && args[0] instanceof Boolean && src instanceof ValueProviderMap) { + value = ((ValueProviderMap)src).get(methodName, (Boolean)args[0]); + } else { + value = src.get(methodName); + } +-------------------- diff --git a/bin/deploy.sh b/bin/deploy.sh index a54aa01..271fb4c 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash -mvn clean jar:jar javadoc:jar source:jar deploy -Prelease +mvn clean jar:jar javadoc:jar source:jar deploy -Prelease diff --git a/bin/format.sh b/bin/format.sh index 10b2bf0..031b9f3 100755 --- a/bin/format.sh +++ b/bin/format.sh @@ -1,7 +1,14 @@ -#!/bin/bash +#!/usr/bin/env bash -for f in $(find ./src -name \*.java); do - echo Formatting $f - java -jar ./lib/google-java-format-1.3-all-deps.jar --replace $f -done +if [ "X$1" == "Xall" ]; then + for f in $(find ./src -name \*.java); do + echo Formatting $f + java -jar ./lib/google-java-format-1.3-all-deps.jar --replace $f + done +else + for file in $(git status --short | awk '{print $2}'); do + echo $file + java -jar ./lib/google-java-format-1.3-all-deps.jar --replace $file + done +fi diff --git a/bin/sign.sh b/bin/sign.sh index ca6b8e5..143db80 100755 --- a/bin/sign.sh +++ b/bin/sign.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash mvn clean jar:jar javadoc:jar source:jar install -Prelease diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 50b24e8..0000000 --- a/build.gradle +++ /dev/null @@ -1,90 +0,0 @@ -// gradle wrapper -// ./gradlew clean generateLock saveLock -// ./gradlew compileJava -// ./gradlew run -// ./gradlew run --debug-jvm -// ./gradlew publishToMavenLocal - - -buildscript { - ext {} - repositories { - jcenter() - mavenLocal() - mavenCentral() - maven { url "https://clojars.org/repo" } - maven { url "https://plugins.gradle.org/m2/" } - } - dependencies { - classpath 'com.netflix.nebula:gradle-dependency-lock-plugin:4.+' - classpath 'com.uber:okbuck:0.19.0' - } -} - -apply plugin: 'java' -apply plugin: 'idea' -apply plugin: 'eclipse' -apply plugin: 'java-library' -apply plugin: 'maven-publish' -apply plugin: 'com.uber.okbuck' -apply plugin: 'nebula.dependency-lock' - -task wrapper(type: Wrapper) { - gradleVersion = '4.0.2' -} - -jar { - baseName = 'helenus' - group = 'net.helenus' - version = '2.0.17-SNAPSHOT' -} - -description = """helenus""" - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' -} - -configurations.all { -} - -repositories { - jcenter() - mavenLocal() - mavenCentral() - maven { url "file:///Users/gburd/ws/helenus/lib" } - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } - maven { url "http://repo.maven.apache.org/maven2" } -} -dependencies { - compile group: 'com.datastax.cassandra', name: 'cassandra-driver-core', version: '3.3.0' - compile group: 'org.aspectj', name: 'aspectjrt', version: '1.8.10' - compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.8.10' - compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.6' - compile group: 'org.springframework', name: 'spring-core', version: '4.3.10.RELEASE' - compile group: 'com.google.guava', name: 'guava', version: '20.0' - compile group: 'com.diffplug.durian', name: 'durian', version: '3.+' - compile group: 'io.zipkin.java', name: 'zipkin', version: '1.29.2' - compile group: 'io.zipkin.brave', name: 'brave', version: '4.0.6' - compile group: 'io.dropwizard.metrics', name: 'metrics-core', version: '3.2.2' - compile group: 'javax.validation', name: 'validation-api', version: '2.0.0.CR3' - compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.1' - - runtime group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.1' - - testCompile group: 'org.codehaus.jackson', name: 'jackson-mapper-asl', version: '1.9.13' - testCompile group: 'com.anthemengineering.mojo', name: 'infer-maven-plugin', version: '0.1.0' - testCompile group: 'org.codehaus.jackson', name: 'jackson-core-asl', version: '1.9.13' - testCompile(group: 'org.cassandraunit', name: 'cassandra-unit', version: '3.1.4.0-SNAPSHOT') { - exclude(module: 'cassandra-driver-core') - } - testCompile group: 'org.apache.cassandra', name: 'cassandra-all', version: '3.11.0' - testCompile group: 'commons-io', name: 'commons-io', version: '2.5' - testCompile group: 'junit', name: 'junit', version: '4.12' - testCompile group: 'com.github.stephenc', name: 'jamm', version: '0.2.5' - testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3' - testCompile group: 'org.hamcrest', name: 'hamcrest-core', version: '1.3' - testCompile group: 'org.mockito', name: 'mockito-core', version: '2.8.47' -} diff --git a/dependencies.lock b/dependencies.lock deleted file mode 100644 index 8222106..0000000 --- a/dependencies.lock +++ /dev/null @@ -1,648 +0,0 @@ -{ - "compile": { - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.google.guava:guava": { - "locked": "20.0", - "requested": "20.0" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "compileClasspath": { - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.google.guava:guava": { - "locked": "20.0", - "requested": "20.0" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "default": { - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.google.guava:guava": { - "locked": "20.0", - "requested": "20.0" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.slf4j:jcl-over-slf4j": { - "locked": "1.7.1", - "requested": "1.7.1" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "runtime": { - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.google.guava:guava": { - "locked": "20.0", - "requested": "20.0" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.slf4j:jcl-over-slf4j": { - "locked": "1.7.1", - "requested": "1.7.1" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "runtimeClasspath": { - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.google.guava:guava": { - "locked": "20.0", - "requested": "20.0" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.slf4j:jcl-over-slf4j": { - "locked": "1.7.1", - "requested": "1.7.1" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "testCompile": { - "com.anthemengineering.mojo:infer-maven-plugin": { - "locked": "0.1.0", - "requested": "0.1.0" - }, - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.github.stephenc:jamm": { - "locked": "0.2.5", - "requested": "0.2.5" - }, - "com.google.guava:guava": { - "locked": "21.0", - "requested": "20.0" - }, - "commons-io:commons-io": { - "locked": "2.5", - "requested": "2.5" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "junit:junit": { - "locked": "4.12", - "requested": "4.12" - }, - "org.apache.cassandra:cassandra-all": { - "locked": "3.11.0", - "requested": "3.11.0" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.cassandraunit:cassandra-unit": { - "locked": "3.1.4.0-SNAPSHOT", - "requested": "3.1.4.0-SNAPSHOT" - }, - "org.codehaus.jackson:jackson-core-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.codehaus.jackson:jackson-mapper-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.hamcrest:hamcrest-core": { - "locked": "1.3", - "requested": "1.3" - }, - "org.hamcrest:hamcrest-library": { - "locked": "1.3", - "requested": "1.3" - }, - "org.mockito:mockito-core": { - "locked": "2.8.47", - "requested": "2.8.47" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "testCompileClasspath": { - "com.anthemengineering.mojo:infer-maven-plugin": { - "locked": "0.1.0", - "requested": "0.1.0" - }, - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.github.stephenc:jamm": { - "locked": "0.2.5", - "requested": "0.2.5" - }, - "com.google.guava:guava": { - "locked": "21.0", - "requested": "20.0" - }, - "commons-io:commons-io": { - "locked": "2.5", - "requested": "2.5" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "junit:junit": { - "locked": "4.12", - "requested": "4.12" - }, - "org.apache.cassandra:cassandra-all": { - "locked": "3.11.0", - "requested": "3.11.0" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.cassandraunit:cassandra-unit": { - "locked": "3.1.4.0-SNAPSHOT", - "requested": "3.1.4.0-SNAPSHOT" - }, - "org.codehaus.jackson:jackson-core-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.codehaus.jackson:jackson-mapper-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.hamcrest:hamcrest-core": { - "locked": "1.3", - "requested": "1.3" - }, - "org.hamcrest:hamcrest-library": { - "locked": "1.3", - "requested": "1.3" - }, - "org.mockito:mockito-core": { - "locked": "2.8.47", - "requested": "2.8.47" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "testRuntime": { - "com.anthemengineering.mojo:infer-maven-plugin": { - "locked": "0.1.0", - "requested": "0.1.0" - }, - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.github.stephenc:jamm": { - "locked": "0.2.5", - "requested": "0.2.5" - }, - "com.google.guava:guava": { - "locked": "21.0", - "requested": "20.0" - }, - "commons-io:commons-io": { - "locked": "2.5", - "requested": "2.5" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "junit:junit": { - "locked": "4.12", - "requested": "4.12" - }, - "org.apache.cassandra:cassandra-all": { - "locked": "3.11.0", - "requested": "3.11.0" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.cassandraunit:cassandra-unit": { - "locked": "3.1.4.0-SNAPSHOT", - "requested": "3.1.4.0-SNAPSHOT" - }, - "org.codehaus.jackson:jackson-core-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.codehaus.jackson:jackson-mapper-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.hamcrest:hamcrest-core": { - "locked": "1.3", - "requested": "1.3" - }, - "org.hamcrest:hamcrest-library": { - "locked": "1.3", - "requested": "1.3" - }, - "org.mockito:mockito-core": { - "locked": "2.8.47", - "requested": "2.8.47" - }, - "org.slf4j:jcl-over-slf4j": { - "locked": "1.7.7", - "requested": "1.7.1" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - }, - "testRuntimeClasspath": { - "com.anthemengineering.mojo:infer-maven-plugin": { - "locked": "0.1.0", - "requested": "0.1.0" - }, - "com.datastax.cassandra:cassandra-driver-core": { - "locked": "3.3.0", - "requested": "3.3.0" - }, - "com.diffplug.durian:durian": { - "locked": "3.5.0-SNAPSHOT", - "requested": "3.+" - }, - "com.github.stephenc:jamm": { - "locked": "0.2.5", - "requested": "0.2.5" - }, - "com.google.guava:guava": { - "locked": "21.0", - "requested": "20.0" - }, - "commons-io:commons-io": { - "locked": "2.5", - "requested": "2.5" - }, - "io.dropwizard.metrics:metrics-core": { - "locked": "3.2.2", - "requested": "3.2.2" - }, - "io.zipkin.brave:brave": { - "locked": "4.0.6", - "requested": "4.0.6" - }, - "io.zipkin.java:zipkin": { - "locked": "1.29.2", - "requested": "1.29.2" - }, - "javax.validation:validation-api": { - "locked": "2.0.0.CR3", - "requested": "2.0.0.CR3" - }, - "junit:junit": { - "locked": "4.12", - "requested": "4.12" - }, - "org.apache.cassandra:cassandra-all": { - "locked": "3.11.0", - "requested": "3.11.0" - }, - "org.apache.commons:commons-lang3": { - "locked": "3.6", - "requested": "3.6" - }, - "org.aspectj:aspectjrt": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.aspectj:aspectjweaver": { - "locked": "1.8.10", - "requested": "1.8.10" - }, - "org.cassandraunit:cassandra-unit": { - "locked": "3.1.4.0-SNAPSHOT", - "requested": "3.1.4.0-SNAPSHOT" - }, - "org.codehaus.jackson:jackson-core-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.codehaus.jackson:jackson-mapper-asl": { - "locked": "1.9.13", - "requested": "1.9.13" - }, - "org.hamcrest:hamcrest-core": { - "locked": "1.3", - "requested": "1.3" - }, - "org.hamcrest:hamcrest-library": { - "locked": "1.3", - "requested": "1.3" - }, - "org.mockito:mockito-core": { - "locked": "2.8.47", - "requested": "2.8.47" - }, - "org.slf4j:jcl-over-slf4j": { - "locked": "1.7.7", - "requested": "1.7.1" - }, - "org.slf4j:slf4j-api": { - "locked": "1.7.25", - "requested": "1.7.1" - }, - "org.springframework:spring-core": { - "locked": "4.3.10.RELEASE", - "requested": "4.3.10.RELEASE" - } - } -} \ No newline at end of file diff --git a/helenus-core.iml b/helenus-core.iml index be96637..d38133a 100644 --- a/helenus-core.iml +++ b/helenus-core.iml @@ -11,7 +11,7 @@ - + @@ -28,16 +28,14 @@ + - + - - - @@ -116,9 +114,9 @@ + - diff --git a/pom.xml b/pom.xml index 71feca8..8b502a7 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,13 @@ com.datastax.cassandra cassandra-driver-core - 3.3.0 + 3.3.2 + + + + com.datastax.cassandra + cassandra-driver-extras + 3.3.2 @@ -118,12 +124,6 @@ 3.4.0 - - org.aspectj - aspectjrt - 1.8.10 - - org.aspectj aspectjweaver @@ -142,25 +142,19 @@ 4.3.10.RELEASE + + javax.cache + cache-api + 1.1.0 + + com.google.guava guava 20.0 - - - io.zipkin.java - zipkin - 1.29.2 - - - - io.zipkin.brave - brave - 4.0.6 - - + io.dropwizard.metrics metrics-core @@ -217,6 +211,24 @@ test + + + ca.exprofesso + guava-jcache + 1.0.4 + + + com.google.guava + guava + + + javax.cache + cache-api + + + test + + commons-io commons-io @@ -231,13 +243,6 @@ test - - com.github.stephenc - jamm - 0.2.5 - test - - org.hamcrest hamcrest-library @@ -272,7 +277,6 @@ 1.7.1 runtime - diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index bad81ae..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'helenus-core' diff --git a/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java b/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java index d6ba093..70e0ae6 100644 --- a/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java +++ b/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java @@ -5,7 +5,7 @@ import com.datastax.driver.core.querybuilder.Select; public class CreateMaterializedView extends Create { - private String viewName; + private final String viewName; private Select.Where selection; private String primaryKey; private String clustering; diff --git a/src/main/java/net/helenus/core/AbstractAuditedEntityDraft.java b/src/main/java/net/helenus/core/AbstractAuditedEntityDraft.java index a9a09e2..d0b76e8 100644 --- a/src/main/java/net/helenus/core/AbstractAuditedEntityDraft.java +++ b/src/main/java/net/helenus/core/AbstractAuditedEntityDraft.java @@ -33,6 +33,6 @@ public abstract class AbstractAuditedEntityDraft extends AbstractEntityDraft< } public Date createdAt() { - return (Date) get("createdAt", Date.class); + return get("createdAt", Date.class); } } diff --git a/src/main/java/net/helenus/core/AbstractEntityDraft.java b/src/main/java/net/helenus/core/AbstractEntityDraft.java index 0101351..448d852 100644 --- a/src/main/java/net/helenus/core/AbstractEntityDraft.java +++ b/src/main/java/net/helenus/core/AbstractEntityDraft.java @@ -2,22 +2,36 @@ package net.helenus.core; import com.google.common.primitives.Primitives; import java.io.Serializable; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import net.helenus.core.reflect.DefaultPrimitiveTypes; import net.helenus.core.reflect.Drafted; import net.helenus.core.reflect.MapExportable; +import net.helenus.mapping.HelenusProperty; import net.helenus.mapping.MappingUtil; import org.apache.commons.lang3.SerializationUtils; public abstract class AbstractEntityDraft implements Drafted { - private final Map backingMap = new HashMap(); private final MapExportable entity; - private final Map entityMap; + private final Map valuesMap; + private final Set readSet; + private final Map mutationsMap = new HashMap(); public AbstractEntityDraft(MapExportable entity) { this.entity = entity; - this.entityMap = entity != null ? entity.toMap() : new HashMap(); + // Entities can mutate their map. + if (entity != null) { + this.valuesMap = entity.toMap(true); + this.readSet = entity.toReadSet(); + } else { + this.valuesMap = new HashMap(); + this.readSet = new HashSet(); + } } public abstract Class getEntityClass(); @@ -33,10 +47,11 @@ public abstract class AbstractEntityDraft implements Drafted { @SuppressWarnings("unchecked") public T get(String key, Class returnType) { - T value = (T) backingMap.get(key); + readSet.add(key); + T value = (T) mutationsMap.get(key); if (value == null) { - value = (T) entityMap.get(key); + value = (T) valuesMap.get(key); if (value == null) { if (Primitives.allPrimitiveTypes().contains(returnType)) { @@ -49,14 +64,9 @@ public abstract class AbstractEntityDraft implements Drafted { return (T) type.getDefaultValue(); } } else { - // Collections fetched from the entityMap + // Collections fetched from the valuesMap if (value instanceof Collection) { - try { - value = MappingUtil.clone(value); - } catch (CloneNotSupportedException e) { - // TODO(gburd): deep?shallow? copy of List, Map, Set to a mutable collection. - value = (T) SerializationUtils.clone((Serializable) value); - } + value = (T) SerializationUtils.clone((Serializable) value); } } } @@ -65,7 +75,17 @@ public abstract class AbstractEntityDraft implements Drafted { } public Object set(Getter getter, Object value) { - return set(this.methodNameFor(getter), value); + HelenusProperty prop = MappingUtil.resolveMappingProperty(getter).getProperty(); + String key = prop.getPropertyName(); + + HelenusValidator.INSTANCE.validate(prop, value); + + if (key == null || value == null) { + return null; + } + + mutationsMap.put(key, value); + return value; } public Object set(String key, Object value) { @@ -73,36 +93,34 @@ public abstract class AbstractEntityDraft implements Drafted { return null; } - backingMap.put(key, value); + mutationsMap.put(key, value); return value; } + public void put(String key, Object value) { + mutationsMap.put(key, value); + } + @SuppressWarnings("unchecked") public T mutate(Getter getter, T value) { return (T) mutate(this.methodNameFor(getter), value); } - public Object mutate(String key, Object value) { + public T mutate(String key, T value) { Objects.requireNonNull(key); - if (value == null) { - return null; - } - - if (entity != null) { - Map map = entity.toMap(); - - if (map.containsKey(key) && !value.equals(map.get(key))) { - backingMap.put(key, value); - return value; + if (value != null) { + if (entity != null) { + T currentValue = this.fetch(key); + if (!value.equals(currentValue)) { + mutationsMap.put(key, value); + return value; + } + } else { + mutationsMap.put(key, value); } - - return map.get(key); - } else { - backingMap.put(key, value); - - return null; } + return null; } private String methodNameFor(Getter getter) { @@ -115,8 +133,8 @@ public abstract class AbstractEntityDraft implements Drafted { public Object unset(String key) { if (key != null) { - Object value = backingMap.get(key); - backingMap.put(key, null); + Object value = mutationsMap.get(key); + mutationsMap.put(key, null); return value; } return null; @@ -126,10 +144,18 @@ public abstract class AbstractEntityDraft implements Drafted { return this.reset(this.methodNameFor(getter), desiredValue); } + private T fetch(String key) { + T value = (T) mutationsMap.get(key); + if (value == null) { + value = (T) valuesMap.get(key); + } + return value; + } + public boolean reset(String key, T desiredValue) { if (key != null && desiredValue != null) { @SuppressWarnings("unchecked") - T currentValue = (T) backingMap.get(key); + T currentValue = (T) this.fetch(key); if (currentValue == null || !currentValue.equals(desiredValue)) { set(key, desiredValue); return true; @@ -140,7 +166,7 @@ public abstract class AbstractEntityDraft implements Drafted { @Override public Map toMap() { - return toMap(entityMap); + return toMap(valuesMap); } public Map toMap(Map entityMap) { @@ -151,21 +177,26 @@ public abstract class AbstractEntityDraft implements Drafted { combined.put(e.getKey(), e.getValue()); } } else { - combined = new HashMap(backingMap.size()); + combined = new HashMap(mutationsMap.size()); } for (String key : mutated()) { - combined.put(key, backingMap.get(key)); + combined.put(key, mutationsMap.get(key)); } return combined; } @Override public Set mutated() { - return backingMap.keySet(); + return mutationsMap.keySet(); + } + + @Override + public Set read() { + return readSet; } @Override public String toString() { - return backingMap.toString(); + return mutationsMap.toString(); } } diff --git a/src/main/java/net/helenus/core/AbstractSessionOperations.java b/src/main/java/net/helenus/core/AbstractSessionOperations.java index ba92e44..a5603e6 100644 --- a/src/main/java/net/helenus/core/AbstractSessionOperations.java +++ b/src/main/java/net/helenus/core/AbstractSessionOperations.java @@ -15,7 +15,6 @@ */ package net.helenus.core; -import brave.Tracer; import com.codahale.metrics.MetricRegistry; import com.datastax.driver.core.*; import com.google.common.base.Stopwatch; @@ -25,7 +24,6 @@ import java.io.PrintStream; import java.util.List; import java.util.concurrent.Executor; import net.helenus.core.cache.Facet; -import net.helenus.core.operation.Operation; import net.helenus.mapping.value.ColumnValuePreparer; import net.helenus.mapping.value.ColumnValueProvider; import net.helenus.support.Either; @@ -43,6 +41,8 @@ public abstract class AbstractSessionOperations { public abstract boolean isShowCql(); + public abstract boolean showValues(); + public abstract PrintStream getPrintStream(); public abstract Executor getExecutor(); @@ -59,7 +59,6 @@ public abstract class AbstractSessionOperations { public PreparedStatement prepare(RegularStatement statement) { try { - logStatement(statement, false); return currentSession().prepare(statement); } catch (RuntimeException e) { throw translateException(e); @@ -68,64 +67,48 @@ public abstract class AbstractSessionOperations { public ListenableFuture prepareAsync(RegularStatement statement) { try { - logStatement(statement, false); return currentSession().prepareAsync(statement); } catch (RuntimeException e) { throw translateException(e); } } - public ResultSet execute(Statement statement, boolean showValues) { - return execute(statement, null, null, showValues); + public ResultSet execute(Statement statement) { + return execute(statement, null, null); } - public ResultSet execute(Statement statement, Stopwatch timer, boolean showValues) { - return execute(statement, null, timer, showValues); + public ResultSet execute(Statement statement, Stopwatch timer) { + return execute(statement, null, timer); } - public ResultSet execute(Statement statement, UnitOfWork uow, boolean showValues) { - return execute(statement, uow, null, showValues); + public ResultSet execute(Statement statement, UnitOfWork uow) { + return execute(statement, uow, null); } - public ResultSet execute( - Statement statement, UnitOfWork uow, Stopwatch timer, boolean showValues) { - return executeAsync(statement, uow, timer, showValues).getUninterruptibly(); + public ResultSet execute(Statement statement, UnitOfWork uow, Stopwatch timer) { + return executeAsync(statement, uow, timer).getUninterruptibly(); } - public ResultSetFuture executeAsync(Statement statement, boolean showValues) { - return executeAsync(statement, null, null, showValues); + public ResultSetFuture executeAsync(Statement statement) { + return executeAsync(statement, null, null); } - public ResultSetFuture executeAsync(Statement statement, Stopwatch timer, boolean showValues) { - return executeAsync(statement, null, timer, showValues); + public ResultSetFuture executeAsync(Statement statement, Stopwatch timer) { + return executeAsync(statement, null, timer); } - public ResultSetFuture executeAsync(Statement statement, UnitOfWork uow, boolean showValues) { - return executeAsync(statement, uow, null, showValues); + public ResultSetFuture executeAsync(Statement statement, UnitOfWork uow) { + return executeAsync(statement, uow, null); } - public ResultSetFuture executeAsync( - Statement statement, UnitOfWork uow, Stopwatch timer, boolean showValues) { + public ResultSetFuture executeAsync(Statement statement, UnitOfWork uow, Stopwatch timer) { try { - logStatement(statement, showValues); return currentSession().executeAsync(statement); } catch (RuntimeException e) { throw translateException(e); } } - private void logStatement(Statement statement, boolean showValues) { - if (isShowCql()) { - printCql(Operation.queryString(statement, showValues)); - } else if (LOG.isDebugEnabled()) { - LOG.info("CQL> " + Operation.queryString(statement, showValues)); - } - } - - public Tracer getZipkinTracer() { - return null; - } - public MetricRegistry getMetricRegistry() { return null; } @@ -145,9 +128,5 @@ public abstract class AbstractSessionOperations { public void updateCache(Object pojo, List facets) {} - void printCql(String cql) { - getPrintStream().println(cql); - } - public void cacheEvict(List facets) {} } diff --git a/src/main/java/net/helenus/core/AbstractUnitOfWork.java b/src/main/java/net/helenus/core/AbstractUnitOfWork.java deleted file mode 100644 index f658956..0000000 --- a/src/main/java/net/helenus/core/AbstractUnitOfWork.java +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright (C) 2015 The Helenus Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.helenus.core; - -import static net.helenus.core.HelenusSession.deleted; - -import com.diffplug.common.base.Errors; -import com.google.common.base.Stopwatch; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.Table; -import com.google.common.collect.TreeTraverser; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import net.helenus.core.cache.CacheUtil; -import net.helenus.core.cache.Facet; -import net.helenus.support.Either; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Encapsulates the concept of a "transaction" as a unit-of-work. */ -public abstract class AbstractUnitOfWork - implements UnitOfWork, AutoCloseable { - - private static final Logger LOG = LoggerFactory.getLogger(AbstractUnitOfWork.class); - - private final List> nested = new ArrayList<>(); - private final HelenusSession session; - private final AbstractUnitOfWork parent; - private final Table>> cache = HashBasedTable.create(); - protected String purpose; - protected List nestedPurposes = new ArrayList(); - protected String info; - protected int cacheHits = 0; - protected int cacheMisses = 0; - protected int databaseLookups = 0; - protected Stopwatch elapsedTime; - protected Map databaseTime = new HashMap<>(); - protected double cacheLookupTime = 0.0; - private List postCommit = new ArrayList(); - private boolean aborted = false; - private boolean committed = false; - - protected AbstractUnitOfWork(HelenusSession session, AbstractUnitOfWork parent) { - Objects.requireNonNull(session, "containing session cannot be null"); - - this.session = session; - this.parent = parent; - } - - @Override - public void addDatabaseTime(String name, Stopwatch amount) { - Double time = databaseTime.get(name); - if (time == null) { - databaseTime.put(name, (double) amount.elapsed(TimeUnit.MICROSECONDS)); - } else { - databaseTime.put(name, time + amount.elapsed(TimeUnit.MICROSECONDS)); - } - } - - @Override - public void addCacheLookupTime(Stopwatch amount) { - cacheLookupTime += amount.elapsed(TimeUnit.MICROSECONDS); - } - - @Override - public void addNestedUnitOfWork(UnitOfWork uow) { - synchronized (nested) { - nested.add((AbstractUnitOfWork) uow); - } - } - - @Override - public synchronized UnitOfWork begin() { - if (LOG.isInfoEnabled()) { - elapsedTime = Stopwatch.createStarted(); - } - // log.record(txn::start) - return this; - } - - @Override - public String getPurpose() { - return purpose; - } - - @Override - public UnitOfWork setPurpose(String purpose) { - this.purpose = purpose; - return this; - } - - @Override - public void setInfo(String info) { - this.info = info; - } - - @Override - public void recordCacheAndDatabaseOperationCount(int cache, int ops) { - if (cache > 0) { - cacheHits += cache; - } else { - cacheMisses += Math.abs(cache); - } - if (ops > 0) { - databaseLookups += ops; - } - } - - public String logTimers(String what) { - double e = (double) elapsedTime.elapsed(TimeUnit.MICROSECONDS) / 1000.0; - double d = 0.0; - double c = cacheLookupTime / 1000.0; - double fc = (c / e) * 100.0; - String database = ""; - if (databaseTime.size() > 0) { - List dbt = new ArrayList<>(databaseTime.size()); - for (Map.Entry dt : databaseTime.entrySet()) { - double t = dt.getValue() / 1000.0; - d += t; - dbt.add(String.format("%s took %,.3fms %,2.2f%%", dt.getKey(), t, (t / e) * 100.0)); - } - double fd = (d / e) * 100.0; - database = - String.format( - ", %d quer%s (%,.3fms %,2.2f%% - %s)", - databaseLookups, (databaseLookups > 1) ? "ies" : "y", d, fd, String.join(", ", dbt)); - } - String cache = ""; - if (cacheLookupTime > 0) { - int cacheLookups = cacheHits + cacheMisses; - cache = - String.format( - " with %d cache lookup%s (%,.3fms %,2.2f%% - %,d hit, %,d miss)", - cacheLookups, cacheLookups > 1 ? "s" : "", c, fc, cacheHits, cacheMisses); - } - String da = ""; - if (databaseTime.size() > 0 || cacheLookupTime > 0) { - double dat = d + c; - double daf = (dat / e) * 100; - da = - String.format( - " consuming %,.3fms for data access, or %,2.2f%% of total UOW time.", dat, daf); - } - String x = nestedPurposes.stream().distinct().collect(Collectors.joining(", ")); - String n = - nested - .stream() - .map(uow -> String.valueOf(uow.hashCode())) - .collect(Collectors.joining(", ")); - String s = - String.format( - Locale.US, - "UOW(%s%s) %s in %,.3fms%s%s%s%s%s%s", - hashCode(), - (nested.size() > 0 ? ", [" + n + "]" : ""), - what, - e, - cache, - database, - da, - (purpose == null ? "" : " " + purpose), - (nestedPurposes.isEmpty()) ? "" : ", " + x, - (info == null) ? "" : " " + info); - return s; - } - - private void applyPostCommitFunctions() { - if (!postCommit.isEmpty()) { - for (CommitThunk f : postCommit) { - f.apply(); - } - } - if (LOG.isInfoEnabled()) { - LOG.info(logTimers("committed")); - } - } - - @Override - public Optional cacheLookup(List facets) { - String tableName = CacheUtil.schemaName(facets); - Optional result = Optional.empty(); - for (Facet facet : facets) { - if (!facet.fixed()) { - String columnName = facet.name() + "==" + facet.value(); - Either> eitherValue = cache.get(tableName, columnName); - if (eitherValue != null) { - Object value = deleted; - if (eitherValue.isLeft()) { - value = eitherValue.getLeft(); - } - result = Optional.of(value); - break; - } - } - } - if (!result.isPresent()) { - // Be sure to check all enclosing UnitOfWork caches as well, we may be nested. - if (parent != null) { - return parent.cacheLookup(facets); - } - } - return result; - } - - @Override - public List cacheEvict(List facets) { - Either> deletedObjectFacets = Either.right(facets); - String tableName = CacheUtil.schemaName(facets); - Optional optionalValue = cacheLookup(facets); - if (optionalValue.isPresent()) { - Object value = optionalValue.get(); - - for (Facet facet : facets) { - if (!facet.fixed()) { - String columnKey = facet.name() + "==" + facet.value(); - // mark the value identified by the facet to `deleted` - cache.put(tableName, columnKey, deletedObjectFacets); - } - } - // look for other row/col pairs that referenced the same object, mark them - // `deleted` - cache - .columnKeySet() - .forEach( - columnKey -> { - Either> eitherCachedValue = cache.get(tableName, columnKey); - if (eitherCachedValue.isLeft()) { - Object cachedValue = eitherCachedValue.getLeft(); - if (cachedValue == value) { - cache.put(tableName, columnKey, deletedObjectFacets); - String[] parts = columnKey.split("=="); - facets.add(new Facet(parts[0], parts[1])); - } - } - }); - } - return facets; - } - - @Override - public void cacheUpdate(Object value, List facets) { - String tableName = CacheUtil.schemaName(facets); - for (Facet facet : facets) { - if (!facet.fixed()) { - String columnName = facet.name() + "==" + facet.value(); - cache.put(tableName, columnName, Either.left(value)); - } - } - } - - private Iterator> getChildNodes() { - return nested.iterator(); - } - - /** - * Checks to see if the work performed between calling begin and now can be committed or not. - * - * @return a function from which to chain work that only happens when commit is successful - * @throws E when the work overlaps with other concurrent writers. - */ - public PostCommitFunction commit() throws E { - // All nested UnitOfWork should be committed (not aborted) before calls to - // commit, check. - boolean canCommit = true; - TreeTraverser> traverser = - TreeTraverser.using(node -> node::getChildNodes); - for (AbstractUnitOfWork uow : traverser.postOrderTraversal(this)) { - if (this != uow) { - canCommit &= (!uow.aborted && uow.committed); - } - } - - // log.record(txn::provisionalCommit) - // examine log for conflicts in read-set and write-set between begin and - // provisional commit - // if (conflict) { throw new ConflictingUnitOfWorkException(this) } - // else return function so as to enable commit.andThen(() -> { do something iff - // commit was successful; }) - - if (canCommit) { - committed = true; - aborted = false; - - nested.forEach((uow) -> Errors.rethrow().wrap(uow::commit)); - elapsedTime.stop(); - - if (parent == null) { - // Apply all post-commit functions, this is the outter-most UnitOfWork. - traverser - .postOrderTraversal(this) - .forEach( - uow -> { - uow.applyPostCommitFunctions(); - }); - - // Merge our cache into the session cache. - session.mergeCache(cache); - - return new PostCommitFunction(this, null); - } else { - - // Merge cache and statistics into parent if there is one. - parent.mergeCache(cache); - if (purpose != null) { - parent.nestedPurposes.add(purpose); - } - parent.cacheHits += cacheHits; - parent.cacheMisses += cacheMisses; - parent.databaseLookups += databaseLookups; - parent.cacheLookupTime += cacheLookupTime; - for (Map.Entry dt : databaseTime.entrySet()) { - String name = dt.getKey(); - if (parent.databaseTime.containsKey(name)) { - double t = parent.databaseTime.get(name); - parent.databaseTime.put(name, t + dt.getValue()); - } else { - parent.databaseTime.put(name, dt.getValue()); - } - } - } - } - // else { - // Constructor ctor = clazz.getConstructor(conflictExceptionClass); - // T object = ctor.newInstance(new Object[] { String message }); - // } - return new PostCommitFunction(this, postCommit); - } - - /* Explicitly discard the work and mark it as as such in the log. */ - public synchronized void abort() { - TreeTraverser> traverser = - TreeTraverser.using(node -> node::getChildNodes); - traverser - .postOrderTraversal(this) - .forEach( - uow -> { - uow.committed = false; - uow.aborted = true; - }); - // log.record(txn::abort) - // cache.invalidateSince(txn::start time) - if (LOG.isInfoEnabled()) { - if (elapsedTime.isRunning()) { - elapsedTime.stop(); - } - LOG.info(logTimers("aborted")); - } - } - - private void mergeCache(Table>> from) { - Table>> to = this.cache; - from.rowMap() - .forEach( - (rowKey, columnMap) -> { - columnMap.forEach( - (columnKey, value) -> { - if (to.contains(rowKey, columnKey)) { - // TODO(gburd):... - to.put( - rowKey, - columnKey, - Either.left( - CacheUtil.merge( - to.get(rowKey, columnKey).getLeft(), - from.get(rowKey, columnKey).getLeft()))); - } else { - to.put(rowKey, columnKey, from.get(rowKey, columnKey)); - } - }); - }); - } - - public String describeConflicts() { - return "it's complex..."; - } - - @Override - public void close() throws E { - // Closing a AbstractUnitOfWork will abort iff we've not already aborted or - // committed this unit of work. - if (aborted == false && committed == false) { - abort(); - } - } - - public boolean hasAborted() { - return aborted; - } - - public boolean hasCommitted() { - return committed; - } -} diff --git a/src/main/java/net/helenus/core/Filter.java b/src/main/java/net/helenus/core/Filter.java index 5edd5bb..fc5534f 100644 --- a/src/main/java/net/helenus/core/Filter.java +++ b/src/main/java/net/helenus/core/Filter.java @@ -79,6 +79,13 @@ public final class Filter { return new Filter(node, postulate); } + public static Filter create( + Getter getter, HelenusPropertyNode node, Postulate postulate) { + Objects.requireNonNull(getter, "empty getter"); + Objects.requireNonNull(postulate, "empty operator"); + return new Filter(node, postulate); + } + public static Filter create(Getter getter, Operator op, V val) { Objects.requireNonNull(getter, "empty getter"); Objects.requireNonNull(op, "empty op"); diff --git a/src/main/java/net/helenus/core/Helenus.java b/src/main/java/net/helenus/core/Helenus.java index edb15e2..7971568 100644 --- a/src/main/java/net/helenus/core/Helenus.java +++ b/src/main/java/net/helenus/core/Helenus.java @@ -81,6 +81,10 @@ public final class Helenus { return new SessionInitializer(session); } + public static SessionInitializer init(Session session, String keyspace) { + return new SessionInitializer(session, keyspace); + } + public static SessionInitializer init(Session session) { if (session == null) { diff --git a/src/main/java/net/helenus/core/HelenusSession.java b/src/main/java/net/helenus/core/HelenusSession.java index 7661f54..97cce0b 100644 --- a/src/main/java/net/helenus/core/HelenusSession.java +++ b/src/main/java/net/helenus/core/HelenusSession.java @@ -17,23 +17,19 @@ package net.helenus.core; import static net.helenus.core.Query.eq; -import brave.Tracer; import com.codahale.metrics.MetricRegistry; import com.datastax.driver.core.*; import com.google.common.collect.Table; import java.io.Closeable; import java.io.PrintStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.Executor; import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.cache.Cache; +import javax.cache.CacheManager; import net.helenus.core.cache.CacheUtil; import net.helenus.core.cache.Facet; -import net.helenus.core.cache.SessionCache; import net.helenus.core.cache.UnboundFacet; import net.helenus.core.operation.*; import net.helenus.core.reflect.Drafted; @@ -47,74 +43,69 @@ import net.helenus.support.*; import net.helenus.support.Fun.Tuple1; import net.helenus.support.Fun.Tuple2; import net.helenus.support.Fun.Tuple6; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class HelenusSession extends AbstractSessionOperations implements Closeable { - public static final Object deleted = new Object(); - private static final Logger LOG = LoggerFactory.getLogger(HelenusSession.class); - private static final Pattern classNameRegex = - Pattern.compile("^(?:\\w+\\.)+(?:(\\w+)|(\\w+)\\$.*)$"); private final Session session; private final CodecRegistry registry; private final ConsistencyLevel defaultConsistencyLevel; private final boolean defaultQueryIdempotency; private final MetricRegistry metricRegistry; - private final Tracer zipkinTracer; private final PrintStream printStream; - private final Class unitOfWorkClass; private final SessionRepository sessionRepository; private final Executor executor; private final boolean dropSchemaOnClose; - private final SessionCache sessionCache; + private final CacheManager cacheManager; private final RowColumnValueProvider valueProvider; private final StatementColumnValuePreparer valuePreparer; private final Metadata metadata; private volatile String usingKeyspace; private volatile boolean showCql; + private volatile boolean showValues; HelenusSession( Session session, String usingKeyspace, CodecRegistry registry, boolean showCql, + boolean showValues, PrintStream printStream, SessionRepositoryBuilder sessionRepositoryBuilder, Executor executor, boolean dropSchemaOnClose, ConsistencyLevel consistencyLevel, boolean defaultQueryIdempotency, - Class unitOfWorkClass, - SessionCache sessionCache, - MetricRegistry metricRegistry, - Tracer tracer) { + CacheManager cacheManager, + MetricRegistry metricRegistry) { this.session = session; this.registry = registry == null ? CodecRegistry.DEFAULT_INSTANCE : registry; this.usingKeyspace = Objects.requireNonNull( usingKeyspace, "keyspace needs to be selected before creating session"); this.showCql = showCql; + this.showValues = showValues; this.printStream = printStream; - this.sessionRepository = sessionRepositoryBuilder.build(); + this.sessionRepository = + sessionRepositoryBuilder == null ? null : sessionRepositoryBuilder.build(); this.executor = executor; this.dropSchemaOnClose = dropSchemaOnClose; this.defaultConsistencyLevel = consistencyLevel; this.defaultQueryIdempotency = defaultQueryIdempotency; - this.unitOfWorkClass = unitOfWorkClass; this.metricRegistry = metricRegistry; - this.zipkinTracer = tracer; - - if (sessionCache == null) { - this.sessionCache = SessionCache.defaultCache(); - } else { - this.sessionCache = sessionCache; - } + this.cacheManager = cacheManager; this.valueProvider = new RowColumnValueProvider(this.sessionRepository); this.valuePreparer = new StatementColumnValuePreparer(this.sessionRepository); - this.metadata = session.getCluster().getMetadata(); + this.metadata = session == null ? null : session.getCluster().getMetadata(); + } + + public UnitOfWork begin() { + return new UnitOfWork(this).begin(); + } + + public UnitOfWork begin(UnitOfWork parent) { + return new UnitOfWork(this, parent).begin(); } @Override @@ -153,6 +144,20 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab return this; } + public HelenusSession showQueryValuesInLog(boolean showValues) { + this.showValues = showValues; + return this; + } + + public HelenusSession showQueryValuesInLog() { + this.showValues = true; + return this; + } + + public boolean showValues() { + return showValues; + } + @Override public Executor getExecutor() { return executor; @@ -173,11 +178,6 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab return valuePreparer; } - @Override - public Tracer getZipkinTracer() { - return zipkinTracer; - } - @Override public MetricRegistry getMetricRegistry() { return metricRegistry; @@ -195,13 +195,16 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab @Override public Object checkCache(String tableName, List facets) { - List facetCombinations = CacheUtil.flattenFacets(facets); Object result = null; - for (String[] combination : facetCombinations) { - String cacheKey = tableName + "." + Arrays.toString(combination); - result = sessionCache.get(cacheKey); - if (result != null) { - return result; + if (cacheManager != null) { + Cache cache = cacheManager.getCache(tableName); + if (cache != null) { + for (String key : CacheUtil.flatKeys(tableName, facets)) { + result = cache.get(key); + if (result != null) { + return result; + } + } } } return null; @@ -209,11 +212,12 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab @Override public void cacheEvict(List facets) { - String tableName = CacheUtil.schemaName(facets); - List facetCombinations = CacheUtil.flattenFacets(facets); - for (String[] combination : facetCombinations) { - String cacheKey = tableName + "." + Arrays.toString(combination); - sessionCache.invalidate(cacheKey); + if (cacheManager != null) { + String tableName = CacheUtil.schemaName(facets); + Cache cache = cacheManager.getCache(tableName); + if (cache != null) { + CacheUtil.flatKeys(tableName, facets).forEach(key -> cache.remove(key)); + } } } @@ -235,7 +239,7 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab } } else { value = valueMap.get(prop.getPropertyName()); - binder.setValueForProperty(prop, value.toString()); + if (value != null) binder.setValueForProperty(prop, value.toString()); } } if (binder.isBound()) { @@ -252,144 +256,96 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab @Override public void mergeCache(Table>> uowCache) { - List items = - uowCache - .values() - .stream() - .filter(Either::isLeft) - .map(Either::getLeft) - .distinct() - .collect(Collectors.toList()); - for (Object pojo : items) { - HelenusEntity entity = Helenus.resolve(MappingUtil.getMappingInterface(pojo)); - Map valueMap = - pojo instanceof MapExportable ? ((MapExportable) pojo).toMap() : null; - if (entity.isCacheable()) { - List boundFacets = new ArrayList<>(); - for (Facet facet : entity.getFacets()) { - if (facet instanceof UnboundFacet) { - UnboundFacet unboundFacet = (UnboundFacet) facet; - UnboundFacet.Binder binder = unboundFacet.binder(); - unboundFacet - .getProperties() - .forEach( - prop -> { - if (valueMap == null) { - Object value = - BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop); - binder.setValueForProperty(prop, value.toString()); - } else { - binder.setValueForProperty( - prop, valueMap.get(prop.getPropertyName()).toString()); - } - }); - if (binder.isBound()) { - boundFacets.add(binder.bind()); + if (cacheManager != null) { + List items = + uowCache + .values() + .stream() + .filter(Either::isLeft) + .map(Either::getLeft) + .distinct() + .collect(Collectors.toList()); + for (Object pojo : items) { + HelenusEntity entity = Helenus.resolve(MappingUtil.getMappingInterface(pojo)); + Map valueMap = + pojo instanceof MapExportable ? ((MapExportable) pojo).toMap() : null; + if (entity.isCacheable()) { + List boundFacets = new ArrayList<>(); + for (Facet facet : entity.getFacets()) { + if (facet instanceof UnboundFacet) { + UnboundFacet unboundFacet = (UnboundFacet) facet; + UnboundFacet.Binder binder = unboundFacet.binder(); + unboundFacet + .getProperties() + .forEach( + prop -> { + if (valueMap == null) { + Object value = + BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop); + binder.setValueForProperty(prop, value.toString()); + } else { + Object v = valueMap.get(prop.getPropertyName()); + if (v != null) { + binder.setValueForProperty(prop, v.toString()); + } + } + }); + if (binder.isBound()) { + boundFacets.add(binder.bind()); + } + } else { + boundFacets.add(facet); } - } else { - boundFacets.add(facet); } + List facetCombinations = CacheUtil.flattenFacets(boundFacets); + String tableName = CacheUtil.schemaName(boundFacets); + replaceCachedFacetValues(pojo, tableName, facetCombinations); } - List facetCombinations = CacheUtil.flattenFacets(boundFacets); - String tableName = CacheUtil.schemaName(boundFacets); - replaceCachedFacetValues(pojo, tableName, facetCombinations); } - } - List> deletedFacetSets = - uowCache - .values() - .stream() - .filter(Either::isRight) - .map(Either::getRight) - .collect(Collectors.toList()); - for (List facets : deletedFacetSets) { - String tableName = CacheUtil.schemaName(facets); - List combinations = CacheUtil.flattenFacets(facets); - for (String[] combination : combinations) { - String cacheKey = tableName + "." + Arrays.toString(combination); - sessionCache.invalidate(cacheKey); + List> deletedFacetSets = + uowCache + .values() + .stream() + .filter(Either::isRight) + .map(Either::getRight) + .collect(Collectors.toList()); + for (List facets : deletedFacetSets) { + String tableName = CacheUtil.schemaName(facets); + Cache cache = cacheManager.getCache(tableName); + if (cache != null) { + List keys = CacheUtil.flatKeys(tableName, facets); + keys.forEach(key -> cache.remove(key)); + } } } } private void replaceCachedFacetValues( Object pojo, String tableName, List facetCombinations) { - for (String[] combination : facetCombinations) { - String cacheKey = tableName + "." + Arrays.toString(combination); - sessionCache.invalidate(cacheKey); - sessionCache.put(cacheKey, pojo); + if (cacheManager != null) { + for (String[] combination : facetCombinations) { + String cacheKey = tableName + "." + Arrays.toString(combination); + Cache cache = cacheManager.getCache(tableName); + if (cache != null) { + if (pojo == null || pojo == HelenusSession.deleted) { + cache.remove(cacheKey); + } else { + cache.put(cacheKey, pojo); + } + } + } } } + public CacheManager getCacheManager() { + return cacheManager; + } + public Metadata getMetadata() { return metadata; } - public UnitOfWork begin() { - return this.begin(null); - } - - private String extractClassNameFromStackFrame(String classNameOnStack) { - String name = null; - Matcher m = classNameRegex.matcher(classNameOnStack); - if (m.find()) { - name = (m.group(1) != null) ? m.group(1) : ((m.group(2) != null) ? m.group(2) : name); - } else { - name = classNameOnStack; - } - return name; - } - - public synchronized UnitOfWork begin(UnitOfWork parent) { - try { - Class clazz = unitOfWorkClass; - Constructor ctor = - clazz.getConstructor(HelenusSession.class, UnitOfWork.class); - UnitOfWork uow = ctor.newInstance(this, parent); - if (LOG.isInfoEnabled() && uow.getPurpose() == null) { - StringBuilder purpose = null; - int frame = 0; - StackTraceElement[] trace = Thread.currentThread().getStackTrace(); - String targetClassName = HelenusSession.class.getSimpleName(); - String stackClassName = null; - do { - frame++; - stackClassName = extractClassNameFromStackFrame(trace[frame].getClassName()); - } while (!stackClassName.equals(targetClassName) && frame < trace.length); - do { - frame++; - stackClassName = extractClassNameFromStackFrame(trace[frame].getClassName()); - } while (stackClassName.equals(targetClassName) && frame < trace.length); - if (frame < trace.length) { - purpose = - new StringBuilder() - .append(trace[frame].getClassName()) - .append(".") - .append(trace[frame].getMethodName()) - .append("(") - .append(trace[frame].getFileName()) - .append(":") - .append(trace[frame].getLineNumber()) - .append(")"); - uow.setPurpose(purpose.toString()); - } - } - if (parent != null) { - parent.addNestedUnitOfWork(uow); - } - return uow.begin(); - } catch (NoSuchMethodException - | InvocationTargetException - | InstantiationException - | IllegalAccessException e) { - throw new HelenusException( - String.format( - "Unable to instantiate %s as a UnitOfWork.", unitOfWorkClass.getSimpleName()), - e); - } - } - public SelectOperation select(E pojo) { Objects.requireNonNull( pojo, "supplied object must be a dsl for a registered entity but cannot be null"); @@ -424,9 +380,17 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab return new SelectOperation(this); } - public SelectOperation selectAll(Class entityClass) { + public SelectOperation selectAll(Class entityClass) { Objects.requireNonNull(entityClass, "entityClass is empty"); - return new SelectOperation(this, Helenus.entity(entityClass)); + HelenusEntity entity = Helenus.entity(entityClass); + + return new SelectOperation( + this, + entity, + (r) -> { + Map map = new ValueProviderMap(r, valueProvider, entity); + return (E) Helenus.map(entityClass, map); + }); } public SelectOperation selectAll(E pojo) { @@ -699,23 +663,23 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab } catch (HelenusMappingException e) { } if (entity != null) { - return new InsertOperation(this, entity.getMappingInterface(), true); + return new InsertOperation(this, entity, entity.getMappingInterface(), true); } else { - return this.insert(pojo, null); + return this.insert(pojo, null, null); } } public InsertOperation insert(Drafted draft) { - return insert(draft.build(), draft.mutated()); + return insert(draft.build(), draft.mutated(), draft.read()); } - private InsertOperation insert(T pojo, Set mutations) { + private InsertOperation insert(T pojo, Set mutations, Set read) { Objects.requireNonNull(pojo, "pojo is empty"); Class iface = MappingUtil.getMappingInterface(pojo); HelenusEntity entity = Helenus.entity(iface); - return new InsertOperation(this, entity, pojo, mutations, true); + return new InsertOperation(this, entity, pojo, mutations, read, true); } public InsertOperation upsert() { @@ -727,7 +691,7 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab } public InsertOperation upsert(Drafted draft) { - return this.upsert((T) draft.build(), draft.mutated()); + return this.upsert((T) draft.build(), draft.mutated(), draft.read()); } public InsertOperation upsert(T pojo) { @@ -740,19 +704,19 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab } catch (HelenusMappingException e) { } if (entity != null) { - return new InsertOperation(this, entity.getMappingInterface(), false); + return new InsertOperation(this, entity, entity.getMappingInterface(), false); } else { - return this.upsert(pojo, null); + return this.upsert(pojo, null, null); } } - private InsertOperation upsert(T pojo, Set mutations) { + private InsertOperation upsert(T pojo, Set mutations, Set read) { Objects.requireNonNull(pojo, "pojo is empty"); Class iface = MappingUtil.getMappingInterface(pojo); HelenusEntity entity = Helenus.entity(iface); - return new InsertOperation(this, entity, pojo, mutations, false); + return new InsertOperation(this, entity, pojo, mutations, read, false); } public DeleteOperation delete() { @@ -773,6 +737,9 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab } public void close() { + if (session == null) { + return; + } if (session.isClosed()) { return; @@ -803,11 +770,11 @@ public class HelenusSession extends AbstractSessionOperations implements Closeab switch (entity.getType()) { case TABLE: - execute(SchemaUtil.dropTable(entity), true); + execute(SchemaUtil.dropTable(entity)); break; case UDT: - execute(SchemaUtil.dropUserType(entity), true); + execute(SchemaUtil.dropUserType(entity)); break; default: diff --git a/src/main/java/net/helenus/core/PostCommitFunction.java b/src/main/java/net/helenus/core/PostCommitFunction.java index 5521304..0c823cf 100644 --- a/src/main/java/net/helenus/core/PostCommitFunction.java +++ b/src/main/java/net/helenus/core/PostCommitFunction.java @@ -6,20 +6,43 @@ import java.util.Objects; public class PostCommitFunction implements java.util.function.Function { private final UnitOfWork uow; - private final List postCommit; + private final List commitThunks; + private final List abortThunks; + private boolean committed; - PostCommitFunction(UnitOfWork uow, List postCommit) { + PostCommitFunction( + UnitOfWork uow, + List postCommit, + List abortThunks, + boolean committed) { this.uow = uow; - this.postCommit = postCommit; + this.commitThunks = postCommit; + this.abortThunks = abortThunks; + this.committed = committed; } - public void andThen(CommitThunk after) { + public PostCommitFunction andThen(CommitThunk after) { Objects.requireNonNull(after); - if (postCommit == null) { - after.apply(); + if (commitThunks == null) { + if (committed) { + after.apply(); + } } else { - postCommit.add(after); + commitThunks.add(after); } + return this; + } + + public PostCommitFunction orElse(CommitThunk after) { + Objects.requireNonNull(after); + if (abortThunks == null) { + if (!committed) { + after.apply(); + } + } else { + abortThunks.add(after); + } + return this; } @Override diff --git a/src/main/java/net/helenus/core/SchemaUtil.java b/src/main/java/net/helenus/core/SchemaUtil.java index 721e512..4cf745b 100644 --- a/src/main/java/net/helenus/core/SchemaUtil.java +++ b/src/main/java/net/helenus/core/SchemaUtil.java @@ -165,6 +165,14 @@ public final class SchemaUtil { } } + if (p.size() == 0 && c.size() == 0) + return "{" + + properties + .stream() + .map(HelenusProperty::getPropertyName) + .collect(Collectors.joining(", ")) + + "}"; + return "(" + ((p.size() > 1) ? "(" + String.join(", ", p) + ")" : p.get(0)) + ((c.size() > 0) @@ -329,7 +337,7 @@ public final class SchemaUtil { public static SchemaStatement createIndex(HelenusProperty prop) { if (prop.caseSensitiveIndex()) { - return SchemaBuilder.createIndex(prop.getIndexName().get().toCql()) + return SchemaBuilder.createIndex(indexName(prop)) .ifNotExists() .onTable(prop.getEntity().getName().toCql()) .andColumn(prop.getColumnName().toCql()); @@ -398,7 +406,7 @@ public final class SchemaUtil { } public static SchemaStatement dropIndex(HelenusProperty prop) { - return SchemaBuilder.dropIndex(prop.getIndexName().get().toCql()).ifExists(); + return SchemaBuilder.dropIndex(indexName(prop)).ifExists(); } private static SchemaBuilder.Direction mapDirection(OrderingDirection o) { @@ -457,4 +465,9 @@ public final class SchemaUtil { } return null; } + + private static String indexName(HelenusProperty prop) { + return prop.getEntity().getName().toCql() + "_" + prop.getIndexName().get().toCql(); + } + } diff --git a/src/main/java/net/helenus/core/SessionInitializer.java b/src/main/java/net/helenus/core/SessionInitializer.java index c77379d..c66d1db 100644 --- a/src/main/java/net/helenus/core/SessionInitializer.java +++ b/src/main/java/net/helenus/core/SessionInitializer.java @@ -15,7 +15,6 @@ */ package net.helenus.core; -import brave.Tracer; import com.codahale.metrics.MetricRegistry; import com.datastax.driver.core.*; import com.google.common.util.concurrent.MoreExecutors; @@ -25,7 +24,7 @@ import java.util.*; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.function.Consumer; -import net.helenus.core.cache.SessionCache; +import javax.cache.CacheManager; import net.helenus.core.reflect.DslExportable; import net.helenus.mapping.HelenusEntity; import net.helenus.mapping.HelenusEntityType; @@ -43,19 +42,26 @@ public final class SessionInitializer extends AbstractSessionOperations { private CodecRegistry registry; private String usingKeyspace; private boolean showCql = false; + private boolean showValues = true; private ConsistencyLevel consistencyLevel; - private boolean idempotent = true; + private boolean idempotent = false; private MetricRegistry metricRegistry = new MetricRegistry(); - private Tracer zipkinTracer; private PrintStream printStream = System.out; private Executor executor = MoreExecutors.directExecutor(); - private Class unitOfWorkClass = UnitOfWorkImpl.class; private SessionRepositoryBuilder sessionRepository; private boolean dropUnusedColumns = false; private boolean dropUnusedIndexes = false; private KeyspaceMetadata keyspaceMetadata; private AutoDdl autoDdl = AutoDdl.UPDATE; - private SessionCache sessionCache = null; + private CacheManager cacheManager = null; + + SessionInitializer(Session session, String keyspace) { + this.session = session; + this.usingKeyspace = keyspace; + if (session != null) { + this.sessionRepository = new SessionRepositoryBuilder(session); + } + } SessionInitializer(Session session) { this.session = Objects.requireNonNull(session, "empty session"); @@ -103,28 +109,32 @@ public final class SessionInitializer extends AbstractSessionOperations { return this; } + public SessionInitializer showQueryValuesInLog(boolean showValues) { + this.showValues = showValues; + return this; + } + + public SessionInitializer showQueryValuesInLog() { + this.showValues = true; + return this; + } + + public boolean showValues() { + return showValues; + } + public SessionInitializer metricRegistry(MetricRegistry metricRegistry) { this.metricRegistry = metricRegistry; return this; } - public SessionInitializer zipkinTracer(Tracer tracer) { - this.zipkinTracer = tracer; - return this; - } - - public SessionInitializer setUnitOfWorkClass(Class e) { - this.unitOfWorkClass = e; - return this; - } - public SessionInitializer consistencyLevel(ConsistencyLevel consistencyLevel) { this.consistencyLevel = consistencyLevel; return this; } - public SessionInitializer setSessionCache(SessionCache sessionCache) { - this.sessionCache = sessionCache; + public SessionInitializer setCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; return this; } @@ -132,6 +142,11 @@ public final class SessionInitializer extends AbstractSessionOperations { return consistencyLevel; } + public SessionInitializer setOperationsIdempotentByDefault() { + this.idempotent = true; + return this; + } + public SessionInitializer idempotentQueryExecution(boolean idempotent) { this.idempotent = idempotent; return this; @@ -233,8 +248,10 @@ public final class SessionInitializer extends AbstractSessionOperations { } public SessionInitializer use(String keyspace) { - session.execute(SchemaUtil.use(keyspace, false)); - this.usingKeyspace = keyspace; + if (session != null) { + session.execute(SchemaUtil.use(keyspace, false)); + this.usingKeyspace = keyspace; + } return this; } @@ -255,16 +272,15 @@ public final class SessionInitializer extends AbstractSessionOperations { usingKeyspace, registry, showCql, + showValues, printStream, sessionRepository, executor, autoDdl == AutoDdl.CREATE_DROP, consistencyLevel, idempotent, - unitOfWorkClass, - sessionCache, - metricRegistry, - zipkinTracer); + cacheManager, + metricRegistry); } private void initialize() { @@ -281,10 +297,16 @@ public final class SessionInitializer extends AbstractSessionOperations { } DslExportable dsl = (DslExportable) Helenus.dsl(iface); - dsl.setCassandraMetadataForHelenusSession(session.getCluster().getMetadata()); - sessionRepository.add(dsl); + if (session != null) { + dsl.setCassandraMetadataForHelenusSession(session.getCluster().getMetadata()); + } + if (sessionRepository != null) { + sessionRepository.add(dsl); + } }); + if (session == null) return; + TableOperations tableOps = new TableOperations(this, dropUnusedColumns, dropUnusedIndexes); UserTypeOperations userTypeOps = new UserTypeOperations(this, dropUnusedColumns); diff --git a/src/main/java/net/helenus/core/TableOperations.java b/src/main/java/net/helenus/core/TableOperations.java index 50108a6..e6ce20e 100644 --- a/src/main/java/net/helenus/core/TableOperations.java +++ b/src/main/java/net/helenus/core/TableOperations.java @@ -35,12 +35,12 @@ public final class TableOperations { } public void createTable(HelenusEntity entity) { - sessionOps.execute(SchemaUtil.createTable(entity), true); + sessionOps.execute(SchemaUtil.createTable(entity)); executeBatch(SchemaUtil.createIndexes(entity)); } public void dropTable(HelenusEntity entity) { - sessionOps.execute(SchemaUtil.dropTable(entity), true); + sessionOps.execute(SchemaUtil.dropTable(entity)); } public void validateTable(TableMetadata tmd, HelenusEntity entity) { @@ -79,17 +79,14 @@ public final class TableOperations { public void createView(HelenusEntity entity) { sessionOps.execute( SchemaUtil.createMaterializedView( - sessionOps.usingKeyspace(), entity.getName().toCql(), entity), - true); - // executeBatch(SchemaUtil.createIndexes(entity)); NOTE: Unfortunately C* 3.10 - // does not yet support 2i on materialized views. + sessionOps.usingKeyspace(), entity.getName().toCql(), entity)); + // executeBatch(SchemaUtil.createIndexes(entity)); NOTE: Unfortunately C* 3.10 does not yet support 2i on materialized views. } public void dropView(HelenusEntity entity) { sessionOps.execute( SchemaUtil.dropMaterializedView( - sessionOps.usingKeyspace(), entity.getName().toCql(), entity), - true); + sessionOps.usingKeyspace(), entity.getName().toCql(), entity)); } public void updateView(TableMetadata tmd, HelenusEntity entity) { @@ -104,9 +101,6 @@ public final class TableOperations { private void executeBatch(List list) { - list.forEach( - s -> { - sessionOps.execute(s, true); - }); + list.forEach(s -> sessionOps.execute(s)); } } diff --git a/src/main/java/net/helenus/core/UnitOfWork.java b/src/main/java/net/helenus/core/UnitOfWork.java index 76bbc43..c5828eb 100644 --- a/src/main/java/net/helenus/core/UnitOfWork.java +++ b/src/main/java/net/helenus/core/UnitOfWork.java @@ -15,12 +15,164 @@ */ package net.helenus.core; -import com.google.common.base.Stopwatch; -import java.util.List; -import java.util.Optional; -import net.helenus.core.cache.Facet; +import static net.helenus.core.HelenusSession.deleted; -public interface UnitOfWork extends AutoCloseable { +import com.google.common.base.Stopwatch; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.common.collect.TreeTraverser; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.integration.CacheLoader; +import javax.cache.integration.CacheLoaderException; +import net.helenus.core.cache.CacheUtil; +import net.helenus.core.cache.Facet; +import net.helenus.core.cache.MapCache; +import net.helenus.core.operation.AbstractOperation; +import net.helenus.core.operation.BatchOperation; +import net.helenus.mapping.MappingUtil; +import net.helenus.support.Either; +import net.helenus.support.HelenusException; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Encapsulates the concept of a "transaction" as a unit-of-work. */ +public class UnitOfWork implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(UnitOfWork.class); + private static final Pattern classNameRegex = + Pattern.compile("^(?:\\w+\\.)+(?:(\\w+)|(\\w+)\\$.*)$"); + + private final List nested = new ArrayList<>(); + private final HelenusSession session; + public final UnitOfWork parent; + private final Table>> cache = HashBasedTable.create(); + private final MapCache statementCache; + protected String purpose; + protected List nestedPurposes = new ArrayList(); + protected String info; + protected int cacheHits = 0; + protected int cacheMisses = 0; + protected int databaseLookups = 0; + protected final Stopwatch elapsedTime; + protected Map databaseTime = new HashMap<>(); + protected double cacheLookupTimeMSecs = 0.0; + private List commitThunks = new ArrayList(); + private List abortThunks = new ArrayList(); + private List> asyncOperationFutures = new ArrayList>(); + private boolean aborted = false; + private boolean committed = false; + private long committedAt = 0L; + private BatchOperation batch; + + private String extractClassNameFromStackFrame(String classNameOnStack) { + String name = null; + Matcher m = classNameRegex.matcher(classNameOnStack); + if (m.find()) { + name = (m.group(1) != null) ? m.group(1) : ((m.group(2) != null) ? m.group(2) : name); + } else { + name = classNameOnStack; + } + return name; + } + + public UnitOfWork(HelenusSession session) { + this(session, null); + } + + public UnitOfWork(HelenusSession session, UnitOfWork parent) { + Objects.requireNonNull(session, "containing session cannot be null"); + + this.parent = parent; + if (parent != null) { + parent.addNestedUnitOfWork(this); + } + this.session = session; + CacheLoader cacheLoader = null; + if (parent != null) { + cacheLoader = + new CacheLoader() { + + Cache cache = parent.getCache(); + + @Override + public Object load(String key) throws CacheLoaderException { + return cache.get(key); + } + + @Override + public Map loadAll(Iterable keys) + throws CacheLoaderException { + Map kvp = new HashMap(); + for (String key : keys) { + kvp.put(key, cache.get(key)); + } + return kvp; + } + }; + } + this.elapsedTime = Stopwatch.createUnstarted(); + this.statementCache = + new MapCache(null, "UOW(" + hashCode() + ")", cacheLoader, true); + + if (LOG.isInfoEnabled()) { + StringBuilder purpose = null; + int frame = 0; + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + String targetClassName = HelenusSession.class.getSimpleName(); + String stackClassName = null; + do { + frame++; + stackClassName = extractClassNameFromStackFrame(trace[frame].getClassName()); + } while (!stackClassName.equals(targetClassName) && frame < trace.length); + do { + frame++; + stackClassName = extractClassNameFromStackFrame(trace[frame].getClassName()); + } while (stackClassName.equals(targetClassName) && frame < trace.length); + if (frame < trace.length) { + purpose = + new StringBuilder() + .append(trace[frame].getClassName()) + .append(".") + .append(trace[frame].getMethodName()) + .append("(") + .append(trace[frame].getFileName()) + .append(":") + .append(trace[frame].getLineNumber()) + .append(")"); + this.purpose = purpose.toString(); + } + } + } + + public void addDatabaseTime(String name, Stopwatch amount) { + Double time = databaseTime.get(name); + if (time == null) { + databaseTime.put(name, (double) amount.elapsed(TimeUnit.MICROSECONDS)); + } else { + databaseTime.put(name, time + amount.elapsed(TimeUnit.MICROSECONDS)); + } + } + + public void addCacheLookupTime(Stopwatch amount) { + cacheLookupTimeMSecs += amount.elapsed(TimeUnit.MICROSECONDS); + } + + public void addNestedUnitOfWork(UnitOfWork uow) { + synchronized (nested) { + nested.add(uow); + } + } /** * Marks the beginning of a transactional section of work. Will write a @@ -28,44 +180,437 @@ public interface UnitOfWork extends AutoCloseable { * * @return the handle used to commit or abort the work. */ - UnitOfWork begin(); + public synchronized UnitOfWork begin() { + elapsedTime.start(); + // log.record(txn::start) + return this; + } - void addNestedUnitOfWork(UnitOfWork uow); + public String getPurpose() { + return purpose; + } + + public UnitOfWork setPurpose(String purpose) { + this.purpose = purpose; + return this; + } + + public void addFuture(CompletableFuture future) { + asyncOperationFutures.add(future); + } + + public void setInfo(String info) { + this.info = info; + } + + public void recordCacheAndDatabaseOperationCount(int cache, int ops) { + if (cache > 0) { + cacheHits += cache; + } else { + cacheMisses += Math.abs(cache); + } + if (ops > 0) { + databaseLookups += ops; + } + } + + public String logTimers(String what) { + double e = (double) elapsedTime.elapsed(TimeUnit.MICROSECONDS) / 1000.0; + double d = 0.0; + double c = cacheLookupTimeMSecs / 1000.0; + double fc = (c / e) * 100.0; + String database = ""; + if (databaseTime.size() > 0) { + List dbt = new ArrayList<>(databaseTime.size()); + for (Map.Entry dt : databaseTime.entrySet()) { + double t = dt.getValue() / 1000.0; + d += t; + dbt.add(String.format("%s took %,.3fms %,2.2f%%", dt.getKey(), t, (t / e) * 100.0)); + } + double fd = (d / e) * 100.0; + database = + String.format( + ", %d quer%s (%,.3fms %,2.2f%% - %s)", + databaseLookups, (databaseLookups > 1) ? "ies" : "y", d, fd, String.join(", ", dbt)); + } + String cache = ""; + if (cacheLookupTimeMSecs > 0) { + int cacheLookups = cacheHits + cacheMisses; + cache = + String.format( + " with %d cache lookup%s (%,.3fms %,2.2f%% - %,d hit, %,d miss)", + cacheLookups, cacheLookups > 1 ? "s" : "", c, fc, cacheHits, cacheMisses); + } + String da = ""; + if (databaseTime.size() > 0 || cacheLookupTimeMSecs > 0) { + double dat = d + c; + double daf = (dat / e) * 100; + da = + String.format( + " consuming %,.3fms for data access, or %,2.2f%% of total UOW time.", dat, daf); + } + String x = nestedPurposes.stream().distinct().collect(Collectors.joining(", ")); + String n = + nested + .stream() + .map(uow -> String.valueOf(uow.hashCode())) + .collect(Collectors.joining(", ")); + String s = + String.format( + Locale.US, + "UOW(%s%s) %s in %,.3fms%s%s%s%s%s%s", + hashCode(), + (nested.size() > 0 ? ", [" + n + "]" : ""), + what, + e, + cache, + database, + da, + (purpose == null ? "" : " " + purpose), + (nestedPurposes.isEmpty()) ? "" : ", " + x, + (info == null) ? "" : " " + info); + return s; + } + + private void applyPostCommitFunctions(String what, List thunks) { + if (!thunks.isEmpty()) { + for (CommitThunk f : thunks) { + f.apply(); + } + } + } + + public Optional cacheLookup(List facets) { + String tableName = CacheUtil.schemaName(facets); + Optional result = Optional.empty(); + for (Facet facet : facets) { + if (!facet.fixed()) { + String columnName = facet.name() + "==" + facet.value(); + Either> eitherValue = cache.get(tableName, columnName); + if (eitherValue != null) { + Object value = deleted; + if (eitherValue.isLeft()) { + value = eitherValue.getLeft(); + } + return Optional.of(value); + } + } + } + + // Be sure to check all enclosing UnitOfWork caches as well, we may be nested. + result = checkParentCache(facets); + if (result.isPresent()) { + Object r = result.get(); + Class iface = MappingUtil.getMappingInterface(r); + if (Helenus.entity(iface).isDraftable()) { + cacheUpdate(r, facets); + } else { + cacheUpdate(SerializationUtils.clone((Serializable) r), facets); + } + } + return result; + } + + private Optional checkParentCache(List facets) { + Optional result = Optional.empty(); + if (parent != null) { + result = parent.checkParentCache(facets); + } + return result; + } + + public List cacheEvict(List facets) { + Either> deletedObjectFacets = Either.right(facets); + String tableName = CacheUtil.schemaName(facets); + Optional optionalValue = cacheLookup(facets); + + for (Facet facet : facets) { + if (!facet.fixed()) { + String columnKey = facet.name() + "==" + facet.value(); + // mark the value identified by the facet to `deleted` + cache.put(tableName, columnKey, deletedObjectFacets); + } + } + + // Now, look for other row/col pairs that referenced the same object, mark them + // `deleted` if the cache had a value before we added the deleted marker objects. + if (optionalValue.isPresent()) { + Object value = optionalValue.get(); + cache + .columnKeySet() + .forEach( + columnKey -> { + Either> eitherCachedValue = cache.get(tableName, columnKey); + if (eitherCachedValue.isLeft()) { + Object cachedValue = eitherCachedValue.getLeft(); + if (cachedValue == value) { + cache.put(tableName, columnKey, deletedObjectFacets); + String[] parts = columnKey.split("=="); + facets.add(new Facet(parts[0], parts[1])); + } + } + }); + } + return facets; + } + + public Cache getCache() { + return statementCache; + } + + public Object cacheUpdate(Object value, List facets) { + Object result = null; + String tableName = CacheUtil.schemaName(facets); + for (Facet facet : facets) { + if (!facet.fixed()) { + if (facet.alone()) { + String columnName = facet.name() + "==" + facet.value(); + if (result == null) result = cache.get(tableName, columnName); + cache.put(tableName, columnName, Either.left(value)); + } + } + } + return result; + } + + public void batch(AbstractOperation s) { + if (batch == null) { + batch = new BatchOperation(session); + } + batch.add(s); + } + + private Iterator getChildNodes() { + return nested.iterator(); + } /** * Checks to see if the work performed between calling begin and now can be committed or not. * * @return a function from which to chain work that only happens when commit is successful - * @throws X when the work overlaps with other concurrent writers. + * @throws HelenusException when the work overlaps with other concurrent writers. */ - PostCommitFunction commit() throws X; + public synchronized PostCommitFunction commit() + throws HelenusException, TimeoutException { + + if (isDone()) { + return new PostCommitFunction(this, null, null, false); + } + + // Only the outer-most UOW batches statements for commit time, execute them. + if (batch != null) { + committedAt = batch.sync(this); //TODO(gburd): update cache with writeTime... + } + + // All nested UnitOfWork should be committed (not aborted) before calls to + // commit, check. + boolean canCommit = true; + TreeTraverser traverser = TreeTraverser.using(node -> node::getChildNodes); + for (UnitOfWork uow : traverser.postOrderTraversal(this)) { + if (this != uow) { + canCommit &= (!uow.aborted && uow.committed); + } + } + + if (!canCommit) { + + if (parent == null) { + + // Apply all post-commit abort functions, this is the outer-most UnitOfWork. + traverser + .postOrderTraversal(this) + .forEach( + uow -> { + applyPostCommitFunctions("aborted", abortThunks); + }); + + elapsedTime.stop(); + if (LOG.isInfoEnabled()) { + LOG.info(logTimers("aborted")); + } + } + + return new PostCommitFunction(this, null, null, false); + } else { + committed = true; + aborted = false; + + if (parent == null) { + + // Apply all post-commit commit functions, this is the outer-most UnitOfWork. + traverser + .postOrderTraversal(this) + .forEach( + uow -> { + applyPostCommitFunctions("committed", uow.commitThunks); + }); + + // Merge our statement cache into the session cache if it exists. + CacheManager cacheManager = session.getCacheManager(); + if (cacheManager != null) { + for (Map.Entry entry : + (Set>) statementCache.unwrap(Map.class).entrySet()) { + String[] keyParts = entry.getKey().split("\\."); + if (keyParts.length == 2) { + String cacheName = keyParts[0]; + String key = keyParts[1]; + if (!StringUtils.isBlank(cacheName) && !StringUtils.isBlank(key)) { + Cache cache = cacheManager.getCache(cacheName); + if (cache != null) { + Object value = entry.getValue(); + if (value == deleted) { + cache.remove(key); + } else { + cache.put(key.toString(), value); + } + } + } + } + } + } + + // Merge our cache into the session cache. + session.mergeCache(cache); + + // Spoil any lingering futures that may be out there. + asyncOperationFutures.forEach( + f -> + f.completeExceptionally( + new HelenusException( + "Futures must be resolved before their unit of work has committed/aborted."))); + + elapsedTime.stop(); + if (LOG.isInfoEnabled()) { + LOG.info(logTimers("committed")); + } + + return new PostCommitFunction(this, null, null, true); + } else { + + // Merge cache and statistics into parent if there is one. + parent.statementCache.putAll(statementCache.unwrap(Map.class)); + parent.mergeCache(cache); + parent.addBatched(batch); + if (purpose != null) { + parent.nestedPurposes.add(purpose); + } + parent.cacheHits += cacheHits; + parent.cacheMisses += cacheMisses; + parent.databaseLookups += databaseLookups; + parent.cacheLookupTimeMSecs += cacheLookupTimeMSecs; + for (Map.Entry dt : databaseTime.entrySet()) { + String name = dt.getKey(); + if (parent.databaseTime.containsKey(name)) { + double t = parent.databaseTime.get(name); + parent.databaseTime.put(name, t + dt.getValue()); + } else { + parent.databaseTime.put(name, dt.getValue()); + } + } + } + } + // TODO(gburd): hopefully we'll be able to detect conflicts here and so we'd want to... + // else { + // Constructor ctor = clazz.getConstructor(conflictExceptionClass); + // T object = ctor.newInstance(new Object[] { String message }); + // } + return new PostCommitFunction(this, commitThunks, abortThunks, true); + } + + private void addBatched(BatchOperation batch) { + if (batch != null) { + if (this.batch == null) { + this.batch = batch; + } else { + this.batch.addAll(batch); + } + } + } /** * Explicitly abort the work within this unit of work. Any nested aborted unit of work will * trigger the entire unit of work to commit. */ - void abort(); + public synchronized void abort() { + if (!aborted) { + aborted = true; - boolean hasAborted(); + // Spoil any pending futures created within the context of this unit of work. + asyncOperationFutures.forEach( + f -> + f.completeExceptionally( + new HelenusException( + "Futures must be resolved before their unit of work has committed/aborted."))); - boolean hasCommitted(); + TreeTraverser traverser = TreeTraverser.using(node -> node::getChildNodes); + traverser + .postOrderTraversal(this) + .forEach( + uow -> { + applyPostCommitFunctions("aborted", uow.abortThunks); + uow.abortThunks.clear(); + }); - Optional cacheLookup(List facets); + if (parent == null) { + elapsedTime.stop(); + if (LOG.isInfoEnabled()) { + LOG.info(logTimers("aborted")); + } + } - void cacheUpdate(Object pojo, List facets); + // TODO(gburd): when we integrate the transaction support we'll need to... + // log.record(txn::abort) + // cache.invalidateSince(txn::start time) + } + } - List cacheEvict(List facets); + private void mergeCache(Table>> from) { + Table>> to = this.cache; + from.rowMap() + .forEach( + (rowKey, columnMap) -> { + columnMap.forEach( + (columnKey, value) -> { + if (to.contains(rowKey, columnKey)) { + to.put( + rowKey, + columnKey, + Either.left( + CacheUtil.merge( + to.get(rowKey, columnKey).getLeft(), + from.get(rowKey, columnKey).getLeft()))); + } else { + to.put(rowKey, columnKey, from.get(rowKey, columnKey)); + } + }); + }); + } - String getPurpose(); + public boolean isDone() { + return aborted || committed; + } - UnitOfWork setPurpose(String purpose); + public String describeConflicts() { + return "it's complex..."; + } - void setInfo(String info); + @Override + public void close() throws HelenusException { + // Closing a UnitOfWork will abort iff we've not already aborted or committed this unit of work. + if (aborted == false && committed == false) { + abort(); + } + } - void addDatabaseTime(String name, Stopwatch amount); + public boolean hasAborted() { + return aborted; + } - void addCacheLookupTime(Stopwatch amount); + public boolean hasCommitted() { + return committed; + } - // Cache > 0 means "cache hit", < 0 means cache miss. - void recordCacheAndDatabaseOperationCount(int cache, int database); + public long committedAt() { + return committedAt; + } } diff --git a/src/main/java/net/helenus/core/UserTypeOperations.java b/src/main/java/net/helenus/core/UserTypeOperations.java index 2c18339..a90ddc1 100644 --- a/src/main/java/net/helenus/core/UserTypeOperations.java +++ b/src/main/java/net/helenus/core/UserTypeOperations.java @@ -33,12 +33,12 @@ public final class UserTypeOperations { public void createUserType(HelenusEntity entity) { - sessionOps.execute(SchemaUtil.createUserType(entity), true); + sessionOps.execute(SchemaUtil.createUserType(entity)); } public void dropUserType(HelenusEntity entity) { - sessionOps.execute(SchemaUtil.dropUserType(entity), true); + sessionOps.execute(SchemaUtil.dropUserType(entity)); } public void validateUserType(UserType userType, HelenusEntity entity) { @@ -71,9 +71,6 @@ public final class UserTypeOperations { private void executeBatch(List list) { - list.forEach( - s -> { - sessionOps.execute(s, true); - }); + list.forEach(s -> sessionOps.execute(s)); } } diff --git a/src/main/java/net/helenus/core/cache/BoundFacet.java b/src/main/java/net/helenus/core/cache/BoundFacet.java index 9ecbd83..648818c 100644 --- a/src/main/java/net/helenus/core/cache/BoundFacet.java +++ b/src/main/java/net/helenus/core/cache/BoundFacet.java @@ -17,6 +17,7 @@ package net.helenus.core.cache; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import net.helenus.mapping.HelenusProperty; @@ -29,6 +30,10 @@ public class BoundFacet extends Facet { this.properties.put(property, value); } + public Set getProperties() { + return properties.keySet(); + } + public BoundFacet(String name, Map properties) { super( name, diff --git a/src/main/java/net/helenus/core/cache/CacheUtil.java b/src/main/java/net/helenus/core/cache/CacheUtil.java index a2c2e9f..aa05200 100644 --- a/src/main/java/net/helenus/core/cache/CacheUtil.java +++ b/src/main/java/net/helenus/core/cache/CacheUtil.java @@ -1,8 +1,17 @@ package net.helenus.core.cache; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import net.helenus.core.Helenus; +import net.helenus.core.reflect.Entity; +import net.helenus.core.reflect.MapExportable; +import net.helenus.mapping.HelenusEntity; +import net.helenus.mapping.HelenusProperty; +import net.helenus.mapping.MappingUtil; +import net.helenus.mapping.value.BeanColumnValueProvider; public class CacheUtil { @@ -29,6 +38,16 @@ public class CacheUtil { } } + public static List flatKeys(String table, List facets) { + return flattenFacets(facets) + .stream() + .map( + combination -> { + return table + "." + Arrays.toString(combination); + }) + .collect(Collectors.toList()); + } + public static List flattenFacets(List facets) { List combinations = CacheUtil.combinations( @@ -41,26 +60,137 @@ public class CacheUtil { return facet.name() + "==" + facet.value(); }) .collect(Collectors.toList())); + // TODO(gburd): rework so as to not generate the combinations at all rather than filter + facets = + facets + .stream() + .filter(f -> !f.fixed()) + .filter(f -> !f.alone() || !f.combined()) + .collect(Collectors.toList()); + for (Facet facet : facets) { + combinations = + combinations + .stream() + .filter( + combo -> { + // When used alone, this facet is not distinct so don't use it as a key. + if (combo.length == 1) { + if (!facet.alone() && combo[0].startsWith(facet.name() + "==")) { + return false; + } + } else { + if (!facet.combined()) { + for (String c : combo) { + // Don't use this facet in combination with others to create keys. + if (c.startsWith(facet.name() + "==")) { + return false; + } + } + } + } + return true; + }) + .collect(Collectors.toList()); + } return combinations; } - public static Object merge(Object to, Object from) { - if (to == from) { + /** Merge changed values in the map behind `from` into `to`. */ + public static Object merge(Object t, Object f) { + HelenusEntity entity = Helenus.resolve(MappingUtil.getMappingInterface(t)); + + if (t == f) return t; + if (f == null) return t; + if (t == null) return f; + + if (t instanceof MapExportable + && t instanceof Entity + && f instanceof MapExportable + && f instanceof Entity) { + Entity to = (Entity) t; + Entity from = (Entity) f; + Map toValueMap = ((MapExportable) to).toMap(); + Map fromValueMap = ((MapExportable) from).toMap(); + for (HelenusProperty prop : entity.getOrderedProperties()) { + switch (prop.getColumnType()) { + case PARTITION_KEY: + case CLUSTERING_COLUMN: + continue; + default: + Object toVal = BeanColumnValueProvider.INSTANCE.getColumnValue(to, -1, prop, false); + Object fromVal = BeanColumnValueProvider.INSTANCE.getColumnValue(from, -1, prop, false); + String ttlKey = ttlKey(prop); + String writeTimeKey = writeTimeKey(prop); + int[] toTtlI = (int[]) toValueMap.get(ttlKey); + int toTtl = (toTtlI != null) ? toTtlI[0] : 0; + Long toWriteTime = (Long) toValueMap.get(writeTimeKey); + int[] fromTtlI = (int[]) fromValueMap.get(ttlKey); + int fromTtl = (fromTtlI != null) ? fromTtlI[0] : 0; + Long fromWriteTime = (Long) fromValueMap.get(writeTimeKey); + + if (toVal != null) { + if (fromVal != null) { + if (toVal == fromVal) { + // Case: object identity + // Goal: ensure write time and ttl are also in sync + if (fromWriteTime != null + && fromWriteTime != 0L + && (toWriteTime == null || fromWriteTime > toWriteTime)) { + ((MapExportable) to).put(writeTimeKey, fromWriteTime); + } + if (fromTtl > 0 && fromTtl > toTtl) { + ((MapExportable) to).put(ttlKey, fromTtl); + } + } else if (fromWriteTime != null && fromWriteTime != 0L) { + // Case: to exists and from exists + // Goal: copy over from -> to iff from.writeTime > to.writeTime + if (toWriteTime != null && toWriteTime != 0L) { + if (fromWriteTime > toWriteTime) { + ((MapExportable) to).put(prop.getPropertyName(), fromVal); + ((MapExportable) to).put(writeTimeKey, fromWriteTime); + if (fromTtl > 0) { + ((MapExportable) to).put(ttlKey, fromTtl); + } + } + } else { + ((MapExportable) to).put(prop.getPropertyName(), fromVal); + ((MapExportable) to).put(writeTimeKey, fromWriteTime); + if (fromTtl > 0) { + ((MapExportable) to).put(ttlKey, fromTtl); + } + } + } else { + if (toWriteTime == null || toWriteTime == 0L) { + // Caution, entering grey area... + if (!toVal.equals(fromVal)) { + // dangerous waters here, values diverge without information that enables resolution, + // policy (for now) is to move value from -> to anyway. + ((MapExportable) to).put(prop.getPropertyName(), fromVal); + if (fromTtl > 0) { + ((MapExportable) to).put(ttlKey, fromTtl); + } + } + } + } + } + } else { + // Case: from exists, but to doesn't (it's null) + // Goal: copy over from -> to, include ttl and writeTime if present + if (fromVal != null) { + ((MapExportable) to).put(prop.getPropertyName(), fromVal); + if (fromWriteTime != null && fromWriteTime != 0L) { + ((MapExportable) to).put(writeTimeKey, fromWriteTime); + } + if (fromTtl > 0) { + ((MapExportable) to).put(ttlKey, fromTtl); + } + } + } + } + } return to; - } else { - return from; } - /* - * // TODO(gburd): take ttl and writeTime into account when merging. Map toValueMap = to instanceof MapExportable ? ((MapExportable) - * to).toMap() : null; Map fromValueMap = to instanceof - * MapExportable ? ((MapExportable) from).toMap() : null; - * - * if (toValueMap != null && fromValueMap != null) { for (String key : - * fromValueMap.keySet()) { if (toValueMap.containsKey(key) && - * toValueMap.get(key) != fromValueMap.get(key)) { toValueMap.put(key, - * fromValueMap.get(key)); } } } return to; - */ + return t; } public static String schemaName(List facets) { @@ -70,4 +200,22 @@ public class CacheUtil { .map(facet -> facet.value().toString()) .collect(Collectors.joining(".")); } + + public static String writeTimeKey(HelenusProperty prop) { + return writeTimeKey(prop.getColumnName().toCql(false)); + } + + public static String ttlKey(HelenusProperty prop) { + return ttlKey(prop.getColumnName().toCql(false)); + } + + public static String writeTimeKey(String columnName) { + String key = "_" + columnName + "_writeTime"; + return key.toLowerCase(); + } + + public static String ttlKey(String columnName) { + String key = "_" + columnName + "_ttl"; + return key.toLowerCase(); + } } diff --git a/src/main/java/net/helenus/core/cache/Facet.java b/src/main/java/net/helenus/core/cache/Facet.java index 27eb52b..a363fa9 100644 --- a/src/main/java/net/helenus/core/cache/Facet.java +++ b/src/main/java/net/helenus/core/cache/Facet.java @@ -21,6 +21,8 @@ public class Facet { private final String name; private T value; private boolean fixed = false; + private boolean alone = true; + private boolean combined = true; public Facet(String name) { this.name = name; @@ -47,4 +49,20 @@ public class Facet { public boolean fixed() { return fixed; } + + public void setUniquelyIdentifyingWhenAlone(boolean alone) { + this.alone = alone; + } + + public void setUniquelyIdentifyingWhenCombined(boolean combined) { + this.combined = combined; + } + + public boolean alone() { + return alone; + } + + public boolean combined() { + return combined; + } } diff --git a/src/main/java/net/helenus/core/cache/MapCache.java b/src/main/java/net/helenus/core/cache/MapCache.java new file mode 100644 index 0000000..2cef0b6 --- /dev/null +++ b/src/main/java/net/helenus/core/cache/MapCache.java @@ -0,0 +1,457 @@ +package net.helenus.core.cache; + + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.CacheEntryListenerConfiguration; +import javax.cache.configuration.Configuration; +import javax.cache.event.CacheEntryRemovedListener; +import javax.cache.integration.CacheLoader; +import javax.cache.integration.CompletionListener; +import javax.cache.processor.EntryProcessor; +import javax.cache.processor.EntryProcessorException; +import javax.cache.processor.EntryProcessorResult; +import javax.cache.processor.MutableEntry; + +public class MapCache implements Cache { + private final CacheManager manager; + private final String name; + private Map map = new ConcurrentHashMap(); + private Set cacheEntryRemovedListeners = new HashSet<>(); + private CacheLoader cacheLoader = null; + private boolean isReadThrough = false; + private Configuration configuration = new MapConfiguration(); + + private static class MapConfiguration implements Configuration { + + @Override + public Class getKeyType() { + return null; + } + + @Override + public Class getValueType() { + return null; + } + + @Override + public boolean isStoreByValue() { + return false; + } + } + + public MapCache( + CacheManager manager, String name, CacheLoader cacheLoader, boolean isReadThrough) { + this.manager = manager; + this.name = name; + this.cacheLoader = cacheLoader; + this.isReadThrough = isReadThrough; + } + + /** {@inheritDoc} */ + @Override + public V get(K key) { + V value = null; + synchronized (map) { + value = map.get(key); + if (value == null && isReadThrough && cacheLoader != null) { + V loadedValue = cacheLoader.load(key); + if (loadedValue != null) { + map.put(key, loadedValue); + value = loadedValue; + } + } + } + return value; + } + + /** {@inheritDoc} */ + @Override + public Map getAll(Set keys) { + Map result = null; + synchronized (map) { + result = new HashMap(keys.size()); + for (K key : keys) { + V value = map.get(key); + if (value != null) { + result.put(key, value); + keys.remove(key); + } + } + if (isReadThrough && cacheLoader != null) { + for (K key : keys) { + Map loadedValues = cacheLoader.loadAll(keys); + for (Map.Entry entry : loadedValues.entrySet()) { + V v = entry.getValue(); + if (v != null) { + K k = entry.getKey(); + map.put(k, v); + result.put(k, v); + } + } + } + } + } + return result; + } + + /** {@inheritDoc} */ + @Override + public boolean containsKey(K key) { + return map.containsKey(key); + } + + /** {@inheritDoc} */ + @Override + public void loadAll( + Set keys, boolean replaceExistingValues, CompletionListener completionListener) { + if (cacheLoader != null) { + try { + synchronized (map) { + Map loadedValues = cacheLoader.loadAll(keys); + for (Map.Entry entry : loadedValues.entrySet()) { + V value = entry.getValue(); + K key = entry.getKey(); + if (value != null) { + boolean existsCurrently = map.containsKey(key); + if (!existsCurrently || replaceExistingValues) { + map.put(key, value); + keys.remove(key); + } + } + } + } + } catch (Exception e) { + if (completionListener != null) { + completionListener.onException(e); + } + } + } + if (completionListener != null) { + if (keys.isEmpty()) { + completionListener.onCompletion(); + } + } + } + + /** {@inheritDoc} */ + @Override + public void put(K key, V value) { + map.put(key, value); + } + + /** {@inheritDoc} */ + @Override + public V getAndPut(K key, V value) { + V result = null; + synchronized (map) { + result = map.get(key); + if (value == null && isReadThrough && cacheLoader != null) { + V loadedValue = cacheLoader.load(key); + if (loadedValue != null) { + map.put(key, value); + value = loadedValue; + } + } + map.put(key, value); + } + return result; + } + + /** {@inheritDoc} */ + @Override + public void putAll(Map map) { + synchronized (map) { + for (Map.Entry entry : map.entrySet()) { + this.map.put(entry.getKey(), entry.getValue()); + } + } + } + + /** {@inheritDoc} */ + @Override + public boolean putIfAbsent(K key, V value) { + synchronized (map) { + if (!map.containsKey(key)) { + map.put(key, value); + return true; + } else { + return false; + } + } + } + + /** {@inheritDoc} */ + @Override + public boolean remove(K key) { + boolean removed = false; + synchronized (map) { + removed = map.remove(key) != null; + notifyRemovedListeners(key); + } + return removed; + } + + /** {@inheritDoc} */ + @Override + public boolean remove(K key, V oldValue) { + synchronized (map) { + V value = map.get(key); + if (value != null && oldValue.equals(value)) { + map.remove(key); + notifyRemovedListeners(key); + return true; + } + } + return false; + } + + /** {@inheritDoc} */ + @Override + public V getAndRemove(K key) { + synchronized (map) { + V oldValue = null; + oldValue = map.get(key); + map.remove(key); + notifyRemovedListeners(key); + return oldValue; + } + } + + /** {@inheritDoc} */ + @Override + public boolean replace(K key, V oldValue, V newValue) { + synchronized (map) { + V value = map.get(key); + if (value != null && oldValue.equals(value)) { + map.put(key, newValue); + return true; + } + } + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean replace(K key, V value) { + synchronized (map) { + if (map.containsKey(key)) { + map.put(key, value); + return true; + } + } + return false; + } + + /** {@inheritDoc} */ + @Override + public V getAndReplace(K key, V value) { + synchronized (map) { + V oldValue = map.get(key); + if (value != null && value.equals(oldValue)) { + map.put(key, value); + return oldValue; + } + } + return null; + } + + /** {@inheritDoc} */ + @Override + public void removeAll(Set keys) { + synchronized (map) { + for (K key : keys) { + if (map.containsKey(key)) { + map.remove(key); + } else { + keys.remove(key); + } + } + } + notifyRemovedListeners(keys); + } + + /** {@inheritDoc} */ + @Override + public void removeAll() { + synchronized (map) { + Set keys = map.keySet(); + map.clear(); + notifyRemovedListeners(keys); + } + } + + /** {@inheritDoc} */ + @Override + public void clear() { + map.clear(); + } + + /** {@inheritDoc} */ + @Override + public > C getConfiguration(Class clazz) { + if (!MapConfiguration.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException(); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public T invoke(K key, EntryProcessor entryProcessor, Object... arguments) + throws EntryProcessorException { + return null; + } + + /** {@inheritDoc} */ + @Override + public Map> invokeAll( + Set keys, EntryProcessor entryProcessor, Object... arguments) { + synchronized (map) { + for (K key : keys) { + V value = map.get(key); + if (value != null) { + entryProcessor.process( + new MutableEntry() { + @Override + public boolean exists() { + return map.containsKey(key); + } + + @Override + public void remove() { + synchronized (map) { + V value = map.get(key); + if (value != null) { + map.remove(key); + notifyRemovedListeners(key); + } + } + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return map.get(value); + } + + @Override + public T unwrap(Class clazz) { + return null; + } + + @Override + public void setValue(V value) { + map.put(key, value); + } + }, + arguments); + } + } + } + return null; + } + + /** {@inheritDoc} */ + @Override + public String getName() { + return name; + } + + /** {@inheritDoc} */ + @Override + public CacheManager getCacheManager() { + return manager; + } + + /** {@inheritDoc} */ + @Override + public void close() {} + + /** {@inheritDoc} */ + @Override + public boolean isClosed() { + return false; + } + + /** {@inheritDoc} */ + @Override + public T unwrap(Class clazz) { + return (T) map; + } + + /** {@inheritDoc} */ + @Override + public void registerCacheEntryListener( + CacheEntryListenerConfiguration cacheEntryListenerConfiguration) { + //cacheEntryRemovedListeners.add(cacheEntryListenerConfiguration.getCacheEntryListenerFactory().create()); + } + + /** {@inheritDoc} */ + @Override + public void deregisterCacheEntryListener( + CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {} + + /** {@inheritDoc} */ + @Override + public Iterator> iterator() { + synchronized (map) { + return new Iterator>() { + + Iterator> entries = map.entrySet().iterator(); + + @Override + public boolean hasNext() { + return entries.hasNext(); + } + + @Override + public Entry next() { + Map.Entry entry = entries.next(); + return new Entry() { + K key = entry.getKey(); + V value = entry.getValue(); + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public T unwrap(Class clazz) { + return null; + } + }; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + private void notifyRemovedListeners(K key) { + // if (cacheEntryRemovedListeners != null) { + // cacheEntryRemovedListeners.forEach(listener -> listener.onRemoved()) + // } + } + + private void notifyRemovedListeners(Set keys) {} +} diff --git a/src/main/java/net/helenus/core/cache/SessionCache.java b/src/main/java/net/helenus/core/cache/SessionCache.java deleted file mode 100644 index 4b6d8d0..0000000 --- a/src/main/java/net/helenus/core/cache/SessionCache.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2015 The Helenus Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.helenus.core.cache; - -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.RemovalListener; -import com.google.common.cache.RemovalNotification; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public interface SessionCache { - - static final Logger LOG = LoggerFactory.getLogger(SessionCache.class); - - static SessionCache defaultCache() { - GuavaCache cache; - RemovalListener listener = - new RemovalListener() { - @Override - public void onRemoval(RemovalNotification n) { - if (n.wasEvicted()) { - String cause = n.getCause().name(); - LOG.info(cause); - } - } - }; - - cache = - new GuavaCache( - CacheBuilder.newBuilder() - .maximumSize(25_000) - .expireAfterAccess(5, TimeUnit.MINUTES) - .softValues() - .removalListener(listener) - .build()); - - return cache; - } - - void invalidate(K key); - - V get(K key); - - void put(K key, V value); -} diff --git a/src/main/java/net/helenus/core/cache/UnboundFacet.java b/src/main/java/net/helenus/core/cache/UnboundFacet.java index 60c0d36..089e8b5 100644 --- a/src/main/java/net/helenus/core/cache/UnboundFacet.java +++ b/src/main/java/net/helenus/core/cache/UnboundFacet.java @@ -25,16 +25,30 @@ import net.helenus.mapping.HelenusProperty; public class UnboundFacet extends Facet { private final List properties; + private final boolean alone; + private final boolean combined; - public UnboundFacet(List properties) { + public UnboundFacet(List properties, boolean alone, boolean combined) { super(SchemaUtil.createPrimaryKeyPhrase(properties)); this.properties = properties; + this.alone = alone; + this.combined = combined; } - public UnboundFacet(HelenusProperty property) { + public UnboundFacet(List properties) { + this(properties, true, true); + } + + public UnboundFacet(HelenusProperty property, boolean alone, boolean combined) { super(property.getPropertyName()); properties = new ArrayList(); properties.add(property); + this.alone = alone; + this.combined = combined; + } + + public UnboundFacet(HelenusProperty property) { + this(property, true, true); } public List getProperties() { @@ -42,18 +56,22 @@ public class UnboundFacet extends Facet { } public Binder binder() { - return new Binder(name(), properties); + return new Binder(name(), properties, alone, combined); } public static class Binder { private final String name; + private final boolean alone; + private final boolean combined; private final List properties = new ArrayList(); private Map boundProperties = new HashMap(); - Binder(String name, List properties) { + Binder(String name, List properties, boolean alone, boolean combined) { this.name = name; this.properties.addAll(properties); + this.alone = alone; + this.combined = combined; } public Binder setValueForProperty(HelenusProperty prop, Object value) { @@ -67,7 +85,10 @@ public class UnboundFacet extends Facet { } public BoundFacet bind() { - return new BoundFacet(name, boundProperties); + BoundFacet facet = new BoundFacet(name, boundProperties); + facet.setUniquelyIdentifyingWhenAlone(alone); + facet.setUniquelyIdentifyingWhenCombined(combined); + return facet; } } } diff --git a/src/main/java/net/helenus/core/operation/AbstractFilterOperation.java b/src/main/java/net/helenus/core/operation/AbstractFilterOperation.java index 93e164e..8a450c0 100644 --- a/src/main/java/net/helenus/core/operation/AbstractFilterOperation.java +++ b/src/main/java/net/helenus/core/operation/AbstractFilterOperation.java @@ -19,6 +19,7 @@ import java.util.*; import net.helenus.core.*; import net.helenus.core.cache.Facet; import net.helenus.core.cache.UnboundFacet; +import net.helenus.core.reflect.HelenusPropertyNode; import net.helenus.mapping.HelenusProperty; public abstract class AbstractFilterOperation> @@ -108,6 +109,28 @@ public abstract class AbstractFilterOperation { + HelenusPropertyNode node = filter.getNode(); + if (node != null) { + HelenusProperty prop = node.getProperty(); + if (prop != null) { + return prop.isIdempotent(); + } + } + return false; + }) + || super.isIdempotentOperation(); + } + protected List bindFacetValues(List facets) { if (facets == null) { return new ArrayList(); diff --git a/src/main/java/net/helenus/core/operation/AbstractFilterStreamOperation.java b/src/main/java/net/helenus/core/operation/AbstractFilterStreamOperation.java index b78daf1..2f707a9 100644 --- a/src/main/java/net/helenus/core/operation/AbstractFilterStreamOperation.java +++ b/src/main/java/net/helenus/core/operation/AbstractFilterStreamOperation.java @@ -42,7 +42,7 @@ public abstract class AbstractFilterStreamOperation< public O where(Getter getter, Operator operator, V val) { - addFilter(Filter.create(getter, operator, val)); + if (val != null) addFilter(Filter.create(getter, operator, val)); return (O) this; } @@ -63,7 +63,7 @@ public abstract class AbstractFilterStreamOperation< public O and(Getter getter, Operator operator, V val) { - addFilter(Filter.create(getter, operator, val)); + if (val != null) addFilter(Filter.create(getter, operator, val)); return (O) this; } @@ -84,7 +84,7 @@ public abstract class AbstractFilterStreamOperation< public O onlyIf(Getter getter, Operator operator, V val) { - addIfFilter(Filter.create(getter, operator, val)); + if (val != null) addIfFilter(Filter.create(getter, operator, val)); return (O) this; } diff --git a/src/main/java/net/helenus/core/operation/AbstractOperation.java b/src/main/java/net/helenus/core/operation/AbstractOperation.java index 9d12a52..9ae3aaa 100644 --- a/src/main/java/net/helenus/core/operation/AbstractOperation.java +++ b/src/main/java/net/helenus/core/operation/AbstractOperation.java @@ -41,13 +41,7 @@ public abstract class AbstractOperation> try { ResultSet resultSet = this.execute( - sessionOps, - null, - traceContext, - queryExecutionTimeout, - queryTimeoutUnits, - showValues, - false); + sessionOps, null, queryExecutionTimeout, queryTimeoutUnits, showValues, false); return transform(resultSet); } finally { context.stop(); @@ -60,14 +54,7 @@ public abstract class AbstractOperation> final Timer.Context context = requestLatency.time(); try { ResultSet resultSet = - execute( - sessionOps, - uow, - traceContext, - queryExecutionTimeout, - queryTimeoutUnits, - showValues, - true); + execute(sessionOps, uow, queryExecutionTimeout, queryTimeoutUnits, showValues, true); E result = transform(resultSet); return result; } finally { @@ -88,13 +75,16 @@ public abstract class AbstractOperation> public CompletableFuture async(UnitOfWork uow) { if (uow == null) return async(); - return CompletableFuture.supplyAsync( - () -> { - try { - return sync(); - } catch (TimeoutException ex) { - throw new CompletionException(ex); - } - }); + CompletableFuture f = + CompletableFuture.supplyAsync( + () -> { + try { + return sync(); + } catch (TimeoutException ex) { + throw new CompletionException(ex); + } + }); + uow.addFuture(f); + return f; } } diff --git a/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java b/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java index 8fe5c11..4c7b290 100644 --- a/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java +++ b/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java @@ -24,15 +24,20 @@ import com.google.common.base.Function; import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import java.io.Serializable; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeoutException; import net.helenus.core.AbstractSessionOperations; +import net.helenus.core.Helenus; import net.helenus.core.UnitOfWork; import net.helenus.core.cache.CacheUtil; import net.helenus.core.cache.Facet; +import net.helenus.mapping.MappingUtil; +import net.helenus.support.Fun; +import org.apache.commons.lang3.SerializationUtils; public abstract class AbstractOptionalOperation> extends AbstractStatementOperation { @@ -64,20 +69,26 @@ public abstract class AbstractOptionalOperation result = Optional.empty(); E cacheResult = null; - boolean updateCache = isSessionCacheable() && checkCache; + boolean updateCache = isSessionCacheable() && !ignoreCache(); - if (checkCache && isSessionCacheable()) { + if (updateCache) { List facets = bindFacetValues(); - String tableName = CacheUtil.schemaName(facets); - cacheResult = (E) sessionOps.checkCache(tableName, facets); - if (cacheResult != null) { - result = Optional.of(cacheResult); - updateCache = false; - sessionCacheHits.mark(); - cacheHits.mark(); + if (facets != null && facets.size() > 0) { + if (facets.stream().filter(f -> !f.fixed()).distinct().count() > 0) { + String tableName = CacheUtil.schemaName(facets); + cacheResult = (E) sessionOps.checkCache(tableName, facets); + if (cacheResult != null) { + result = Optional.of(cacheResult); + updateCache = false; + sessionCacheHits.mark(); + cacheHits.mark(); + } else { + sessionCacheMiss.mark(); + cacheMiss.mark(); + } + } } else { - sessionCacheMiss.mark(); - cacheMiss.mark(); + //TODO(gburd): look in statement cache for results } } @@ -87,20 +98,24 @@ public abstract class AbstractOptionalOperation facets = getFacets(); - if (facets != null && facets.size() > 1) { - sessionOps.updateCache(result.get(), facets); + E r = result.get(); + Class resultClass = r.getClass(); + if (!(resultClass.getEnclosingClass() != null + && resultClass.getEnclosingClass() == Fun.class)) { + List facets = getFacets(); + if (facets != null && facets.size() > 1) { + sessionOps.updateCache(r, facets); + } } } return result; @@ -109,7 +124,7 @@ public abstract class AbstractOptionalOperation sync(UnitOfWork uow) throws TimeoutException { + public Optional sync(UnitOfWork uow) throws TimeoutException { if (uow == null) return sync(); final Timer.Context context = requestLatency.time(); @@ -119,35 +134,54 @@ public abstract class AbstractOptionalOperation facets = bindFacetValues(); - if (facets != null) { - cachedResult = checkCache(uow, facets); - if (cachedResult != null) { - updateCache = false; - result = Optional.of(cachedResult); - uowCacheHits.mark(); - cacheHits.mark(); - uow.recordCacheAndDatabaseOperationCount(1, 0); - } else { - updateCache = true; - uowCacheMiss.mark(); - if (isSessionCacheable()) { - String tableName = CacheUtil.schemaName(facets); - cachedResult = (E) sessionOps.checkCache(tableName, facets); - if (cachedResult != null) { - result = Optional.of(cachedResult); - sessionCacheHits.mark(); - cacheHits.mark(); - uow.recordCacheAndDatabaseOperationCount(1, 0); + if (facets != null && facets.size() > 0) { + if (facets.stream().filter(f -> !f.fixed()).distinct().count() > 0) { + cachedResult = checkCache(uow, facets); + if (cachedResult != null) { + updateCache = false; + result = Optional.of(cachedResult); + uowCacheHits.mark(); + cacheHits.mark(); + uow.recordCacheAndDatabaseOperationCount(1, 0); + } else { + uowCacheMiss.mark(); + if (isSessionCacheable()) { + String tableName = CacheUtil.schemaName(facets); + cachedResult = (E) sessionOps.checkCache(tableName, facets); + if (cachedResult != null) { + Class iface = MappingUtil.getMappingInterface(cachedResult); + if (Helenus.entity(iface).isDraftable()) { + result = Optional.of(cachedResult); + } else { + result = + Optional.of( + (E) + SerializationUtils.clone( + (Serializable) cachedResult)); + } + updateCache = false; + sessionCacheHits.mark(); + cacheHits.mark(); + uow.recordCacheAndDatabaseOperationCount(1, 0); + } else { + updateCache = true; + sessionCacheMiss.mark(); + cacheMiss.mark(); + uow.recordCacheAndDatabaseOperationCount(-1, 0); + } } else { - sessionCacheMiss.mark(); - cacheMiss.mark(); - uow.recordCacheAndDatabaseOperationCount(-1, 0); + updateCache = false; } } + } else { + //TODO(gburd): look in statement cache for results + updateCache = false; //true; + cacheMiss.mark(); + uow.recordCacheAndDatabaseOperationCount(-1, 0); } } else { updateCache = false; @@ -171,14 +205,7 @@ public abstract class AbstractOptionalOperation> async(UnitOfWork uow) { + public CompletableFuture> async(UnitOfWork uow) { if (uow == null) return async(); - return CompletableFuture.>supplyAsync( - () -> { - try { - return sync(); - } catch (TimeoutException ex) { - throw new CompletionException(ex); - } - }); + CompletableFuture> f = + CompletableFuture.>supplyAsync( + () -> { + try { + return sync(); + } catch (TimeoutException ex) { + throw new CompletionException(ex); + } + }); + uow.addFuture(f); + return f; } } diff --git a/src/main/java/net/helenus/core/operation/AbstractStatementOperation.java b/src/main/java/net/helenus/core/operation/AbstractStatementOperation.java index 791f2ca..79855c6 100644 --- a/src/main/java/net/helenus/core/operation/AbstractStatementOperation.java +++ b/src/main/java/net/helenus/core/operation/AbstractStatementOperation.java @@ -15,8 +15,6 @@ */ package net.helenus.core.operation; -import brave.Tracer; -import brave.propagation.TraceContext; import com.datastax.driver.core.ConsistencyLevel; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.RegularStatement; @@ -43,19 +41,14 @@ import net.helenus.support.HelenusException; public abstract class AbstractStatementOperation> extends Operation { - - protected boolean checkCache = true; - protected boolean showValues = true; - protected TraceContext traceContext; - long queryExecutionTimeout = 10; - TimeUnit queryTimeoutUnits = TimeUnit.SECONDS; + private boolean ignoreCache = false; private ConsistencyLevel consistencyLevel; private ConsistencyLevel serialConsistencyLevel; private RetryPolicy retryPolicy; - private boolean idempotent = false; private boolean enableTracing = false; private long[] defaultTimestamp = null; private int[] fetchSize = null; + protected boolean idempotent = false; public AbstractStatementOperation(AbstractSessionOperations sessionOperations) { super(sessionOperations); @@ -66,12 +59,12 @@ public abstract class AbstractStatementOperation uow, List facets) { + protected boolean ignoreCache() { + return ignoreCache; + } + + protected E checkCache(UnitOfWork uow, List facets) { E result = null; Optional optionalCachedResult = Optional.empty(); @@ -327,7 +318,7 @@ public abstract class AbstractStatementOperation uow, E pojo, List identifyingFacets) { + protected Object cacheUpdate(UnitOfWork uow, E pojo, List identifyingFacets) { List facets = new ArrayList<>(); Map valueMap = pojo instanceof MapExportable ? ((MapExportable) pojo).toMap() : null; @@ -359,6 +350,6 @@ public abstract class AbstractStatementOperation> extends AbstractStatementOperation { @@ -67,18 +72,24 @@ public abstract class AbstractStreamOperation facets = bindFacetValues(); - String tableName = CacheUtil.schemaName(facets); - cacheResult = (E) sessionOps.checkCache(tableName, facets); - if (cacheResult != null) { - resultStream = Stream.of(cacheResult); - updateCache = false; - sessionCacheHits.mark(); - cacheHits.mark(); - } else { - sessionCacheMiss.mark(); - cacheMiss.mark(); + if (facets != null && facets.size() > 0) { + if (facets.stream().filter(f -> !f.fixed()).distinct().count() > 0) { + String tableName = CacheUtil.schemaName(facets); + cacheResult = (E) sessionOps.checkCache(tableName, facets); + if (cacheResult != null) { + resultStream = Stream.of(cacheResult); + updateCache = false; + sessionCacheHits.mark(); + cacheHits.mark(); + } else { + sessionCacheMiss.mark(); + cacheMiss.mark(); + } + } else { + //TODO(gburd): look in statement cache for results + } } } @@ -88,11 +99,10 @@ public abstract class AbstractStreamOperation again = new ArrayList<>(); resultStream.forEach( result -> { - sessionOps.updateCache(result, facets); + Class resultClass = result.getClass(); + if (!(resultClass.getEnclosingClass() != null + && resultClass.getEnclosingClass() == Fun.class)) { + sessionOps.updateCache(result, facets); + } again.add(result); }); resultStream = again.stream(); @@ -126,35 +140,53 @@ public abstract class AbstractStreamOperation facets = bindFacetValues(); - if (facets != null) { - cachedResult = checkCache(uow, facets); - if (cachedResult != null) { - updateCache = false; - resultStream = Stream.of(cachedResult); - uowCacheHits.mark(); - cacheHits.mark(); - uow.recordCacheAndDatabaseOperationCount(1, 0); - } else { - updateCache = true; - uowCacheMiss.mark(); - if (isSessionCacheable()) { - String tableName = CacheUtil.schemaName(facets); - cachedResult = (E) sessionOps.checkCache(tableName, facets); - if (cachedResult != null) { - resultStream = Stream.of(cachedResult); - sessionCacheHits.mark(); - cacheHits.mark(); - uow.recordCacheAndDatabaseOperationCount(1, 0); + if (facets != null && facets.size() > 0) { + if (facets.stream().filter(f -> !f.fixed()).distinct().count() > 0) { + cachedResult = checkCache(uow, facets); + if (cachedResult != null) { + updateCache = false; + resultStream = Stream.of(cachedResult); + uowCacheHits.mark(); + cacheHits.mark(); + uow.recordCacheAndDatabaseOperationCount(1, 0); + } else { + uowCacheMiss.mark(); + if (isSessionCacheable()) { + String tableName = CacheUtil.schemaName(facets); + cachedResult = (E) sessionOps.checkCache(tableName, facets); + if (cachedResult != null) { + Class iface = MappingUtil.getMappingInterface(cachedResult); + E result = null; + if (Helenus.entity(iface).isDraftable()) { + result = cachedResult; + } else { + result = + (E) SerializationUtils.clone((Serializable) cachedResult); + } + updateCache = false; + resultStream = Stream.of(result); + sessionCacheHits.mark(); + cacheHits.mark(); + uow.recordCacheAndDatabaseOperationCount(1, 0); + } else { + updateCache = true; + sessionCacheMiss.mark(); + cacheMiss.mark(); + uow.recordCacheAndDatabaseOperationCount(-1, 0); + } } else { - sessionCacheMiss.mark(); - cacheMiss.mark(); - uow.recordCacheAndDatabaseOperationCount(-1, 0); + updateCache = false; } } + } else { + //TODO(gburd): look in statement cache for results + updateCache = false; //true; + cacheMiss.mark(); + uow.recordCacheAndDatabaseOperationCount(-1, 0); } } else { updateCache = false; @@ -170,32 +202,28 @@ public abstract class AbstractStreamOperation again = new ArrayList<>(); - List facets = getFacets(); - resultStream.forEach( - result -> { - if (result != deleted) { - if (updateCache) { - cacheUpdate(uow, result, facets); + if (updateCache) { + List again = new ArrayList<>(); + List facets = getFacets(); + resultStream.forEach( + result -> { + Class resultClass = result.getClass(); + if (result != deleted + && !(resultClass.getEnclosingClass() != null + && resultClass.getEnclosingClass() == Fun.class)) { + result = (E) cacheUpdate(uow, result, facets); } again.add(result); - } - }); - resultStream = again.stream(); + }); + resultStream = again.stream(); + } } return resultStream; @@ -217,13 +245,16 @@ public abstract class AbstractStreamOperation> async(UnitOfWork uow) { if (uow == null) return async(); - return CompletableFuture.>supplyAsync( - () -> { - try { - return sync(); - } catch (TimeoutException ex) { - throw new CompletionException(ex); - } - }); + CompletableFuture> f = + CompletableFuture.>supplyAsync( + () -> { + try { + return sync(); + } catch (TimeoutException ex) { + throw new CompletionException(ex); + } + }); + uow.addFuture(f); + return f; } } diff --git a/src/main/java/net/helenus/core/operation/BatchOperation.java b/src/main/java/net/helenus/core/operation/BatchOperation.java new file mode 100644 index 0000000..8aca9cf --- /dev/null +++ b/src/main/java/net/helenus/core/operation/BatchOperation.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.core.operation; + +import com.codahale.metrics.Timer; +import com.datastax.driver.core.AtomicMonotonicTimestampGenerator; +import com.datastax.driver.core.BatchStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.TimestampGenerator; +import com.google.common.base.Stopwatch; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import net.helenus.core.AbstractSessionOperations; +import net.helenus.core.UnitOfWork; +import net.helenus.support.HelenusException; + +public class BatchOperation extends Operation { + //TODO(gburd): find the way to get the driver's timestamp generator + private static final TimestampGenerator timestampGenerator = + new AtomicMonotonicTimestampGenerator(); + + private final BatchStatement batch; + private List> operations = new ArrayList>(); + private boolean logged = true; + + public BatchOperation(AbstractSessionOperations sessionOperations) { + super(sessionOperations); + batch = new BatchStatement(); + } + + public void add(AbstractOperation operation) { + operations.add(operation); + } + + @Override + public BatchStatement buildStatement(boolean cached) { + batch.addAll( + operations.stream().map(o -> o.buildStatement(cached)).collect(Collectors.toList())); + batch.setConsistencyLevel(sessionOps.getDefaultConsistencyLevel()); + return batch; + } + + public BatchOperation logged() { + logged = true; + return this; + } + + public BatchOperation setLogged(boolean logStatements) { + logged = logStatements; + return this; + } + + public Long sync() throws TimeoutException { + if (operations.size() == 0) return 0L; + final Timer.Context context = requestLatency.time(); + try { + batch.setDefaultTimestamp(timestampGenerator.next()); + ResultSet resultSet = + this.execute( + sessionOps, null, queryExecutionTimeout, queryTimeoutUnits, showValues, false); + if (!resultSet.wasApplied()) { + throw new HelenusException("Failed to apply batch."); + } + } finally { + context.stop(); + } + return batch.getDefaultTimestamp(); + } + + public Long sync(UnitOfWork uow) throws TimeoutException { + if (operations.size() == 0) return 0L; + if (uow == null) return sync(); + + final Timer.Context context = requestLatency.time(); + final Stopwatch timer = Stopwatch.createStarted(); + try { + uow.recordCacheAndDatabaseOperationCount(0, 1); + batch.setDefaultTimestamp(timestampGenerator.next()); + ResultSet resultSet = + this.execute( + sessionOps, uow, queryExecutionTimeout, queryTimeoutUnits, showValues, false); + if (!resultSet.wasApplied()) { + throw new HelenusException("Failed to apply batch."); + } + } finally { + context.stop(); + timer.stop(); + } + uow.addDatabaseTime("Cassandra", timer); + return batch.getDefaultTimestamp(); + } + + public void addAll(BatchOperation batch) { + batch.operations.forEach(o -> this.operations.add(o)); + } + + public String toString() { + return toString(true); //TODO(gburd): sessionOps.showQueryValues() + } + + public String toString(boolean showValues) { + StringBuilder s = new StringBuilder(); + s.append("BEGIN "); + if (!logged) { + s.append("UNLOGGED "); + } + s.append("BATCH "); + + if (batch.getDefaultTimestamp() > -9223372036854775808L) { + s.append("USING TIMESTAMP ").append(String.valueOf(batch.getDefaultTimestamp())).append(" "); + } + s.append( + operations + .stream() + .map(o -> Operation.queryString(o.buildStatement(showValues), showValues)) + .collect(Collectors.joining(" "))); + s.append(" APPLY BATCH;"); + return s.toString(); + } +} diff --git a/src/main/java/net/helenus/core/operation/CountOperation.java b/src/main/java/net/helenus/core/operation/CountOperation.java index b751cfb..9b84bc0 100644 --- a/src/main/java/net/helenus/core/operation/CountOperation.java +++ b/src/main/java/net/helenus/core/operation/CountOperation.java @@ -37,6 +37,7 @@ public final class CountOperation extends AbstractFilterOperation { @@ -133,6 +134,10 @@ public final class DeleteOperation extends AbstractFilterOperation getFacets() { return entity.getFacets(); diff --git a/src/main/java/net/helenus/core/operation/InsertOperation.java b/src/main/java/net/helenus/core/operation/InsertOperation.java index 3d88eb7..032bc07 100644 --- a/src/main/java/net/helenus/core/operation/InsertOperation.java +++ b/src/main/java/net/helenus/core/operation/InsertOperation.java @@ -22,15 +22,17 @@ import com.datastax.driver.core.querybuilder.QueryBuilder; import java.util.*; import java.util.concurrent.TimeoutException; import java.util.function.Function; +import java.util.stream.Collectors; import net.helenus.core.AbstractSessionOperations; import net.helenus.core.Getter; import net.helenus.core.Helenus; import net.helenus.core.UnitOfWork; +import net.helenus.core.cache.CacheUtil; import net.helenus.core.cache.Facet; import net.helenus.core.cache.UnboundFacet; import net.helenus.core.reflect.DefaultPrimitiveTypes; -import net.helenus.core.reflect.Drafted; import net.helenus.core.reflect.HelenusPropertyNode; +import net.helenus.core.reflect.MapExportable; import net.helenus.mapping.HelenusEntity; import net.helenus.mapping.HelenusProperty; import net.helenus.mapping.MappingUtil; @@ -45,26 +47,44 @@ public final class InsertOperation extends AbstractOperation>(); private final T pojo; private final Class resultType; + private final Set readSet; private HelenusEntity entity; private boolean ifNotExists; private int[] ttl; private long[] timestamp; + private long writeTime = 0L; public InsertOperation(AbstractSessionOperations sessionOperations, boolean ifNotExists) { super(sessionOperations); - this.ifNotExists = ifNotExists; this.pojo = null; + this.readSet = null; + this.ifNotExists = ifNotExists; this.resultType = ResultSet.class; } + public InsertOperation( + AbstractSessionOperations sessionOperations, + HelenusEntity entity, + Class resultType, + boolean ifNotExists) { + super(sessionOperations); + + this.pojo = null; + this.readSet = null; + this.ifNotExists = ifNotExists; + this.resultType = resultType; + this.entity = entity; + } + public InsertOperation( AbstractSessionOperations sessionOperations, Class resultType, boolean ifNotExists) { super(sessionOperations); - this.ifNotExists = ifNotExists; this.pojo = null; + this.readSet = null; + this.ifNotExists = ifNotExists; this.resultType = resultType; } @@ -73,11 +93,13 @@ public final class InsertOperation extends AbstractOperation mutations, + Set read, boolean ifNotExists) { super(sessionOperations); - this.entity = entity; this.pojo = pojo; + this.readSet = read; + this.entity = entity; this.ifNotExists = ifNotExists; this.resultType = entity.getMappingInterface(); @@ -136,8 +158,32 @@ public final class InsertOperation extends AbstractOperation addPropertyNode(t._1)); + List entities = + values + .stream() + .map(t -> t._1.getProperty().getEntity()) + .distinct() + .collect(Collectors.toList()); + if (entities.size() != 1) { + throw new HelenusMappingException( + "you can insert only single entity at a time, found: " + + entities + .stream() + .map(e -> e.getMappingInterface().toString()) + .collect(Collectors.joining(", "))); + } + HelenusEntity entity = entities.get(0); + if (this.entity != null) { + if (this.entity != entity) { + throw new HelenusMappingException( + "you can insert only single entity at a time, found: " + + this.entity.getMappingInterface().toString() + + ", " + + entity.getMappingInterface().toString()); + } + } else { + this.entity = entity; + } if (values.isEmpty()) return null; @@ -156,6 +202,8 @@ public final class InsertOperation extends AbstractOperation extends AbstractOperation iface) { + if (values.size() > 0) { + boolean immutable = entity.isDraftable(); + Collection properties = entity.getOrderedProperties(); + Map backingMap = new HashMap(properties.size()); + + // First, add all the inserted values into our new map. + values.forEach(t -> backingMap.put(t._1.getProperty().getPropertyName(), t._2)); + + // Then, fill in all the rest of the properties. + for (HelenusProperty prop : properties) { + String key = prop.getPropertyName(); + if (backingMap.containsKey(key)) { + // Some values man need to be converted (e.g. from String to Enum). This is done + // within the BeanColumnValueProvider below. + Optional> converter = + prop.getReadConverter(sessionOps.getSessionRepository()); + if (converter.isPresent()) { + backingMap.put(key, converter.get().apply(backingMap.get(key))); + } + } else { + // If we started this operation with an instance of this type, use values from + // that. + if (pojo != null) { + backingMap.put( + key, BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, immutable)); + } else { + // Otherwise we'll use default values for the property type if available. + Class propType = prop.getJavaType(); + if (propType.isPrimitive()) { + DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(propType); + if (type == null) { + throw new HelenusException("unknown primitive type " + propType); + } + backingMap.put(key, type.getDefaultValue()); + } + } + } + } + + // Lastly, create a new proxy object for the entity and return the new instance. + return (T) Helenus.map(iface, backingMap); + } + return null; + } + @Override public T transform(ResultSet resultSet) { if ((ifNotExists == true) && (resultSet.wasApplied() == false)) { @@ -174,50 +268,12 @@ public final class InsertOperation extends AbstractOperation iface = entity.getMappingInterface(); if (resultType == iface) { - if (values.size() > 0) { - boolean immutable = iface.isAssignableFrom(Drafted.class); - Collection properties = entity.getOrderedProperties(); - Map backingMap = new HashMap(properties.size()); - - // First, add all the inserted values into our new map. - values.forEach(t -> backingMap.put(t._1.getProperty().getPropertyName(), t._2)); - - // Then, fill in all the rest of the properties. - for (HelenusProperty prop : properties) { - String key = prop.getPropertyName(); - if (backingMap.containsKey(key)) { - // Some values man need to be converted (e.g. from String to Enum). This is done - // within the BeanColumnValueProvider below. - Optional> converter = - prop.getReadConverter(sessionOps.getSessionRepository()); - if (converter.isPresent()) { - backingMap.put(key, converter.get().apply(backingMap.get(key))); - } - } else { - // If we started this operation with an instance of this type, use values from - // that. - if (pojo != null) { - backingMap.put( - key, BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, immutable)); - } else { - // Otherwise we'll use default values for the property type if available. - Class propType = prop.getJavaType(); - if (propType.isPrimitive()) { - DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(propType); - if (type == null) { - throw new HelenusException("unknown primitive type " + propType); - } - backingMap.put(key, type.getDefaultValue()); - } - } - } - } - - // Lastly, create a new proxy object for the entity and return the new instance. - return (T) Helenus.map(iface, backingMap); + T o = newInstance(iface); + if (o == null) { + // Oddly, this insert didn't change anything so simply return the pojo. + return (T) pojo; } - // Oddly, this insert didn't change anything so simply return the pojo. - return (T) pojo; + return o; } return (T) resultSet; } @@ -234,23 +290,48 @@ public final class InsertOperation extends AbstractOperation columnNames = + values + .stream() + .map(t -> t._1.getProperty()) + .filter( + prop -> { + switch (prop.getColumnType()) { + case PARTITION_KEY: + case CLUSTERING_COLUMN: + return false; + default: + return true; + } + }) + .map(prop -> prop.getColumnName().toCql(false)) + .collect(Collectors.toList()); + + if (columnNames.size() > 0) { + if (ttl != null) { + columnNames.forEach(name -> pojo.put(CacheUtil.ttlKey(name), ttl)); + } + if (writeTime != 0L) { + columnNames.forEach(name -> pojo.put(CacheUtil.writeTimeKey(name), writeTime)); + } + } } } + @Override + protected boolean isIdempotentOperation() { + return values.stream().map(v -> v._1.getProperty()).allMatch(prop -> prop.isIdempotent()) + || super.isIdempotentOperation(); + } + @Override public T sync() throws TimeoutException { T result = super.sync(); if (entity.isCacheable() && result != null) { - sessionOps.updateCache(result, entity.getFacets()); + adjustTtlAndWriteTime((MapExportable) result); + sessionOps.updateCache(result, bindFacetValues()); } return result; } @@ -272,15 +353,35 @@ public final class InsertOperation extends AbstractOperation iface = entity.getMappingInterface(); if (resultType == iface) { - cacheUpdate(uow, result, entity.getFacets()); - } else { - if (entity.isCacheable()) { - sessionOps.cacheEvict(bindFacetValues()); + if (entity != null && MapExportable.class.isAssignableFrom(entity.getMappingInterface())) { + adjustTtlAndWriteTime((MapExportable) result); } + cacheUpdate(uow, result, bindFacetValues()); } return result; } + public T batch(UnitOfWork uow) throws TimeoutException { + if (uow == null) { + throw new HelenusException("UnitOfWork cannot be null when batching operations."); + } + + if (this.entity != null) { + Class iface = this.entity.getMappingInterface(); + if (resultType == iface) { + final T result = (pojo == null) ? newInstance(iface) : pojo; + if (result != null) { + adjustTtlAndWriteTime((MapExportable) result); + cacheUpdate(uow, result, bindFacetValues()); + } + uow.batch(this); + return (T) result; + } + } + + return sync(uow); + } + @Override public List bindFacetValues() { List facets = getFacets(); diff --git a/src/main/java/net/helenus/core/operation/Operation.java b/src/main/java/net/helenus/core/operation/Operation.java index 8618293..eac4cf8 100644 --- a/src/main/java/net/helenus/core/operation/Operation.java +++ b/src/main/java/net/helenus/core/operation/Operation.java @@ -15,9 +15,6 @@ */ package net.helenus.core.operation; -import brave.Span; -import brave.Tracer; -import brave.propagation.TraceContext; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; @@ -42,6 +39,9 @@ public abstract class Operation { private static final Logger LOG = LoggerFactory.getLogger(Operation.class); protected final AbstractSessionOperations sessionOps; + protected boolean showValues; + protected long queryExecutionTimeout = 10; + protected TimeUnit queryTimeoutUnits = TimeUnit.SECONDS; protected final Meter uowCacheHits; protected final Meter uowCacheMiss; protected final Meter sessionCacheHits; @@ -52,6 +52,7 @@ public abstract class Operation { Operation(AbstractSessionOperations sessionOperations) { this.sessionOps = sessionOperations; + this.showValues = sessionOps.showValues(); MetricRegistry metrics = sessionOperations.getMetricRegistry(); if (metrics == null) { metrics = new MetricRegistry(); @@ -65,6 +66,10 @@ public abstract class Operation { this.requestLatency = metrics.timer("net.helenus.request-latency"); } + public static String queryString(BatchOperation operation, boolean includeValues) { + return operation.toString(includeValues); + } + public static String queryString(Statement statement, boolean includeValues) { String query = null; if (statement instanceof BuiltStatement) { @@ -87,83 +92,79 @@ public abstract class Operation { public ResultSet execute( AbstractSessionOperations session, UnitOfWork uow, - TraceContext traceContext, long timeout, TimeUnit units, boolean showValues, boolean cached) throws TimeoutException { - // Start recording in a Zipkin sub-span our execution time to perform this - // operation. - Tracer tracer = session.getZipkinTracer(); - Span span = null; - if (tracer != null && traceContext != null) { - span = tracer.newChild(traceContext); + Statement statement = options(buildStatement(cached)); + + if (session.isShowCql()) { + String stmt = + (this instanceof BatchOperation) + ? queryString((BatchOperation) this, showValues) + : queryString(statement, showValues); + session.getPrintStream().println(stmt); + } else if (LOG.isDebugEnabled()) { + String stmt = + (this instanceof BatchOperation) + ? queryString((BatchOperation) this, showValues) + : queryString(statement, showValues); + LOG.info("CQL> " + stmt); } + Stopwatch timer = Stopwatch.createStarted(); try { - - if (span != null) { - span.name("cassandra"); - span.start(); - } - - Statement statement = options(buildStatement(cached)); - Stopwatch timer = Stopwatch.createStarted(); - try { - ResultSetFuture futureResultSet = session.executeAsync(statement, uow, timer, showValues); - if (uow != null) uow.recordCacheAndDatabaseOperationCount(0, 1); - ResultSet resultSet = futureResultSet.getUninterruptibly(timeout, units); - ColumnDefinitions columnDefinitions = resultSet.getColumnDefinitions(); - if (LOG.isDebugEnabled()) { - ExecutionInfo ei = resultSet.getExecutionInfo(); - Host qh = ei.getQueriedHost(); - String oh = - ei.getTriedHosts() - .stream() - .map(Host::getAddress) - .map(InetAddress::toString) - .collect(Collectors.joining(", ")); - ConsistencyLevel cl = ei.getAchievedConsistencyLevel(); - int se = ei.getSpeculativeExecutions(); - String warn = ei.getWarnings().stream().collect(Collectors.joining(", ")); - String ri = - String.format( - "%s %s %s %s %s %s%sspec-retries: %d", - "server v" + qh.getCassandraVersion(), - qh.getAddress().toString(), - (oh != null && !oh.equals("")) ? " [tried: " + oh + "]" : "", - qh.getDatacenter(), - qh.getRack(), - (cl != null) - ? (" consistency: " - + cl.name() - + (cl.isDCLocal() ? " DC " : "") - + (cl.isSerial() ? " SC " : "")) - : "", - (warn != null && !warn.equals("")) ? ": " + warn : "", - se); - if (uow != null) uow.setInfo(ri); - else LOG.debug(ri); + ResultSetFuture futureResultSet = session.executeAsync(statement, uow, timer); + if (uow != null) uow.recordCacheAndDatabaseOperationCount(0, 1); + ResultSet resultSet = futureResultSet.getUninterruptibly(timeout, units); + ColumnDefinitions columnDefinitions = resultSet.getColumnDefinitions(); + if (LOG.isDebugEnabled()) { + ExecutionInfo ei = resultSet.getExecutionInfo(); + Host qh = ei.getQueriedHost(); + String oh = + ei.getTriedHosts() + .stream() + .map(Host::getAddress) + .map(InetAddress::toString) + .collect(Collectors.joining(", ")); + ConsistencyLevel cl = ei.getAchievedConsistencyLevel(); + if (cl == null) { + cl = statement.getConsistencyLevel(); } - if (!resultSet.wasApplied() - && !(columnDefinitions.size() > 1 || !columnDefinitions.contains("[applied]"))) { - throw new HelenusException("Operation Failed"); - } - return resultSet; - - } finally { - timer.stop(); - if (uow != null) uow.addDatabaseTime("Cassandra", timer); - log(statement, uow, timer, showValues); + int se = ei.getSpeculativeExecutions(); + String warn = ei.getWarnings().stream().collect(Collectors.joining(", ")); + String ri = + String.format( + "%s %s ~%s %s %s%s%sspec-retries: %d", + "server v" + qh.getCassandraVersion(), + qh.getAddress().toString(), + (oh != null && !oh.equals("")) ? " [tried: " + oh + "]" : "", + qh.getDatacenter(), + qh.getRack(), + (cl != null) + ? (" consistency: " + + cl.name() + + " " + + (cl.isDCLocal() ? " DC " : "") + + (cl.isSerial() ? " SC " : "")) + : "", + (warn != null && !warn.equals("")) ? ": " + warn : "", + se); + if (uow != null) uow.setInfo(ri); + else LOG.debug(ri); } + if (!resultSet.wasApplied() + && !(columnDefinitions.size() > 1 || !columnDefinitions.contains("[applied]"))) { + throw new HelenusException("Operation Failed"); + } + return resultSet; } finally { - - if (span != null) { - span.finish(); - } + timer.stop(); + if (uow != null) uow.addDatabaseTime("Cassandra", timer); + log(statement, uow, timer, showValues); } } @@ -178,10 +179,15 @@ public abstract class Operation { timerString = String.format(" %s ", timer.toString()); } LOG.info( - String.format("%s%s%s", uowString, timerString, Operation.queryString(statement, false))); + String.format( + "%s%s%s", uowString, timerString, Operation.queryString(statement, showValues))); } } + protected boolean isIdempotentOperation() { + return false; + } + public Statement options(Statement statement) { return statement; } diff --git a/src/main/java/net/helenus/core/operation/SelectFirstOperation.java b/src/main/java/net/helenus/core/operation/SelectFirstOperation.java index f5d28eb..f6ebb5f 100644 --- a/src/main/java/net/helenus/core/operation/SelectFirstOperation.java +++ b/src/main/java/net/helenus/core/operation/SelectFirstOperation.java @@ -63,4 +63,9 @@ public final class SelectFirstOperation public boolean isSessionCacheable() { return delegate.isSessionCacheable(); } + + @Override + public boolean ignoreCache() { + return delegate.ignoreCache(); + } } diff --git a/src/main/java/net/helenus/core/operation/SelectFirstTransformingOperation.java b/src/main/java/net/helenus/core/operation/SelectFirstTransformingOperation.java index 07325ba..1ff19c8 100644 --- a/src/main/java/net/helenus/core/operation/SelectFirstTransformingOperation.java +++ b/src/main/java/net/helenus/core/operation/SelectFirstTransformingOperation.java @@ -56,4 +56,9 @@ public final class SelectFirstTransformingOperation public boolean isSessionCacheable() { return delegate.isSessionCacheable(); } + + @Override + public boolean ignoreCache() { + return delegate.ignoreCache(); + } } diff --git a/src/main/java/net/helenus/core/operation/SelectOperation.java b/src/main/java/net/helenus/core/operation/SelectOperation.java index b70c595..4023947 100644 --- a/src/main/java/net/helenus/core/operation/SelectOperation.java +++ b/src/main/java/net/helenus/core/operation/SelectOperation.java @@ -23,14 +23,15 @@ import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select.Selection; import com.datastax.driver.core.querybuilder.Select.Where; -import com.google.common.collect.Iterables; import java.util.*; import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; import net.helenus.core.*; +import net.helenus.core.cache.CacheUtil; import net.helenus.core.cache.Facet; import net.helenus.core.cache.UnboundFacet; +import net.helenus.core.reflect.Entity; import net.helenus.core.reflect.HelenusPropertyNode; import net.helenus.mapping.HelenusEntity; import net.helenus.mapping.HelenusProperty; @@ -52,8 +53,10 @@ public final class SelectOperation extends AbstractFilterStreamOperation ordering = null; protected Integer limit = null; protected boolean allowFiltering = false; + protected String alternateTableName = null; protected boolean isCacheable = false; + protected boolean implementsEntityType = false; @SuppressWarnings("unchecked") public SelectOperation(AbstractSessionOperations sessionOperations) { @@ -89,7 +92,8 @@ public final class SelectOperation extends AbstractFilterStreamOperation new HelenusPropertyNode(p, Optional.empty())) .forEach(p -> this.props.add(p)); - isCacheable = entity.isCacheable(); + this.isCacheable = entity.isCacheable(); + this.implementsEntityType = Entity.class.isAssignableFrom(entity.getMappingInterface()); } public SelectOperation( @@ -106,7 +110,8 @@ public final class SelectOperation extends AbstractFilterStreamOperation new HelenusPropertyNode(p, Optional.empty())) .forEach(p -> this.props.add(p)); - isCacheable = entity.isCacheable(); + this.isCacheable = entity.isCacheable(); + this.implementsEntityType = Entity.class.isAssignableFrom(entity.getMappingInterface()); } public SelectOperation( @@ -118,6 +123,10 @@ public final class SelectOperation extends AbstractFilterStreamOperation extends AbstractFilterStreamOperation extends AbstractFilterStreamOperation filter : filters.values()) { where.and(filter.getClause(sessionOps.getValuePreparer())); - HelenusProperty prop = filter.getNode().getProperty(); - if (allowFiltering == false) { + HelenusProperty filterProp = filter.getNode().getProperty(); + HelenusProperty prop = + props + .stream() + .map(HelenusPropertyNode::getProperty) + .filter(thisProp -> thisProp.getPropertyName().equals(filterProp.getPropertyName())) + .findFirst() + .orElse(null); + if (allowFiltering == false && prop != null) { switch (prop.getColumnType()) { case PARTITION_KEY: - case CLUSTERING_COLUMN: break; + case CLUSTERING_COLUMN: default: // When using non-Cassandra-standard 2i types or when using more than one // indexed column or non-indexed columns the query must include ALLOW FILTERING. - if (prop.caseSensitiveIndex()) { + if (prop.caseSensitiveIndex() == false) { allowFiltering = true; } else if (prop.getIndexName() != null) { allowFiltering |= !isFirstIndex; diff --git a/src/main/java/net/helenus/core/operation/SelectTransformingOperation.java b/src/main/java/net/helenus/core/operation/SelectTransformingOperation.java index a7ed832..8cfc24a 100644 --- a/src/main/java/net/helenus/core/operation/SelectTransformingOperation.java +++ b/src/main/java/net/helenus/core/operation/SelectTransformingOperation.java @@ -56,4 +56,14 @@ public final class SelectTransformingOperation public Stream transform(ResultSet resultSet) { return delegate.transform(resultSet).map(fn); } + + @Override + public boolean isSessionCacheable() { + return delegate.isSessionCacheable(); + } + + @Override + public boolean ignoreCache() { + return delegate.ignoreCache(); + } } diff --git a/src/main/java/net/helenus/core/operation/UpdateOperation.java b/src/main/java/net/helenus/core/operation/UpdateOperation.java index bbc4935..aa5b2ef 100644 --- a/src/main/java/net/helenus/core/operation/UpdateOperation.java +++ b/src/main/java/net/helenus/core/operation/UpdateOperation.java @@ -26,6 +26,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import net.helenus.core.*; import net.helenus.core.cache.BoundFacet; +import net.helenus.core.cache.CacheUtil; import net.helenus.core.cache.Facet; import net.helenus.core.reflect.HelenusPropertyNode; import net.helenus.core.reflect.MapExportable; @@ -33,7 +34,6 @@ import net.helenus.mapping.HelenusEntity; import net.helenus.mapping.HelenusProperty; import net.helenus.mapping.MappingUtil; import net.helenus.mapping.value.BeanColumnValueProvider; -import net.helenus.mapping.value.ValueProviderMap; import net.helenus.support.HelenusException; import net.helenus.support.HelenusMappingException; import net.helenus.support.Immutables; @@ -43,15 +43,18 @@ public final class UpdateOperation extends AbstractFilterOperation assignments = new HashMap<>(); private final AbstractEntityDraft draft; private final Map draftMap; + private final Set readSet; private HelenusEntity entity = null; private Object pojo; private int[] ttl; private long[] timestamp; + private long writeTime = 0L; public UpdateOperation(AbstractSessionOperations sessionOperations) { super(sessionOperations); this.draft = null; this.draftMap = null; + this.readSet = null; } public UpdateOperation( @@ -59,14 +62,25 @@ public final class UpdateOperation extends AbstractFilterOperation extends AbstractFilterOperation extends AbstractFilterOperation map = ((MapExportable) pojo).toMap(); - if (!(map instanceof ValueProviderMap)) { - if (map.get(key) != v) { - map.put(key, v); - } - } + ((MapExportable) pojo).put(key, v); } } @@ -133,13 +143,14 @@ public final class UpdateOperation extends AbstractFilterOperation extends AbstractFilterOperation extends AbstractFilterOperation list; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - List list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); list.add(0, value); facet = new BoundFacet(prop, list); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - List list = (List) draftMap.get(key); + String key = prop.getPropertyName(); + list = (List) draftMap.get(key); list.add(0, value); + facet = new BoundFacet(prop, list); + } else { + list = null; + facet = null; } assignments.put(QueryBuilder.prepend(p.getColumnName(), valueObj), facet); @@ -220,18 +235,21 @@ public final class UpdateOperation extends AbstractFilterOperation list; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - List list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); list.addAll(0, value); facet = new BoundFacet(prop, list); } else if (draft != null && value.size() > 0) { String key = p.getProperty().getPropertyName(); - List list = (List) draftMap.get(key); + list = (List) draftMap.get(key); list.addAll(0, value); + facet = new BoundFacet(prop, list); + } else { + list = null; + facet = null; } assignments.put(QueryBuilder.prependAll(p.getColumnName(), valueObj), facet); @@ -249,16 +267,14 @@ public final class UpdateOperation extends AbstractFilterOperation list; - HelenusProperty prop = p.getProperty(); + final List list; if (pojo != null) { - list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); } else { - String key = p.getProperty().getPropertyName(); + String key = prop.getPropertyName(); list = (List) draftMap.get(key); } if (idx < 0) { @@ -270,6 +286,8 @@ public final class UpdateOperation extends AbstractFilterOperation extends AbstractFilterOperation list; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - List list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); list.add(value); facet = new BoundFacet(prop, list); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - List list = (List) draftMap.get(key); + String key = prop.getPropertyName(); + list = (List) draftMap.get(key); list.add(value); + facet = new BoundFacet(prop, list); + } else { + list = null; + facet = null; } assignments.put(QueryBuilder.append(p.getColumnName(), valueObj), facet); @@ -315,18 +336,21 @@ public final class UpdateOperation extends AbstractFilterOperation list; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - List list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); list.addAll(value); facet = new BoundFacet(prop, list); } else if (draft != null && value.size() > 0) { - String key = p.getProperty().getPropertyName(); - List list = (List) draftMap.get(key); + String key = prop.getPropertyName(); + list = (List) draftMap.get(key); list.addAll(value); + facet = new BoundFacet(prop, list); + } else { + list = null; + facet = null; } assignments.put(QueryBuilder.appendAll(p.getColumnName(), valueObj), facet); @@ -343,18 +367,21 @@ public final class UpdateOperation extends AbstractFilterOperation list; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - List list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); list.remove(value); facet = new BoundFacet(prop, list); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - List list = (List) draftMap.get(key); + String key = prop.getPropertyName(); + list = (List) draftMap.get(key); list.remove(value); + facet = new BoundFacet(prop, list); + } else { + list = null; + facet = null; } assignments.put(QueryBuilder.discard(p.getColumnName(), valueObj), facet); @@ -371,18 +398,21 @@ public final class UpdateOperation extends AbstractFilterOperation list; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - List list = - new ArrayList( - (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + list = (List) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); list.removeAll(value); facet = new BoundFacet(prop, list); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - List list = (List) draftMap.get(key); + String key = prop.getPropertyName(); + list = (List) draftMap.get(key); list.removeAll(value); + facet = new BoundFacet(prop, list); + } else { + list = null; + facet = null; } assignments.put(QueryBuilder.discardAll(p.getColumnName(), valueObj), facet); @@ -437,17 +467,21 @@ public final class UpdateOperation extends AbstractFilterOperation set; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - Set set = - new HashSet((Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + set = (Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); set.add(value); facet = new BoundFacet(prop, set); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - Set set = (Set) draftMap.get(key); + String key = prop.getPropertyName(); + set = (Set) draftMap.get(key); set.add(value); + facet = new BoundFacet(prop, set); + } else { + set = null; + facet = null; } assignments.put(QueryBuilder.add(p.getColumnName(), valueObj), facet); @@ -464,17 +498,21 @@ public final class UpdateOperation extends AbstractFilterOperation set; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - Set set = - new HashSet((Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + set = (Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); set.addAll(value); facet = new BoundFacet(prop, set); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - Set set = (Set) draftMap.get(key); + String key = prop.getPropertyName(); + set = (Set) draftMap.get(key); set.addAll(value); + facet = new BoundFacet(prop, set); + } else { + set = null; + facet = null; } assignments.put(QueryBuilder.addAll(p.getColumnName(), valueObj), facet); @@ -491,17 +529,21 @@ public final class UpdateOperation extends AbstractFilterOperation set; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - Set set = - new HashSet((Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + set = (Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); set.remove(value); facet = new BoundFacet(prop, set); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - Set set = (Set) draftMap.get(key); + String key = prop.getPropertyName(); + set = (Set) draftMap.get(key); set.remove(value); + facet = new BoundFacet(prop, set); + } else { + set = null; + facet = null; } assignments.put(QueryBuilder.remove(p.getColumnName(), valueObj), facet); @@ -518,17 +560,21 @@ public final class UpdateOperation extends AbstractFilterOperation set; + final BoundFacet facet; + HelenusProperty prop = p.getProperty(); if (pojo != null) { - HelenusProperty prop = p.getProperty(); - Set set = - new HashSet((Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + set = (Set) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); set.removeAll(value); facet = new BoundFacet(prop, set); } else if (draft != null) { - String key = p.getProperty().getPropertyName(); - Set set = (Set) draftMap.get(key); + String key = prop.getPropertyName(); + set = (Set) draftMap.get(key); set.removeAll(value); + facet = new BoundFacet(prop, set); + } else { + set = null; + facet = null; } assignments.put(QueryBuilder.removeAll(p.getColumnName(), valueObj), facet); @@ -582,15 +628,19 @@ public final class UpdateOperation extends AbstractFilterOperation map; + final BoundFacet facet; if (pojo != null) { - Map map = - new HashMap( - (Map) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + map = (Map) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); map.put(key, value); facet = new BoundFacet(prop, map); } else if (draft != null) { - ((Map) draftMap.get(prop.getPropertyName())).put(key, value); + map = (Map) draftMap.get(prop.getPropertyName()); + map.put(key, value); + facet = new BoundFacet(prop, map); + } else { + map = null; + facet = null; } Optional> converter = @@ -618,15 +668,19 @@ public final class UpdateOperation extends AbstractFilterOperation newMap; + final BoundFacet facet; if (pojo != null) { - Map newMap = - new HashMap( - (Map) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop)); + newMap = (Map) BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop, false); newMap.putAll(map); facet = new BoundFacet(prop, newMap); } else if (draft != null) { - ((Map) draftMap.get(prop.getPropertyName())).putAll(map); + newMap = (Map) draftMap.get(prop.getPropertyName()); + newMap.putAll(map); + facet = new BoundFacet(prop, newMap); + } else { + newMap = null; + facet = null; } Optional> converter = @@ -718,13 +772,58 @@ public final class UpdateOperation extends AbstractFilterOperation names = new ArrayList(assignments.size()); + for (BoundFacet facet : assignments.values()) { + for (HelenusProperty prop : facet.getProperties()) { + names.add(prop.getColumnName().toCql(false)); + } + } + + if (names.size() > 0) { + if (ttl != null) { + names.forEach(name -> pojo.put(CacheUtil.ttlKey(name), ttl)); + } + if (writeTime != 0L) { + names.forEach(name -> pojo.put(CacheUtil.writeTimeKey(name), writeTime)); + } + } + } + } + + @Override + protected boolean isIdempotentOperation() { + return assignments + .values() + .stream() + .allMatch( + facet -> { + if (facet != null) { + Set props = facet.getProperties(); + if (props != null && props.size() > 0) { + return props.stream().allMatch(prop -> prop.isIdempotent()); + } else { + return true; + } + } else { + // In this case our UPDATE statement made mutations via the List, Set, Map methods only. + return false; + } + }) + || super.isIdempotentOperation(); + } + @Override public E sync() throws TimeoutException { E result = super.sync(); - if (entity.isCacheable()) { + if (result != null && entity.isCacheable()) { if (draft != null) { - sessionOps.updateCache(draft, bindFacetValues()); + adjustTtlAndWriteTime(draft); + adjustTtlAndWriteTime((MapExportable) result); + sessionOps.updateCache(result, bindFacetValues()); } else if (pojo != null) { + adjustTtlAndWriteTime((MapExportable) pojo); sessionOps.updateCache(pojo, bindFacetValues()); } else { sessionOps.cacheEvict(bindFacetValues()); @@ -739,15 +838,47 @@ public final class UpdateOperation extends AbstractFilterOperation bindFacetValues() { List facets = bindFacetValues(entity.getFacets()); diff --git a/src/main/java/net/helenus/core/reflect/Drafted.java b/src/main/java/net/helenus/core/reflect/Drafted.java index 17196b8..4ecc667 100644 --- a/src/main/java/net/helenus/core/reflect/Drafted.java +++ b/src/main/java/net/helenus/core/reflect/Drafted.java @@ -22,4 +22,6 @@ public interface Drafted extends MapExportable { Set mutated(); T build(); + + Set read(); } diff --git a/src/main/java/net/helenus/core/reflect/Entity.java b/src/main/java/net/helenus/core/reflect/Entity.java new file mode 100644 index 0000000..50af637 --- /dev/null +++ b/src/main/java/net/helenus/core/reflect/Entity.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.core.reflect; + +import net.helenus.core.Getter; + +public interface Entity { + String WRITTEN_AT_METHOD = "writtenAt"; + String TTL_OF_METHOD = "ttlOf"; + String TOKEN_OF_METHOD = "tokenOf"; + + /** + * The write time for the property in question referenced by the getter. + * + * @param getter the property getter + * @return the timestamp associated with the property identified by the getter + */ + default Long writtenAt(Getter getter) { + return 0L; + } + + /** + * The write time for the property in question referenced by the property name. + * + * @param prop the name of a property in this entity + * @return the timestamp associated with the property identified by the property name if it exists + */ + default Long writtenAt(String prop) { + return 0L; + }; + + /** + * The time-to-live for the property in question referenced by the getter. + * + * @param getter the property getter + * @return the time-to-live in seconds associated with the property identified by the getter + */ + default Integer ttlOf(Getter getter) { + return 0; + }; + + /** + * The time-to-live for the property in question referenced by the property name. + * + * @param prop the name of a property in this entity + * @return the time-to-live in seconds associated with the property identified by the property name if it exists + */ + default Integer ttlOf(String prop) { + return 0; + }; + + /** + * The token (partition identifier) for this entity which can change over time if + * the cluster grows or shrinks but should be stable otherwise. + * + * @return the token for the entity + */ + default Long tokenOf() { return 0L; } +} diff --git a/src/main/java/net/helenus/core/reflect/HelenusNamedProperty.java b/src/main/java/net/helenus/core/reflect/HelenusNamedProperty.java index 9c4e448..c295fc3 100644 --- a/src/main/java/net/helenus/core/reflect/HelenusNamedProperty.java +++ b/src/main/java/net/helenus/core/reflect/HelenusNamedProperty.java @@ -63,6 +63,11 @@ public final class HelenusNamedProperty implements HelenusProperty { return false; } + @Override + public boolean isIdempotent() { + return false; + } + @Override public Class getJavaType() { throw new HelenusMappingException("will never called"); diff --git a/src/main/java/net/helenus/core/reflect/MapExportable.java b/src/main/java/net/helenus/core/reflect/MapExportable.java index 9160cc8..b121aa7 100644 --- a/src/main/java/net/helenus/core/reflect/MapExportable.java +++ b/src/main/java/net/helenus/core/reflect/MapExportable.java @@ -16,10 +16,25 @@ package net.helenus.core.reflect; import java.util.Map; +import java.util.Set; +import net.helenus.core.Getter; public interface MapExportable { - - public static final String TO_MAP_METHOD = "toMap"; + String TO_MAP_METHOD = "toMap"; + String TO_READ_SET_METHOD = "toReadSet"; + String PUT_METHOD = "put"; Map toMap(); + + default Map toMap(boolean mutable) { + return null; + } + + default Set toReadSet() { + return null; + } + + default void put(String key, Object value) {} + + default void put(Getter getter, T value) {} } diff --git a/src/main/java/net/helenus/core/reflect/MapperInvocationHandler.java b/src/main/java/net/helenus/core/reflect/MapperInvocationHandler.java index e33291a..53d9f2b 100644 --- a/src/main/java/net/helenus/core/reflect/MapperInvocationHandler.java +++ b/src/main/java/net/helenus/core/reflect/MapperInvocationHandler.java @@ -15,6 +15,9 @@ */ package net.helenus.core.reflect; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectStreamException; @@ -24,10 +27,11 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; +import net.helenus.core.Getter; import net.helenus.core.Helenus; +import net.helenus.core.cache.CacheUtil; +import net.helenus.mapping.MappingUtil; import net.helenus.mapping.annotation.Transient; import net.helenus.mapping.value.ValueProviderMap; import net.helenus.support.HelenusException; @@ -35,7 +39,8 @@ import net.helenus.support.HelenusException; public class MapperInvocationHandler implements InvocationHandler, Serializable { private static final long serialVersionUID = -7044209982830584984L; - private final Map src; + private Map src; + private final Set read = new HashSet(); private final Class iface; public MapperInvocationHandler(Class iface, Map src) { @@ -95,15 +100,96 @@ public class MapperInvocationHandler implements InvocationHandler, Serializab return true; } } - if (otherObj instanceof MapExportable && src.equals(((MapExportable) otherObj).toMap())) { - return true; - } - if (src instanceof MapExportable && otherObj.equals(((MapExportable) src).toMap())) { - return true; + if (otherObj instanceof MapExportable) { + return MappingUtil.compareMaps((MapExportable) otherObj, src); } return false; } + if (MapExportable.PUT_METHOD.equals(methodName) && method.getParameterCount() == 2) { + final String key; + if (args[0] instanceof String) { + key = (String) args[0]; + } else if (args[0] instanceof Getter) { + key = MappingUtil.resolveMappingProperty((Getter) args[0]).getProperty().getPropertyName(); + } else { + key = null; + } + if (key != null) { + final Object value = (Object) args[1]; + if (src instanceof ValueProviderMap) { + this.src = fromValueProviderMap(src); + } + src.put(key, value); + } + return null; + } + + if (Entity.WRITTEN_AT_METHOD.equals(methodName) && method.getParameterCount() == 1) { + final String key; + if (args[0] instanceof String) { + key = CacheUtil.writeTimeKey((String) args[0]); + } else if (args[0] instanceof Getter) { + Getter getter = (Getter) args[0]; + key = + CacheUtil.writeTimeKey( + MappingUtil.resolveMappingProperty(getter) + .getProperty() + .getColumnName() + .toCql(false)); + } else { + return 0L; + } + Long v = (Long) src.get(key); + if (v != null) { + return v; + } + return 0L; + } + + if (Entity.TOKEN_OF_METHOD.equals(methodName) && method.getParameterCount() == 0) { + Long v = (Long) src.get(""); + if (v != null) { + return v; + } + return 0L; + } + + if (Entity.TTL_OF_METHOD.equals(methodName) && method.getParameterCount() == 1) { + final String key; + if (args[0] instanceof String) { + key = CacheUtil.ttlKey((String) args[0]); + } else if (args[0] instanceof Getter) { + Getter getter = (Getter) args[0]; + key = + CacheUtil.ttlKey( + MappingUtil.resolveMappingProperty(getter) + .getProperty() + .getColumnName() + .toCql(false)); + } else { + return 0; + } + int v[] = (int[]) src.get(key); + if (v != null) { + return v[0]; + } + return 0; + } + + if (MapExportable.TO_MAP_METHOD.equals(methodName)) { + if (method.getParameterCount() == 1 && args[0] instanceof Boolean) { + if ((boolean) args[0] == true) { + return fromValueProviderMap(src, true); + } + } + return Collections.unmodifiableMap(src); + } + + if (MapExportable.TO_READ_SET_METHOD.equals(methodName)) { + return read; + } + if (method.getParameterCount() != 0 || method.getReturnType() == void.class) { throw new HelenusException("invalid getter method " + method); } @@ -128,26 +214,21 @@ public class MapperInvocationHandler implements InvocationHandler, Serializab return Helenus.dsl(iface); } - if (MapExportable.TO_MAP_METHOD.equals(methodName)) { - return src; // Collections.unmodifiableMap(src); - } - - Object value = src.get(methodName); - - Class returnType = method.getReturnType(); + final Object value = src.get(methodName); + read.add(methodName); if (value == null) { + Class returnType = method.getReturnType(); + // Default implementations of non-Transient methods in entities are the default - // value when the - // map contains 'null'. + // value when the map contains 'null'. if (method.isDefault()) { return invokeDefault(proxy, method, args); } // Otherwise, if the return type of the method is a primitive Java type then - // we'll return the standard - // default values to avoid a NPE in user code. + // we'll return the standard default values to avoid a NPE in user code. if (returnType.isPrimitive()) { DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(returnType); if (type == null) { @@ -160,6 +241,35 @@ public class MapperInvocationHandler implements InvocationHandler, Serializab return value; } + static Map fromValueProviderMap(Map v) { + return fromValueProviderMap(v, false); + } + + static Map fromValueProviderMap(Map v, boolean mutable) { + if (v instanceof ValueProviderMap) { + Map m = new HashMap(v.size()); + Set keys = v.keySet(); + for (String key : keys) { + Object value = v.get(key); + if (value != null && mutable) { + if (ImmutableList.class.isAssignableFrom(value.getClass())) { + m.put(key, new ArrayList((List) value)); + } else if (ImmutableMap.class.isAssignableFrom(value.getClass())) { + m.put(key, new HashMap((Map) value)); + } else if (ImmutableSet.class.isAssignableFrom(value.getClass())) { + m.put(key, new HashSet((Set) value)); + } else { + m.put(key, value); + } + } else { + m.put(key, value); + } + } + return m; + } + return v; + } + static class SerializationProxy implements Serializable { private static final long serialVersionUID = -5617583940055969353L; @@ -170,11 +280,7 @@ public class MapperInvocationHandler implements InvocationHandler, Serializab public SerializationProxy(MapperInvocationHandler mapper) { this.iface = mapper.iface; if (mapper.src instanceof ValueProviderMap) { - this.src = new HashMap(mapper.src.size()); - Set keys = mapper.src.keySet(); - for (String key : keys) { - this.src.put(key, mapper.src.get(key)); - } + this.src = fromValueProviderMap(mapper.src); } else { this.src = mapper.src; } diff --git a/src/main/java/net/helenus/mapping/HelenusEntity.java b/src/main/java/net/helenus/mapping/HelenusEntity.java index b19fee6..9ec0744 100644 --- a/src/main/java/net/helenus/mapping/HelenusEntity.java +++ b/src/main/java/net/helenus/mapping/HelenusEntity.java @@ -34,4 +34,6 @@ public interface HelenusEntity { HelenusProperty getProperty(String name); List getFacets(); + + boolean isDraftable(); } diff --git a/src/main/java/net/helenus/mapping/HelenusMappingEntity.java b/src/main/java/net/helenus/mapping/HelenusMappingEntity.java index cf343bb..484318a 100644 --- a/src/main/java/net/helenus/mapping/HelenusMappingEntity.java +++ b/src/main/java/net/helenus/mapping/HelenusMappingEntity.java @@ -31,6 +31,7 @@ import net.helenus.mapping.annotation.*; import net.helenus.mapping.validator.DistinctValidator; import net.helenus.support.HelenusMappingException; import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.StringUtils; public final class HelenusMappingEntity implements HelenusEntity { @@ -38,6 +39,7 @@ public final class HelenusMappingEntity implements HelenusEntity { private final HelenusEntityType type; private final IdentityName name; private final boolean cacheable; + private final boolean draftable; private final ImmutableMap methods; private final ImmutableMap props; private final ImmutableList orderedProps; @@ -111,9 +113,25 @@ public final class HelenusMappingEntity implements HelenusEntity { // Caching cacheable = (null != iface.getDeclaredAnnotation(Cacheable.class)); + // Draft + Class draft; + try { + draft = Class.forName(iface.getName() + "$Draft"); + } catch (Exception ignored) { + draft = null; + } + draftable = (draft != null); + + // Materialized view List primaryKeyProperties = new ArrayList<>(); ImmutableList.Builder facetsBuilder = ImmutableList.builder(); - facetsBuilder.add(new Facet("table", name.toCql()).setFixed()); + if (iface.getDeclaredAnnotation(MaterializedView.class) == null) { + facetsBuilder.add(new Facet("table", name.toCql()).setFixed()); + } else { + facetsBuilder.add( + new Facet("table", Helenus.entity(iface.getInterfaces()[0]).getName().toCql()) + .setFixed()); + } for (HelenusProperty prop : orderedProps) { switch (prop.getColumnType()) { case PARTITION_KEY: @@ -127,8 +145,25 @@ public final class HelenusMappingEntity implements HelenusEntity { } for (ConstraintValidator constraint : MappingUtil.getValidators(prop.getGetterMethod())) { - if (constraint.getClass().isAssignableFrom(DistinctValidator.class)) { - UnboundFacet facet = new UnboundFacet(prop); + if (constraint instanceof DistinctValidator) { + DistinctValidator validator = (DistinctValidator) constraint; + String[] values = validator.constraintAnnotation.value(); + UnboundFacet facet; + if (values != null && values.length >= 1 && !(StringUtils.isBlank(values[0]))) { + List props = new ArrayList(values.length + 1); + props.add(prop); + for (String value : values) { + for (HelenusProperty p : orderedProps) { + String name = p.getPropertyName(); + if (name.equals(value) && !name.equals(prop.getPropertyName())) { + props.add(p); + } + } + } + facet = new UnboundFacet(props, validator.alone(), validator.combined()); + } else { + facet = new UnboundFacet(prop, validator.alone(), validator.combined()); + } facetsBuilder.add(facet); break; } @@ -188,6 +223,11 @@ public final class HelenusMappingEntity implements HelenusEntity { return cacheable; } + @Override + public boolean isDraftable() { + return draftable; + } + @Override public Class getMappingInterface() { return iface; diff --git a/src/main/java/net/helenus/mapping/HelenusMappingProperty.java b/src/main/java/net/helenus/mapping/HelenusMappingProperty.java index fbea1dd..c3646a6 100644 --- a/src/main/java/net/helenus/mapping/HelenusMappingProperty.java +++ b/src/main/java/net/helenus/mapping/HelenusMappingProperty.java @@ -35,6 +35,7 @@ public final class HelenusMappingProperty implements HelenusProperty { private final String propertyName; private final Optional indexName; private final boolean caseSensitiveIndex; + private final boolean idempotent; private final ColumnInformation columnInfo; @@ -56,6 +57,15 @@ public final class HelenusMappingProperty implements HelenusProperty { this.columnInfo = new ColumnInformation(getter); + switch (this.columnInfo.getColumnType()) { + case PARTITION_KEY: + case CLUSTERING_COLUMN: + this.idempotent = true; + break; + default: + this.idempotent = MappingUtil.idempotent(getter); + } + this.genericJavaType = getter.getGenericReturnType(); this.javaType = getter.getReturnType(); this.abstractJavaType = MappingJavaTypes.resolveJavaType(this.javaType); @@ -112,6 +122,11 @@ public final class HelenusMappingProperty implements HelenusProperty { return caseSensitiveIndex; } + @Override + public boolean isIdempotent() { + return idempotent; + } + @Override public String getPropertyName() { return propertyName; diff --git a/src/main/java/net/helenus/mapping/HelenusProperty.java b/src/main/java/net/helenus/mapping/HelenusProperty.java index 08b428e..99b0dbf 100644 --- a/src/main/java/net/helenus/mapping/HelenusProperty.java +++ b/src/main/java/net/helenus/mapping/HelenusProperty.java @@ -37,6 +37,8 @@ public interface HelenusProperty { boolean caseSensitiveIndex(); + boolean isIdempotent(); + Class getJavaType(); AbstractDataType getDataType(); diff --git a/src/main/java/net/helenus/mapping/MappingUtil.java b/src/main/java/net/helenus/mapping/MappingUtil.java index 4bff364..bb68587 100644 --- a/src/main/java/net/helenus/mapping/MappingUtil.java +++ b/src/main/java/net/helenus/mapping/MappingUtil.java @@ -16,11 +16,12 @@ package net.helenus.mapping; import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import net.helenus.core.Getter; @@ -122,10 +123,29 @@ public final class MappingUtil { return false; } + public static boolean idempotent(Method getterMethod) { + Column column = getterMethod.getDeclaredAnnotation(Column.class); + + if (column != null) { + return column.idempotent(); + } + return false; + } + public static String getPropertyName(Method getter) { return getter.getName(); } + public static HelenusProperty getPropertyForColumn(HelenusEntity entity, String name) { + if (name == null) return null; + return entity + .getOrderedProperties() + .stream() + .filter(p -> p.getColumnName().equals(name)) + .findFirst() + .orElse(null); + } + public static String getDefaultColumnName(Method getter) { return Helenus.settings().getPropertyToColumnConverter().apply(getPropertyName(getter)); } @@ -284,28 +304,6 @@ public final class MappingUtil { } } - // https://stackoverflow.com/a/4882306/366692 - public static T clone(T object) throws CloneNotSupportedException { - Object clone = null; - - // Use reflection, because there is no other way - try { - Method method = object.getClass().getMethod("clone"); - clone = method.invoke(object); - } catch (InvocationTargetException e) { - rethrow(e.getCause()); - } catch (Exception cause) { - rethrow(cause); - } - if (object.getClass().isInstance(clone)) { - @SuppressWarnings("unchecked") // clone class <= object class <= T - T t = (T) clone; - return t; - } else { - throw new ClassCastException(clone.getClass().getName()); - } - } - private static void rethrow(Throwable cause) throws CloneNotSupportedException { if (cause instanceof RuntimeException) { throw (RuntimeException) cause; @@ -320,4 +318,32 @@ public final class MappingUtil { e.initCause(cause); throw e; } + + public static boolean compareMaps(MapExportable me, Map m2) { + Map m1 = me.toMap(); + List matching = + m2.entrySet() + .stream() + .filter(e -> !e.getKey().matches("^_.*_(ttl|writeTime)$")) + .filter( + e -> { + String k = e.getKey(); + if (m1.containsKey(k)) { + Object o1 = e.getValue(); + Object o2 = m1.get(k); + if (o1 == o2 || o1.equals(o2)) return true; + } + return false; + }) + .map(e -> e.getKey()) + .collect(Collectors.toList()); + List divergent = + m1.entrySet() + .stream() + .filter(e -> !e.getKey().matches("^_.*_(ttl|writeTime)$")) + .filter(e -> !matching.contains(e.getKey())) + .map(e -> e.getKey()) + .collect(Collectors.toList()); + return divergent.size() > 0 ? false : true; + } } diff --git a/src/main/java/net/helenus/mapping/annotation/Column.java b/src/main/java/net/helenus/mapping/annotation/Column.java index ab4b2d4..ca91166 100644 --- a/src/main/java/net/helenus/mapping/annotation/Column.java +++ b/src/main/java/net/helenus/mapping/annotation/Column.java @@ -59,4 +59,13 @@ public @interface Column { * @return true if name have to be quoted */ boolean forceQuote() default false; + + /** + * Used to determine if mutations (insert, upsert, update) can be retried by the server. When all + * fields in a query are idempotent the query is marked idempotent. Optionally, a user can + * explicitly mark a query idempotent even if all fields are not marked as such. + * + * @return + */ + boolean idempotent() default false; } diff --git a/src/main/java/net/helenus/mapping/annotation/Constraints.java b/src/main/java/net/helenus/mapping/annotation/Constraints.java index d1b99d2..cf594ff 100644 --- a/src/main/java/net/helenus/mapping/annotation/Constraints.java +++ b/src/main/java/net/helenus/mapping/annotation/Constraints.java @@ -228,10 +228,107 @@ public final class Constraints { public @interface Distinct { /** - * User defined Enum to further restrict the items in the set. + * User defined list of properties that combine with this one to result in a distinct + * combination in the table. * * @return Java */ - Class value() default Enum.class; + String[] value() default ""; + + boolean alone() default true; + + boolean combined() default true; + } + + /** + * Distinct annotation is used to signal, but not ensure that a value should be distinct in the + * database. + * + *

Can be used only for @java.lang.CharSequence + * + *

It does not have effect on selects and data retrieval operations + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Constraint(validatedBy = OneToOneRelationshipValidator.class) + public @interface OneToOne { + + /** + * User defined list of properties that combine with this one to result in a distinct + * combination in the table. + * + * @return Java + */ + String[] value() default ""; + } + + /** + * Distinct annotation is used to signal, but not ensure that a value should be distinct in the + * database. + * + *

Can be used only for @java.lang.CharSequence + * + *

It does not have effect on selects and data retrieval operations + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Constraint(validatedBy = OneToManyRelationshipValidator.class) + public @interface OneToMany { + + /** + * User defined list of properties that combine with this one to result in a distinct + * combination in the table. + * + * @return Java + */ + String[] value() default ""; + } + + /** + * Distinct annotation is used to signal, but not ensure that a value should be distinct in the + * database. + * + *

Can be used only for @java.lang.CharSequence + * + *

It does not have effect on selects and data retrieval operations + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Constraint(validatedBy = ManyToOneRelationshipValidator.class) + public @interface ManyToOne { + + /** + * User defined list of properties that combine with this one to result in a distinct + * combination in the table. + * + * @return Java + */ + String[] value() default ""; + } + + /** + * Distinct annotation is used to signal, but not ensure that a value should be distinct in the + * database. + * + *

Can be used only for @java.lang.CharSequence + * + *

It does not have effect on selects and data retrieval operations + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Constraint(validatedBy = ManyToManyRelationshipValidator.class) + public @interface ManyToMany { + + /** + * User defined list of properties that combine with this one to result in a distinct + * combination in the table. + * + * @return Java + */ + String[] value() default ""; } } diff --git a/src/main/java/net/helenus/core/cache/GuavaCache.java b/src/main/java/net/helenus/mapping/validator/AbstractConstraintValidator.java similarity index 56% rename from src/main/java/net/helenus/core/cache/GuavaCache.java rename to src/main/java/net/helenus/mapping/validator/AbstractConstraintValidator.java index 5418c3a..b56b05b 100644 --- a/src/main/java/net/helenus/core/cache/GuavaCache.java +++ b/src/main/java/net/helenus/mapping/validator/AbstractConstraintValidator.java @@ -13,31 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package net.helenus.mapping.validator; -package net.helenus.core.cache; +import java.lang.annotation.Annotation; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; -import com.google.common.cache.Cache; +public abstract class AbstractConstraintValidator + implements ConstraintValidator { -public class GuavaCache implements SessionCache { + public A constraintAnnotation; - final Cache cache; - - GuavaCache(Cache cache) { - this.cache = cache; + @Override + public void initialize(A constraintAnnotation) { + this.constraintAnnotation = constraintAnnotation; } @Override - public void invalidate(K key) { - cache.invalidate(key); - } - - @Override - public V get(K key) { - return cache.getIfPresent(key); - } - - @Override - public void put(K key, V value) { - cache.put(key, value); - } + public abstract boolean isValid(T value, ConstraintValidatorContext context); } diff --git a/src/main/java/net/helenus/mapping/validator/DistinctValidator.java b/src/main/java/net/helenus/mapping/validator/DistinctValidator.java index 8dc4711..872b64f 100644 --- a/src/main/java/net/helenus/mapping/validator/DistinctValidator.java +++ b/src/main/java/net/helenus/mapping/validator/DistinctValidator.java @@ -20,15 +20,32 @@ import javax.validation.ConstraintValidatorContext; import net.helenus.mapping.annotation.Constraints; public final class DistinctValidator + extends AbstractConstraintValidator implements ConstraintValidator { + private Constraints.Distinct annotation; + @Override - public void initialize(Constraints.Distinct constraintAnnotation) {} + public void initialize(Constraints.Distinct constraintAnnotation) { + super.initialize(constraintAnnotation); + this.annotation = constraintAnnotation; + } @Override public boolean isValid(CharSequence value, ConstraintValidatorContext context) { - // TODO(gburd): if there is an Enum type supplied, check that value is valid - // Enum.name() + // TODO(gburd): check that the list contains valid property names. return true; } + + public String[] value() { + return annotation == null ? null : annotation.value(); + } + + public boolean alone() { + return annotation == null ? true : annotation.alone(); + } + + public boolean combined() { + return annotation == null ? true : annotation.combined(); + } } diff --git a/src/main/java/net/helenus/mapping/validator/ManyToManyRelationshipValidator.java b/src/main/java/net/helenus/mapping/validator/ManyToManyRelationshipValidator.java new file mode 100644 index 0000000..1757fda --- /dev/null +++ b/src/main/java/net/helenus/mapping/validator/ManyToManyRelationshipValidator.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.mapping.validator; + +import javax.validation.ConstraintValidatorContext; +import net.helenus.mapping.annotation.Constraints; + +public class ManyToManyRelationshipValidator extends RelationshipValidator { + + @Override + public void initialize(Constraints.ManyToMany constraintAnnotation) { + super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + // TODO(gburd): check that the list contains valid property names. + return true; + } +} diff --git a/src/main/java/net/helenus/mapping/validator/ManyToOneRelationshipValidator.java b/src/main/java/net/helenus/mapping/validator/ManyToOneRelationshipValidator.java new file mode 100644 index 0000000..bc40ec7 --- /dev/null +++ b/src/main/java/net/helenus/mapping/validator/ManyToOneRelationshipValidator.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.mapping.validator; + +import javax.validation.ConstraintValidatorContext; +import net.helenus.mapping.annotation.Constraints; + +public class ManyToOneRelationshipValidator extends RelationshipValidator { + + @Override + public void initialize(Constraints.ManyToOne constraintAnnotation) { + super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + // TODO(gburd): check that the list contains valid property names. + return true; + } +} diff --git a/src/main/java/net/helenus/mapping/validator/OneToManyRelationshipValidator.java b/src/main/java/net/helenus/mapping/validator/OneToManyRelationshipValidator.java new file mode 100644 index 0000000..9a76cfd --- /dev/null +++ b/src/main/java/net/helenus/mapping/validator/OneToManyRelationshipValidator.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.mapping.validator; + +import javax.validation.ConstraintValidatorContext; +import net.helenus.mapping.annotation.Constraints; + +public class OneToManyRelationshipValidator extends RelationshipValidator { + + @Override + public void initialize(Constraints.OneToMany constraintAnnotation) { + super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + // TODO(gburd): check that the list contains valid property names. + return true; + } +} diff --git a/src/main/java/net/helenus/core/UnitOfWorkImpl.java b/src/main/java/net/helenus/mapping/validator/OneToOneRelationshipValidator.java similarity index 53% rename from src/main/java/net/helenus/core/UnitOfWorkImpl.java rename to src/main/java/net/helenus/mapping/validator/OneToOneRelationshipValidator.java index 52cae59..918b584 100644 --- a/src/main/java/net/helenus/core/UnitOfWorkImpl.java +++ b/src/main/java/net/helenus/mapping/validator/OneToOneRelationshipValidator.java @@ -13,14 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package net.helenus.core; +package net.helenus.mapping.validator; -import net.helenus.support.HelenusException; +import javax.validation.ConstraintValidatorContext; +import net.helenus.mapping.annotation.Constraints; -class UnitOfWorkImpl extends AbstractUnitOfWork { +public class OneToOneRelationshipValidator extends RelationshipValidator { - @SuppressWarnings("unchecked") - public UnitOfWorkImpl(HelenusSession session, UnitOfWork parent) { - super(session, (AbstractUnitOfWork) parent); + @Override + public void initialize(Constraints.OneToOne constraintAnnotation) { + super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + // TODO(gburd): check that the list contains valid property names. + return true; } } diff --git a/src/main/java/net/helenus/mapping/validator/RelationshipValidator.java b/src/main/java/net/helenus/mapping/validator/RelationshipValidator.java new file mode 100644 index 0000000..36aa54e --- /dev/null +++ b/src/main/java/net/helenus/mapping/validator/RelationshipValidator.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.mapping.validator; + +import java.lang.annotation.Annotation; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public abstract class RelationshipValidator + extends AbstractConstraintValidator + implements ConstraintValidator { + + @Override + public void initialize(A constraintAnnotation) { + super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + return false; + } +} diff --git a/src/main/java/net/helenus/mapping/value/BeanColumnValueProvider.java b/src/main/java/net/helenus/mapping/value/BeanColumnValueProvider.java index 9c1c44d..e3c6551 100644 --- a/src/main/java/net/helenus/mapping/value/BeanColumnValueProvider.java +++ b/src/main/java/net/helenus/mapping/value/BeanColumnValueProvider.java @@ -32,6 +32,7 @@ public enum BeanColumnValueProvider implements ColumnValueProvider { Object value = null; try { + getter.setAccessible(true); value = getter.invoke(bean, new Object[] {}); } catch (InvocationTargetException e) { if (e.getCause() != null) { diff --git a/src/main/java/net/helenus/mapping/value/ValueProviderMap.java b/src/main/java/net/helenus/mapping/value/ValueProviderMap.java index 24b60ab..8daf81e 100644 --- a/src/main/java/net/helenus/mapping/value/ValueProviderMap.java +++ b/src/main/java/net/helenus/mapping/value/ValueProviderMap.java @@ -15,11 +15,11 @@ */ package net.helenus.mapping.value; +import com.google.common.collect.ImmutableMap; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import net.helenus.core.reflect.Drafted; import net.helenus.mapping.HelenusEntity; import net.helenus.mapping.HelenusProperty; import net.helenus.support.HelenusMappingException; @@ -35,7 +35,7 @@ public final class ValueProviderMap implements Map { this.source = source; this.valueProvider = valueProvider; this.entity = entity; - this.immutable = entity.getMappingInterface().isAssignableFrom(Drafted.class); + this.immutable = entity.isDraftable(); } private static void throwShouldNeverCall(String methodName) { @@ -45,8 +45,7 @@ public final class ValueProviderMap implements Map { methodName)); } - @Override - public Object get(Object key) { + public Object get(Object key, boolean immutable) { if (key instanceof String) { String name = (String) key; HelenusProperty prop = entity.getProperty(name); @@ -57,6 +56,11 @@ public final class ValueProviderMap implements Map { return null; } + @Override + public Object get(Object key) { + return get(key, this.immutable); + } + @Override public Set keySet() { return entity @@ -78,7 +82,7 @@ public final class ValueProviderMap implements Map { @Override public boolean containsKey(Object key) { - if (key instanceof Object) { + if (key instanceof String) { String s = (String) key; return keySet().contains(s); } @@ -149,8 +153,10 @@ public final class ValueProviderMap implements Map { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || (!o.getClass().isAssignableFrom(Map.class) && getClass() != o.getClass())) - return false; + if (o == null + || !((Map.class.isAssignableFrom(o.getClass()) + || ImmutableMap.class.isAssignableFrom(o.getClass())) + || o.getClass().getSimpleName().equals("UnmodifiableMap"))) return false; Map that = (Map) o; if (this.size() != that.size()) return false; diff --git a/src/test/java/net/helenus/test/integration/core/ContextInitTest.java b/src/test/java/net/helenus/test/integration/core/ContextInitTest.java index 0c8f6fd..ba6471d 100644 --- a/src/test/java/net/helenus/test/integration/core/ContextInitTest.java +++ b/src/test/java/net/helenus/test/integration/core/ContextInitTest.java @@ -17,6 +17,7 @@ package net.helenus.test.integration.core; import net.helenus.core.Helenus; import net.helenus.core.HelenusSession; +import net.helenus.core.UnitOfWork; import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest; import org.junit.Test; @@ -29,4 +30,11 @@ public class ContextInitTest extends AbstractEmbeddedCassandraTest { System.out.println("Works! " + session); } + + @Test + public void testWithNullSession() { + HelenusSession session = Helenus.init(null, "foo").get(); + UnitOfWork uow = session.begin(); + uow.abort(); + } } diff --git a/src/test/java/net/helenus/test/integration/core/draft/EntityDraftBuilderTest.java b/src/test/java/net/helenus/test/integration/core/draft/EntityDraftBuilderTest.java index 88da2b0..e162041 100644 --- a/src/test/java/net/helenus/test/integration/core/draft/EntityDraftBuilderTest.java +++ b/src/test/java/net/helenus/test/integration/core/draft/EntityDraftBuilderTest.java @@ -21,9 +21,11 @@ import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.UUID; import java.util.concurrent.TimeoutException; import net.helenus.core.Helenus; import net.helenus.core.HelenusSession; +import net.helenus.core.UnitOfWork; import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest; import org.junit.Assert; import org.junit.BeforeClass; @@ -34,6 +36,8 @@ public class EntityDraftBuilderTest extends AbstractEmbeddedCassandraTest { static Supply supply; static HelenusSession session; static Supply.Draft draft = null; + static UUID id = null; + static String region = null; @BeforeClass public static void beforeTest() throws TimeoutException { @@ -68,6 +72,8 @@ public class EntityDraftBuilderTest extends AbstractEmbeddedCassandraTest { }); Supply s1 = session.insert(draft).sync(); + id = s1.id(); + region = s1.region(); } @Test @@ -76,7 +82,8 @@ public class EntityDraftBuilderTest extends AbstractEmbeddedCassandraTest { Supply s1 = session .select(Supply.class) - .where(supply::id, eq(draft.id())) + .where(supply::id, eq(id)) + .and(supply::region, eq(region)) .single() .sync() .orElse(null); @@ -87,6 +94,7 @@ public class EntityDraftBuilderTest extends AbstractEmbeddedCassandraTest { .update(s1.update()) .prepend(supply::suppliers, "Pignose Supply, LLC.") .sync(); + Assert.assertEquals(s2.suppliers().get(0), "Pignose Supply, LLC."); // Set @@ -99,6 +107,59 @@ public class EntityDraftBuilderTest extends AbstractEmbeddedCassandraTest { Assert.assertEquals((long) s4.demand().get("NORAM"), 10L); } + @Test + public void testDraftMergeInNestedUow() throws Exception { + Supply s1, s2, s3, s4, s5; + Supply.Draft d1; + + s1 = + session + .select(Supply.class) + .where(supply::id, eq(id)) + .and(supply::region, eq(region)) + .single() + .sync() + .orElse(null); + + try (UnitOfWork uow1 = session.begin()) { + s2 = + session + .select(Supply.class) + .where(supply::id, eq(id)) + .and(supply::region, eq(region)) + .single() + .sync(uow1) + .orElse(null); + + try (UnitOfWork uow2 = session.begin(uow1)) { + s3 = + session + .select(Supply.class) + .where(supply::id, eq(id)) + .and(supply::region, eq(region)) + .single() + .sync(uow2) + .orElse(null); + + d1 = s3.update().setCode("WIDGET-002-UPDATED"); + + s4 = + session.update(d1).usingTtl(20).defaultTimestamp(System.currentTimeMillis()).sync(uow2); + + uow2.commit(); + } + + s5 = + session + .select(Supply.class) + .where(supply::id, eq(id)) + .and(supply::region, eq(region)) + .single() + .sync(uow1) + .orElse(null); + } + } + @Test public void testSerialization() throws Exception { Supply s1, s2; diff --git a/src/test/java/net/helenus/test/integration/core/draft/Inventory.java b/src/test/java/net/helenus/test/integration/core/draft/Inventory.java index 0689fcb..23ec43f 100644 --- a/src/test/java/net/helenus/test/integration/core/draft/Inventory.java +++ b/src/test/java/net/helenus/test/integration/core/draft/Inventory.java @@ -3,11 +3,13 @@ package net.helenus.test.integration.core.draft; import java.util.UUID; import net.helenus.core.AbstractAuditedEntityDraft; import net.helenus.core.Helenus; +import net.helenus.core.reflect.Drafted; +import net.helenus.core.reflect.Entity; import net.helenus.core.reflect.MapExportable; import net.helenus.mapping.annotation.*; @Table -public interface Inventory { +public interface Inventory extends Entity { static Inventory inventory = Helenus.dsl(Inventory.class); @@ -36,7 +38,7 @@ public interface Inventory { return new Draft(this); } - class Draft extends AbstractAuditedEntityDraft { + class Draft extends AbstractAuditedEntityDraft implements Drafted { // Entity/Draft pattern-enabling methods: Draft(UUID id) { diff --git a/src/test/java/net/helenus/test/integration/core/draft/Supply.java b/src/test/java/net/helenus/test/integration/core/draft/Supply.java index b7134ff..3b382cc 100644 --- a/src/test/java/net/helenus/test/integration/core/draft/Supply.java +++ b/src/test/java/net/helenus/test/integration/core/draft/Supply.java @@ -7,11 +7,15 @@ import java.util.Set; import java.util.UUID; import net.helenus.core.AbstractEntityDraft; import net.helenus.core.Helenus; +import net.helenus.core.annotation.Cacheable; +import net.helenus.core.reflect.Drafted; +import net.helenus.core.reflect.Entity; import net.helenus.core.reflect.MapExportable; import net.helenus.mapping.annotation.*; @Table -public interface Supply { +@Cacheable +public interface Supply extends Entity { static Supply supply = Helenus.dsl(Supply.class); @@ -48,8 +52,7 @@ public interface Supply { return new Draft(this); } - class Draft extends AbstractEntityDraft { - + class Draft extends AbstractEntityDraft implements Drafted { // Entity/Draft pattern-enabling methods: Draft(String region) { super(null); diff --git a/src/test/java/net/helenus/test/integration/core/simple/SimpleUserTest.java b/src/test/java/net/helenus/test/integration/core/simple/SimpleUserTest.java index 341f4f7..32ae87f 100644 --- a/src/test/java/net/helenus/test/integration/core/simple/SimpleUserTest.java +++ b/src/test/java/net/helenus/test/integration/core/simple/SimpleUserTest.java @@ -17,13 +17,11 @@ package net.helenus.test.integration.core.simple; import static net.helenus.core.Query.eq; -import com.datastax.driver.core.ResultSet; import java.util.Optional; import java.util.concurrent.TimeoutException; import net.helenus.core.Helenus; import net.helenus.core.HelenusSession; import net.helenus.core.Operator; -import net.helenus.core.operation.UpdateOperation; import net.helenus.support.Fun; import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest; import org.junit.Assert; @@ -184,7 +182,6 @@ public class SimpleUserTest extends AbstractEmbeddedCassandraTest { .set(user::age, null) .set(user::type, null) .where(user::id, eq(100L)) - .zipkinContext(null) .sync(); Fun.Tuple3 tuple = @@ -207,18 +204,14 @@ public class SimpleUserTest extends AbstractEmbeddedCassandraTest { Assert.assertEquals(0L, cnt); } - public void testZipkin() throws TimeoutException { - session - .update() - .set(user::name, null) - .set(user::age, null) - .set(user::type, null) - .where(user::id, eq(100L)) - .zipkinContext(null) - .sync(); - - UpdateOperation update = session.update(); - update.set(user::name, null).zipkinContext(null).sync(); + public void testFunTuple() throws TimeoutException { + Fun.Tuple1 tf = + session.select(user::name).where(user::id, eq(100L)).single().sync().orElse(null); + if (tf != null) { + Assert.assertEquals(Fun.class, tf.getClass().getEnclosingClass()); + String name = tf._1; + Assert.assertEquals("greg", name); + } } private void assertUsers(User expected, User actual) { diff --git a/src/test/java/net/helenus/test/integration/core/unitofwork/AndThenOrderTest.java b/src/test/java/net/helenus/test/integration/core/unitofwork/AndThenOrderTest.java index 2b3ebcd..5f9b79a 100644 --- a/src/test/java/net/helenus/test/integration/core/unitofwork/AndThenOrderTest.java +++ b/src/test/java/net/helenus/test/integration/core/unitofwork/AndThenOrderTest.java @@ -90,22 +90,38 @@ public class AndThenOrderTest extends AbstractEmbeddedCassandraTest { .andThen( () -> { q.add("1"); + }) + .orElse( + () -> { + q.add("a"); }); uow2 = session.begin(uow3); uow2.commit() .andThen( () -> { q.add("2"); + }) + .orElse( + () -> { + q.add("b"); }); uow3.commit() .andThen( () -> { q.add("3"); + }) + .orElse( + () -> { + q.add("c"); }); uow4.commit() .andThen( () -> { q.add("4"); + }) + .orElse( + () -> { + q.add("d"); }); throw new Exception(); } catch (Exception e) { @@ -115,10 +131,15 @@ public class AndThenOrderTest extends AbstractEmbeddedCassandraTest { .andThen( () -> { q.add("5"); + }) + .orElse( + () -> { + q.add("e"); }); System.out.println(q); - Assert.assertTrue(q.isEmpty() == true); + Assert.assertTrue( + Arrays.equals(q.toArray(new String[5]), new String[] {"a", "b", "c", "d", "e"})); } @Test diff --git a/src/test/java/net/helenus/test/integration/core/unitofwork/UnitOfWorkTest.java b/src/test/java/net/helenus/test/integration/core/unitofwork/UnitOfWorkTest.java index 1b5e01f..b08a3ae 100644 --- a/src/test/java/net/helenus/test/integration/core/unitofwork/UnitOfWorkTest.java +++ b/src/test/java/net/helenus/test/integration/core/unitofwork/UnitOfWorkTest.java @@ -17,14 +17,23 @@ package net.helenus.test.integration.core.unitofwork; import static net.helenus.core.Query.eq; +import ca.exprofesso.guava.jcache.GuavaCachingProvider; import com.datastax.driver.core.ConsistencyLevel; import com.datastax.driver.core.utils.UUIDs; +import java.io.Serializable; +import java.util.Date; import java.util.UUID; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; import net.bytebuddy.utility.RandomString; import net.helenus.core.Helenus; import net.helenus.core.HelenusSession; import net.helenus.core.UnitOfWork; import net.helenus.core.annotation.Cacheable; +import net.helenus.core.reflect.Entity; +import net.helenus.mapping.MappingUtil; import net.helenus.mapping.annotation.Constraints; import net.helenus.mapping.annotation.Index; import net.helenus.mapping.annotation.PartitionKey; @@ -36,13 +45,24 @@ import org.junit.Test; @Table @Cacheable -interface Widget { +interface Widget extends Entity, Serializable { @PartitionKey UUID id(); @Index - @Constraints.Distinct() + @Constraints.Distinct String name(); + + @Constraints.Distinct(value = {"b"}) + String a(); + + String b(); + + @Constraints.Distinct(alone = false) + String c(); + + @Constraints.Distinct(combined = false) + String d(); } public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { @@ -52,6 +72,14 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { @BeforeClass public static void beforeTest() { + CachingProvider cachingProvider = + Caching.getCachingProvider(GuavaCachingProvider.class.getName()); + CacheManager cacheManager = cachingProvider.getCacheManager(); + MutableConfiguration configuration = new MutableConfiguration<>(); + configuration.setStoreByValue(false).setReadThrough(false); + cacheManager.createCache( + MappingUtil.getTableName(Widget.class, true).toString(), configuration); + session = Helenus.init(getSession()) .showCql() @@ -59,6 +87,7 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .autoCreateDrop() .consistencyLevel(ConsistencyLevel.ONE) .idempotentQueryExecution(true) + .setCacheManager(cacheManager) .get(); widget = session.dsl(Widget.class); } @@ -74,6 +103,10 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .insert(widget) .value(widget::id, key) .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) .sync(); try (UnitOfWork uow = session.begin()) { @@ -107,9 +140,11 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { @Test public void testSelectAfterNestedSelect() throws Exception { - Widget w1, w2, w3, w4; + Widget w1, w1a, w2, w3, w4; UUID key1 = UUIDs.timeBased(); UUID key2 = UUIDs.timeBased(); + String cacheKey1 = MappingUtil.getTableName(Widget.class, false) + "." + key1.toString(); + String cacheKey2 = MappingUtil.getTableName(Widget.class, false) + "." + key2.toString(); // This should inserted Widget, and not cache it in uow1. try (UnitOfWork uow1 = session.begin()) { @@ -118,10 +153,27 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .insert(widget) .value(widget::id, key1) .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) .sync(uow1); + uow1.getCache().put(cacheKey1, w1); + Assert.assertEquals(w1, uow1.getCache().get(cacheKey1)); try (UnitOfWork uow2 = session.begin(uow1)) { + // A "SELECT * FROM widget" query does not contain enough information to fetch an item from cache. + // This will miss, until we implement a statement cache. + w1a = + session + .selectAll(Widget.class) + .sync(uow2) + .filter(w -> w.id().equals(key1)) + .findFirst() + .orElse(null); + Assert.assertTrue(w1.equals(w1a)); + // This should read from uow1's cache and return the same Widget. w2 = session @@ -132,14 +184,21 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .orElse(null); Assert.assertEquals(w1, w2); + uow2.getCache().put(cacheKey2, w2); w3 = session .insert(widget) .value(widget::id, key2) .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) .sync(uow2); + Assert.assertEquals(w1, uow2.getCache().get(cacheKey1)); + Assert.assertEquals(w2, uow2.getCache().get(cacheKey2)); uow2.commit() .andThen( () -> { @@ -151,7 +210,8 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { w4 = session .select(widget) - .where(widget::id, eq(key2)) + .where(widget::a, eq(w3.a())) + .and(widget::b, eq(w3.b())) .single() .sync(uow1) .orElse(null); @@ -175,6 +235,10 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .insert(widget) .value(widget::id, key) .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) .sync(uow); // This should read from the database and return a Widget. @@ -209,6 +273,10 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .insert(widget) .value(widget::id, key) .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) .sync(); try (UnitOfWork uow = session.begin()) { @@ -219,13 +287,18 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { Assert.assertEquals(w1, w2); // This should remove the object from the session cache. + session.update(w2).set(widget::name, "Bill").where(widget::id, eq(key)).sync(uow); w3 = - session.update(w2).set(widget::name, "Bill").where(widget::id, eq(key)).sync(uow); + session + .update(w2) + .set(widget::name, w1.name()) + .where(widget::id, eq(key)) + .sync(uow); - // Fetch from session cache, should have old name. + // Fetch from session cache will cache miss (as it was updated) and trigger a SELECT. w4 = session.select(widget).where(widget::id, eq(key)).single().sync().orElse(null); Assert.assertEquals(w4, w2); - Assert.assertEquals(w4.name(), w1.name()); + Assert.assertEquals(w4.name(), w3.name()); // This should skip the cache. w5 = @@ -237,15 +310,13 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .sync() .orElse(null); - Assert.assertNotEquals(w5, w2); // Not the same instance, - Assert.assertTrue(w2.equals(w5)); // but they have the same values, - Assert.assertFalse(w5.equals(w2)); // regardless of the order when comparing. - Assert.assertEquals(w5.name(), "Bill"); + Assert.assertTrue(w5.equals(w2)); + Assert.assertTrue(w2.equals(w5)); uow.commit() .andThen( () -> { - Assert.assertEquals(w1, w2); + Assert.assertEquals(w2, w3); }); } @@ -271,6 +342,10 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { .insert(widget) .value(widget::id, key) .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) .sync(); try (UnitOfWork uow = session.begin()) { @@ -279,74 +354,200 @@ public class UnitOfWorkTest extends AbstractEmbeddedCassandraTest { w2 = session.select(widget).where(widget::id, eq(key)).single().sync(uow).orElse(null); + String cacheKey = MappingUtil.getTableName(Widget.class, false) + "." + key.toString(); + uow.getCache().put(cacheKey, w1); + // This should remove the object from the cache. session.delete(widget).where(widget::id, eq(key)).sync(uow); + uow.getCache().remove(cacheKey); // This should fail to read from the cache. w3 = session.select(widget).where(widget::id, eq(key)).single().sync(uow).orElse(null); - Assert.assertEquals(w3, null); + Assert.assertEquals(null, w3); uow.commit() .andThen( () -> { Assert.assertEquals(w1, w2); - Assert.assertEquals(w3, null); + Assert.assertEquals(null, w3); }); } + w4 = session.select(widget).where(widget::id, eq(key)).single().sync().orElse(null); + + Assert.assertEquals(null, w4); + } + + @Test + public void testBatchingUpdatesAndInserts() throws Exception { + Widget w1, w2, w3, w4, w5, w6; + Long committedAt = 0L; + UUID key = UUIDs.timeBased(); + String cacheKey = MappingUtil.getTableName(Widget.class, false) + "." + key.toString(); + + try (UnitOfWork uow = session.begin()) { + w1 = + session + .upsert(widget) + .value(widget::id, key) + .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) + .batch(uow); + Assert.assertTrue(0L == w1.writtenAt(widget::name)); + Assert.assertTrue(0 == w1.ttlOf(widget::name)); + uow.getCache().put(cacheKey, w1); + w2 = + session + .update(w1) + .set(widget::name, RandomString.make(10)) + .where(widget::id, eq(key)) + .usingTtl(30) + .batch(uow); + Assert.assertEquals(w1, w2); + Assert.assertTrue(0L == w2.writtenAt(widget::name)); + Assert.assertTrue(30 == w1.ttlOf(widget::name)); + w3 = + session + .select(Widget.class) + .where(widget::id, eq(key)) + .single() + .sync(uow) + .orElse(null); + Assert.assertEquals(w2, w3); + Assert.assertTrue(0L == w3.writtenAt(widget::name)); + Assert.assertTrue(30 <= w3.ttlOf(widget::name)); + + w6 = + session + .upsert(widget) + .value(widget::id, UUIDs.timeBased()) + .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) + .batch(uow); + + uow.getCache().put(cacheKey, w1); + uow.commit(); + committedAt = uow.committedAt(); + Date d = new Date(committedAt * 1000); + String date = d.toString(); + } + // 'c' is distinct, but not on it's own so this should miss cache + w4 = + session + .select(Widget.class) + .where(widget::c, eq(w6.c())) + .single() + .sync() + .orElse(null); + Assert.assertEquals(w6, w4); + //TODO(gburd): fix these. + //long at = w4.writtenAt(widget::name); + //Assert.assertTrue(at == committedAt); + //int ttl4 = w4.ttlOf(widget::name); + //Assert.assertTrue(ttl4 <= 30 && ttl4 > 0); + w5 = + session + .select(Widget.class) + .where(widget::id, eq(w6.id())) + .uncached() + .single() + .sync() + .orElse(null); + Assert.assertTrue(w4.equals(w5)); + //Assert.assertTrue(w5.writtenAt(widget::name) == committedAt); + int ttl5 = w5.ttlOf(widget::name); + Assert.assertTrue(ttl5 <= 30); + //Assert.assertTrue(w4.writtenAt(widget::name) == w6.writtenAt(widget::name)); + } + + @Test + public void testInsertNoOp() throws Exception { + Widget w1, w2; + UUID key1 = UUIDs.timeBased(); + + try (UnitOfWork uow = session.begin()) { + // This should inserted Widget, but not cache it. + w1 = + session + .insert(widget) + .value(widget::id, key1) + .value(widget::name, RandomString.make(20)) + .sync(uow); + + String cacheKey = MappingUtil.getTableName(Widget.class, false) + "." + key1.toString(); + uow.getCache().put(cacheKey, w1); + /* + w2 = session.upsert(w1) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) + .sync(uow); + uow.commit(); + */ + uow.abort(); + } + //Assert.assertEquals(w1, w2); + } + + @Test + public void testSelectAfterInsertProperlyCachesEntity() throws Exception { + Widget w1, w2, w3, w4; + UUID key = UUIDs.timeBased(); + + try (UnitOfWork uow = session.begin()) { + // This should cache the inserted Widget. + w1 = + session + .insert(widget) + .value(widget::id, key) + .value(widget::name, RandomString.make(20)) + .value(widget::a, RandomString.make(10)) + .value(widget::b, RandomString.make(10)) + .value(widget::c, RandomString.make(10)) + .value(widget::d, RandomString.make(10)) + .sync(uow); + + String cacheKey = MappingUtil.getTableName(Widget.class, false) + "." + key.toString(); + uow.getCache().put(cacheKey, w1); + // This should read from the cache and get the same instance of a Widget. + w2 = + session.select(widget).where(widget::id, eq(key)).single().sync(uow).orElse(null); + uow.getCache().put(cacheKey, w1); + + uow.commit() + .andThen( + () -> { + Assert.assertEquals(w1, w2); + }); + } + + // This should read the widget from the session cache and maintain object identity. + w3 = session.select(widget).where(widget::id, eq(key)).single().sync().orElse(null); + + Assert.assertEquals(w1, w3); + + // This should read the widget from the database, no object identity but + // values should match. w4 = session .select(widget) - .where(widget::name, eq(w1.name())) + .where(widget::id, eq(key)) + .uncached() .single() .sync() .orElse(null); - Assert.assertEquals(w4, null); + Assert.assertFalse(w1 == w4); + Assert.assertTrue(w1.equals(w4)); + Assert.assertTrue(w4.equals(w1)); } - /* - @Test - public void testInsertNoOp() throws Exception { - Widget w1, w2; - UUID key = UUIDs.timeBased(); - - - try (UnitOfWork uow = session.begin()) { - // This should inserted Widget, but not cache it. - w1 = session.insert(widget).value(widget::id, key).value(widget::name, RandomString.make(20)).sync(uow); - w2 = session.insert(w1).value(widget::id, key).sync(uow); - } - Assert.assertEquals(w1, w2); - } - */ - /* - * @Test public void testSelectAfterInsertProperlyCachesEntity() throws - * Exception { Widget w1, w2, w3, w4; UUID key = UUIDs.timeBased(); - * - * try (UnitOfWork uow = session.begin()) { - * - * // This should cache the inserted Widget. w1 = session.insert(widget) - * .value(widget::id, key) .value(widget::name, RandomString.make(20)) - * .sync(uow); - * - * // This should read from the cache and get the same instance of a Widget. w2 - * = session.select(widget) .where(widget::id, eq(key)) .single() - * .sync(uow) .orElse(null); - * - * uow.commit() .andThen(() -> { Assert.assertEquals(w1, w2); }); } - * - * // This should read the widget from the session cache and maintain object - * identity. w3 = session.select(widget) .where(widget::id, eq(key)) - * .single() .sync() .orElse(null); - * - * Assert.assertEquals(w1, w3); - * - * // This should read the widget from the database, no object identity but - * values should match. w4 = session.select(widget) .where(widget::id, - * eq(key)) .uncached() .single() .sync() .orElse(null); - * - * Assert.assertNotEquals(w1, w4); Assert.assertTrue(w1.equals(w4)); } - */ } diff --git a/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java b/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java index cfd3b7f..dd6b514 100644 --- a/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java +++ b/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java @@ -22,9 +22,12 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; import java.util.concurrent.TimeoutException; +import net.helenus.core.ConflictingUnitOfWorkException; import net.helenus.core.Helenus; import net.helenus.core.HelenusSession; +import net.helenus.core.UnitOfWork; import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -78,6 +81,34 @@ public class MaterializedViewTest extends AbstractEmbeddedCassandraTest { .from(CyclistsByAge.class) .where(cyclist::age, eq(18)) .allowFiltering() + .single() .sync(); } + + @Test + public void testMvUnitOfWork() + throws TimeoutException, ConflictingUnitOfWorkException, Exception { + Cyclist c1, c2; + + UnitOfWork uow = session.begin(); + c1 = + session + .select(Cyclist.class) + .from(CyclistsByAge.class) + .where(cyclist::age, eq(18)) + .single() + .sync(uow) + .orElse(null); + + c2 = + session + .select(Cyclist.class) + .from(CyclistsByAge.class) + .where(cyclist::age, eq(18)) + .single() + .sync(uow) + .orElse(null); + Assert.assertEquals(c1, c2); + uow.commit(); + } } diff --git a/src/test/java/net/helenus/test/unit/core/dsl/Account.java b/src/test/java/net/helenus/test/unit/core/dsl/Account.java index 626363f..587059e 100644 --- a/src/test/java/net/helenus/test/unit/core/dsl/Account.java +++ b/src/test/java/net/helenus/test/unit/core/dsl/Account.java @@ -39,7 +39,7 @@ public interface Account { return new Draft(); } - class Draft implements Drafted { // TODO + class Draft implements Drafted { @Override public Set mutated() { @@ -47,7 +47,12 @@ public interface Account { } @Override - public Object build() { + public Account build() { + return null; + } + + @Override + public Set read() { return null; }