Enable partial updates using partial entity maps.

This commit is contained in:
Greg Burd 2017-08-01 12:48:14 -04:00
parent eb9dd05147
commit 03567dc57e
9 changed files with 199 additions and 124 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
*.iml
.idea
infer-out
*.class

View file

@ -35,11 +35,7 @@
<orderEntry type="library" name="Maven: org.apache.commons:commons-lang3:3.6" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:4.3.10.RELEASE" level="project" />
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:22.0" level="project" />
<orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.0.18" level="project" />
<orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.1" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.mojo:animal-sniffer-annotations:1.14" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:20.0" level="project" />
<orderEntry type="library" name="Maven: javax.validation:validation-api:2.0.0.CR3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.codehaus.jackson:jackson-mapper-asl:1.9.13" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: com.anthemengineering.mojo:infer-maven-plugin:0.1.0" level="project" />

View file

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>net.helenus</groupId>
<artifactId>helenus-core</artifactId>
<version>2.0.3-SNAPSHOT</version>
<version>2.0.4-SNAPSHOT</version>
<packaging>jar</packaging>
<name>helenus</name>

View file

@ -60,11 +60,9 @@ public final class InsertOperation extends AbstractOperation<ResultSet, InsertOp
for (HelenusProperty prop : entity.getOrderedProperties()) {
Object value = BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop);
value = sessionOps.getValuePreparer().prepareColumnValue(value, prop);
if (value != null) {
HelenusPropertyNode node = new HelenusPropertyNode(prop, Optional.empty());
values.add(Fun.Tuple2.of(node, value));
}
@ -89,7 +87,6 @@ public final class InsertOperation extends AbstractOperation<ResultSet, InsertOp
if (val != null) {
HelenusPropertyNode node = MappingUtil.resolveMappingProperty(getter);
Object value = sessionOps.getValuePreparer().prepareColumnValue(val, node.getProperty());
if (value != null) {

View file

@ -1,111 +1,111 @@
/*
* 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 java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Map;
import net.helenus.support.HelenusException;
public class MapperInvocationHandler<E> implements InvocationHandler {
private final Map<String, Object> src;
private final Class<E> iface;
public MapperInvocationHandler(Class<E> iface, Map<String, Object> src) {
this.src = src;
this.iface = iface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isDefault()) {
// NOTE: This is reflection magic to invoke (non-recursively) a default method implemented on an interface
// that we've proxied (in ReflectionDslInstantiator). I found the answer in this article.
// https://zeroturnaround.com/rebellabs/recognize-and-conquer-java-proxies-default-methods-and-method-handles/
// First, we need an instance of a private inner-class found in MethodHandles.
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
constructor.setAccessible(true);
// Now we need to lookup and invoke special the default method on the interface class.
final Class<?> declaringClass = method.getDeclaringClass();
Object result = constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);
return result;
}
String methodName = method.getName();
if ("equals".equals(methodName) && method.getParameterCount() == 1) {
Object otherObj = args[0];
if (otherObj == null) {
return false;
}
if (Proxy.isProxyClass(otherObj.getClass())) {
return this == Proxy.getInvocationHandler(otherObj);
}
return false;
}
if (method.getParameterCount() != 0 || method.getReturnType() == void.class) {
throw new HelenusException("invalid getter method " + method);
}
if ("hashCode".equals(methodName)) {
return hashCode();
}
if ("toString".equals(methodName)) {
return iface.getSimpleName() + ": " + src.toString();
}
if (MapExportable.TO_MAP_METHOD.equals(methodName)) {
return Collections.unmodifiableMap(src);
}
Object value = src.get(methodName);
if (value == null) {
Class<?> returnType = method.getReturnType();
if (returnType.isPrimitive()) {
DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(returnType);
if (type == null) {
throw new HelenusException("unknown primitive type " + returnType);
}
return type.getDefaultValue();
}
}
return value;
}
}
/*
* 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 java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Map;
import net.helenus.support.HelenusException;
public class MapperInvocationHandler<E> implements InvocationHandler {
private final Map<String, Object> src;
private final Class<E> iface;
public MapperInvocationHandler(Class<E> iface, Map<String, Object> src) {
this.src = src;
this.iface = iface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isDefault()) {
// NOTE: This is reflection magic to invoke (non-recursively) a default method implemented on an interface
// that we've proxied (in ReflectionDslInstantiator). I found the answer in this article.
// https://zeroturnaround.com/rebellabs/recognize-and-conquer-java-proxies-default-methods-and-method-handles/
// First, we need an instance of a private inner-class found in MethodHandles.
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
constructor.setAccessible(true);
// Now we need to lookup and invoke special the default method on the interface class.
final Class<?> declaringClass = method.getDeclaringClass();
Object result = constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);
return result;
}
String methodName = method.getName();
if ("equals".equals(methodName) && method.getParameterCount() == 1) {
Object otherObj = args[0];
if (otherObj == null) {
return false;
}
if (Proxy.isProxyClass(otherObj.getClass())) {
return this == Proxy.getInvocationHandler(otherObj);
}
return false;
}
if (method.getParameterCount() != 0 || method.getReturnType() == void.class) {
throw new HelenusException("invalid getter method " + method);
}
if ("hashCode".equals(methodName)) {
return hashCode();
}
if ("toString".equals(methodName)) {
return iface.getSimpleName() + ": " + src.toString();
}
if (MapExportable.TO_MAP_METHOD.equals(methodName)) {
return Collections.unmodifiableMap(src);
}
Object value = src.get(methodName);
if (value == null && src.containsKey(methodName)) {
Class<?> returnType = method.getReturnType();
if (returnType.isPrimitive()) {
DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(returnType);
if (type == null) {
throw new HelenusException("unknown primitive type " + returnType);
}
return type.getDefaultValue();
}
}
return value;
}
}

View file

@ -35,6 +35,9 @@ public final class StatementColumnValuePreparer implements ColumnValuePreparer {
@Override
public Object prepareColumnValue(Object value, HelenusProperty prop) {
if (value == null)
return null;
if (value instanceof BindMarker) {
return value;
}

View file

@ -0,0 +1,69 @@
/*
* 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.test.integration.core.simple;
import net.helenus.core.Helenus;
import net.helenus.core.HelenusSession;
import net.helenus.core.operation.InsertOperation;
import net.helenus.support.DslPropertyException;
import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class InsertPartialTest extends AbstractEmbeddedCassandraTest {
static HelenusSession session;
static User user;
static Random rnd = new Random();
@BeforeClass
public static void beforeTests() {
session = Helenus.init(getSession()).showCql().add(User.class).autoCreateDrop().get();
user = Helenus.dsl(User.class);
}
@Test
public void testPartialInsert() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
Long id = rnd.nextLong();
map.put("id", id);
map.put("age", 5);
InsertOperation insert = session.insert(Helenus.map(User.class, map));
String cql = "INSERT INTO simple_users (id,age) VALUES (" + id.toString() + ",5) IF NOT EXISTS;";
Assert.assertEquals(cql, insert.cql());
insert.sync();
}
@Test
public void testPartialUpsert() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
Long id = rnd.nextLong();
map.put("id", id);
map.put("age", 5);
InsertOperation upsert = session.upsert(Helenus.map(User.class, map));
String cql = "INSERT INTO simple_users (id,age) VALUES (" + id.toString() + ",5);";
Assert.assertEquals(cql, upsert.cql());
upsert.sync();
}
}

View file

@ -23,20 +23,20 @@ import org.junit.Test;
public class DslTest {
static Account account;
@BeforeClass
public static void beforeTests() {
account = Helenus.dsl(Account.class);
}
@Test
public void testToString() throws Exception {
System.out.println(account);
}
@Test(expected=DslPropertyException.class)
public void test() throws Exception {
account.id();
}
}

View file

@ -18,6 +18,8 @@ package net.helenus.test.unit.core.dsl;
import java.util.HashMap;
import java.util.Map;
import net.helenus.core.HelenusSession;
import net.helenus.mapping.value.ValueProviderMap;
import org.junit.Assert;
import org.junit.Test;
@ -45,9 +47,15 @@ public class WrapperTest {
@Test
public void testPrimitive() throws Exception {
// NOTE: noramlly a ValueProviderMap for the entity would include all keys for an entity
// at creation time. This test need to validate that MapperInvocationHander will return
// the correct default value for an entity, the twist is that if the key doesn't exist
// in the map then it returns null (so as to support the partial update feature). Here we
// need to setup the test with a null value for the key we'd like to test.
Map<String, Object> map = new HashMap<String, Object>();
map.put("id", 123L);
map.put("active", null);
Account account = Helenus.map(Account.class, map);