Enable partial updates using partial entity maps.
This commit is contained in:
parent
eb9dd05147
commit
03567dc57e
9 changed files with 199 additions and 124 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
*.iml
|
||||
.idea
|
||||
infer-out
|
||||
|
||||
*.class
|
||||
|
|
|
@ -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" />
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue