diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index 380438cc2..0a2a3b777 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean api diff --git a/ebean-api/src/main/java/io/ebean/CancelableQuery.java b/ebean-api/src/main/java/io/ebean/CancelableQuery.java new file mode 100644 index 000000000..fb3100bd2 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/CancelableQuery.java @@ -0,0 +1,19 @@ +package io.ebean; + +/** + * Defines a cancelable query. + *

+ * Typically holds a representation of the PreparedStatement to perform the + * actual cancel. + *

+ */ +public interface CancelableQuery { + + /** + * Cancel the query. + *

+ * For JDBC this translates to calling cancel on the PreparedStatement. + *

+ */ + void cancel(); +} diff --git a/ebean-api/src/main/java/io/ebean/DtoQuery.java b/ebean-api/src/main/java/io/ebean/DtoQuery.java index 33f439b93..69ed034ef 100644 --- a/ebean-api/src/main/java/io/ebean/DtoQuery.java +++ b/ebean-api/src/main/java/io/ebean/DtoQuery.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; /** * Query for performing native SQL queries that return DTO Bean's. @@ -37,7 +38,7 @@ import java.util.function.Predicate; * * } */ -public interface DtoQuery { +public interface DtoQuery extends CancelableQuery { /** * Execute the query returning a list. @@ -45,6 +46,26 @@ public interface DtoQuery { @Nonnull List findList(); + /** + * Execute the query iterating a row at a time. + *

+ * Note that the QueryIterator holds resources related to the underlying + * resultSet and potentially connection and MUST be closed. We should use + * QueryIterator in a try with resource block. + */ + @Nonnull + QueryIterator findIterate(); + + /** + * Execute the query returning a Stream. + *

+ * Note that the Stream holds resources related to the underlying + * resultSet and potentially connection and MUST be closed. We should use + * the Stream in a try with resource block. + */ + @Nonnull + Stream findStream(); + /** * Execute the query iterating a row at a time. *

diff --git a/ebean-api/src/main/java/io/ebean/OrderBy.java b/ebean-api/src/main/java/io/ebean/OrderBy.java index 820a76314..8b39fc19e 100644 --- a/ebean-api/src/main/java/io/ebean/OrderBy.java +++ b/ebean-api/src/main/java/io/ebean/OrderBy.java @@ -16,7 +16,7 @@ import java.util.Objects; * on the Query object. *

*/ -public final class OrderBy implements Serializable { +public class OrderBy implements Serializable { private static final long serialVersionUID = 9157089257745730539L; @@ -69,7 +69,6 @@ public final class OrderBy implements Serializable { * Add a property with ascending order to this OrderBy. */ public Query asc(String propertyName) { - list.add(new Property(propertyName, true)); return query; } @@ -98,7 +97,6 @@ public final class OrderBy implements Serializable { return query; } - /** * Return true if the property is known to be contained in the order by clause. */ @@ -207,7 +205,6 @@ public final class OrderBy implements Serializable { if (!(obj instanceof OrderBy)) { return false; } - OrderBy e = (OrderBy) obj; return e.list.equals(list); } @@ -249,7 +246,7 @@ public final class OrderBy implements Serializable { /** * A property and its ascending descending order. */ - public static final class Property implements Serializable { + public static class Property implements Serializable { private static final long serialVersionUID = 1546009780322478077L; @@ -415,13 +412,10 @@ public final class OrderBy implements Serializable { } private void parse(String orderByClause) { - if (orderByClause == null) { return; } - - String[] chunks = orderByClause.split(","); - for (String chunk : chunks) { + for (String chunk : orderByClause.split(",")) { Property p = parseProperty(chunk); if (p != null) { list.add(p); @@ -467,8 +461,7 @@ public final class OrderBy implements Serializable { if (s.startsWith("desc")) { return false; } - String m = "Expecting [" + s + "] to be asc or desc?"; - throw new RuntimeException(m); + throw new RuntimeException("Expecting [" + s + "] to be asc or desc?"); } private boolean isEmptyString(String s) { diff --git a/ebean-api/src/main/java/io/ebean/Query.java b/ebean-api/src/main/java/io/ebean/Query.java index db7bf8cb9..0dc86905e 100644 --- a/ebean-api/src/main/java/io/ebean/Query.java +++ b/ebean-api/src/main/java/io/ebean/Query.java @@ -177,7 +177,7 @@ import java.util.stream.Stream; * * @param the type of Entity bean this query will fetch. */ -public interface Query { +public interface Query extends CancelableQuery { /** * The lock type (strength) to use with query FOR UPDATE row locking. @@ -291,15 +291,6 @@ public interface Query { */ UpdateQuery asUpdate(); - /** - * Cancel the query execution if supported by the underlying database and - * driver. - *

- * This must be called from a different thread to the query executor. - *

- */ - void cancel(); - /** * Return a copy of the query. *

diff --git a/ebean-api/src/main/java/io/ebean/QueryIterator.java b/ebean-api/src/main/java/io/ebean/QueryIterator.java index c158e62c6..6ad523151 100644 --- a/ebean-api/src/main/java/io/ebean/QueryIterator.java +++ b/ebean-api/src/main/java/io/ebean/QueryIterator.java @@ -56,7 +56,7 @@ import java.util.Iterator; * * @param the type of entity bean in the iteration */ -public interface QueryIterator extends Iterator, java.io.Closeable { +public interface QueryIterator extends Iterator, AutoCloseable { /** * Returns true if the iteration has more elements. @@ -70,12 +70,6 @@ public interface QueryIterator extends Iterator, java.io.Closeable { @Override T next(); - /** - * Remove is not allowed. - */ - @Override - void remove(); - /** * Close the underlying resources held by this iterator. */ diff --git a/ebean-api/src/main/java/io/ebean/SqlQuery.java b/ebean-api/src/main/java/io/ebean/SqlQuery.java index 8b5452ee7..9a878f233 100644 --- a/ebean-api/src/main/java/io/ebean/SqlQuery.java +++ b/ebean-api/src/main/java/io/ebean/SqlQuery.java @@ -37,7 +37,7 @@ import java.util.function.Predicate; * * } */ -public interface SqlQuery extends Serializable { +public interface SqlQuery extends Serializable, CancelableQuery { /** * Execute the query returning a list. @@ -365,5 +365,10 @@ public interface SqlQuery extends Serializable { * Return the list of values. */ List findList(); + + /** + * Find streaming the result effectively consuming a row at a time. + */ + void findEach(Consumer consumer); } } diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java index 3ec5163e3..6271d4051 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -13,12 +13,10 @@ import java.util.Set; * from the Map Set or List. The purpose of gathering the additions and removals * is to support persisting ManyToMany objects. The additions and removals * become inserts and deletes from the intersection table. - *

*

* Technically this is NOT an extension of * java.util.Collection. The reason being that java.util.Map is not a * Collection. I realise this makes this name confusing so I apologise for that. - *

*/ public interface BeanCollection extends Serializable { @@ -68,7 +66,6 @@ public interface BeanCollection extends Serializable { * Return true if the collection is uninitialised or is empty without any held modifications. *

* Returning true means can safely skip cascade save for this bean collection. - *

*/ boolean isSkipSave(); @@ -93,7 +90,6 @@ public interface BeanCollection extends Serializable { *

* That is, if the collection was not loaded due to filterMany predicates etc * then make sure the collection is set to empty. - *

*/ boolean checkEmptyLazyLoad(); @@ -136,10 +132,7 @@ public interface BeanCollection extends Serializable { boolean isReadOnly(); /** - * Add the bean to the collection. - *

- * This is disallowed for BeanMap. - *

+ * Add the bean to the collection. This is disallowed for BeanMap. */ void internalAdd(Object bean); @@ -168,7 +161,6 @@ public interface BeanCollection extends Serializable { * Map.Entry. *

* For maps this returns the entrySet as we need the keys of the map. - *

*/ Collection getActualEntries(); @@ -185,6 +177,11 @@ public interface BeanCollection extends Serializable { */ boolean isReference(); + /** + * Return true if the collection is modify listening and has modifications. + */ + boolean hasModifications(); + /** * Set modify listening on or off. This is used to keep track of objects that * have been added to or removed from the list set or map. @@ -192,7 +189,6 @@ public interface BeanCollection extends Serializable { * This is required only for ManyToMany collections. The additions and * deletions are used to insert or delete entries from the intersection table. * Otherwise modifyListening is false. - *

*/ void setModifyListening(ModifyListenMode modifyListenMode); @@ -206,7 +202,6 @@ public interface BeanCollection extends Serializable { *

* This will potentially end up as an insert into a intersection table for a * ManyToMany. - *

*/ void modifyAddition(E bean); @@ -215,7 +210,6 @@ public interface BeanCollection extends Serializable { *

* This will potentially end up as an delete from an intersection table for a * ManyToMany. - *

*/ void modifyRemoval(Object bean); diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 12f3fda8f..4695c4112 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -32,39 +32,47 @@ public final class EntityBeanIntercept implements Serializable { private static final int STATE_REFERENCE = 1; private static final int STATE_LOADED = 2; + /** + * Used when a bean is partially filled. + */ + private static final byte FLAG_LOADED_PROP = 1; + private static final byte FLAG_CHANGED_PROP = 2; + private static final byte FLAG_CHANGEDLOADED_PROP = 3; + /** + * Flags indicating if a property is a dirty embedded bean. Used to distinguish + * between an embedded bean being completely overwritten and one of its + * embedded properties being made dirty. + */ + private static final byte FLAG_EMBEDDED_DIRTY = 4; + /** + * Flags indicating if a property is a dirty embedded bean. Used to distinguish + * between an embedded bean being completely overwritten and one of its + * embedded properties being made dirty. + */ + private static final byte FLAG_ORIG_VALUE_SET = 8; + private transient final ReentrantLock lock = new ReentrantLock(); - private transient NodeUsageCollector nodeUsageCollector; - private transient PersistenceContext persistenceContext; - private transient BeanLoader beanLoader; - private transient PreGetterCallback preGetterCallback; private String ebeanServerName; - private boolean deletedFromCollection; /** * The actual entity bean that 'owns' this intercept. */ private final EntityBean owner; - private EntityBean embeddedOwner; private int embeddedOwnerIndex; - /** * One of NEW, REF, UPD. */ private int state; - private boolean forceUpdate; - private boolean readOnly; - private boolean dirty; - /** * Flag set to disable lazy loading - typically for SQL "report" type entity beans. */ @@ -74,35 +82,9 @@ public final class EntityBeanIntercept implements Serializable { * Flag set when lazy loading failed due to the underlying bean being deleted in the DB. */ private boolean lazyLoadFailure; - - /** - * Used when a bean is partially filled. - */ - private static final byte FLAG_LOADED_PROP = 1; - - /** - * Set of changed properties. - */ - private static final byte FLAG_CHANGED_PROP = 2; - - /** - * Flags indicating if a property is a dirty embedded bean. Used to distinguish - * between an embedded bean being completely overwritten and one of its - * embedded properties being made dirty. - */ - private static final byte FLAG_EMBEDDED_DIRTY = 4; - - /** - * Flags indicating if a property is a dirty embedded bean. Used to distinguish - * between an embedded bean being completely overwritten and one of its - * embedded properties being made dirty. - */ - private static final byte FLAG_ORIG_VALUE_SET = 8; - - private final byte[] flags; - private boolean fullyLoadedBean; private boolean loadedFromCache; + private final byte[] flags; private Object[] origValues; private Exception[] loadErrors; private int lazyLoadProperty = -1; @@ -569,6 +551,10 @@ public final class EntityBeanIntercept implements Serializable { flags[propertyIndex] |= FLAG_CHANGED_PROP; } + private void setChangeLoaded(int propertyIndex) { + flags[propertyIndex] |= FLAG_CHANGEDLOADED_PROP; + } + /** * Set that an embedded bean has had one of its properties changed. */ @@ -937,7 +923,7 @@ public final class EntityBeanIntercept implements Serializable { if (readOnly) { throw new IllegalStateException("This bean is readOnly"); } - setChangedProperty(propertyIndex); + setChangeLoaded(propertyIndex); } } @@ -977,7 +963,6 @@ public final class EntityBeanIntercept implements Serializable { } } - /** * Check for primitive boolean. */ diff --git a/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java b/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java index 8c861af1f..9d67499d1 100644 --- a/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java +++ b/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java @@ -1,13 +1,10 @@ package io.ebean.bean; -import java.util.List; - /** * Holds entity beans by there type and id. *

* This is used to ensure only one instance for a given entity type and id is * used to build object graphs from queries and lazy loading. - *

*/ public interface PersistenceContext { @@ -22,7 +19,6 @@ public interface PersistenceContext { *

* Returns an existing entity bean (if one is already there) and otherwise * returns null. - *

*/ Object putIfAbsent(Class rootType, Object id, Object bean); @@ -79,17 +75,11 @@ public interface PersistenceContext { */ boolean resetLimit(); - /** - * Return the list of dirty beans held by this persistence context. - */ - List dirtyBeans(); - /** * Wrapper on a bean to also indicate if a bean has been deleted. *

* If a bean has been deleted then for the same persistence context is should * not be able to be fetched from persistence context or L2 cache. - *

*/ class WithOption { diff --git a/ebean-api/src/main/java/io/ebean/common/AbstractBeanCollection.java b/ebean-api/src/main/java/io/ebean/common/AbstractBeanCollection.java index f1a608361..809ca13b3 100644 --- a/ebean-api/src/main/java/io/ebean/common/AbstractBeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/common/AbstractBeanCollection.java @@ -135,6 +135,11 @@ abstract class AbstractBeanCollection implements BeanCollection { // Support for modify additions deletions etc - ManyToMany // --------------------------------------------------------- + @Override + public boolean hasModifications() { + return modifyHolder != null && modifyHolder.hasModifications(); + } + @Override public ModifyListenMode getModifyListening() { return modifyListenMode; @@ -145,7 +150,6 @@ abstract class AbstractBeanCollection implements BeanCollection { */ @Override public void setModifyListening(ModifyListenMode mode) { - this.modifyListenMode = mode; this.modifyListening = mode != null && ModifyListenMode.NONE != mode; if (modifyListening) { diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java index 0aa774a81..ba0adca99 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -198,10 +198,9 @@ public final class BeanList extends AbstractBeanCollection implements List } if (list == null) { sb.append("deferred "); - } else { sb.append("size[").append(list.size()).append("] "); - sb.append("list").append(list).append(""); + sb.append("list").append(list); } return sb.toString(); } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java index 09b37d38a..643febc14 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java @@ -175,10 +175,6 @@ public final class BeanMap extends AbstractBeanCollection implements Ma /** * Returns the map entrySet. - *

- * This is because the key values may need to be set against the details (so - * they don't need to be set twice). - *

*/ @Override public Collection getActualEntries() { @@ -194,7 +190,6 @@ public final class BeanMap extends AbstractBeanCollection implements Ma } if (map == null) { sb.append("deferred "); - } else { sb.append("size[").append(map.size()).append("]"); sb.append(" map").append(map); @@ -243,17 +238,12 @@ public final class BeanMap extends AbstractBeanCollection implements Ma } @Override - @SuppressWarnings({"unchecked"}) public Set> entrySet() { init(); if (isReadOnly()) { return Collections.unmodifiableSet(map.entrySet()); } - if (modifyListening) { - Set> s = map.entrySet(); - return new ModifySet(this, s); - } - return map.entrySet(); + return modifyListening ? new ModifyEntrySet<>(this, map.entrySet()) : map.entrySet(); } @Override @@ -274,8 +264,7 @@ public final class BeanMap extends AbstractBeanCollection implements Ma if (isReadOnly()) { return Collections.unmodifiableSet(map.keySet()); } - // we don't really care about modifications to the ketSet? - return map.keySet(); + return modifyListening ? new ModifyKeySet<>(this, map.keySet()) : map.keySet(); } @Override @@ -346,11 +335,7 @@ public final class BeanMap extends AbstractBeanCollection implements Ma if (isReadOnly()) { return Collections.unmodifiableCollection(map.values()); } - if (modifyListening) { - Collection c = map.values(); - return new ModifyCollection<>(this, c); - } - return map.values(); + return modifyListening ? new ModifyCollection<>(this, map.values()) : map.values(); } @Override diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java index 880ece106..0c6f0c508 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java @@ -176,7 +176,6 @@ public final class BeanSet extends AbstractBeanCollection implements Set implements Collection { * The owner is notified of the additions and removals. *

*/ - public ModifyCollection(BeanCollection owner, Collection c) { + ModifyCollection(BeanCollection owner, Collection c) { this.owner = owner; this.c = c; } diff --git a/ebean-api/src/main/java/io/ebean/common/ModifyEntrySet.java b/ebean-api/src/main/java/io/ebean/common/ModifyEntrySet.java new file mode 100644 index 000000000..174863a74 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/common/ModifyEntrySet.java @@ -0,0 +1,131 @@ +package io.ebean.common; + +import java.util.*; + +/** + * Handles the Entry Set for BeanMap. + */ +class ModifyEntrySet implements Set> { + + private final BeanMap owner; + private final Set> entrySet; + + ModifyEntrySet(BeanMap owner, Set> entrySet) { + this.owner = owner; + this.entrySet = entrySet; + } + + @Override + public int size() { + return entrySet.size(); + } + + @Override + public boolean isEmpty() { + return entrySet.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return entrySet.contains(o); + } + + @Override + public Object[] toArray() { + return entrySet.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return entrySet.toArray(a); + } + + @Override + public boolean containsAll(Collection entries) { + return entrySet.containsAll(entries); + } + + @Override + public void clear() { + owner.clear(); + } + + @Override + public boolean add(Map.Entry entry) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> c) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("rawtypes") + @Override + public boolean remove(Object o) { + if (o instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) o; + final E val = owner.get(entry.getKey()); + if (Objects.equals(val, entry.getValue())) { + owner.remove(entry.getKey()); + return true; + } + } + return false; + } + + @Override + public boolean retainAll(Collection entries) { + boolean modified = false; + final Iterator> it = iterator(); + while (it.hasNext()) { + if (!entries.contains(it.next())) { + it.remove(); + modified = true; + } + } + return modified; + } + + @Override + public boolean removeAll(Collection entries) { + boolean modified = false; + for (Object entry : entries) { + modified |= remove(entry); + } + return modified; + } + + @Override + public Iterator> iterator() { + return new EntrySetIterator(new ArrayList<>(entrySet).iterator()); + } + + class EntrySetIterator implements Iterator> { + + private final Iterator> iterator; + private Map.Entry entry; + + EntrySetIterator(Iterator> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Map.Entry next() { + entry = iterator.next(); + return entry; + } + + @Override + public void remove() { + owner.remove(entry.getKey()); + iterator.remove(); + } + } + +} diff --git a/ebean-api/src/main/java/io/ebean/common/ModifyKeySet.java b/ebean-api/src/main/java/io/ebean/common/ModifyKeySet.java new file mode 100644 index 000000000..e9f829b9b --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/common/ModifyKeySet.java @@ -0,0 +1,126 @@ +package io.ebean.common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +/** + * Handle the Key Set for BeanMap. + */ +class ModifyKeySet implements Set { + + private final Set keySet; + private final BeanMap owner; + + ModifyKeySet(BeanMap owner, Set keySet) { + this.owner = owner; + this.keySet = keySet; + } + + @Override + public int size() { + return keySet.size(); + } + + @Override + public boolean isEmpty() { + return keySet.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return keySet.contains(o); + } + + @Override + public Object[] toArray() { + return keySet.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return keySet.toArray(a); + } + + @Override + public boolean add(E key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection keys) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + return owner.remove(o) != null; + } + + @Override + public boolean containsAll(Collection keys) { + return keySet.containsAll(keys); + } + + @Override + public void clear() { + owner.clear(); + } + + @Override + public Iterator iterator() { + return new KeySetIterator<>(new ArrayList<>(keySet).iterator()); + } + + @Override + public boolean retainAll(Collection keys) { + return keysMatch(keys, false); + } + + @Override + public boolean removeAll(Collection keys) { + return keysMatch(keys, true); + } + + private boolean keysMatch(Collection keys, boolean containsMatch) { + boolean changed = false; + final Iterator iterator = iterator(); + while (iterator.hasNext()) { + final E key = iterator.next(); + if (keys.contains(key) == containsMatch) { + iterator.remove(); + changed = true; + } + } + return changed; + } + + + class KeySetIterator implements Iterator { + + private final Iterator iterator; + private K key; + + KeySetIterator(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public K next() { + key = iterator.next(); + return key; + } + + @Override + public void remove() { + owner.remove(key); + iterator.remove(); + } + } +} diff --git a/ebean-api/src/main/java/io/ebean/common/ModifySet.java b/ebean-api/src/main/java/io/ebean/common/ModifySet.java deleted file mode 100644 index 1a0a2e1bf..000000000 --- a/ebean-api/src/main/java/io/ebean/common/ModifySet.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.ebean.common; - -import io.ebean.bean.BeanCollection; - -import java.util.Set; - -/** - * Wraps a Set for the purposes of notifying removals and additions to the - * BeanCollection owner. - *

- * This is required for persisting ManyToMany objects. Additions and removals - * become inserts and deletes to the intersection table. - *

- */ -class ModifySet extends ModifyCollection implements Set { - - /** - * Create with an Owner that is notified of any additions or deletions. - */ - public ModifySet(BeanCollection owner, Set s) { - super(owner, s); - } - -} diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index be2ac6c1f..bc3ae587f 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -193,6 +193,12 @@ public class DatabaseConfig { */ private JsonConfig.Include jsonInclude = JsonConfig.Include.ALL; + /** + * When true then by default DbJson beans are assumed to be dirty. + * I believe we want to change this default to false in the future. + */ + private boolean jsonDirtyByDefault = true; + /** * The database platform name. Used to imply a DatabasePlatform to use. */ @@ -737,6 +743,26 @@ public class DatabaseConfig { this.jsonInclude = jsonInclude; } + /** + * Return true if DbJson beans are assumed dirty by default. + *

+ * That is, when true beans that do not implement ModifyAwareType are by + * default assumed to be dirty and included in updates. + */ + public boolean isJsonDirtyByDefault() { + return jsonDirtyByDefault; + } + + /** + * Set to false if we want DbJson beans to not be assumed to be dirty. + *

+ * That is, when true beans that do not implement ModifyAwareType are by + * default assumed to be dirty and included in updates. + */ + public void setJsonDirtyByDefault(boolean jsonDirtyByDefault) { + this.jsonDirtyByDefault = jsonDirtyByDefault; + } + /** * Return the name of the Database. */ @@ -2909,6 +2935,7 @@ public class DatabaseConfig { jsonInclude = p.getEnum(JsonConfig.Include.class, "jsonInclude", jsonInclude); jsonDateTime = p.getEnum(JsonConfig.DateTime.class, "jsonDateTime", jsonDateTime); jsonDate = p.getEnum(JsonConfig.Date.class, "jsonDate", jsonDate); + jsonDirtyByDefault = p.getBoolean("jsonDirtyByDefault", jsonDirtyByDefault); runMigration = p.getBoolean("migration.run", runMigration); ddlGenerate = p.getBoolean("ddl.generate", ddlGenerate); @@ -3369,7 +3396,7 @@ public class DatabaseConfig { this.loadModuleInfo = loadModuleInfo; } - public enum UuidVersion { + public enum UuidVersion { VERSION4, VERSION1, VERSION1RND diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/AbstractDbEncrypt.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/AbstractDbEncrypt.java index be2c0941e..6f8e27556 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/AbstractDbEncrypt.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/AbstractDbEncrypt.java @@ -9,9 +9,6 @@ import java.sql.Types; * functions for varchar, date and timestamp. If they are left null then that is * treated as though that data type can not be encrypted in the DB and will * instead use java client encryption. - *

- * - * @author rbygrave */ public abstract class AbstractDbEncrypt implements DbEncrypt { @@ -47,13 +44,10 @@ public abstract class AbstractDbEncrypt implements DbEncrypt { case Types.CHAR: case Types.LONGVARCHAR: return varcharEncryptFunction; - case Types.DATE: return dateEncryptFunction; - case Types.TIMESTAMP: return timestampEncryptFunction; - default: return null; } diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2DbEncrypt.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2DbEncrypt.java index ec7b1d9a7..f3eeeffa0 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2DbEncrypt.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2DbEncrypt.java @@ -5,8 +5,6 @@ import io.ebean.config.dbplatform.DbEncryptFunction; /** * H2 encryption support via encrypt decrypt function. - * - * @author rbygrave */ public class H2DbEncrypt extends AbstractDbEncrypt { diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2HistoryTrigger.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2HistoryTrigger.java index 785e24a0c..323dd1120 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2HistoryTrigger.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/h2/H2HistoryTrigger.java @@ -4,11 +4,7 @@ import org.h2.api.Trigger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; +import java.sql.*; import java.util.Arrays; /** @@ -43,7 +39,6 @@ public class H2HistoryTrigger implements Trigger { @Override public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { - // get the columns for the table ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null); @@ -79,7 +74,6 @@ public class H2HistoryTrigger implements Trigger { @Override public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { - if (oldRow != null) { // a delete or update event Timestamp now = new Timestamp(System.currentTimeMillis()); @@ -99,7 +93,6 @@ public class H2HistoryTrigger implements Trigger { * Insert the data into the history table. */ private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException { - try (PreparedStatement stmt = connection.prepareStatement(insertHistorySql)) { for (int i = 0; i < oldRow.length; i++) { stmt.setObject(i + 1, oldRow[i]); @@ -110,11 +103,11 @@ public class H2HistoryTrigger implements Trigger { @Override public void close() throws SQLException { - + // do nothing } @Override public void remove() throws SQLException { - + // do nothing } } diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/mysql/MySqlDbEncrypt.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/mysql/MySqlDbEncrypt.java index 2a22d8ab2..f8717936b 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/mysql/MySqlDbEncrypt.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/mysql/MySqlDbEncrypt.java @@ -5,8 +5,6 @@ import io.ebean.config.dbplatform.DbEncryptFunction; /** * MySql aes_encrypt aes_decrypt based encryption support. - * - * @author rbygrave */ public class MySqlDbEncrypt extends AbstractDbEncrypt { diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/Oracle11Platform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/Oracle11Platform.java index cb32a7470..5934b6dd4 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/Oracle11Platform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/Oracle11Platform.java @@ -11,7 +11,6 @@ public class Oracle11Platform extends OraclePlatform { public Oracle11Platform() { super(); this.platform = Platform.ORACLE11; - this.columnAliasPrefix = "c"; this.sqlLimiter = new OracleRownumSqlLimiter(); this.basicSqlLimiter = new OracleRownumBasicLimiter(); dbIdentity.setIdType(IdType.SEQUENCE); diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OracleDbEncrypt.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OracleDbEncrypt.java index e1194ac37..2eae251a9 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OracleDbEncrypt.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OracleDbEncrypt.java @@ -60,7 +60,6 @@ public class OracleDbEncrypt extends AbstractDbEncrypt { * @param decryptFunction the decrypt stored procedure */ public OracleDbEncrypt(String encryptFunction, String decryptFunction) { - this.varcharEncryptFunction = new OraVarcharFunction(encryptFunction, decryptFunction); this.dateEncryptFunction = new OraDateFunction(encryptFunction, decryptFunction); } diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OraclePlatform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OraclePlatform.java index c12ca8c74..d6896ad3e 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OraclePlatform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/oracle/OraclePlatform.java @@ -22,6 +22,7 @@ public class OraclePlatform extends DatabasePlatform { public OraclePlatform() { super(); this.platform = Platform.ORACLE; + this.columnAliasPrefix = "c"; this.supportsDeleteTableAlias = true; this.maxTableNameLength = 30; this.maxConstraintNameLength = 30; diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerBasePlatform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerBasePlatform.java index 8a9c5b4be..4490c148d 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerBasePlatform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerBasePlatform.java @@ -25,6 +25,7 @@ abstract class SqlServerBasePlatform extends DatabasePlatform { this.platform = Platform.SQLSERVER; // disable persistBatchOnCascade mode for // SQL Server unless we are using sequences + this.dbEncrypt = new SqlServerDbEncrypt(); this.persistBatchOnCascade = PersistBatch.NONE; this.idInExpandedForm = true; this.selectCountWithAlias = true; diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerDbEncrypt.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerDbEncrypt.java new file mode 100644 index 000000000..5647483cd --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/sqlserver/SqlServerDbEncrypt.java @@ -0,0 +1,46 @@ +package io.ebean.config.dbplatform.sqlserver; + +import io.ebean.config.dbplatform.AbstractDbEncrypt; +import io.ebean.config.dbplatform.DbEncryptFunction; + +/** + * SQL Server EncryptByPassPhrase DecryptByPassPhrase based encryption support. + */ +public class SqlServerDbEncrypt extends AbstractDbEncrypt { + + public SqlServerDbEncrypt() { + this.varcharEncryptFunction = new VarcharFunction(); + this.dateEncryptFunction = new DateFunction(); + } + + @Override + public boolean isBindEncryptDataFirst() { + return false; + } + + private static class VarcharFunction implements DbEncryptFunction { + + @Override + public String getDecryptSql(String columnWithTableAlias) { + return "convert(nvarchar,DecryptByPassPhrase(?," + columnWithTableAlias + "))"; + } + + @Override + public String getEncryptBindSql() { + return "EncryptByPassPhrase(?,?)"; + } + } + + private static class DateFunction implements DbEncryptFunction { + + @Override + public String getDecryptSql(String columnWithTableAlias) { + return "cast(convert(nvarchar,DecryptByPassPhrase(?," + columnWithTableAlias + ")) as date)"; + } + + @Override + public String getEncryptBindSql() { + return "EncryptByPassPhrase(?,format(?,'yyyy-MM-dd'))"; + } + } +} diff --git a/ebean-api/src/main/java/io/ebean/util/JdbcClose.java b/ebean-api/src/main/java/io/ebean/util/JdbcClose.java index 92b3f7e28..b72f59c4d 100644 --- a/ebean-api/src/main/java/io/ebean/util/JdbcClose.java +++ b/ebean-api/src/main/java/io/ebean/util/JdbcClose.java @@ -66,4 +66,17 @@ public class JdbcClose { logger.warn("Error on connection rollback", e); } } + + /** + * Cancels the statement + */ + public static void cancel(Statement stmt) { + try { + if (stmt != null) { + stmt.cancel(); + } + } catch (SQLException e) { + logger.warn("Error on cancelling statement", e); + } + } } diff --git a/ebean-autotune/pom.xml b/ebean-autotune/pom.xml index 590680c8a..6b6e9511a 100644 --- a/ebean-autotune/pom.xml +++ b/ebean-autotune/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT @@ -26,7 +26,7 @@ io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided diff --git a/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/AutoTuneDiffCollection.java b/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/AutoTuneDiffCollection.java index 9eb5439f2..e9ebe5af1 100644 --- a/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/AutoTuneDiffCollection.java +++ b/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/AutoTuneDiffCollection.java @@ -113,7 +113,7 @@ public class AutoTuneDiffCollection { diffCount++; - Origin origin = createOrigin(entry, point, tuneDetail.toString()); + Origin origin = createOrigin(entry, point, tuneDetail.asString()); ProfileDiff diff = document.getProfileDiff(); if (diff == null) { diff = new ProfileDiff(); @@ -147,7 +147,7 @@ public class AutoTuneDiffCollection { Origin origin = new Origin(); origin.setKey(point.getKey()); origin.setBeanType(point.getBeanType()); - origin.setDetail(entry.getDetail().toString()); + origin.setDetail(entry.getDetail().asString()); origin.setCallStack(point.getCallOrigin().getFullDescription()); origin.setOriginal(query); diff --git a/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/ProfileManager.java b/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/ProfileManager.java index f334208f9..170a3a406 100644 --- a/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/ProfileManager.java +++ b/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/ProfileManager.java @@ -46,7 +46,6 @@ public class ProfileManager implements ProfilingListener { @Override public boolean isProfileRequest(ObjectGraphNode origin, SpiQuery query) { - ProfileOrigin profileOrigin = profileMap.get(origin.getOriginQueryPoint().getKey()); if (profileOrigin == null) { profileMap.put(origin.getOriginQueryPoint().getKey(), createProfileOrigin(origin, query)); @@ -61,12 +60,11 @@ public class ProfileManager implements ProfilingListener { *

* For new profiling entries it is useful to compare the profiling against the current * query detail that is specified in the code (as the query might already be manually optimised). - *

*/ private ProfileOrigin createProfileOrigin(ObjectGraphNode origin, SpiQuery query) { ProfileOrigin profileOrigin = new ProfileOrigin(origin.getOriginQueryPoint(), queryTuningAddVersion, profilingBase, profilingRate); // set the current query detail (fetch group) so that we can compare against profiling for new entries - profileOrigin.setOriginalQuery(query.getDetail().toString()); + profileOrigin.setOriginalQuery(query.getDetail().asString()); return profileOrigin; } @@ -77,7 +75,6 @@ public class ProfileManager implements ProfilingListener { */ @Override public void collectQueryInfo(ObjectGraphNode node, long beans, long micros) { - if (node != null) { ObjectGraphOrigin origin = node.getOriginQueryPoint(); if (origin != null) { @@ -92,11 +89,9 @@ public class ProfileManager implements ProfilingListener { *

* This is sent to use from a EntityBeanIntercept when the finalise method * is called on the bean. - *

*/ @Override public void collectNodeUsage(NodeUsageCollector usageCollector) { - ProfileOrigin profileOrigin = getProfileOrigin(usageCollector.getNode().getOriginQueryPoint()); profileOrigin.collectUsageInfo(usageCollector); } @@ -114,16 +109,13 @@ public class ProfileManager implements ProfilingListener { * Collect all the profiling information. */ public AutoTuneCollection profilingCollection(boolean reset) { - AutoTuneCollection req = new AutoTuneCollection(); - for (ProfileOrigin origin : profileMap.values()) { BeanDescriptor desc = server.getBeanDescriptorById(origin.getOrigin().getBeanType()); if (desc != null) { origin.profilingCollection(desc, req, reset); } } - return req; } diff --git a/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/TunedQueryInfo.java b/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/TunedQueryInfo.java index 374a8d722..43e66c5af 100644 --- a/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/TunedQueryInfo.java +++ b/ebean-autotune/src/main/java/io/ebeaninternal/server/autotune/service/TunedQueryInfo.java @@ -65,7 +65,7 @@ public class TunedQueryInfo implements Serializable { @Override public String toString() { - return tunedDetail.toString(); + return tunedDetail.asString(); } } diff --git a/ebean-autotune/src/test/java/io/ebeaninternal/server/autotune/service/ProfileOriginTest.java b/ebean-autotune/src/test/java/io/ebeaninternal/server/autotune/service/ProfileOriginTest.java index c3995d215..b1f982024 100644 --- a/ebean-autotune/src/test/java/io/ebeaninternal/server/autotune/service/ProfileOriginTest.java +++ b/ebean-autotune/src/test/java/io/ebeaninternal/server/autotune/service/ProfileOriginTest.java @@ -27,7 +27,7 @@ public class ProfileOriginTest extends BaseTestCase { OrmQueryDetail detail = po.buildDetail(desc); - assertThat(detail.asStringDebug().trim()).isEqualTo("fetch customer (name)"); + assertThat(detail.asString().trim()).isEqualTo("fetch customer (name)"); } @Test @@ -46,7 +46,7 @@ public class ProfileOriginTest extends BaseTestCase { OrmQueryDetail detail = po.buildDetail(desc); - assertThat(detail.asStringDebug()).isEqualTo("select (orderDate) fetch customer (name)"); + assertThat(detail.asString()).isEqualTo("select (orderDate) fetch customer (name)"); } @Test @@ -64,7 +64,7 @@ public class ProfileOriginTest extends BaseTestCase { OrmQueryDetail detail = po.buildDetail(desc); - assertThat(detail.asStringDebug().trim()).isEqualTo("select (orderDate,customer)"); + assertThat(detail.asString().trim()).isEqualTo("select (orderDate,customer)"); } @Test @@ -87,7 +87,7 @@ public class ProfileOriginTest extends BaseTestCase { OrmQueryDetail detail = po.buildDetail(desc); - assertThat(detail.asStringDebug()).isEqualTo("select (orderDate) fetch customer (billingAddress)"); + assertThat(detail.asString()).isEqualTo("select (orderDate) fetch customer (billingAddress)"); } @@ -116,7 +116,7 @@ public class ProfileOriginTest extends BaseTestCase { po.collectUsageInfo(c); OrmQueryDetail detail = po.buildDetail(desc); - assertThat(detail.asStringDebug()).isEqualTo("fetch customer (name,note) fetch customer.billingAddress (line1)"); + assertThat(detail.asString()).isEqualTo("fetch customer (name,note) fetch customer.billingAddress (line1)"); } private NodeUsageCollector node(String path) { diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index aaa2b5961..446c823f2 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean bom @@ -18,8 +18,8 @@ 12.4.0 4.1 7.0 - 12.8.2 - 12.8.2 + 12.9.0 + 12.9.1 @@ -81,88 +81,88 @@ io.ebean ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-core-type - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-ddl-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-externalmapping-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-externalmapping-xml - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-autotune - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-querybean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean querybean-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided io.ebean kotlin-querybean-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided io.ebean ebean-test - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test io.ebean ebean-postgis - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-redis - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 04497471c..9d9d22dfc 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java b/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java index 45fba0e0e..a8ace12be 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java @@ -11,8 +11,6 @@ public interface DataReader { boolean next() throws SQLException; - void resetColumnPosition(); - void incrementPos(int increment); byte[] getBinaryBytes() throws SQLException; diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index dbde9f167..5febb1ea6 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean-core @@ -72,7 +72,7 @@ io.ebean ebean-ddl-generator - 12.8.4a + 12.9.4-RC1 test @@ -87,19 +87,19 @@ io.ebean ebean-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-core-type - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-externalmapping-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT @@ -119,6 +119,15 @@ provided + + + com.h2database + h2 + 1.4.199 + provided + + javax.transaction jta @@ -173,7 +182,7 @@ org.postgresql postgresql - 42.2.10 + 42.2.20 true @@ -214,15 +223,6 @@ test - - - com.h2database - h2 - 1.4.199 - provided - - org.xerial sqlite-jdbc @@ -282,7 +282,7 @@ commons-io commons-io - 2.5 + 2.7 test @@ -302,7 +302,7 @@ io.ebean ebean-maven-plugin - 12.8.2 + 12.9.1 test diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/BindParams.java b/ebean-core/src/main/java/io/ebeaninternal/api/BindParams.java index a45d98e6d..afb1e44b8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/BindParams.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/BindParams.java @@ -155,7 +155,6 @@ public class BindParams implements Serializable { * Set an In Out parameter using position. */ public void setParameter(int position, Object value, int outType) { - Param p = getParam(position); p.setInValue(value); p.setOutType(outType); @@ -178,8 +177,8 @@ public class BindParams implements Serializable { * Using position set the In value of a parameter. Note that for nulls you * must use setNullParameter. */ + @SuppressWarnings("rawtypes") public void setParameter(int position, Object value) { - Param p = getParam(position); if (value instanceof Collection) { // use of postgres ANY with positioned parameter @@ -214,7 +213,6 @@ public class BindParams implements Serializable { * Set a named In Out parameter. */ public void setParameter(String name, Object value, int outType) { - Param p = getParam(name); p.setInValue(value); p.setOutType(outType); @@ -232,7 +230,6 @@ public class BindParams implements Serializable { * Set a named In parameter that is not null. */ public Param setParameter(String name, Object value) { - Param p = getParam(name); p.setInValue(value); return p; @@ -299,7 +296,6 @@ public class BindParams implements Serializable { * Return true if the bind hash and count has not changed. */ public boolean isSameBindHash() { - if (bindHash == null) { bindHash = calcQueryPlanHash(); return false; @@ -330,10 +326,6 @@ public class BindParams implements Serializable { private final StringBuilder preparedSql; - public OrderedList() { - this(new ArrayList<>()); - } - public OrderedList(List paramList) { this.paramList = paramList; this.preparedSql = new StringBuilder(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/LoadManyRequest.java b/ebean-core/src/main/java/io/ebeaninternal/api/LoadManyRequest.java index d7dc2f56a..0343b8579 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/LoadManyRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/LoadManyRequest.java @@ -71,7 +71,6 @@ public class LoadManyRequest extends LoadRequest { * This for use when lazy loading is invoked on methods such as clear() and removeAll() where it * generally makes sense to only fetch the Id values as the other property information is not * used. - *

*/ private boolean isOnlyIds() { return onlyIds; @@ -91,18 +90,16 @@ public class LoadManyRequest extends LoadRequest { return loadContext.getBatchSize(); } - private List getParentIdList() { - + private List parentIdList(SpiEbeanServer server) { List idList = new ArrayList<>(); - BeanPropertyAssocMany many = getMany(); for (BeanCollection bc : batch) { idList.add(many.getParentId(bc.getOwnerBean())); + bc.setLoader(server); // don't use the load buffer again } if (many.getTargetDescriptor().isPadInExpression()) { BindPadding.padIds(idList); } - return idList; } @@ -111,9 +108,7 @@ public class LoadManyRequest extends LoadRequest { } public SpiQuery createQuery(SpiEbeanServer server) { - BeanPropertyAssocMany many = getMany(); - SpiQuery query = many.newQuery(server); String orderBy = many.getLazyFetchOrderBy(); if (orderBy != null) { @@ -124,11 +119,11 @@ public class LoadManyRequest extends LoadRequest { if (extraWhere != null) { // replace special ${ta} placeholder with the base table alias // which is always t0 and add the extra where clause - query.where().raw(extraWhere.replace("${ta}", "t0")); + query.where().raw(extraWhere.replace("${ta}", "t0").replace("${mta}", "int_")); } query.setLazyLoadForParents(many); - many.addWhereParentIdIn(query, getParentIdList(), loadContext.isUseDocStore()); + many.addWhereParentIdIn(query, parentIdList(server), loadContext.isUseDocStore()); query.setPersistenceContext(loadContext.getPersistenceContext()); String mode = isLazy() ? "+lazy" : "+query"; @@ -146,7 +141,6 @@ public class LoadManyRequest extends LoadRequest { // override to just select the Id values query.select(many.getTargetIdProperty()); } - return query; } @@ -154,10 +148,8 @@ public class LoadManyRequest extends LoadRequest { * After the query execution check for empty collections and load L2 cache if desired. */ public void postLoad() { - BeanDescriptor desc = loadContext.getBeanDescriptor(); BeanPropertyAssocMany many = getMany(); - // check for BeanCollection's that where never processed // in the +query or +lazy load due to no rows (predicates) for (BeanCollection bc : batch) { @@ -172,6 +164,5 @@ public class LoadManyRequest extends LoadRequest { desc.cacheManyPropPut(many, bc, parentId); } } - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanType.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanType.java new file mode 100644 index 000000000..9923fff3f --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanType.java @@ -0,0 +1,17 @@ +package io.ebeaninternal.api; + +import io.ebean.bean.EntityBean; + +/** + * SPI interface for underlying BeanDescriptor. + */ +public interface SpiBeanType { + + /** + * Return true if the bean contains a many property that has modifications. + *

+ * That is a ManyToMany or a OneToMany with orphan removal with additions + * or removals from the collection. + */ + boolean isToManyDirty(EntityBean bean); +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanTypeManager.java new file mode 100644 index 000000000..0a6b79d78 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanTypeManager.java @@ -0,0 +1,13 @@ +package io.ebeaninternal.api; + +/** + * Manager of SpiBeanTypes. + */ +public interface SpiBeanTypeManager { + + /** + * Return the bean type for the given entity class. + */ + SpiBeanType getBeanType(Class entityType); + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiCancelableQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiCancelableQuery.java new file mode 100644 index 000000000..10a53b9a7 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiCancelableQuery.java @@ -0,0 +1,26 @@ +package io.ebeaninternal.api; + +import javax.persistence.PersistenceException; + +import io.ebean.CancelableQuery; + +/** + * Cancellable query, that has a delegate. + * + * @author Roland Praml, FOCONIS AG + * + */ +public interface SpiCancelableQuery extends CancelableQuery { + + /** + * Checks if the query was cancelled. + * @throws PersistenceException if query was cancelled. + */ + void checkCancelled(); + + /** + * Set the underlying cancelable query (with the PreparedStatement). + */ + void setCancelableQuery(CancelableQuery cancelableQuery); + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java index deb9c281c..daf6418d0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java @@ -1,14 +1,6 @@ package io.ebeaninternal.api; -import io.ebean.DtoQuery; -import io.ebean.EbeanServer; -import io.ebean.ExtendedServer; -import io.ebean.PersistenceContextScope; -import io.ebean.Query; -import io.ebean.RowConsumer; -import io.ebean.RowMapper; -import io.ebean.Transaction; -import io.ebean.TxScope; +import io.ebean.*; import io.ebean.bean.BeanCollectionLoader; import io.ebean.bean.CallOrigin; import io.ebean.config.DatabaseConfig; @@ -25,6 +17,7 @@ import io.ebeaninternal.server.transaction.RemoteTransactionEvent; import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; /** * Service Provider extension to EbeanServer. @@ -234,6 +227,11 @@ public interface SpiEbeanServer extends ExtendedServer, EbeanServer, BeanCollect */ List findSingleAttributeList(SpiSqlQuery query, Class cls); + /** + * SqlQuery find single attribute streaming the result to a consumer. + */ + void findSingleAttributeEach(SpiSqlQuery query, Class cls, Consumer consumer); + /** * SqlQuery find one with mapper. */ @@ -249,6 +247,16 @@ public interface SpiEbeanServer extends ExtendedServer, EbeanServer, BeanCollect */ void findEachRow(SpiSqlQuery query, RowConsumer consumer); + /** + * DTO findIterate query. + */ + QueryIterator findDtoIterate(SpiDtoQuery query); + + /** + * DTO findStream query. + */ + Stream findDtoStream(SpiDtoQuery query); + /** * DTO findList query. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java index cad22c50a..007e9f29f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpression.java @@ -105,4 +105,9 @@ public interface SpiExpression extends Expression { * Check for match to a natural key query returning false if it doesn't match. */ boolean naturalKey(NaturalKeyQueryData data); + + /** + * Apply property prefix when filterMany expressions included into main query. + */ + void prefixProperty(String path); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionList.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionList.java index 3cf5c2e64..c4764b30f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionList.java @@ -43,4 +43,9 @@ public interface SpiExpressionList extends ExpressionList, SpiExpression { default void applyRowLimits(SpiQuery query) { // do nothing by default } + + /** + * Apply property prefix when filterMany expressions included in main query. + */ + void prefixProperty(String path); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java new file mode 100644 index 000000000..a11f9d2f4 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java @@ -0,0 +1,17 @@ +package io.ebeaninternal.api; + +import io.ebean.bean.PersistenceContext; + +import java.util.List; + +/** + * SPI extension to PersistenceContext. + */ +public interface SpiPersistenceContext extends PersistenceContext { + + /** + * Return the list of dirty beans held by this persistence context. + */ + List dirtyBeans(SpiBeanTypeManager manager); + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java index 4686a3644..12942e7d6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -17,7 +17,6 @@ import io.ebeaninternal.server.core.SpiOrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.TableJoin; -import io.ebeaninternal.server.query.CancelableQuery; import io.ebeaninternal.server.querydefn.NaturalKeyBindParam; import io.ebeaninternal.server.querydefn.OrmQueryDetail; import io.ebeaninternal.server.querydefn.OrmQueryProperties; @@ -31,7 +30,7 @@ import java.util.Set; /** * Object Relational query - Internal extension to Query object. */ -public interface SpiQuery extends Query, SpiQueryFetch, TxnProfileEventCodes { +public interface SpiQuery extends Query, SpiQueryFetch, TxnProfileEventCodes, SpiCancelableQuery { enum Mode { NORMAL(false), LAZYLOAD_MANY(false), LAZYLOAD_BEAN(true), REFRESH_BEAN(true); @@ -847,16 +846,6 @@ public interface SpiQuery extends Query, SpiQueryFetch, TxnProfileEventCod */ ReadEvent getFutureFetchAudit(); - /** - * Set the underlying cancelable query (with the PreparedStatement). - */ - void setCancelableQuery(CancelableQuery cancelableQuery); - - /** - * Return true if this query has been cancelled. - */ - boolean isCancelled(); - /** * Return the base table to use if user defined on the query. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java index 849ad0cb0..62b93d34c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java @@ -3,7 +3,7 @@ package io.ebeaninternal.api; /** * SQL query binding (for SqlQuery and DtoQuery). */ -public interface SpiSqlBinding { +public interface SpiSqlBinding extends SpiCancelableQuery { /** * Return the named or positioned parameters. diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java index 8c59fa7dc..d26ab3ecc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java @@ -3,7 +3,6 @@ package io.ebeaninternal.api; import io.ebean.ProfileLocation; import io.ebean.Transaction; import io.ebean.annotation.DocStoreMode; -import io.ebean.bean.PersistenceContext; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; import io.ebeaninternal.server.core.PersistDeferredRelationship; @@ -196,7 +195,7 @@ public interface SpiTransaction extends Transaction { * later. This is along the lines of 'extended persistence context' * behaviour. */ - PersistenceContext getPersistenceContext(); + SpiPersistenceContext getPersistenceContext(); /** * Set the persistence context to this transaction. @@ -208,7 +207,7 @@ public interface SpiTransaction extends Transaction { * and setPersistenceContext() enable a developer to reuse a single * PersistenceContext with multiple transactions. */ - void setPersistenceContext(PersistenceContext context); + void setPersistenceContext(SpiPersistenceContext context); /** * Return the underlying Connection for internal use. diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index 5fd2a3c6e..a63c04cc6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -3,7 +3,6 @@ package io.ebeaninternal.api; import io.ebean.ProfileLocation; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; -import io.ebean.bean.PersistenceContext; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; import io.ebeaninternal.server.core.PersistDeferredRelationship; @@ -379,12 +378,12 @@ public abstract class SpiTransactionProxy implements SpiTransaction { } @Override - public PersistenceContext getPersistenceContext() { + public SpiPersistenceContext getPersistenceContext() { return transaction.getPersistenceContext(); } @Override - public void setPersistenceContext(PersistenceContext context) { + public void setPersistenceContext(SpiPersistenceContext context) { transaction.setPersistenceContext(context); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java index c12534146..67844c631 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java @@ -1,14 +1,10 @@ package io.ebeaninternal.server.core; -import io.ebean.EbeanServer; +import io.ebean.CancelableQuery; import io.ebean.Transaction; import io.ebean.util.JdbcClose; -import io.ebeaninternal.api.BindParams; -import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.SpiQuery; -import io.ebeaninternal.api.SpiSqlBinding; -import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.server.lib.Str; +import io.ebeaninternal.api.*; +import io.ebeaninternal.server.util.Str; import io.ebeaninternal.server.persist.Binder; import io.ebeaninternal.server.persist.TrimLogSql; import io.ebeaninternal.server.util.BindParamsParser; @@ -17,11 +13,14 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantLock; + +import javax.persistence.PersistenceException; /** * Wraps the objects involved in executing a SQL / Relational Query. */ -public abstract class AbstractSqlQueryRequest { +public abstract class AbstractSqlQueryRequest implements CancelableQuery { protected final SpiSqlBinding query; @@ -41,6 +40,8 @@ public abstract class AbstractSqlQueryRequest { protected long startNano; + private final ReentrantLock lock = new ReentrantLock(); + /** * Create the BeanFindRequest. */ @@ -48,6 +49,7 @@ public abstract class AbstractSqlQueryRequest { this.server = server; this.query = query; this.transaction = (SpiTransaction) t; + this.query.setCancelableQuery(this); } /** @@ -83,11 +85,6 @@ public abstract class AbstractSqlQueryRequest { return transaction.isLogSql(); } - /** - * Set the resultSet and associated query plan if known. - */ - abstract void setResultSet(ResultSet resultSet, Object queryPlanKey) throws SQLException; - /** * Return the bindLog for this request. */ @@ -95,12 +92,15 @@ public abstract class AbstractSqlQueryRequest { return bindLog; } + /** + * Set the resultSet and associated query plan if known. + */ + abstract void setResultSet(ResultSet resultSet, Object queryPlanKey) throws SQLException; + /** * Return true if we can navigate to the next row. */ - public boolean next() throws SQLException { - return resultSet.next(); - } + public abstract boolean next() throws SQLException; protected abstract void requestComplete(); @@ -113,7 +113,6 @@ public abstract class AbstractSqlQueryRequest { JdbcClose.close(pstmt); } - /** * Prepare the SQL taking into account named bind parameters. */ @@ -145,24 +144,30 @@ public abstract class AbstractSqlQueryRequest { } protected void executeAsSql(Binder binder) throws SQLException { - prepareSql(); - Connection conn = transaction.getInternalConnection(); - pstmt = conn.prepareStatement(sql); - if (query.getTimeout() > 0) { - pstmt.setQueryTimeout(query.getTimeout()); + lock.lock(); + try { + query.checkCancelled(); + prepareSql(); + Connection conn = transaction.getInternalConnection(); + pstmt = conn.prepareStatement(sql); + if (query.getTimeout() > 0) { + pstmt.setQueryTimeout(query.getTimeout()); + } + if (query.getBufferFetchSizeHint() > 0) { + pstmt.setFetchSize(query.getBufferFetchSizeHint()); + } + BindParams bindParams = query.getBindParams(); + if (!bindParams.isEmpty()) { + this.bindLog = binder.bind(bindParams, pstmt, conn); + } + if (isLogSql()) { + transaction.logSql(Str.add(TrimLogSql.trim(sql), "; --bind(", bindLog, ")")); + } + } finally { + lock.unlock(); } - if (query.getBufferFetchSizeHint() > 0) { - pstmt.setFetchSize(query.getBufferFetchSizeHint()); - } - BindParams bindParams = query.getBindParams(); - if (!bindParams.isEmpty()) { - this.bindLog = binder.bind(bindParams, pstmt, conn); - } - if (isLogSql()) { - transaction.logSql(Str.add(TrimLogSql.trim(sql), "; --bind(", bindLog, ")")); - } - setResultSet(pstmt.executeQuery(), null); + query.checkCancelled(); } /** @@ -172,4 +177,13 @@ public abstract class AbstractSqlQueryRequest { return sql; } + @Override + public void cancel() { + lock.lock(); + try { + JdbcClose.cancel(pstmt); + } finally { + lock.unlock(); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 6595d9816..87580f90b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -1382,7 +1382,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { @Nonnull @Override public FutureList findFutureList(Query query, Transaction t) { - SpiQuery spiQuery = (SpiQuery) query; + SpiQuery spiQuery = (SpiQuery) query.copy(); spiQuery.setFutureFetch(true); // FutureList query always run in it's own persistence content spiQuery.setPersistenceContext(new DefaultPersistenceContext()); @@ -1433,32 +1433,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { @Nonnull @Override public Stream findStream(Query query, Transaction transaction) { - SpiOrmQueryRequest request = createQueryRequest(Type.ITERATE, query, transaction); - try { - request.initTransIfRequired(); - return toStream(request.findIterate()); - } catch (RuntimeException ex) { - request.endTransIfRequired(); - throw ex; - } + return toStream(findIterate(query, transaction)); } private Stream toStream(QueryIterator queryIterator) { - return stream(spliteratorUnknownSize(queryIterator, Spliterator.ORDERED), false) - .onClose(new QueryIteratorClose(queryIterator)); - } - - private static class QueryIteratorClose implements Runnable { - private final QueryIterator iterator; - - private QueryIteratorClose(QueryIterator iterator) { - this.iterator = iterator; - } - - @Override - public void run() { - iterator.close(); - } + return stream(spliteratorUnknownSize(queryIterator, Spliterator.ORDERED), false).onClose(queryIterator::close); } @Override @@ -1606,6 +1585,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return executeSqlQuery((req) -> req.findOneMapper(mapper), query); } + @Override + public void findSingleAttributeEach(SpiSqlQuery query, Class cls, Consumer consumer) { + executeSqlQuery((req) -> req.findSingleAttributeEach(cls, consumer), query); + } + @Override public List findSingleAttributeList(SpiSqlQuery query, Class cls) { return executeSqlQuery((req) -> req.findSingleAttributeList(cls), query); @@ -1649,6 +1633,23 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } + @Override + public QueryIterator findDtoIterate(SpiDtoQuery query) { + DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + try { + request.initTransIfRequired(); + return request.findIterate(); + } catch (RuntimeException ex) { + request.endTransIfRequired(); + throw ex; + } + } + + @Override + public Stream findDtoStream(SpiDtoQuery query) { + return toStream(findDtoIterate(query)); + } + @Override public List findDtoList(SpiDtoQuery query) { DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java index 2372ed7c3..2b1a55f6e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.core; +import io.ebean.QueryIterator; import io.ebean.core.type.DataReader; import io.ebeaninternal.api.SpiDtoQuery; import io.ebeaninternal.api.SpiEbeanServer; @@ -52,6 +53,7 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { ormQuery.setType(type); ormQuery.setManualId(); + query.setCancelableQuery(ormQuery); // execute the underlying ORM query returning the ResultSet SpiResultSet result = server.findResultSet(ormQuery, transaction); this.pstmt = result.getStatement(); @@ -90,6 +92,11 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { } } + public QueryIterator findIterate() { + flushJdbcBatchOnQuery(); + return queryEngine.findIterate(this); + } + public void findEach(Consumer consumer) { flushJdbcBatchOnQuery(); queryEngine.findEach(this, consumer); @@ -110,9 +117,13 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { return queryEngine.findList(this); } + public boolean next() throws SQLException { + query.checkCancelled(); + return dataReader.next(); + } + @SuppressWarnings("unchecked") public T readNextBean() throws SQLException { - dataReader.resetColumnPosition(); return (T) plan.readRow(dataReader); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index 3ec5deac7..0fb83a18f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.core; import io.ebean.CacheMode; +import io.ebean.CancelableQuery; import io.ebean.OrderBy; import io.ebean.PersistenceContextScope; import io.ebean.QueryIterator; @@ -35,7 +36,6 @@ import io.ebeaninternal.server.deploy.DeployPropertyParserMap; import io.ebeaninternal.server.el.ElPropertyValue; import io.ebeaninternal.server.loadcontext.DLoadContext; import io.ebeaninternal.server.query.CQueryPlan; -import io.ebeaninternal.server.query.CancelableQuery; import io.ebeaninternal.server.transaction.DefaultPersistenceContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -219,10 +219,10 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery */ @Override public void prepareQuery() { + secondaryQueries = query.convertJoins(); beanDescriptor.prepareQuery(query); adapterPreQuery(); - this.secondaryQueries = query.convertJoins(); - this.queryPlanKey = query.prepare(this); + queryPlanKey = query.prepare(this); } public boolean isNativeSql() { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index ce6a39878..8b3285c7a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -379,9 +379,11 @@ public final class PersistRequestBean extends PersistRequest implements BeanP public void setBatched() { batched = true; if (type == Type.INSERT || type == Type.UPDATE) { - // used to trigger automatic jdbc batch flush - intercept.registerGetterCallback(this); - getterCallback = true; + if (beanDescriptor.hasSingleIdProperty()) { + // used to trigger automatic jdbc batch flush + intercept.registerGetterCallback(this); + getterCallback = true; + } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java index 80471f5aa..f16a9d7cd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestUpdateSql.java @@ -3,7 +3,7 @@ package io.ebeaninternal.server.core; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiSqlUpdate; import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.server.lib.Str; +import io.ebeaninternal.server.util.Str; import io.ebeaninternal.server.persist.BatchControl; import io.ebeaninternal.server.persist.PersistExecute; import io.ebeaninternal.server.persist.TrimLogSql; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java index 40f6d1a3c..e9d18797a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java @@ -20,17 +20,22 @@ public interface RelationalQueryEngine { /** * Find a list of beans using relational query. */ - List findList(RelationalQueryRequest request); - - /** - * Find each query using relational query. - */ - void findEach(RelationalQueryRequest request, Consumer consumer); + List findList(RelationalQueryRequest request, RowReader reader); /** * Find each while query using relational query. */ - void findEach(RelationalQueryRequest request, Predicate consumer); + void findEach(RelationalQueryRequest request, RowReader reader, Predicate consumer); + + /** + * Find each via raw consumer. + */ + void findEach(RelationalQueryRequest request, RowConsumer mapper); + + /** + * Find one via mapper. + */ + T findOne(RelationalQueryRequest request, RowMapper mapper); /** * Find single attribute. @@ -43,19 +48,9 @@ public interface RelationalQueryEngine { List findSingleAttributeList(RelationalQueryRequest request, Class cls); /** - * Find one via mapper. + * Find single attribute streaming the result to a consumer. */ - T findOneMapper(RelationalQueryRequest request, RowMapper mapper); - - /** - * Find list via mapper. - */ - List findListMapper(RelationalQueryRequest request, RowMapper mapper); - - /** - * Find each via raw consumer. - */ - void findEachRow(RelationalQueryRequest request, RowConsumer mapper); + void findSingleAttributeEach(RelationalQueryRequest request, Class cls, Consumer consumer); /** * Collect SQL query execution statistics. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java index 7850139c7..a5130ef22 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java @@ -1,10 +1,6 @@ package io.ebeaninternal.server.core; -import io.ebean.RowConsumer; -import io.ebean.RowMapper; -import io.ebean.SqlQuery; -import io.ebean.SqlRow; -import io.ebean.Transaction; +import io.ebean.*; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiSqlBinding; @@ -57,18 +53,24 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest { boolean findEachRow(RowConsumer mapper) { flushJdbcBatchOnQuery(); - queryEngine.findEachRow(this, mapper); + queryEngine.findEach(this, mapper); return true; } List findListMapper(RowMapper mapper) { flushJdbcBatchOnQuery(); - return queryEngine.findListMapper(this, mapper); + return queryEngine.findList(this, () -> mapper.map(resultSet, rows++)); } T findOneMapper(RowMapper mapper) { flushJdbcBatchOnQuery(); - return queryEngine.findOneMapper(this, mapper); + return queryEngine.findOne(this, mapper); + } + + public boolean findSingleAttributeEach(Class cls, Consumer consumer) { + flushJdbcBatchOnQuery(); + queryEngine.findSingleAttributeEach(this, cls, consumer); + return true; } public List findSingleAttributeList(Class cls) { @@ -83,17 +85,17 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest { public void findEach(Consumer consumer) { flushJdbcBatchOnQuery(); - queryEngine.findEach(this, consumer); + queryEngine.findEach(this, (resultSet, rowNum) -> consumer.accept(createNewRow())); } public void findEachWhile(Predicate consumer) { flushJdbcBatchOnQuery(); - queryEngine.findEach(this, consumer); + queryEngine.findEach(this, this::createNewRow, consumer); } public List findList() { flushJdbcBatchOnQuery(); - return queryEngine.findList(this); + return queryEngine.findList(this, this::createNewRow); } /** @@ -135,16 +137,15 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest { return resultSet; } - public void incrementRows() { - rows++; - } - - public List mapList(RowMapper mapper) throws SQLException { - List list = new ArrayList<>(); - while (next()) { - list.add(mapper.map(resultSet, rows++)); + @Override + public boolean next() throws SQLException { + query.checkCancelled(); + if (!resultSet.next()) { + return false; + } else { + rows++; + return true; } - return list; } public T mapOne(RowMapper mapper) throws SQLException { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/RowReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/RowReader.java new file mode 100644 index 000000000..2a7d0b119 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/RowReader.java @@ -0,0 +1,14 @@ +package io.ebeaninternal.server.core; + +import java.sql.SQLException; + +/** + * Read a row building a result for that row. + */ +public interface RowReader { + + /** + * Build and return a result for a row. + */ + T read() throws SQLException; +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index a6841d449..63599b7ad 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -38,15 +38,7 @@ import io.ebean.plugin.BeanType; import io.ebean.plugin.ExpressionPath; import io.ebean.plugin.Property; import io.ebean.util.SplitName; -import io.ebeaninternal.api.BeanCacheResult; -import io.ebeaninternal.api.CQueryPlanKey; -import io.ebeaninternal.api.ConcurrencyMode; -import io.ebeaninternal.api.LoadBeanContext; -import io.ebeaninternal.api.LoadContext; -import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.SpiQuery; -import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.api.SpiUpdatePlan; +import io.ebeaninternal.api.*; import io.ebeaninternal.api.TransactionEventTable.TableIUD; import io.ebeaninternal.api.json.SpiJsonReader; import io.ebeaninternal.api.json.SpiJsonWriter; @@ -119,7 +111,7 @@ import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero; /** * Describes Beans including their deployment information. */ -public class BeanDescriptor implements BeanType, STreeType { +public class BeanDescriptor implements BeanType, STreeType, SpiBeanType { private static final Logger logger = LoggerFactory.getLogger(BeanDescriptor.class); @@ -1755,6 +1747,13 @@ public class BeanDescriptor implements BeanType, STreeType { return idProperty != null; } + /** + * Return false for IdClass case with multiple @Id properties. + */ + public boolean hasSingleIdProperty() { + return idPropertyIndex != -1; + } + /** * Return true if this type has a simple Id and the platform supports mutli-value binding. */ @@ -1880,7 +1879,7 @@ public class BeanDescriptor implements BeanType, STreeType { if (refBean == null) { refBean = createReference(readOnly, false, id, pc); } - return (EntityBean)refBean; + return (EntityBean) refBean; } /** @@ -2017,6 +2016,13 @@ public class BeanDescriptor implements BeanType, STreeType { return owner.getBeanDescriptor(otherType); } + /** + * Returns true, if the table is managed (i.e. an existing m2m relation). + */ + public boolean isTableManaged(String tableName) { + return owner.isTableManaged(tableName); + } + /** * Return the order column property. */ @@ -2937,6 +2943,20 @@ public class BeanDescriptor implements BeanType, STreeType { return false; } + @Override + public boolean isToManyDirty(EntityBean bean) { + final EntityBeanIntercept ebi = bean._ebean_getIntercept(); + for (BeanPropertyAssocMany many : propertiesManySave) { + if (ebi.isLoadedProperty(many.getPropertyIndex())) { + final BeanCollection value = (BeanCollection) many.getValue(bean); + if (value != null && value.hasModifications()) { + return true; + } + } + } + return false; + } + /** * Return true if the bean is draftable and considered a 'live' instance. */ @@ -3139,7 +3159,8 @@ public class BeanDescriptor implements BeanType, STreeType { } public boolean isIdLoaded(EntityBeanIntercept ebi) { - return ebi.isLoadedProperty(idPropertyIndex); + // assume id loaded for IdClass case with idPropertyIndex == -1 + return idPropertyIndex == -1 ? true : ebi.isLoadedProperty(idPropertyIndex); } boolean hasIdValue(EntityBean bean) { @@ -3179,7 +3200,7 @@ public class BeanDescriptor implements BeanType, STreeType { int propertyIndex = beanProperty.getPropertyIndex(); if (!ebi.isDirtyProperty(propertyIndex) && ebi.isLoadedProperty(propertyIndex)) { Object value = beanProperty.getValue(ebi.getOwner()); - if (value == null || beanProperty.isDirtyValue(value)) { + if (value != null && beanProperty.isDirtyValue(value)) { // mutable scalar value which is considered dirty so mark // it as such so that it is included in an update ebi.markPropertyAsChanged(propertyIndex); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index dca6b2b09..84b412513 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -26,9 +26,7 @@ import io.ebean.meta.MetricVisitor; import io.ebean.meta.QueryPlanInit; import io.ebean.plugin.BeanType; import io.ebean.util.AnnotationUtil; -import io.ebeaninternal.api.ConcurrencyMode; -import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.TransactionEventTable; +import io.ebeaninternal.api.*; import io.ebeaninternal.server.cache.CacheChangeSet; import io.ebeaninternal.server.cache.SpiCacheManager; import io.ebeaninternal.server.core.InternString; @@ -88,7 +86,7 @@ import java.util.concurrent.TimeUnit; /** * Creates BeanDescriptors. */ -public class BeanDescriptorManager implements BeanDescriptorMap { +public class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTypeManager { private static final Logger logger = LoggerFactory.getLogger(BeanDescriptorManager.class); @@ -263,6 +261,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { return descQueueMap.get(queueId); } + @Override + public SpiBeanType getBeanType(Class entityType) { + return getBeanDescriptor(entityType); + } + @Override @SuppressWarnings("unchecked") public BeanDescriptor getBeanDescriptor(Class entityType) { @@ -442,6 +445,12 @@ public class BeanDescriptorManager implements BeanDescriptorMap { return tableToDescMap.get(tableName.toLowerCase()); } + @Override + public boolean isTableManaged(String tableName) { + return tableToDescMap.get(tableName.toLowerCase()) != null + || tableToViewDescMap.get(tableName.toLowerCase()) != null; + } + /** * Invalidate entity beans based on views via their dependent tables. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java index 7c7dd6e0c..94a9fded9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java @@ -76,4 +76,10 @@ public interface BeanDescriptorMap { * Return true if Jackson core is present on the classpath. */ boolean isJacksonCorePresent(); + + /** + * Returns true, if the given table (or view) is managed by ebean + * (= an entity exists) + */ + boolean isTableManaged(String tableName); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 8e1ac1a14..c823e9a7e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -439,6 +439,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST @Override public String getAssocIsEmpty(SpiExpressionRequest request, String path) { boolean softDelete = targetDescriptor.isSoftDelete(); + boolean needsX2Table = softDelete || getExtraWhere() != null; StringBuilder sb = new StringBuilder(50); SpiQuery query = request.getQueryRequest().getQuery(); if (hasJoinTable()) { @@ -446,7 +447,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST } else { sb.append(targetDescriptor.getBaseTable(query.getTemporalMode())); } - if (softDelete && hasJoinTable()) { + if (needsX2Table && hasJoinTable()) { sb.append(" x join "); sb.append(targetDescriptor.getBaseTable(query.getTemporalMode())); sb.append(" x2 on "); @@ -461,6 +462,16 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST } exportedProperties[i].appendWhere(sb, "x.", path); } + if (getExtraWhere() != null) { + sb.append(" and "); + if (hasJoinTable()) { + sb.append(getExtraWhere().replace("${ta}", "x2").replace("${mta}", "x")); + } else { + sb.append(getExtraWhere().replace("${ta}", "x")); + } + } + + if (softDelete) { String alias = hasJoinTable() ? "x2" : "x"; sb.append(" and ").append(targetDescriptor.getSoftDeletePredicate(alias)); @@ -1061,4 +1072,16 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST public void bindElementValue(SqlUpdate insert, Object value) { targetDescriptor.bindElementValue(insert, value); } + + /** + * Returns true, if we must create a m2m join table. + */ + public boolean createJoinTable() { + if (hasJoinTable() && getMappedBy() == null) { + // only create on other 'owning' side + return !descriptor.isTableManaged(intersectionJoin.getTable()); + } else { + return false; + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java index 257893820..1e078c0fd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java @@ -10,7 +10,7 @@ public interface DbSqlContext { /** * Add a join to the sql query. */ - void addJoin(String type, String table, TableJoinColumn[] cols, String a1, String a2); + void addJoin(String type, String table, TableJoinColumn[] cols, String a1, String a2, String extraWhere); /** * Push the current table alias onto the stack. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java index e88e56034..8c1624d05 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/TableJoin.java @@ -34,6 +34,8 @@ public final class TableJoin { private final int queryHash; private final PropertyForeignKey foreignKey; + + private final String extraWhere; public TableJoin(DeployTableJoin deploy) { this(deploy, null); @@ -44,6 +46,7 @@ public final class TableJoin { */ public TableJoin(DeployTableJoin deploy, PropertyForeignKey foreignKey) { this.foreignKey = foreignKey; + this.extraWhere = deploy.getExtraWhere(); this.table = InternString.intern(deploy.getTable()); this.type = deploy.getType(); this.inheritInfo = deploy.getInheritInfo(); @@ -57,6 +60,7 @@ public final class TableJoin { private TableJoin(TableJoin source, String overrideColumn) { this.foreignKey = null; + this.extraWhere = source.extraWhere; this.table = source.table; this.type = source.type; this.inheritInfo = source.inheritInfo; @@ -146,7 +150,7 @@ public final class TableJoin { public SqlJoinType addJoin(SqlJoinType joinType, String a1, String a2, DbSqlContext ctx) { String joinLiteral = joinType.getLiteral(type); - ctx.addJoin(joinLiteral, table, columns(), a1, a2); + ctx.addJoin(joinLiteral, table, columns(), a1, a2, extraWhere); return joinType.autoToOuter(type); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java index 472a71db3..be4b77aa0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java @@ -86,6 +86,7 @@ public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty { * collection. */ public void setExtraWhere(String extraWhere) { + this.tableJoin.setExtraWhere(extraWhere); this.extraWhere = extraWhere; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployTableJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployTableJoin.java index d24024774..c3451f9f4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployTableJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployTableJoin.java @@ -33,6 +33,8 @@ public class DeployTableJoin { private ArrayList columns = new ArrayList<>(4); private InheritInfo inheritInfo; + + private String extraWhere; /** * Create a DeployTableJoin. @@ -137,6 +139,18 @@ public class DeployTableJoin { this.type = type; } + /** + * Returns the clause of an extra @Where annotation. + * @return + */ + public String getExtraWhere() { + return extraWhere; + } + + public void setExtraWhere(String extraWhere) { + this.extraWhere = extraWhere; + } + public DeployTableJoin createInverse(String tableName) { DeployTableJoin inverse = new DeployTableJoin(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java index b47612d65..1ca59dbc8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -118,7 +118,7 @@ class AnnotationAssocManys extends AnnotationAssoc { Where where = prop.getMetaAnnotationWhere(platform); if (where != null) { - prop.setExtraWhere(where.clause()); + prop.setExtraWhere(processFormula(where.clause())); } FetchPreference fetchPreference = get(prop, FetchPreference.class); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java index d313ce925..36fa99b57 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -54,7 +54,6 @@ public class AnnotationAssocOnes extends AnnotationAssoc { } private void readAssocOne(DeployBeanPropertyAssocOne prop) { - ManyToOne manyToOne = get(prop, ManyToOne.class); if (manyToOne != null) { readManyToOne(manyToOne, prop); @@ -97,7 +96,7 @@ public class AnnotationAssocOnes extends AnnotationAssoc { Where where = prop.getMetaAnnotationWhere(platform); if (where != null) { // not expecting this to be used on assoc one properties - prop.setExtraWhere(where.clause()); + prop.setExtraWhere(processFormula(where.clause())); } PrimaryKeyJoinColumn primaryKeyJoin = get(prop, PrimaryKeyJoinColumn.class); @@ -183,7 +182,6 @@ public class AnnotationAssocOnes extends AnnotationAssoc { } private void readManyToOne(ManyToOne propAnn, DeployBeanPropertyAssocOne beanProp) { - setCascadeTypes(propAnn.cascade(), beanProp.getCascadeInfo()); setTargetType(propAnn.targetEntity(), beanProp); setBeanTable(beanProp); @@ -194,7 +192,6 @@ public class AnnotationAssocOnes extends AnnotationAssoc { } private void readOneToOne(OneToOne propAnn, DeployBeanPropertyAssocOne prop) { - prop.setOneToOne(); prop.setDbInsertable(true); prop.setDbUpdateable(true); @@ -223,21 +220,19 @@ public class AnnotationAssocOnes extends AnnotationAssoc { } private void readPrimaryKeyJoin(PrimaryKeyJoinColumn primaryKeyJoin, DeployBeanPropertyAssocOne prop) { - if (!prop.isOneToOne()) { throw new IllegalStateException("Expecting property " + prop.getFullBeanName() + " with PrimaryKeyJoinColumn to be a OneToOne?"); } prop.setPrimaryKeyJoin(true); if (!primaryKeyJoin.name().isEmpty()) { - log.warn("Automatically determining join columns and ignoring PrimaryKeyJoinColumn.name {} on {}", primaryKeyJoin.name(), prop.getFullBeanName()); + log.info("Automatically determining join columns for @PrimaryKeyJoinColumn - ignoring PrimaryKeyJoinColumn.name attribute [{}] on {}", primaryKeyJoin.name(), prop.getFullBeanName()); } if (!primaryKeyJoin.referencedColumnName().isEmpty()) { - log.warn("Automatically determining join columns and Ignoring PrimaryKeyJoinColumn.referencedColumnName {} on {}", primaryKeyJoin.referencedColumnName(), prop.getFullBeanName()); + log.info("Automatically determining join columns for @PrimaryKeyJoinColumn - Ignoring PrimaryKeyJoinColumn.referencedColumnName attribute [{}] on {}", primaryKeyJoin.referencedColumnName(), prop.getFullBeanName()); } BeanTable baseBeanTable = factory.getBeanTable(info.getDescriptor().getBeanType()); - String localPrimaryKey = baseBeanTable.getIdColumn(); String foreignColumn = getBeanTable(prop).getIdColumn(); @@ -245,7 +240,6 @@ public class AnnotationAssocOnes extends AnnotationAssoc { } private void readEmbedded(DeployBeanPropertyAssocOne prop, Embedded embedded) { - if (descriptor.isDocStoreOnly() && prop.getDocStoreDoc() == null) { prop.setDocStoreEmbedded(""); } @@ -257,7 +251,6 @@ public class AnnotationAssocOnes extends AnnotationAssoc { } catch (NoSuchMethodError e) { // using standard JPA API without prefix option, maybe in EE container } - readEmbeddedAttributeOverrides(prop); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java index 8079f156f..5a0caa287 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -153,7 +153,7 @@ public class AnnotationFields extends AnnotationParser { Formula formula = prop.getMetaAnnotationFormula(platform); if (formula != null) { - prop.setSqlFormula(formula.select(), formula.join()); + prop.setSqlFormula(processFormula(formula.select()), processFormula(formula.join())); } initWhoProperties(prop); @@ -334,7 +334,7 @@ public class AnnotationFields extends AnnotationParser { } Formula formula = prop.getMetaAnnotationFormula(platform); if (formula != null) { - prop.setSqlFormula(formula.select(), formula.join()); + prop.setSqlFormula(processFormula(formula.select()), processFormula(formula.join())); } final Aggregation aggregation = prop.getMetaAnnotation(Aggregation.class); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationParser.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationParser.java index af015e1f5..ea880a18b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationParser.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationParser.java @@ -129,4 +129,11 @@ public abstract class AnnotationParser extends AnnotationBase { } return columnNames; } + + /** + * Process any formula from @Formula or @Where. + */ + protected String processFormula(String source) { + return source == null ? null : source.replace("${dbTableName}", descriptor.getBaseTable()); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java index 0ae5cd6fc..c3090e38f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java @@ -16,12 +16,17 @@ import io.ebeaninternal.api.NaturalKeyQueryData; */ public abstract class AbstractExpression implements SpiExpression { - protected final String propName; + protected String propName; protected AbstractExpression(String propName) { this.propName = propName; } + @Override + public void prefixProperty(String path) { + this.propName = path + "." + propName; + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // by default can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/BetweenPropertyExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/BetweenPropertyExpression.java index 798908335..a50ec888f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/BetweenPropertyExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/BetweenPropertyExpression.java @@ -17,8 +17,8 @@ class BetweenPropertyExpression extends NonPrepareExpression { private static final String BETWEEN = " between "; - private final String lowProperty; - private final String highProperty; + private String lowProperty; + private String highProperty; private final Object value; BetweenPropertyExpression(String lowProperty, String highProperty, Object value) { @@ -27,6 +27,12 @@ class BetweenPropertyExpression extends NonPrepareExpression { this.value = value; } + @Override + public void prefixProperty(String path) { + this.lowProperty = path + "." + lowProperty; + this.highProperty = path + "." + highProperty; + } + protected String name(String propName) { return propName; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java index c6fa5d087..aade99f5a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java @@ -88,6 +88,13 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio } } + @Override + public void prefixProperty(String path) { + for (SpiExpression exp : list) { + exp.prefixProperty(path); + } + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index c034b2992..ab80ae9e5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -126,6 +126,13 @@ public class DefaultExpressionList implements SpiExpressionList { } } + @Override + public void prefixProperty(String path) { + for (SpiExpression exp : list) { + exp.prefixProperty(path); + } + } + @Override public Junction toJunction() { return new JunctionExpression<>(Junction.Type.FILTER, this); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java index 8918bd05d..32ae2bccc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java @@ -36,6 +36,11 @@ class ExistsQueryExpression implements SpiExpression, UnsupportedDocStoreExpress this.subQuery = null; } + @Override + public void prefixProperty(String path) { + // do nothing + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/IdExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/IdExpression.java index 12910fb37..b5280b868 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/IdExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/IdExpression.java @@ -19,6 +19,11 @@ class IdExpression extends NonPrepareExpression implements SpiExpression { this.value = value; } + @Override + public void prefixProperty(String path) { + throw new IllegalStateException("Not allowed?"); + } + @Override public void writeDocQuery(DocQueryContext context) throws IOException { context.writeId(value); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index f048f1163..0aae30886 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -66,6 +66,11 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression this.exprList = exprList; } + @Override + public void prefixProperty(String path) { + exprList.prefixProperty(path); + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java index 0fa0d9b37..482aac6e8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java @@ -57,6 +57,12 @@ abstract class LogicExpression implements SpiExpression { this.expTwo = (SpiExpression) expTwo; } + @Override + public void prefixProperty(String path) { + expOne.prefixProperty(path); + expTwo.prefixProperty(path); + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java index c7f5d193e..77f46828b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java @@ -24,6 +24,11 @@ class NestedPathWrapperExpression implements SpiExpression { this.delegate = delegate; } + @Override + public void prefixProperty(String path) { + // do nothing + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java index 675e641ee..e384bbd1e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java @@ -9,6 +9,11 @@ import io.ebeaninternal.api.NaturalKeyQueryData; */ abstract class NonPrepareExpression implements SpiExpression { + @Override + public void prefixProperty(String path) { + // do nothing + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java index c95c8585b..28c9e3c78 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java @@ -17,6 +17,11 @@ class NoopExpression implements SpiExpression { protected static final NoopExpression INSTANCE = new NoopExpression(); + @Override + public void prefixProperty(String path) { + // do nothing + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NotExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NotExpression.java index 29535cd95..60ade47b0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/NotExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/NotExpression.java @@ -22,6 +22,11 @@ final class NotExpression implements SpiExpression { this.exp = (SpiExpression) exp; } + @Override + public void prefixProperty(String path) { + exp.prefixProperty(path); + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/lib/package.html b/ebean-core/src/main/java/io/ebeaninternal/server/lib/package.html deleted file mode 100644 index a114cce65..000000000 --- a/ebean-core/src/main/java/io/ebeaninternal/server/lib/package.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - AvajeLib - - -Core services for an application. -

- Core services including Logging, Deployment properties and Background thread for - running frequent tasks. -

- - diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java index 5035f0802..5d2b781c7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -201,7 +201,6 @@ class DLoadManyContext extends DLoadBaseContext implements LoadManyContext { @Override public void loadMany(BeanCollection bc, boolean onlyIds) { - lock.lock(); try { boolean useCache = !onlyIds && context.hitCache && context.property.isUseCache(); @@ -215,6 +214,7 @@ class DLoadManyContext extends DLoadBaseContext implements LoadManyContext { // find it using instance equality - avoiding equals() and potential deadlock issue if (list.get(i) == bc) { list.remove(i); + bc.setLoader(context.parent.getEbeanServer()); return; } } @@ -222,10 +222,9 @@ class DLoadManyContext extends DLoadBaseContext implements LoadManyContext { } } - // Should reduce the list by checking each beanCollection in the L2 first before executing the query - - LoadManyRequest req = new LoadManyRequest(this, onlyIds, useCache); - context.parent.getEbeanServer().loadMany(req); + context.parent.getEbeanServer().loadMany(new LoadManyRequest(this, onlyIds, useCache)); + // clear the buffer as all entries have been loaded + list.clear(); } finally { lock.unlock(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java index ac870b65e..189613808 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java @@ -3,7 +3,7 @@ package io.ebeaninternal.server.persist.dml; import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.server.core.PersistRequestBean; import io.ebeaninternal.server.deploy.BeanProperty; -import io.ebeaninternal.server.lib.Str; +import io.ebeaninternal.server.util.Str; import io.ebeaninternal.server.persist.BatchedPstmt; import io.ebeaninternal.server.persist.BatchedPstmtHolder; import io.ebeaninternal.server.persist.dmlbind.BindableRequest; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java index 9f30e9a29..e7674a31c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java @@ -1,14 +1,9 @@ package io.ebeaninternal.server.query; +import io.ebean.CancelableQuery; import io.ebean.QueryIterator; import io.ebean.Version; -import io.ebean.bean.BeanCollection; -import io.ebean.bean.EntityBean; -import io.ebean.bean.EntityBeanIntercept; -import io.ebean.bean.NodeUsageCollector; -import io.ebean.bean.NodeUsageListener; -import io.ebean.bean.ObjectGraphNode; -import io.ebean.bean.PersistenceContext; +import io.ebean.bean.*; import io.ebean.core.type.DataReader; import io.ebean.event.readaudit.ReadEvent; import io.ebean.util.JdbcClose; @@ -19,12 +14,7 @@ import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.server.autotune.ProfilingListener; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.core.SpiOrmQueryRequest; -import io.ebeaninternal.server.deploy.BeanCollectionHelpFactory; -import io.ebeaninternal.server.deploy.BeanDescriptor; -import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; -import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; -import io.ebeaninternal.server.deploy.DbReadContext; -import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.deploy.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,11 +24,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; +import java.util.*; import java.util.concurrent.locks.ReentrantLock; /** @@ -47,11 +33,9 @@ import java.util.concurrent.locks.ReentrantLock; * The SqlSelect is based on a tree (Object Graph). The tree is traversed to see * what parts are included in the tree according to the value of * find.getInclude(); - *

*

* The tree structure is flattened into a SqlSelectChain. The SqlSelectChain is * the key object used in reading the flat resultSet back into Objects. - *

*/ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTransactionEvent { @@ -156,8 +140,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran */ private PreparedStatement pstmt; - private boolean cancelled; - private String bindLog; private final CQueryPlan queryPlan; @@ -205,19 +187,15 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran this.query = request.getQuery(); this.queryMode = query.getMode(); this.lazyLoadManyProperty = query.getLazyLoadMany(); - this.readOnly = request.isReadOnly(); this.disableLazyLoading = query.isDisableLazyLoading(); - this.objectGraphNode = query.getParentNode(); this.profilingListener = query.getProfilingListener(); this.autoTuneProfiling = profilingListener != null; this.profilingListenerRef = autoTuneProfiling ? new WeakReference<>(profilingListener) : null; - // set the generated sql back to the query // so its available to the user... query.setGeneratedSql(queryPlan.getSql()); - SqlTree sqlTree = queryPlan.getSqlTree(); this.rootNode = sqlTree.getRootNode(); this.manyProperty = sqlTree.getManyProperty(); @@ -293,15 +271,7 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran public void cancel() { lock.lock(); try { - this.cancelled = true; - if (pstmt != null) { - try { - logger.debug("Cancelling query"); - pstmt.cancel(); - } catch (SQLException e) { - throw new PersistenceException("Error cancelling query", e); - } - } + JdbcClose.cancel(pstmt); } finally { lock.unlock(); } @@ -333,9 +303,8 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran ResultSet prepareResultSet(boolean forwardOnlyHint) throws SQLException { lock.lock(); try { - if (cancelled) { - throw new SQLException("Query cancelled"); - } + // cancelled before we started + query.checkCancelled(); startNano = System.nanoTime(); SpiTransaction t = request.getTransaction(); profileOffset = t.profileOffset(); @@ -363,17 +332,18 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran pstmt.setFetchSize(query.getBufferFetchSizeHint()); } bindLog = predicates.bind(queryPlan.bindEncryptedProperties(pstmt, conn)); - return pstmt.executeQuery(); } finally { lock.unlock(); } + ResultSet ret = pstmt.executeQuery(); + query.checkCancelled(); + return ret; } /** * Close the resources. *

* The JDBC resultSet and statement need to be closed. Its important that this method is called. - *

*/ public void close() { try { @@ -405,14 +375,12 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran @Override public void setLazyLoadedChildBean(EntityBean bean, Object lazyLoadParentId) { - if (lazyLoadParentId != null) { if (!lazyLoadParentId.equals(this.lazyLoadParentId)) { // get the appropriate parent bean from the persistence context this.lazyLoadParentBean = (EntityBean) lazyLoadManyProperty.getBeanDescriptor().contextGet(getPersistenceContext(), lazyLoadParentId); this.lazyLoadParentId = lazyLoadParentId; } - // add the loadedBean to the appropriate collection of lazyLoadParentBean lazyLoadManyProperty.addBeanToCollectionWithCreate(lazyLoadParentBean, bean, true); } @@ -423,10 +391,8 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran *

* If the query includes a many then the first object in the returned array is * the one/master and the second the many/detail. - *

*/ private boolean readNextBean() throws SQLException { - if (!moveToNextRow()) { if (currentBean == null) { nextBean = null; @@ -440,7 +406,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran } loadedBeanCount++; - if (manyProperty == null) { // only single resultSet row required to build object so we are done // read a single resultSet row into single bean @@ -486,14 +451,11 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran * Return true if we can move to the next resultSet row. */ private boolean moveToNextRow() throws SQLException { - if (!dataReader.next()) { noMoreRows = true; return false; } - rowCount++; - dataReader.resetColumnPosition(); return true; } @@ -502,7 +464,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran } boolean readBean() throws SQLException { - boolean result = hasNext(); updateExecutionStatistics(); return result; @@ -522,7 +483,8 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran boolean hasNext() throws SQLException { lock.lock(); try { - if (noMoreRows || cancelled) { + query.checkCancelled(); + if (noMoreRows) { return false; } if (hasNextCache) { @@ -539,20 +501,16 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran * Read version beans and their effective dates. */ List> readVersions() throws SQLException { - List> versionList = new ArrayList<>(); - Version version; while ((version = readNextVersion()) != null) { versionList.add(version); } - updateExecutionStatistics(); return versionList; } private Version readNextVersion() throws SQLException { - if (moveToNextRow()) { return rootNode.loadVersion(this); } @@ -696,7 +654,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran * Should we create profileNodes for beans created in this query. *

* This is true for all queries except lazy load bean queries. - *

*/ @Override public boolean isAutoTuneProfiling() { @@ -707,13 +664,11 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran } private String getPath(String propertyName) { - if (currentPrefix == null) { return propertyName; } else if (propertyName == null) { return currentPrefix; } - String path = currentPathMap.get(propertyName); if (path != null) { return path; @@ -724,7 +679,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran @Override public void profileBean(EntityBeanIntercept ebi, String prefix) { - ObjectGraphNode node = request.getGraphContext().getObjectGraphNode(prefix); ebi.setNodeUsageCollector(new NodeUsageCollector(node, profilingListenerRef)); } @@ -749,7 +703,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran * a find many query with read auditing so build the ReadEvent and log it. */ void auditFindMany() { - if (auditIds != null && !auditIds.isEmpty()) { // get the id values of the underlying collection ReadEvent futureReadEvent = query.getFutureFetchAudit(); @@ -787,7 +740,6 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran * Add the id to the audit id buffer and flush if needed in batches of 100. */ private void auditNextBean() { - if (auditIds == null) { auditIds = new ArrayList<>(100); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java index ab008a5cf..1162e914d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java @@ -616,11 +616,19 @@ class CQueryBuilder { if (request.isInlineCountDistinct()) { sb.append(")"); } - if (distinct && dbOrderBy != null && !query.isSingleAttribute()) { + if (distinct && dbOrderBy != null) { // add the orderBy columns to the select clause (due to distinct) final OrderBy orderBy = query.getOrderBy(); if (orderBy != null && orderBy.supportsSelect()) { - sb.append(", ").append(DbOrderByTrim.trim(dbOrderBy)); + String trimmed = DbOrderByTrim.trim(dbOrderBy); + if (query.isSingleAttribute() && trimmed.equals(select.getSelectSql())) { + // NOP, already in SQL + // TODO: what to do if we select("id").orderBy("prop,id")? + // Can we live with a query like "select t0.id, t0.prop, t0.id from" + // or should we elliminate the second "t0.id" from select + } else { + sb.append(", ").append(trimmed); + } } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 6e4804ca6..a1a211c0a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -16,7 +16,7 @@ import io.ebeaninternal.server.core.DiffHelp; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.core.SpiResultSet; import io.ebeaninternal.server.deploy.BeanDescriptor; -import io.ebeaninternal.server.lib.Str; +import io.ebeaninternal.server.util.Str; import io.ebeaninternal.server.persist.Binder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,11 +66,13 @@ public class CQueryEngine { public int delete(OrmQueryRequest request) { CQueryUpdate query = queryBuilder.buildUpdateQuery(true, request); + request.setCancelableQuery(query); return executeUpdate(request, query); } public int update(OrmQueryRequest request) { CQueryUpdate query = queryBuilder.buildUpdateQuery(false, request); + request.setCancelableQuery(query); return executeUpdate(request, query); } @@ -97,6 +99,7 @@ public class CQueryEngine { public List findSingleAttributeList(OrmQueryRequest request) { CQueryFetchSingleAttribute rcQuery = queryBuilder.buildFetchAttributeQuery(request); + request.setCancelableQuery(rcQuery); return findAttributeList(request, rcQuery); } @@ -151,6 +154,7 @@ public class CQueryEngine { public List findIds(OrmQueryRequest request) { CQueryFetchSingleAttribute rcQuery = queryBuilder.buildFetchIdsQuery(request); + request.setCancelableQuery(rcQuery); return findAttributeList(request, rcQuery); } @@ -164,6 +168,7 @@ public class CQueryEngine { public int findCount(OrmQueryRequest request) { CQueryRowCount rcQuery = queryBuilder.buildRowCountQuery(request); + request.setCancelableQuery(rcQuery); try { int count = rcQuery.findCount(); @@ -235,8 +240,10 @@ public class CQueryEngine { } catch (SQLException e) { try { + PersistenceException pex = cquery.createPersistenceException(e); + // create exception before closing connection cquery.close(); - throw cquery.createPersistenceException(e); + throw pex; } finally { request.rollbackTransIfRequired(); } @@ -259,6 +266,7 @@ public class CQueryEngine { // order by lower sys period desc query.order().desc(sysPeriodLower); CQuery cquery = queryBuilder.buildQuery(request); + request.setCancelableQuery(cquery); try { cquery.prepareBindExecuteQuery(); if (request.isLogSql()) { @@ -327,6 +335,7 @@ public class CQueryEngine { */ public SpiResultSet findResultSet(OrmQueryRequest request) { CQuery cquery = queryBuilder.buildQuery(request); + request.setCancelableQuery(cquery); try { boolean fwdOnly; if (request.isFindIterate()) { @@ -411,6 +420,7 @@ public class CQueryEngine { EntityBean bean = null; CQuery cquery = queryBuilder.buildQuery(request); + request.setCancelableQuery(cquery); try { cquery.prepareBindExecuteQuery(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java index 45fabfde7..b68813bd4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.CancelableQuery; import io.ebean.CountedValue; import io.ebean.core.type.ScalarDataReader; import io.ebean.util.JdbcClose; @@ -18,11 +19,12 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; /** * Base compiled query request for single attribute queries. */ -class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent { +class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent, CancelableQuery { private static final Logger logger = LoggerFactory.getLogger(CQueryFetchSingleAttribute.class); @@ -65,6 +67,8 @@ class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent { private final boolean containsCounts; private long profileOffset; + + private final ReentrantLock lock = new ReentrantLock(); /** * Create the Sql select based on the request. @@ -111,7 +115,6 @@ class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent { value = new CountedValue<>(value, dataReader.getLong()); } result.add(value); - dataReader.resetColumnPosition(); rowCount++; } @@ -148,21 +151,27 @@ class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent { } private void prepareExecute() throws SQLException { - - SpiTransaction t = getTransaction(); - profileOffset = t.profileOffset(); - Connection conn = t.getInternalConnection(); - pstmt = conn.prepareStatement(sql); - - if (query.getBufferFetchSizeHint() > 0) { - pstmt.setFetchSize(query.getBufferFetchSizeHint()); + lock.lock(); + try { + query.checkCancelled(); + SpiTransaction t = getTransaction(); + profileOffset = t.profileOffset(); + Connection conn = t.getInternalConnection(); + pstmt = conn.prepareStatement(sql); + + if (query.getBufferFetchSizeHint() > 0) { + pstmt.setFetchSize(query.getBufferFetchSizeHint()); + } + if (query.getTimeout() > 0) { + pstmt.setQueryTimeout(query.getTimeout()); + } + + bindLog = predicates.bind(pstmt, conn); + } finally { + lock.unlock(); } - if (query.getTimeout() > 0) { - pstmt.setQueryTimeout(query.getTimeout()); - } - - bindLog = predicates.bind(pstmt, conn); dataReader = new RsetDataReader(request.getDataTimeZone(), pstmt.executeQuery()); + query.checkCancelled(); } /** @@ -195,4 +204,14 @@ class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent { Set getDependentTables() { return queryPlan.getDependentTables(); } + + @Override + public void cancel() { + lock.lock(); + try { + JdbcClose.cancel(pstmt); + } finally { + lock.unlock(); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java index fb434a6de..17cec9d15 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java @@ -14,7 +14,7 @@ class CQueryIteratorSimple implements QueryIterator { private final CQuery cquery; private final OrmQueryRequest request; - + private boolean closed; CQueryIteratorSimple(CQuery cquery, OrmQueryRequest request) { @@ -54,8 +54,4 @@ class CQueryIteratorSimple implements QueryIterator { } } - @Override - public void remove() { - throw new PersistenceException("Remove not allowed"); - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java index 2ba431da5..e2a9bdad2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java @@ -76,8 +76,4 @@ class CQueryIteratorWithBuffer implements QueryIterator { } } - @Override - public void remove() { - throw new PersistenceException("Remove not allowed"); - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java index 86e8d209d..295cbe7c2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java @@ -13,7 +13,7 @@ import io.ebeaninternal.api.SpiQueryBindCapture; import io.ebeaninternal.api.SpiQueryPlan; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.core.timezone.DataTimeZone; -import io.ebeaninternal.server.lib.Str; +import io.ebeaninternal.server.util.Str; import io.ebeaninternal.server.query.CQueryPlanStats.Snapshot; import io.ebeaninternal.server.type.DataBind; import io.ebeaninternal.server.type.DataBindCapture; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java index 6a3d40a59..248fa9058 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.CancelableQuery; import io.ebean.util.JdbcClose; import io.ebeaninternal.api.SpiProfileTransactionEvent; import io.ebeaninternal.api.SpiQuery; @@ -13,11 +14,12 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; /** * Executes the select row count query. */ -class CQueryRowCount implements SpiProfileTransactionEvent { +class CQueryRowCount implements SpiProfileTransactionEvent, CancelableQuery { private final CQueryPlan queryPlan; @@ -57,6 +59,8 @@ class CQueryRowCount implements SpiProfileTransactionEvent { private int rowCount; private long profileOffset; + + private final ReentrantLock lock = new ReentrantLock(); /** * Create the Sql select based on the request. @@ -110,14 +114,22 @@ class CQueryRowCount implements SpiProfileTransactionEvent { SpiTransaction t = getTransaction(); profileOffset = t.profileOffset(); Connection conn = t.getInternalConnection(); - pstmt = conn.prepareStatement(sql); + lock.lock(); + try { + query.checkCancelled(); + pstmt = conn.prepareStatement(sql); - if (query.getTimeout() > 0) { - pstmt.setQueryTimeout(query.getTimeout()); + if (query.getTimeout() > 0) { + pstmt.setQueryTimeout(query.getTimeout()); + } + + bindLog = predicates.bind(pstmt, conn); + } finally { + lock.unlock(); } - - bindLog = predicates.bind(pstmt, conn); rset = pstmt.executeQuery(); + query.checkCancelled(); + if (!rset.next()) { throw new PersistenceException("Expecting 1 row but got none?"); } @@ -161,4 +173,14 @@ class CQueryRowCount implements SpiProfileTransactionEvent { Set getDependentTables() { return queryPlan.getDependentTables(); } + + @Override + public void cancel() { + lock.lock(); + try { + JdbcClose.cancel(pstmt); + } finally { + lock.unlock(); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java index 76858c271..eec5d2e6f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryUpdate.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.CancelableQuery; import io.ebean.util.JdbcClose; import io.ebeaninternal.api.SpiProfileTransactionEvent; import io.ebeaninternal.api.SpiQuery; @@ -10,11 +11,12 @@ import io.ebeaninternal.server.deploy.BeanDescriptor; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantLock; /** - * Executes the delete query. + * Executes the update query. */ -class CQueryUpdate implements SpiProfileTransactionEvent { +class CQueryUpdate implements SpiProfileTransactionEvent, CancelableQuery { private final CQueryPlan queryPlan; @@ -45,6 +47,8 @@ class CQueryUpdate implements SpiProfileTransactionEvent { private long profileOffset; + private final ReentrantLock lock = new ReentrantLock(); + /** * Create the Sql select based on the request. */ @@ -82,15 +86,22 @@ class CQueryUpdate implements SpiProfileTransactionEvent { SpiTransaction t = getTransaction(); profileOffset = t.profileOffset(); Connection conn = t.getInternalConnection(); - pstmt = conn.prepareStatement(sql); + lock.lock(); + try { + query.checkCancelled(); + pstmt = conn.prepareStatement(sql); - if (query.getTimeout() > 0) { - pstmt.setQueryTimeout(query.getTimeout()); + if (query.getTimeout() > 0) { + pstmt.setQueryTimeout(query.getTimeout()); + } + + bindLog = predicates.bind(pstmt, conn); + } finally { + lock.unlock(); } - - bindLog = predicates.bind(pstmt, conn); rowCount = pstmt.executeUpdate(); - + query.checkCancelled(); + long executionTimeMicros = (System.nanoTime() - startNano) / 1000L; request.slowQueryCheck(executionTimeMicros, rowCount); if (queryPlan.executionTime(executionTimeMicros)) { @@ -122,4 +133,14 @@ class CQueryUpdate implements SpiProfileTransactionEvent { .profileStream() .addQueryEvent(query.profileEventId(), profileOffset, desc.getName(), rowCount, query.getProfileId()); } + + @Override + public void cancel() { + lock.lock(); + try { + JdbcClose.cancel(pstmt); + } finally { + lock.unlock(); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java index 981abae74..378647818 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -11,11 +11,10 @@ import java.util.HashSet; class DefaultDbSqlContext implements DbSqlContext { private static final String COMMA = ", "; - private static final String PERIOD = "."; private static final int STRING_BUILDER_INITIAL_CAPACITY = 140; - private static final String tableAliasPlaceHolder = "${ta}"; + private static final String tableAliasManyPlaceHolder = "${mta}"; private final String columnAliasPrefix; @@ -111,27 +110,22 @@ class DefaultDbSqlContext implements DbSqlContext { } @Override - public void addJoin(String type, String table, TableJoinColumn[] cols, String a1, String a2) { - + public void addJoin(String type, String table, TableJoinColumn[] cols, String a1, String a2, String extraWhere) { if (tableJoins == null) { tableJoins = new HashSet<>(); } - String joinKey = table + "-" + a1 + "-" + a2; if (tableJoins.contains(joinKey)) { return; } tableJoins.add(joinKey); - sb.append(" ").append(type); boolean addAsOfOnClause = false; if (draftSupport != null) { appendTable(table, draftSupport.getDraftTable(table)); - } else if (!historyQuery) { sb.append(" ").append(table).append(" "); - } else { // check if there is an associated history table and if so // use the unionAll view - we expect an additional predicate to match @@ -164,13 +158,17 @@ class DefaultDbSqlContext implements DbSqlContext { if (addAsOfOnClause) { sb.append(" and ").append(historySupport.getAsOfPredicate(a2)); } + if (extraWhere != null && !extraWhere.isEmpty()) { + sb.append(" and "); + // we will also need a many-table alias here + sb.append(extraWhere.replace(tableAliasPlaceHolder, a2).replace(tableAliasManyPlaceHolder, a1)); + } } private void appendTable(String table, String draftTable) { if (draftTable != null) { // there is an associated history table and view so use that sb.append(" ").append(draftTable).append(" "); - } else { sb.append(" ").append(table).append(" "); } @@ -193,7 +191,6 @@ class DefaultDbSqlContext implements DbSqlContext { @Override public String getRelativePrefix(String propName) { - return currentPrefix == null ? propName : currentPrefix + "." + propName; } @@ -228,7 +225,6 @@ class DefaultDbSqlContext implements DbSqlContext { // the same join has already been added. return; } - // we only want to add this join once formulaJoins.add(converted); sb.append(" "); @@ -263,13 +259,10 @@ class DefaultDbSqlContext implements DbSqlContext { @Override public void appendHistorySysPeriod() { - String tableAlias = tableAliasStack.peek(); - sb.append(COMMA); sb.append(historySupport.getSysPeriodLower(tableAlias)); appendColumnAlias(); - sb.append(COMMA); sb.append(historySupport.getSysPeriodUpper(tableAlias)); appendColumnAlias(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java index f49df1d20..78b9717d9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java @@ -3,6 +3,7 @@ package io.ebeaninternal.server.query; import io.ebean.RowConsumer; import io.ebean.RowMapper; import io.ebean.SqlRow; +import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; import io.ebean.meta.MetricVisitor; import io.ebean.metric.MetricFactory; @@ -10,10 +11,10 @@ import io.ebean.metric.TimedMetricMap; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.core.RelationalQueryEngine; import io.ebeaninternal.server.core.RelationalQueryRequest; +import io.ebeaninternal.server.core.RowReader; import io.ebeaninternal.server.persist.Binder; import javax.persistence.PersistenceException; -import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -59,12 +60,26 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { } @Override - public void findEach(RelationalQueryRequest request, Predicate consumer) { + public void findEach(RelationalQueryRequest request, RowConsumer consumer) { + try { + request.executeSql(binder, SpiQuery.Type.ITERATE); + request.mapEach(consumer); + request.logSummary(); + } catch (Exception e) { + throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); + + } finally { + request.close(); + } + } + + @Override + public void findEach(RelationalQueryRequest request, RowReader reader, Predicate consumer) { try { request.executeSql(binder, SpiQuery.Type.ITERATE); while (request.next()) { - if (!consumer.test(readRow(request))) { + if (!consumer.test(reader.read())) { break; } } @@ -79,25 +94,7 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { } @Override - public void findEach(RelationalQueryRequest request, Consumer consumer) { - - try { - request.executeSql(binder, SpiQuery.Type.ITERATE); - while (request.next()) { - consumer.accept(readRow(request)); - } - request.logSummary(); - - } catch (Exception e) { - throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); - - } finally { - request.close(); - } - } - - @Override - public T findOneMapper(RelationalQueryRequest request, RowMapper mapper) { + public T findOne(RelationalQueryRequest request, RowMapper mapper) { try { request.executeSql(binder, SpiQuery.Type.BEAN); T value = request.mapOne(mapper); @@ -113,13 +110,15 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { } @Override - public List findListMapper(RelationalQueryRequest request, RowMapper mapper) { + public List findList(RelationalQueryRequest request, RowReader reader) { try { request.executeSql(binder, SpiQuery.Type.LIST); - List list = request.mapList(mapper); + List rows = new ArrayList<>(); + while (request.next()) { + rows.add(reader.read()); + } request.logSummary(); - return list; - + return rows; } catch (Exception e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); @@ -128,12 +127,19 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { } } + @SuppressWarnings("unchecked") @Override - public void findEachRow(RelationalQueryRequest request, RowConsumer consumer) { + public T findSingleAttribute(RelationalQueryRequest request, Class cls) { + ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.LIST); - request.mapEach(consumer); + request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + final DataReader dataReader = binder.createDataReader(request.getResultSet()); + T value = null; + if (dataReader.next()) { + value = scalarType.read(dataReader); + } request.logSummary(); + return value; } catch (Exception e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); @@ -147,68 +153,13 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { @Override public List findSingleAttributeList(RelationalQueryRequest request, Class cls) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); - return findScalarList(request, scalarType); - } - - private List findScalarList(RelationalQueryRequest request, ScalarType scalarType) { try { request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); - List list = new ArrayList<>(); - while (request.next()) { - request.incrementRows(); - list.add(scalarType.read(binder.createDataReader(request.getResultSet()))); + final DataReader dataReader = binder.createDataReader(request.getResultSet()); + List rows = new ArrayList<>(); + while (dataReader.next()) { + rows.add(scalarType.read(dataReader)); } - - request.logSummary(); - return list; - - } catch (Exception e) { - throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); - - } finally { - request.close(); - } - } - - @SuppressWarnings("unchecked") - @Override - public T findSingleAttribute(RelationalQueryRequest request, Class cls) { - - ScalarType scalarType = (ScalarType) binder.getScalarType(cls); - return findScalar(request, scalarType); - } - - private T findScalar(RelationalQueryRequest request, ScalarType scalarType) { - try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); - - T value = null; - if (request.next()) { - request.incrementRows(); - value = scalarType.read(binder.createDataReader(request.getResultSet())); - } - - request.logSummary(); - return value; - - } catch (Exception e) { - throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); - - } finally { - request.close(); - } - } - - @Override - public List findList(RelationalQueryRequest request) { - - try { - request.executeSql(binder, SpiQuery.Type.LIST); - List rows = new ArrayList<>(); - while (request.next()) { - rows.add(readRow(request)); - } - request.logSummary(); return rows; @@ -220,11 +171,23 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { } } - /** - * Read the row from the ResultSet and return as a MapBean. - */ - private SqlRow readRow(RelationalQueryRequest request) throws SQLException { - return request.createNewRow(); - } + @SuppressWarnings("unchecked") + @Override + public void findSingleAttributeEach(RelationalQueryRequest request, Class cls, Consumer consumer) { + ScalarType scalarType = (ScalarType) binder.getScalarType(cls); + try { + request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + final DataReader dataReader = binder.createDataReader(request.getResultSet()); + while (dataReader.next()) { + consumer.accept(scalarType.read(dataReader)); + } + request.logSummary(); + } catch (Exception e) { + throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); + + } finally { + request.close(); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java index bbd111f1c..526d36dee 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java @@ -1,10 +1,12 @@ package io.ebeaninternal.server.query; +import io.ebean.QueryIterator; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.core.DtoQueryRequest; import io.ebeaninternal.server.persist.Binder; import javax.persistence.PersistenceException; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -27,20 +29,29 @@ public class DtoQueryEngine { } return rows; - } catch (Throwable e) { + } catch (SQLException e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); } finally { request.close(); } } + public QueryIterator findIterate(DtoQueryRequest request) { + try { + request.executeSql(binder, SpiQuery.Type.ITERATE); + return new DtoQueryIterator<>(request); + } catch (SQLException e) { + throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); + } + } + public void findEach(DtoQueryRequest request, Consumer consumer) { try { request.executeSql(binder, SpiQuery.Type.ITERATE); while (request.next()) { consumer.accept(request.readNextBean()); } - } catch (Exception e) { + } catch (SQLException e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); } finally { request.close(); @@ -77,9 +88,8 @@ public class DtoQueryEngine { break; } } - } catch (Exception e) { + } catch (SQLException e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); - } finally { request.close(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryIterator.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryIterator.java new file mode 100644 index 000000000..bdf4e2df6 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryIterator.java @@ -0,0 +1,51 @@ +package io.ebeaninternal.server.query; + +import io.ebean.QueryIterator; +import io.ebeaninternal.server.core.DtoQueryRequest; + +import javax.persistence.PersistenceException; +import java.sql.SQLException; + +class DtoQueryIterator implements QueryIterator { + + private final DtoQueryRequest request; + private boolean closed; + + DtoQueryIterator(DtoQueryRequest request) { + this.request = request; + } + + @Override + public boolean hasNext() { + boolean result = false; + try { + result = request.next(); + return result; + } catch (SQLException e) { + throw new PersistenceException(e); + } finally { + if (!result) { + close(); + } + } + } + + @Override + public T next() { + try { + return request.readNextBean(); + } catch (SQLException e) { + throw new PersistenceException(e); + } + } + + @Override + public void close() { + if (!closed) { + closed = true; + request.close(); + request.endTransIfRequired(); + } + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java index 074457a30..1b31ee08c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java @@ -5,6 +5,7 @@ import io.ebean.bean.EntityBean; import io.ebean.core.type.ScalarType; import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.deploy.DbReadContext; import io.ebeaninternal.server.deploy.DbSqlContext; import io.ebeaninternal.server.deploy.TableJoin; @@ -130,6 +131,14 @@ class SqlTreeNodeExtraJoin implements SqlTreeNode { } } + boolean oneToOneExported = false; + if (assocBeanProperty instanceof BeanPropertyAssocOne) { + BeanPropertyAssocOne oneToOneProp = (BeanPropertyAssocOne) assocBeanProperty; + if (oneToOneProp.isOneToOneExported()) { + oneToOneExported = true; + } + } + if (pathContainsMany) { // "promote" to left join as the path contains a many joinType = SqlJoinType.OUTER; @@ -140,7 +149,7 @@ class SqlTreeNodeExtraJoin implements SqlTreeNode { assocBeanProperty.appendFrom(ctx, joinType); } joinType = assocBeanProperty.addJoin(joinType, prefix, ctx); - if (assocBeanProperty.isTargetSoftDelete() && temporalMode != SpiQuery.TemporalMode.SOFT_DELETED) { + if (!oneToOneExported && assocBeanProperty.isTargetSoftDelete() && temporalMode != SpiQuery.TemporalMode.SOFT_DELETED) { ctx.append(" and ").append(assocBeanProperty.getSoftDeletePredicate(ctx.getTableAlias(prefix))); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java index 8c5517a76..d920b57ef 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java @@ -47,16 +47,6 @@ final class SqlTreeNodeManyRoot extends SqlTreeNodeBean { ctx.popTableAlias(); } - /** - * append extraWhere to the join. - */ - @Override - protected SqlJoinType appendFromAsJoin(DbSqlContext ctx, SqlJoinType joinType) { - SqlJoinType join = super.appendFromAsJoin(ctx, joinType); - super.appendExtraWhere(ctx); - return join; - } - @Override protected void appendExtraWhere(DbSqlContext ctx) { // extraWhere is already appended to the tableJoin diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/AbstractQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/AbstractQuery.java new file mode 100644 index 000000000..62fd3cdf0 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/AbstractQuery.java @@ -0,0 +1,56 @@ +package io.ebeaninternal.server.querydefn; + +import java.util.concurrent.locks.ReentrantLock; + +import javax.persistence.PersistenceException; + +import io.ebean.CancelableQuery; +import io.ebeaninternal.api.SpiCancelableQuery; + +/** + * Common code for Dto/Orm/RelationalQuery + * + * @author Roland Praml, FOCONIS AG + * + */ +public class AbstractQuery implements SpiCancelableQuery { + + private boolean cancelled; + + private CancelableQuery cancelableQuery; + + private final ReentrantLock lock = new ReentrantLock(); + + @Override + public void cancel() { + lock.lock(); + try { + if (!cancelled) { + cancelled = true; + if (cancelableQuery != null) { + cancelableQuery.cancel(); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void checkCancelled() { + if (cancelled) { + throw new PersistenceException("Query was cancelled"); + } + } + + @Override + public void setCancelableQuery(CancelableQuery cancelableQuery) { + lock.lock(); + try { + checkCancelled(); + this.cancelableQuery = cancelableQuery; + } finally { + lock.unlock(); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java index d5877572a..c2cb02198 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.querydefn; import io.ebean.DtoQuery; import io.ebean.ProfileLocation; +import io.ebean.QueryIterator; import io.ebean.Transaction; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.SpiDtoQuery; @@ -15,11 +16,12 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; /** * Default implementation of DtoQuery. */ -public class DefaultDtoQuery implements SpiDtoQuery { +public class DefaultDtoQuery extends AbstractQuery implements SpiDtoQuery { private final SpiEbeanServer server; @@ -112,6 +114,16 @@ public class DefaultDtoQuery implements SpiDtoQuery { server.findDtoEachWhile(this, consumer); } + @Override + public QueryIterator findIterate() { + return server.findDtoIterate(this); + } + + @Override + public Stream findStream() { + return server.findDtoStream(this); + } + @Override public List findList() { return server.findDtoList(this); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 867e16dfa..96a923637 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -55,10 +55,10 @@ import io.ebeaninternal.server.deploy.BeanNaturalKey; import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.InheritInfo; import io.ebeaninternal.server.deploy.TableJoin; +import io.ebeaninternal.server.el.ElPropertyDeploy; import io.ebeaninternal.server.expression.DefaultExpressionList; import io.ebeaninternal.server.expression.IdInExpression; import io.ebeaninternal.server.expression.SimpleExpression; -import io.ebeaninternal.server.query.CancelableQuery; import io.ebeaninternal.server.query.NativeSqlQueryPlanKey; import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.server.transaction.ExternalJdbcTransaction; @@ -81,7 +81,7 @@ import java.util.stream.Stream; /** * Default implementation of an Object Relational query. */ -public class DefaultOrmQuery implements SpiQuery { +public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { private static final String DEFAULT_QUERY_NAME = "default"; @@ -113,10 +113,6 @@ public class DefaultOrmQuery implements SpiQuery { private ProfilingListener profilingListener; - private boolean cancelled; - - private CancelableQuery cancelableQuery; - private Type type; private String label; @@ -539,6 +535,14 @@ public class DefaultOrmQuery implements SpiQuery { if (havingExpressions != null) { havingExpressions.containsMany(beanDescriptor, manyWhereJoins); } + if (orderBy != null) { + for (Property orderProperty : orderBy.getProperties()) { + ElPropertyDeploy elProp = beanDescriptor.getElPropertyDeploy(orderProperty.getProperty()); + if (elProp != null && elProp.containsFormulaWithJoin()) { + manyWhereJoins.addFormulaWithJoin(orderProperty.getProperty()); + } + } + } } /** @@ -856,6 +860,7 @@ public class DefaultOrmQuery implements SpiQuery { copy.parentNode = parentNode; copy.forUpdate = forUpdate; copy.rawSql = rawSql; + setCancelableQuery(copy); // required to cancel findId query return copy; } @@ -1238,6 +1243,7 @@ public class DefaultOrmQuery implements SpiQuery { * Prepare the expressions (compile sub-queries etc). */ private void prepareExpressions(BeanQueryRequest request) { + detail.prepareExpressions(request); if (whereExpressions != null) { whereExpressions.prepareExpression(request); } @@ -2045,16 +2051,6 @@ public class DefaultOrmQuery implements SpiQuery { return futureFetchAudit; } - @Override - public void setCancelableQuery(CancelableQuery cancelableQuery) { - lock.lock(); - try { - this.cancelableQuery = cancelableQuery; - } finally { - lock.unlock(); - } - } - @Override public Query setBaseTable(String baseTable) { this.baseTable = baseTable; @@ -2082,28 +2078,6 @@ public class DefaultOrmQuery implements SpiQuery { return rootTableAlias != null ? rootTableAlias : defaultAlias; } - @Override - public void cancel() { - lock.lock(); - try { - if (!cancelled && cancelableQuery != null) { - cancelled = true; - cancelableQuery.cancel(); - } - } finally { - lock.unlock(); - } - } - - @Override - public boolean isCancelled() { - lock.lock(); - try { - return cancelled; - } finally { - lock.unlock(); - } - } @Override public Set validate() { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultRelationalQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultRelationalQuery.java index ec5b3c01a..6afe54a1f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultRelationalQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultRelationalQuery.java @@ -17,15 +17,15 @@ import java.util.function.Predicate; /** * Default implementation of SQuery - SQL Query. */ -public class DefaultRelationalQuery implements SpiSqlQuery { +public class DefaultRelationalQuery extends AbstractQuery implements SpiSqlQuery { private static final long serialVersionUID = -1098305779779591068L; private final transient SpiEbeanServer server; - private String label; + private final String query; - private String query; + private String label; private int firstRow; @@ -208,19 +208,25 @@ public class DefaultRelationalQuery implements SpiSqlQuery { return query; } - T mapperFindOne(RowMapper mapper) { + private T mapperFindOne(RowMapper mapper) { return server.findOneMapper(this, mapper); } - List mapperFindList(RowMapper mapper) { + private List mapperFindList(RowMapper mapper) { return server.findListMapper(this, mapper); } + private void mapperFindEach(RowMapper mapper, Consumer consumer) { + server.findEachRow(this, (resultSet, rowNum) -> consumer.accept(mapper.map(resultSet, rowNum))); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public TypeQuery mapToScalar(Class attributeType) { return new Scalar(attributeType); } + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public TypeQuery mapTo(RowMapper mapper) { return new Mapper(mapper); @@ -248,6 +254,15 @@ public class DefaultRelationalQuery implements SpiSqlQuery { public List findList() { return findSingleAttributeList(type); } + + @Override + public void findEach(Consumer consumer) { + scalarFindEach(type, consumer); + } + } + + private void scalarFindEach(Class type, Consumer consumer) { + server.findSingleAttributeEach(this, type, consumer); } private class Mapper implements SqlQuery.TypeQuery { @@ -272,5 +287,10 @@ public class DefaultRelationalQuery implements SpiSqlQuery { public List findList() { return mapperFindList(mapper); } + + @Override + public void findEach(Consumer consumer) { + mapperFindEach(mapper, consumer); + } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java index 6f96fbbea..2fabfdd79 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.querydefn; import io.ebean.FetchConfig; +import io.ebean.event.BeanQueryRequest; import io.ebean.util.SplitName; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanPropertyAssoc; @@ -9,25 +10,16 @@ import io.ebeaninternal.server.el.ElPropertyValue; import javax.persistence.PersistenceException; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Represents the internal structure of an Object Relational query. *

* Holds the select() and join() details of a ORM query. - *

*

* It is worth noting that for AutoTune a "tuned fetch info" builds an instance of OrmQueryDetail. * Tuning a query is a matter of replacing an instance of this class with one that has been tuned * with select() and join() set. - *

*/ public class OrmQueryDetail implements Serializable { @@ -81,7 +73,6 @@ public class OrmQueryDetail implements Serializable { * Return true if equal in terms of autoTune (select and fetch without property ordering). */ public boolean isAutoTuneEqual(OrmQueryDetail otherDetail) { - if (!isSameByAutoTune(baseProps, otherDetail.baseProps)) { return false; } @@ -109,7 +100,7 @@ public class OrmQueryDetail implements Serializable { /** * Return the detail in string form. */ - public String asStringDebug() { + public String asString() { StringBuilder sb = new StringBuilder(); if (baseProps.hasProperties()) { baseProps.asStringDebug("select ", sb); @@ -175,9 +166,7 @@ public class OrmQueryDetail implements Serializable { } private List removeSecondaryQueries(boolean lazyQuery) { - ArrayList matchingPaths = new ArrayList<>(2); - for (OrmQueryProperties chunk : fetchPaths.values()) { boolean match = lazyQuery ? chunk.isLazyFetch() : chunk.isQueryFetch(); if (match) { @@ -191,7 +180,6 @@ public class OrmQueryDetail implements Serializable { // sort into depth order to remove Collections.sort(matchingPaths); - // the list of secondary queries ArrayList props = new ArrayList<>(); @@ -225,14 +213,11 @@ public class OrmQueryDetail implements Serializable { OrmQueryProperties chunk = getChunk(split[0], true); chunk.addSecondaryQueryJoin(split[1]); } - return props; } boolean tuneFetchProperties(OrmQueryDetail tunedDetail) { - boolean tuned = false; - OrmQueryProperties tunedRoot = tunedDetail.getChunk(null, false); if (tunedRoot != null) { tuned = true; @@ -257,10 +242,7 @@ public class OrmQueryDetail implements Serializable { } /** - * Remove all joins and properties. - *

- * Typically for the row count query. - *

+ * Remove all joins and properties. Typically for the row count query. */ public void clear() { fetchPaths.clear(); @@ -306,7 +288,6 @@ public class OrmQueryDetail implements Serializable { } private void sortFetchPaths(BeanDescriptor d, boolean addIds) { - if (!fetchPaths.isEmpty()) { LinkedHashMap sorted = new LinkedHashMap<>(); for (OrmQueryProperties p : fetchPaths.values()) { @@ -317,7 +298,6 @@ public class OrmQueryDetail implements Serializable { } private void sortFetchPaths(BeanDescriptor d, OrmQueryProperties p, LinkedHashMap sorted, boolean addId) { - String path = p.getPath(); if (!sorted.containsKey(path)) { String parentPath = p.getParentPath(); @@ -339,7 +319,6 @@ public class OrmQueryDetail implements Serializable { parentProp = new OrmQueryProperties(parentPath, Collections.emptySet()); } } - sortFetchPaths(d, parentProp, sorted, addId); sorted.put(path, p); } @@ -350,14 +329,12 @@ public class OrmQueryDetail implements Serializable { * Mark 'fetch joins' to 'many' properties over to 'query joins' where needed. */ void markQueryJoins(BeanDescriptor beanDescriptor, String lazyLoadManyPath, boolean allowOne, boolean addIds) { - if (fetchPaths.isEmpty()) { return; } // the name of the many fetch property if there is one String manyFetchProperty = null; - // flag that is set once the many fetch property is chosen boolean fetchJoinFirstMany = allowOne; @@ -375,6 +352,7 @@ public class OrmQueryDetail implements Serializable { // letting the first one remain a 'fetch join' fetchJoinFirstMany = false; manyFetchProperty = pair.getPath(); + chunk.filterManyInline(); } else { // convert this one over to a 'query join' chunk.markForQueryJoin(); @@ -388,7 +366,6 @@ public class OrmQueryDetail implements Serializable { * Sort the fetch entries taking into account fetchPreference on the path. */ private List sortByFetchPreference(BeanDescriptor desc) { - List entries = new ArrayList<>(fetchPaths.size()); int idx = 0; for (Map.Entry entry : fetchPaths.entrySet()) { @@ -452,11 +429,9 @@ public class OrmQueryDetail implements Serializable { *

*/ public void setDefaultSelectClause(BeanDescriptor desc) { - if (desc.hasDefaultSelectClause() && !hasSelectClause()) { baseProps = new OrmQueryProperties(null, desc.getDefaultSelectClause()); } - for (OrmQueryProperties joinProps : fetchPaths.values()) { if (!joinProps.hasSelectClause()) { BeanDescriptor assocDesc = desc.getBeanDescriptor(joinProps.getPath()); @@ -506,7 +481,6 @@ public class OrmQueryDetail implements Serializable { props = new OrmQueryProperties(path); fetch(props); return props; - } else { return props; } @@ -516,9 +490,7 @@ public class OrmQueryDetail implements Serializable { * Return true if the fetch path is included. */ public boolean includesPath(String path) { - OrmQueryProperties chunk = fetchPaths.get(path); - // may not have fetch properties if just +cache etc return chunk != null && !chunk.isCache(); } @@ -537,6 +509,15 @@ public class OrmQueryDetail implements Serializable { return fetchPaths.entrySet(); } + /** + * Prepare filterMany expressions that are being included into the main query. + */ + public void prepareExpressions(BeanQueryRequest request) { + for (OrmQueryProperties value : fetchPaths.values()) { + value.prepareExpressions(request); + } + } + private static class FetchEntry implements Comparable { private final int index; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java index 466bb3eb9..0a8b51465 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java @@ -4,6 +4,7 @@ import io.ebean.ExpressionFactory; import io.ebean.FetchConfig; import io.ebean.OrderBy; import io.ebean.Query; +import io.ebean.event.BeanQueryRequest; import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionFactory; @@ -178,8 +179,6 @@ public class OrmQueryProperties implements Serializable { SpiExpressionFactory queryEf = (SpiExpressionFactory) rootQuery.getExpressionFactory(); ExpressionFactory filterEf = queryEf.createExpressionFactory();// exprPath); filterMany = new FilterExpressionList(exprPath, filterEf, rootQuery); - // by default we need to make this a 'query join' now - markForQueryJoin = true; } return filterMany; } @@ -194,6 +193,24 @@ public class OrmQueryProperties implements Serializable { return filterMany.trimPath(trimPath); } + /** + * Adjust filterMany expressions for inclusion in main query. + */ + public void filterManyInline() { + if (filterMany != null){ + filterMany.prefixProperty(path); + } + } + + /** + * Prepare filterMany expressions for query plan key. + */ + public void prepareExpressions(BeanQueryRequest request) { + if (filterMany != null) { + filterMany.prepareExpression(request); + } + } + /** * Return the filterMany expression list (can be null). */ @@ -206,7 +223,6 @@ public class OrmQueryProperties implements Serializable { */ public void setFilterMany(SpiExpressionList filterMany) { this.filterMany = filterMany; - this.markForQueryJoin = true; } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java index 27eea2e4c..1064bf8bc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java @@ -2,6 +2,9 @@ package io.ebeaninternal.server.transaction; import io.ebean.bean.EntityBean; import io.ebean.bean.PersistenceContext; +import io.ebeaninternal.api.SpiBeanType; +import io.ebeaninternal.api.SpiBeanTypeManager; +import io.ebeaninternal.api.SpiPersistenceContext; import java.util.*; import java.util.concurrent.locks.ReentrantLock; @@ -11,19 +14,16 @@ import java.util.concurrent.locks.ReentrantLock; *

* Ensures only one instance of a bean is used according to its type and unique * id. - *

*

* PersistenceContext lives on a Transaction and as such is expected to only * have a single thread accessing it at a time. This is not expected to be used * concurrently. - *

*

* Duplicate beans are ones having the same type and unique id value. These are * considered duplicates and replaced by the bean instance that was already * loaded into the PersistenceContext. - *

*/ -public final class DefaultPersistenceContext implements PersistenceContext { +public final class DefaultPersistenceContext implements SpiPersistenceContext { /** * Map used hold caches. One cache per bean type. @@ -199,12 +199,12 @@ public final class DefaultPersistenceContext implements PersistenceContext { } @Override - public List dirtyBeans() { + public List dirtyBeans(SpiBeanTypeManager manager) { lock.lock(); try { List list = new ArrayList<>(); for (ClassContext classContext : typeCache.values()) { - classContext.dirtyBeans(list); + classContext.dirtyBeans(manager, list); } return list; } finally { @@ -223,26 +223,26 @@ public final class DefaultPersistenceContext implements PersistenceContext { } private ClassContext getClassContext(Class rootType) { - return typeCache.computeIfAbsent(rootType, k -> new ClassContext()); + return typeCache.computeIfAbsent(rootType, k -> new ClassContext(rootType)); } private static class ClassContext { private final Map map = new HashMap<>(); - + private final Class rootType; private Set deleteSet; - private int initialSize; - private ClassContext parent; - private ClassContext() { + private ClassContext(Class rootType) { + this.rootType = rootType; } /** * Create as a shallow copy. */ private ClassContext(ClassContext source, boolean initial) { + this.rootType = source.rootType; if (initial || source.isTransfer()) { parent = source.transferParent(); initialSize = parent.size(); @@ -334,9 +334,11 @@ public final class DefaultPersistenceContext implements PersistenceContext { /** * Add the dirty beans to the list. */ - void dirtyBeans(List list) { + void dirtyBeans(SpiBeanTypeManager manager, List list) { + final SpiBeanType beanType = manager.getBeanType(rootType); for (Object value : map.values()) { - if (((EntityBean) value)._ebean_getIntercept().isDirty()) { + EntityBean bean = (EntityBean) value; + if (bean._ebean_getIntercept().isDirty() || beanType.isToManyDirty(bean)) { list.add(value); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java index 5f2b5d195..e4e36e337 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java @@ -3,13 +3,9 @@ package io.ebeaninternal.server.transaction; import io.ebean.ProfileLocation; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; -import io.ebean.bean.PersistenceContext; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; -import io.ebeaninternal.api.SpiProfileTransactionEvent; -import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.api.TransactionEvent; -import io.ebeaninternal.api.TxnProfileEventCodes; +import io.ebeaninternal.api.*; import io.ebeaninternal.server.core.PersistDeferredRelationship; import io.ebeaninternal.server.core.PersistRequestBean; import io.ebeaninternal.server.persist.BatchControl; @@ -58,7 +54,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode /** * Holder of the objects fetched to ensure unique objects are used. */ - private PersistenceContext persistenceContext; + private SpiPersistenceContext persistenceContext; private Object tenantId; @@ -421,7 +417,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode * Return the persistence context associated with this transaction. */ @Override - public PersistenceContext getPersistenceContext() { + public SpiPersistenceContext getPersistenceContext() { return persistenceContext; } @@ -434,7 +430,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode *

*/ @Override - public void setPersistenceContext(PersistenceContext context) { + public void setPersistenceContext(SpiPersistenceContext context) { if (!isActive()) { throw new IllegalStateException(illegalStateMessage); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java index 7d3b3ddf1..f61f03699 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java @@ -3,20 +3,16 @@ package io.ebeaninternal.server.transaction; import io.ebean.ProfileLocation; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; -import io.ebean.bean.PersistenceContext; import io.ebean.config.DatabaseConfig; import io.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; -import io.ebeaninternal.api.SpiProfileTransactionEvent; -import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.api.TransactionEvent; -import io.ebeaninternal.api.TxnProfileEventCodes; +import io.ebeaninternal.api.*; import io.ebeaninternal.server.core.PersistDeferredRelationship; import io.ebeaninternal.server.core.PersistRequestBean; -import io.ebeaninternal.server.lib.Str; import io.ebeaninternal.server.persist.BatchControl; import io.ebeaninternal.server.persist.BatchedSqlException; +import io.ebeaninternal.server.util.Str; import io.ebeanservice.docstore.api.DocStoreTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,12 +21,7 @@ import javax.persistence.PersistenceException; import javax.persistence.RollbackException; import java.sql.Connection; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Consumer; /** @@ -99,7 +90,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { /** * Holder of the objects fetched to ensure unique objects are used. */ - private PersistenceContext persistenceContext; + private SpiPersistenceContext persistenceContext; /** * Used to give developers more control over the insert update and delete @@ -806,7 +797,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { * Return the persistence context associated with this transaction. */ @Override - public PersistenceContext getPersistenceContext() { + public SpiPersistenceContext getPersistenceContext() { return persistenceContext; } @@ -816,10 +807,9 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { * This could be considered similar to EJB3 Extended PersistanceContext. In * that you get the PersistanceContext from a transaction, hold onto it, and * then set it back later to a second transaction. - *

*/ @Override - public void setPersistenceContext(PersistenceContext context) { + public void setPersistenceContext(SpiPersistenceContext context) { if (!isActive()) { throw new IllegalStateException(illegalStateMessage); } @@ -951,11 +941,13 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { */ private void connectionEndForQueryOnly() { try { + withEachCallback(TransactionCallback::preCommit); if (onQueryOnly == OnQueryOnly.COMMIT) { performCommit(); } else { performRollback(); } + withEachCallback(TransactionCallback::postCommit); } catch (SQLException e) { logger.error("Error when ending a query only transaction via " + onQueryOnly, e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java index 0d3c82cd6..d435e93fd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java @@ -3,9 +3,9 @@ package io.ebeaninternal.server.transaction; import io.ebean.ProfileLocation; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; -import io.ebean.bean.PersistenceContext; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; +import io.ebeaninternal.api.SpiPersistenceContext; import io.ebeaninternal.api.SpiProfileTransactionEvent; import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.api.TransactionEvent; @@ -349,12 +349,12 @@ class NoTransaction implements SpiTransaction { } @Override - public PersistenceContext getPersistenceContext() { + public SpiPersistenceContext getPersistenceContext() { return null; } @Override - public void setPersistenceContext(PersistenceContext context) { + public void setPersistenceContext(SpiPersistenceContext context) { } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java index b50122f5c..dafaa05b5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java @@ -2,7 +2,7 @@ package io.ebeaninternal.server.transaction; import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.api.SpiTransactionProxy; -import io.ebeaninternal.server.lib.Str; +import io.ebeaninternal.server.util.Str; import javax.persistence.PersistenceException; import java.sql.Connection; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index b71868e5d..e3fbdd425 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -5,7 +5,6 @@ import io.ebean.ProfileLocation; import io.ebean.TxScope; import io.ebean.annotation.PersistBatch; import io.ebean.annotation.TxType; -import io.ebean.bean.PersistenceContext; import io.ebean.cache.ServerCacheNotification; import io.ebean.cache.ServerCacheNotify; import io.ebean.config.CurrentTenantProvider; @@ -19,15 +18,7 @@ import io.ebean.metric.MetricFactory; import io.ebean.metric.TimedMetric; import io.ebean.metric.TimedMetricMap; import io.ebean.plugin.SpiServer; -import io.ebeaninternal.api.ScopeTrans; -import io.ebeaninternal.api.ScopedTransaction; -import io.ebeaninternal.api.SpiLogManager; -import io.ebeaninternal.api.SpiLogger; -import io.ebeaninternal.api.SpiProfileHandler; -import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.api.SpiTransactionManager; -import io.ebeaninternal.api.TransactionEvent; -import io.ebeaninternal.api.TransactionEventTable; +import io.ebeaninternal.api.*; import io.ebeaninternal.api.TransactionEventTable.TableIUD; import io.ebeaninternal.server.cache.CacheChangeSet; import io.ebeaninternal.server.cluster.ClusterManager; @@ -53,7 +44,6 @@ import java.util.concurrent.atomic.AtomicLong; * Manages transactions. *

* Keeps the Cache and Cluster in sync when transactions are committed. - *

*/ public class TransactionManager implements SpiTransactionManager { @@ -198,8 +188,6 @@ public class TransactionManager implements SpiTransactionManager { this.txnMain = metricFactory.createTimedMetric("txn.main"); this.txnReadOnly = metricFactory.createTimedMetric("txn.readonly"); this.txnNamed = metricFactory.createTimedMetricMap("txn.named."); - // Add gauges for db pool size - scopeManager.register(this); } @@ -304,20 +292,16 @@ public class TransactionManager implements SpiTransactionManager { * There is a potential optimisation available when read committed is the default * isolation level. If it is, then Connections used only for queries do not require * commit or rollback but instead can just be put back into the pool via close(). - *

*

* If the Isolation level is higher (say SERIALIZABLE) then Connections used * just for queries do need to be committed or rollback after the query. - *

*/ OnQueryOnly initOnQueryOnly(OnQueryOnly dbPlatformOnQueryOnly) { - // first check for a system property 'override' String systemPropertyValue = System.getProperty("ebean.transaction.onqueryonly"); if (systemPropertyValue != null) { return OnQueryOnly.valueOf(systemPropertyValue.trim().toUpperCase()); } - // default to rollback if not defined on the platform return dbPlatformOnQueryOnly == null ? OnQueryOnly.COMMIT : dbPlatformOnQueryOnly; } @@ -359,9 +343,7 @@ public class TransactionManager implements SpiTransactionManager { * Wrap an externally supplied Connection with a known transaction id. */ private SpiTransaction wrapExternalConnection(String id, Connection c) { - ExternalJdbcTransaction t = new ExternalJdbcTransaction(id, true, c, this); - // set the default batch mode t.setBatchMode(persistBatch); t.setBatchOnCascade(persistBatchOnCascade); @@ -409,7 +391,6 @@ public class TransactionManager implements SpiTransactionManager { */ @Override public void notifyOfRollback(SpiTransaction transaction, Throwable cause) { - try { if (txnLogger.isDebug()) { String msg = transaction.getLogPrefix() + "Rollback"; @@ -418,7 +399,6 @@ public class TransactionManager implements SpiTransactionManager { } txnLogger.debug(msg); } - } catch (Exception ex) { logger.error("Error while notifying TransactionEventListener of rollback event", ex); } @@ -445,7 +425,6 @@ public class TransactionManager implements SpiTransactionManager { } private void formatThrowable(Throwable e, StringBuilder sb) { - sb.append(e.toString()); StackTraceElement[] stackTrace = e.getStackTrace(); if (stackTrace.length > 0) { @@ -468,11 +447,9 @@ public class TransactionManager implements SpiTransactionManager { if (txnLogger.isDebug()) { txnLogger.debug(transaction.getLogPrefix() + "Commit"); } - PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, transaction); postCommit.notifyLocalCache(); backgroundExecutor.execute(postCommit.backgroundNotify()); - } catch (Exception ex) { logger.error("NotifyOfCommit failed. L2 Cache potentially not notified.", ex); } @@ -488,7 +465,6 @@ public class TransactionManager implements SpiTransactionManager { } private void externalModificationEvent(TransactionEventTable tableEvents) { - TransactionEvent event = new TransactionEvent(); event.add(tableEvents); @@ -501,25 +477,21 @@ public class TransactionManager implements SpiTransactionManager { * Notify local BeanPersistListeners etc of events from another server in the cluster. */ public void remoteTransactionEvent(RemoteTransactionEvent remoteEvent) { - if (clusterLogger.isDebugEnabled()) { clusterLogger.debug("processing {}", remoteEvent); } - CacheChangeSet changeSet = new CacheChangeSet(); RemoteTableMod tableMod = remoteEvent.getRemoteTableMod(); if (tableMod != null) { changeSet.addInvalidate(tableMod.getTables()); } - List tableIUDList = remoteEvent.getTableIUDList(); if (tableIUDList != null) { for (TableIUD tableIUD : tableIUDList) { beanDescriptorManager.cacheNotify(tableIUD, changeSet); } } - // note DeleteById is written as BeanPersistIds and getBeanPersistList() // processes both Bean IUD and DeleteById List beanPersistList = remoteEvent.getBeanPersistList(); @@ -528,7 +500,6 @@ public class TransactionManager implements SpiTransactionManager { persistIds.notifyCache(changeSet); } } - changeSet.apply(); } @@ -543,10 +514,8 @@ public class TransactionManager implements SpiTransactionManager { * Prepare and then send/log the changeSet. */ void sendChangeLog(final ChangeSet changeSet) { - // can set userId, userIpAddress & userContext if desired if (changeLogPrepare.prepare(changeSet)) { - if (changeLogAsync) { // call the log method in background backgroundExecutor.execute(() -> changeLogListener.log(changeSet)); @@ -653,9 +622,7 @@ public class TransactionManager implements SpiTransactionManager { * Begin a scoped transaction. */ public ScopedTransaction beginScopedTransaction(TxScope txScope) { - txScope = initTxScope(txScope); - ScopedTransaction txnContainer = getActiveScoped(); boolean setToScope; @@ -678,7 +645,6 @@ public class TransactionManager implements SpiTransactionManager { if (nestedSavepoint && (type == TxType.REQUIRED || type == TxType.REQUIRES_NEW)) { createTransaction = true; transaction = createSavepoint(transaction, this); - } else { createTransaction = isCreateNewTransaction(transaction, type); if (createTransaction) { @@ -748,32 +714,26 @@ public class TransactionManager implements SpiTransactionManager { * Determine whether to create a new transaction or not. *

* This will also potentially throw exceptions for MANDATORY and NEVER types. - *

*/ private boolean isCreateNewTransaction(SpiTransaction current, TxType type) { switch (type) { case REQUIRED: case SUPPORTS: return current == null; - case REQUIRES_NEW: return true; - case MANDATORY: if (current == null) { throw new PersistenceException("Transaction missing when MANDATORY"); } return false; - case NEVER: if (current != null) { throw new PersistenceException("Transaction exists for Transactional NEVER"); } return true; // always use NoTransaction instance - case NOT_SUPPORTED: return true; // always use NoTransaction instance - default: throw new RuntimeException("Should never get here?"); } @@ -801,8 +761,8 @@ public class TransactionManager implements SpiTransactionManager { /** * Experimental - find dirty beans in the persistence context and persist them. */ - public void flushTransparent(PersistenceContext persistenceContext, SpiTransaction transaction) { - List dirtyBeans = persistenceContext.dirtyBeans(); + public void flushTransparent(SpiPersistenceContext persistenceContext, SpiTransaction transaction) { + List dirtyBeans = persistenceContext.dirtyBeans(beanDescriptorManager); if (!dirtyBeans.isEmpty()) { server.updateAll(dirtyBeans, transaction); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/CheckMarkedDirty.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/CheckMarkedDirty.java deleted file mode 100644 index 321a35fec..000000000 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/CheckMarkedDirty.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.ebeaninternal.server.type; - -import io.ebean.ModifyAwareType; - -/** - * Check dirty state of json value which might be modify aware. - */ -class CheckMarkedDirty { - - /** - * Return true if the value should be considered dirty (and included in an update). - */ - static boolean isDirty(Object value) { - if (value instanceof ModifyAwareType) { - ModifyAwareType modifyAware = (ModifyAwareType) value; - if (modifyAware.isMarkedDirty()) { - // reset the dirty state (consider not dirty after update) - modifyAware.setMarkedDirty(false); - return true; - } else { - return false; - } - } - return true; - } -} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 7a2d82baa..befe46b20 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -3,11 +3,7 @@ package io.ebeaninternal.server.type; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.AnnotatedField; -import io.ebean.annotation.DbArray; -import io.ebean.annotation.DbEnumType; -import io.ebean.annotation.DbEnumValue; -import io.ebean.annotation.EnumValue; -import io.ebean.annotation.Platform; +import io.ebean.annotation.*; import io.ebean.config.DatabaseConfig; import io.ebean.config.JsonConfig; import io.ebean.config.PlatformConfig; @@ -34,125 +30,70 @@ import org.slf4j.LoggerFactory; import javax.persistence.AttributeConverter; import javax.persistence.EnumType; import java.io.File; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.URI; -import java.net.URL; +import java.net.*; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.Instant; -import java.time.Month; -import java.time.MonthDay; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.Year; -import java.time.YearMonth; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Currency; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.TimeZone; -import java.util.UUID; +import java.time.*; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * Default implementation of TypeManager. *

* Manages the list of ScalarType that is available. - *

*/ public final class DefaultTypeManager implements TypeManager { private static final Logger logger = LoggerFactory.getLogger(DefaultTypeManager.class); private final ConcurrentHashMap, ScalarType> typeMap; - private final ConcurrentHashMap> nativeMap; - private final ConcurrentHashMap> logicalMap; private final DefaultTypeFactory extraTypeFactory; private final ScalarType hstoreType = new ScalarTypePostgresHstore(); - private final ScalarTypeFile fileType = new ScalarTypeFile(); - private final ScalarType charType = new ScalarTypeChar(); - private final ScalarType charArrayType = new ScalarTypeCharArray(); - private final ScalarType longVarcharType = new ScalarTypeLongVarchar(); - private final ScalarType clobType = new ScalarTypeClob(); - private final ScalarType byteType = new ScalarTypeByte(); - private final ScalarType binaryType = new ScalarTypeBytesBinary(); - private final ScalarType blobType = new ScalarTypeBytesBlob(); - private final ScalarType varbinaryType = new ScalarTypeBytesVarbinary(); - private final ScalarType longVarbinaryType = new ScalarTypeBytesLongVarbinary(); - private final ScalarType shortType = new ScalarTypeShort(); - private final ScalarType integerType = ScalarTypeInteger.INSTANCE; - private final ScalarType longType = new ScalarTypeLong(); - private final ScalarType doubleType = new ScalarTypeDouble(); - private final ScalarType floatType = new ScalarTypeFloat(); - private final ScalarType bigDecimalType = new ScalarTypeBigDecimal(); - private final ScalarType timeType = new ScalarTypeTime(); - private final ScalarType urlType = new ScalarTypeURL(); private final ScalarType uriType = new ScalarTypeURI(); private final ScalarType localeType = new ScalarTypeLocale(); private final ScalarType currencyType = new ScalarTypeCurrency(); private final ScalarType timeZoneType = new ScalarTypeTimeZone(); - private final ScalarType stringType = ScalarTypeString.INSTANCE; - private final ScalarType classType = new ScalarTypeClass(); private final JsonConfig.DateTime jsonDateTime; private final JsonConfig.Date jsonDate; private final Object objectMapper; - private final boolean objectMapperPresent; - private final boolean postgres; - + private final TypeJsonManager jsonManager; private final boolean offlineMigrationGeneration; - private final EnumType defaultEnumType; // OPTIONAL ScalarTypes registered if Jackson/JsonNode is in the classpath @@ -191,24 +132,20 @@ public final class DefaultTypeManager implements TypeManager { this.typeMap = new ConcurrentHashMap<>(); this.nativeMap = new ConcurrentHashMap<>(); this.logicalMap = new ConcurrentHashMap<>(); - + this.postgres = isPostgres(config.getDatabasePlatform()); this.objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent(); this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : null; - + this.jsonManager = (objectMapperPresent) ? new TypeJsonManager(postgres, objectMapper, config.isJsonDirtyByDefault()) : null; this.extraTypeFactory = new DefaultTypeFactory(config); - this.postgres = isPostgres(config.getDatabasePlatform()); this.arrayTypeListFactory = arrayTypeListFactory(config.getDatabasePlatform()); this.arrayTypeSetFactory = arrayTypeSetFactory(config.getDatabasePlatform()); - this.offlineMigrationGeneration = DbOffline.isGenerateMigration(); - this.defaultEnumType = config.getDefaultEnumType(); initialiseStandard(config); initialiseJavaTimeTypes(config); initialiseJodaTypes(config); initialiseJacksonTypes(config); - loadTypesFromProviders(config, objectMapper); loadGeoTypeBinder(config); @@ -489,7 +426,7 @@ public final class DefaultTypeManager implements TypeManager { if (objectMapper == null) { throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping - Jackson ObjectMapper not present"); } - return ScalarTypeJsonObjectMapper.createTypeFor(postgres, (AnnotatedField) prop.getJacksonField(), (ObjectMapper) objectMapper, dbType, docType); + return ScalarTypeJsonObjectMapper.createTypeFor(jsonManager, (AnnotatedField) prop.getJacksonField(), dbType, docType); } /** @@ -588,8 +525,7 @@ public final class DefaultTypeManager implements TypeManager { private ScalarTypeEnum createEnumScalarType2(Class enumType) { boolean integerType = true; Map nameValueMap = new LinkedHashMap<>(); - Field[] fields = enumType.getDeclaredFields(); - for (Field field : fields) { + for (Field field : enumType.getDeclaredFields()) { EnumValue enumValue = AnnotationUtil.get(field, EnumValue.class); if (enumValue != null) { nameValueMap.put(field.getName(), enumValue.value()); @@ -652,8 +588,7 @@ public final class DefaultTypeManager implements TypeManager { } private ScalarTypeEnum createEnumScalarTypePerExtentions(Class> enumType) { - Method[] methods = enumType.getMethods(); - for (Method method : methods) { + for (Method method : enumType.getMethods()) { DbEnumValue dbValue = AnnotationUtil.get(method, DbEnumValue.class); if (dbValue != null) { boolean integerValues = DbEnumType.INTEGER == dbValue.storage(); @@ -668,7 +603,6 @@ public final class DefaultTypeManager implements TypeManager { * Create the Mapping of Enum fields to DB values using EnumValue annotations. *

* Return null if the EnumValue annotations are not present/used. - *

*/ private ScalarTypeEnum createEnumScalarTypeDbValue(Class> enumType, Method method, boolean integerType, int length, boolean withConstraint) { Map nameValueMap = new LinkedHashMap<>(); @@ -697,11 +631,11 @@ public final class DefaultTypeManager implements TypeManager { EnumToDbValueMap beanDbMap = EnumToDbValueMap.create(integerType); int maxValueLen = 0; for (Map.Entry entry : nameValueMap.entrySet()) { - String name = entry.getKey(); + String name = entry.getKey().trim(); String value = entry.getValue(); maxValueLen = Math.max(maxValueLen, value.length()); - Object enumValue = Enum.valueOf(enumType, name.trim()); - beanDbMap.add(enumValue, value, name.trim()); + Object enumValue = Enum.valueOf(enumType, name); + beanDbMap.add(enumValue, value, name); } if (dbColumnLength == 0 && !integerType) { dbColumnLength = maxValueLen; @@ -711,31 +645,24 @@ public final class DefaultTypeManager implements TypeManager { /** * Automatically find any ScalarTypes by searching through the class path. - *

- * In avaje.properties define a list of packages in which ScalarTypes are - * found. This will search for any class that implements the ScalarType - * interface and register it with this TypeManager. - *

*/ private void initialiseCustomScalarTypes(BootupClasses bootupClasses) { for (Class> cls : bootupClasses.getScalarTypes()) { try { ScalarType scalarType; if (objectMapper == null) { - scalarType = cls.newInstance(); + scalarType = cls.getDeclaredConstructor().newInstance(); } else { try { // first try objectMapper constructor - Constructor> constructor = cls.getConstructor(ObjectMapper.class); - scalarType = constructor.newInstance((ObjectMapper) objectMapper); + scalarType = cls.getDeclaredConstructor(ObjectMapper.class).newInstance(objectMapper); } catch (NoSuchMethodException e) { - scalarType = cls.newInstance(); + scalarType = cls.getDeclaredConstructor().newInstance(); } } addCustomType(scalarType); } catch (Exception e) { - String msg = "Error loading ScalarType [" + cls.getName() + "]"; - logger.error(msg, e); + logger.error("Error loading ScalarType [" + cls.getName() + "]", e); } } } @@ -755,8 +682,7 @@ public final class DefaultTypeManager implements TypeManager { @SuppressWarnings({"unchecked", "rawtypes"}) private void initialiseScalarConverters(BootupClasses bootupClasses) { - List>> foundTypes = bootupClasses.getScalarConverters(); - for (Class> foundType : foundTypes) { + for (Class> foundType : bootupClasses.getScalarConverters()) { try { Class[] paramTypes = TypeReflectHelper.getParams(foundType, ScalarTypeConverter.class); if (paramTypes.length != 2) { @@ -768,7 +694,7 @@ public final class DefaultTypeManager implements TypeManager { if (wrappedType == null) { throw new IllegalStateException("Could not find ScalarType for: " + paramTypes[1]); } - ScalarTypeConverter converter = foundType.newInstance(); + ScalarTypeConverter converter = foundType.getDeclaredConstructor().newInstance(); ScalarTypeWrapper stw = new ScalarTypeWrapper(logicalType, wrappedType, converter); logger.debug("Register ScalarTypeWrapper from {} -> {} using:{}", logicalType, persistType, foundType); add(stw); @@ -780,8 +706,7 @@ public final class DefaultTypeManager implements TypeManager { @SuppressWarnings({"unchecked", "rawtypes"}) private void initialiseAttributeConverters(BootupClasses bootupClasses) { - List>> foundTypes = bootupClasses.getAttributeConverters(); - for (Class> foundType : foundTypes) { + for (Class> foundType : bootupClasses.getAttributeConverters()) { try { Class[] paramTypes = TypeReflectHelper.getParams(foundType, AttributeConverter.class); if (paramTypes.length != 2) { @@ -793,7 +718,7 @@ public final class DefaultTypeManager implements TypeManager { if (wrappedType == null) { throw new IllegalStateException("Could not find ScalarType for: " + paramTypes[1]); } - AttributeConverter converter = foundType.newInstance(); + AttributeConverter converter = foundType.getDeclaredConstructor().newInstance(); ScalarTypeWrapper stw = new ScalarTypeWrapper(logicalType, wrappedType, new AttributeConverterAdapter(converter)); logger.debug("Register ScalarTypeWrapper from {} -> {} using:{}", logicalType, persistType, foundType); add(stw); @@ -825,12 +750,15 @@ public final class DefaultTypeManager implements TypeManager { } private void initialiseJavaTimeTypes(DatabaseConfig config) { + + ZoneId zoneId = getZoneId(config); + typeMap.put(java.nio.file.Path.class, new ScalarTypePath()); addType(java.time.Period.class, new ScalarTypePeriod()); addType(java.time.LocalDate.class, new ScalarTypeLocalDate(jsonDate)); addType(java.time.LocalDateTime.class, new ScalarTypeLocalDateTime(jsonDateTime)); - addType(OffsetDateTime.class, new ScalarTypeOffsetDateTime(jsonDateTime)); - addType(ZonedDateTime.class, new ScalarTypeZonedDateTime(jsonDateTime)); + addType(OffsetDateTime.class, new ScalarTypeOffsetDateTime(jsonDateTime, zoneId)); + addType(ZonedDateTime.class, new ScalarTypeZonedDateTime(jsonDateTime, zoneId)); addType(Instant.class, new ScalarTypeInstant(jsonDateTime)); addType(DayOfWeek.class, new ScalarTypeDayOfWeek()); addType(Month.class, new ScalarTypeMonth()); @@ -846,6 +774,11 @@ public final class DefaultTypeManager implements TypeManager { addType(Duration.class, (durationNanos) ? new ScalarTypeDurationWithNanos() : new ScalarTypeDuration()); } + private ZoneId getZoneId(DatabaseConfig config) { + final String dataTimeZone = config.getDataTimeZone(); + return (dataTimeZone == null) ? ZoneOffset.systemDefault() : TimeZone.getTimeZone(dataTimeZone).toZoneId(); + } + private void addType(Class clazz, ScalarType scalarType) { typeMap.put(clazz, scalarType); logicalMap.putIfAbsent(clazz.getSimpleName(), scalarType); @@ -890,23 +823,16 @@ public final class DefaultTypeManager implements TypeManager { nativeMap.put(DbPlatformType.HSTORE, hstoreType); - ScalarType utilDateType = extraTypeFactory.createUtilDate(jsonDateTime, jsonDate); - addType(java.util.Date.class, utilDateType); - - ScalarType calType = extraTypeFactory.createCalendar(jsonDateTime); - addType(Calendar.class, calType); - - ScalarType mathBigIntType = extraTypeFactory.createMathBigInteger(); - addType(BigInteger.class, mathBigIntType); + addType(java.util.Date.class, extraTypeFactory.createUtilDate(jsonDateTime, jsonDate)); + addType(Calendar.class, extraTypeFactory.createCalendar(jsonDateTime)); + addType(BigInteger.class, extraTypeFactory.createMathBigInteger()); ScalarTypeBool booleanType = extraTypeFactory.createBoolean(); addType(Boolean.class, booleanType); addType(boolean.class, booleanType); - // register the boolean literals to the platform for DDL default values databasePlatform.setDbTrueLiteral(booleanType.getDbTrueLiteral()); databasePlatform.setDbFalseLiteral(booleanType.getDbFalseLiteral()); - // always register Types.BOOLEAN to our boolean type nativeMap.put(Types.BOOLEAN, booleanType); if (booleanType.getJdbcType() == Types.BIT) { @@ -915,7 +841,6 @@ public final class DefaultTypeManager implements TypeManager { } PlatformConfig.DbUuid dbUuid = config.getPlatformConfig().getDbUuid(); - if (offlineMigrationGeneration || (databasePlatform.isNativeUuidType() && dbUuid.useNativeType())) { addType(UUID.class, new ScalarTypeUUIDNative()); } else { @@ -1027,6 +952,7 @@ public final class DefaultTypeManager implements TypeManager { nativeMap.put(Types.TIMESTAMP, timestampType); } + @SuppressWarnings("rawtypes") private void addInetAddressType(ScalarType scalarType) { addType(InetAddress.class, scalarType); addType(Inet4Address.class, scalarType); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/JacksonTypeHelper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/JacksonTypeHelper.java new file mode 100644 index 000000000..1e1db244d --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/JacksonTypeHelper.java @@ -0,0 +1,46 @@ +package io.ebeaninternal.server.type; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.introspect.AnnotatedField; + +class JacksonTypeHelper { + + private final AnnotatedField field; + private final ObjectMapper objectMapper; + private final JavaType javaType; + private final DeserializationConfig deserConfig; + private final AnnotationIntrospector ai; + + JacksonTypeHelper(AnnotatedField field, ObjectMapper objectMapper) { + this.field = field; + this.objectMapper = objectMapper; + this.javaType = field.getType(); + this.deserConfig = objectMapper.getDeserializationConfig(); + this.ai = deserConfig.getAnnotationIntrospector(); + } + + JavaType type() { + if (ai == null || javaType == null || javaType.hasRawClass(Object.class)) { + return javaType; + } else { + try { + return ai.refineDeserializationType(deserConfig, field, javaType); + } catch (JsonMappingException e) { + throw new RuntimeException(e); + } + } + } + + ObjectWriter objectWriter() { + if (ai == null || javaType == null || javaType.hasRawClass(Object.class)) { + return objectMapper.writerFor(javaType); + } else { + try { + JavaType serType = ai.refineSerializationType(objectMapper.getSerializationConfig(), field, javaType); + return objectMapper.writerFor(serType); + } catch (JsonMappingException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java index 3448251d5..9763a5667 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java @@ -43,12 +43,8 @@ public class RsetDataReader implements DataReader { @Override public boolean next() throws SQLException { - return rset.next(); - } - - @Override - public void resetColumnPosition() { pos = 0; + return rset.next(); } @Override @@ -75,7 +71,6 @@ public class RsetDataReader implements DataReader { return rset.getBigDecimal(pos()); } - @Override public InputStream getBinaryStream() throws SQLException { return rset.getBinaryStream(pos()); @@ -84,19 +79,13 @@ public class RsetDataReader implements DataReader { @Override public Boolean getBoolean() throws SQLException { boolean v = rset.getBoolean(pos()); - if (rset.wasNull()) { - return null; - } - return v; + return rset.wasNull() ? null : v; } @Override public Byte getByte() throws SQLException { byte v = rset.getByte(pos()); - if (rset.wasNull()) { - return null; - } - return v; + return rset.wasNull() ? null : v; } @Override @@ -117,62 +106,42 @@ public class RsetDataReader implements DataReader { @Override public Double getDouble() throws SQLException { double v = rset.getDouble(pos()); - if (rset.wasNull()) { - return null; - } - return v; + return rset.wasNull() ? null : v; } @Override public Float getFloat() throws SQLException { float v = rset.getFloat(pos()); - if (rset.wasNull()) { - return null; - } - return v; + return rset.wasNull() ? null : v; } @Override public Integer getInt() throws SQLException { int v = rset.getInt(pos()); - if (rset.wasNull()) { - return null; - } - return v; + return rset.wasNull() ? null : v; } - @Override public Long getLong() throws SQLException { long v = rset.getLong(pos()); - if (rset.wasNull()) { - return null; - } - return v; + return rset.wasNull() ? null : v; } - public Ref getRef() throws SQLException { return rset.getRef(pos()); } - @Override public Short getShort() throws SQLException { short s = rset.getShort(pos()); - if (rset.wasNull()) { - return null; - } - return s; + return rset.wasNull() ? null : s; } - @Override public String getString() throws SQLException { return rset.getString(pos()); } - @Override public Time getTime() throws SQLException { Calendar cal = dataTimeZone.getTimeZone(); @@ -196,14 +165,10 @@ public class RsetDataReader implements DataReader { @Override public String getStringFromStream() throws SQLException { Reader reader = rset.getCharacterStream(pos()); - if (reader == null) { - return null; - } - return readStringLob(reader); + return reader == null ? null : readStringLob(reader); } protected String readStringLob(Reader reader) throws SQLException { - char[] buffer = new char[clobBufferSize]; int readLength; StringBuilder out = new StringBuilder(stringInitialSize); @@ -215,7 +180,6 @@ public class RsetDataReader implements DataReader { } catch (IOException e) { throw new SQLException("IOException reading Clob " + e.getMessage()); } - return out.toString(); } @@ -226,19 +190,16 @@ public class RsetDataReader implements DataReader { } protected byte[] getBinaryLob(InputStream in) throws SQLException { - if (in == null) { return null; } try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buf = new byte[bufferSize]; int len; while ((len = in.read(buf, 0, buf.length)) != -1) { out.write(buf, 0, len); } byte[] data = out.toByteArray(); - if (data.length == 0) { data = null; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollection.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollection.java index a6c51ff05..e40e1a9ae 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollection.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollection.java @@ -61,7 +61,7 @@ abstract class ScalarTypeJsonCollection extends ScalarTypeBase implements */ @Override public boolean isDirty(Object value) { - return CheckMarkedDirty.isDirty(value); + return TypeJsonManager.checkIsDirty(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java index f0714c467..6dd4f900b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -123,7 +123,7 @@ public abstract class ScalarTypeJsonMap extends ScalarTypeBase { */ @Override public boolean isDirty(Object value) { - return CheckMarkedDirty.isDirty(value); + return TypeJsonManager.checkIsDirty(value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index fc88f10da..5e24412b5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -3,15 +3,10 @@ package io.ebeaninternal.server.type; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.AnnotationIntrospector; -import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.introspect.AnnotatedField; -import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -34,38 +29,23 @@ import java.util.Set; /** * Supports @DbJson properties using Jackson ObjectMapper. */ -public class ScalarTypeJsonObjectMapper { +class ScalarTypeJsonObjectMapper { /** * Create and return the appropriate ScalarType. */ - public static ScalarType createTypeFor(boolean postgres, AnnotatedField field, ObjectMapper objectMapper, - int dbType, DocPropertyType docType) { - + static ScalarType createTypeFor(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { Class type = field.getRawType(); - String pgType = getPostgresType(postgres, dbType); if (Set.class.equals(type)) { - return new OmSet(objectMapper, field, dbType, pgType, docType); + return new OmSet(jsonManager, field, dbType, docType); } if (List.class.equals(type)) { - return new OmList(objectMapper, field, dbType, pgType, docType); + return new OmList(jsonManager, field, dbType, docType); } if (Map.class.equals(type)) { - return new OmMap(objectMapper, field, dbType, pgType); + return new OmMap(jsonManager, field, dbType); } - return new GenericObject(objectMapper, field, dbType, pgType); - } - - private static String getPostgresType(boolean postgres, int dbType) { - if (postgres) { - switch (dbType) { - case DbPlatformType.JSON: - return PostgresHelper.JSON_TYPE; - case DbPlatformType.JSONB: - return PostgresHelper.JSONB_TYPE; - } - } - return null; + return new GenericObject(jsonManager, field, dbType, type); } /** @@ -73,8 +53,8 @@ public class ScalarTypeJsonObjectMapper { */ private static class GenericObject extends Base { - public GenericObject(ObjectMapper objectMapper, AnnotatedField field, int dbType, String pgType) { - super(Object.class, objectMapper, field, dbType, pgType, DocPropertyType.OBJECT); + GenericObject(TypeJsonManager jsonManager, AnnotatedField field, int dbType, Class rawType) { + super(Object.class, jsonManager, field, dbType, DocPropertyType.OBJECT, rawType); } } @@ -84,8 +64,8 @@ public class ScalarTypeJsonObjectMapper { @SuppressWarnings("rawtypes") private static class OmSet extends Base { - public OmSet(ObjectMapper objectMapper, AnnotatedField field, int dbType, String pgType, DocPropertyType docType) { - super(Set.class, objectMapper, field, dbType, pgType, docType); + OmSet(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { + super(Set.class, jsonManager, field, dbType, docType); } @Override @@ -102,8 +82,8 @@ public class ScalarTypeJsonObjectMapper { @SuppressWarnings("rawtypes") private static class OmList extends Base { - public OmList(ObjectMapper objectMapper, AnnotatedField field, int dbType, String pgType, DocPropertyType docType) { - super(List.class, objectMapper, field, dbType, pgType, docType); + OmList(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { + super(List.class, jsonManager, field, dbType, docType); } @Override @@ -120,8 +100,8 @@ public class ScalarTypeJsonObjectMapper { @SuppressWarnings("rawtypes") private static class OmMap extends Base { - public OmMap(ObjectMapper objectMapper, AnnotatedField field, int dbType, String pgType) { - super(Map.class, objectMapper, field, dbType, pgType, DocPropertyType.OBJECT); + OmMap(TypeJsonManager jsonManager, AnnotatedField field, int dbType) { + super(Map.class, jsonManager, field, dbType, DocPropertyType.OBJECT); } @Override @@ -139,51 +119,25 @@ public class ScalarTypeJsonObjectMapper { private static abstract class Base extends ScalarTypeBase { private final ObjectWriter objectWriter; - private final ObjectMapper objectReader; - - private JavaType deserType; - + private final JavaType deserType; private final String pgType; - private final DocPropertyType docType; + private final TypeJsonManager.DirtyHandler dirtyHandler; - /** - * Construct given the object mapper, property type and DB type for storage. - */ - public Base(Class cls, ObjectMapper objectMapper, AnnotatedField field, int dbType, String pgType, DocPropertyType docType) { + Base(Class cls, TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { + this(cls, jsonManager, field, dbType, docType, cls); + } + + Base(Class cls, TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType, Class rawType) { super(cls, false, dbType); - this.pgType = pgType; + this.objectReader = jsonManager.objectMapper(); + this.pgType = jsonManager.postgresType(dbType); this.docType = docType; - this.objectReader = objectMapper; - - JavaType javaType = field.getType(); - DeserializationConfig deserConfig = objectMapper.getDeserializationConfig(); - AnnotationIntrospector ai = deserConfig.getAnnotationIntrospector(); - - if (ai != null && javaType != null && !javaType.hasRawClass(Object.class)) { - try { - this.deserType = ai.refineDeserializationType(deserConfig, field, javaType); - } catch (JsonMappingException e) { - throw new RuntimeException(e); - } - } else { - this.deserType = javaType; - } - - SerializationConfig serConfig = objectMapper.getSerializationConfig(); - ai = deserConfig.getAnnotationIntrospector(); - - if (ai != null && javaType != null && !javaType.hasRawClass(Object.class)) { - try { - JavaType serType = ai.refineSerializationType(serConfig, field, javaType); - this.objectWriter = objectMapper.writerFor(serType); - } catch (JsonMappingException e) { - throw new RuntimeException(e); - } - } else { - this.objectWriter = objectMapper.writerFor(javaType); - } + this.dirtyHandler = jsonManager.dirtyHandler(cls, rawType); + final JacksonTypeHelper helper = new JacksonTypeHelper(field, objectReader); + this.deserType = helper.type(); + this.objectWriter = helper.objectWriter(); } /** @@ -199,7 +153,7 @@ public class ScalarTypeJsonObjectMapper { */ @Override public boolean isDirty(Object value) { - return CheckMarkedDirty.isDirty(value); + return dirtyHandler.isDirty(value); } @Override @@ -224,12 +178,7 @@ public class ScalarTypeJsonObjectMapper { if (value == null) { binder.setNull(Types.VARCHAR); // use varchar, otherwise SqlServer/db2 will fail with 'Invalid JDBC data type 5.001.' } else { - try { - String json = objectWriter.writeValueAsString(value); - binder.setString(json); - } catch (JsonProcessingException e) { - throw new SQLException("Unable to create JSON", e); - } + binder.setString(formatValue(value)); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java index a4bc91aae..12b884e82 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTime.java @@ -1,7 +1,10 @@ package io.ebeaninternal.server.type; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import io.ebean.config.JsonConfig; +import java.io.IOException; import java.sql.Timestamp; import java.sql.Types; import java.time.Instant; @@ -29,12 +32,37 @@ public class ScalarTypeLocalDateTime extends ScalarTypeBaseDateTime { - public ScalarTypeOffsetDateTime(JsonConfig.DateTime mode) { + private final ZoneId zoneId; + + public ScalarTypeOffsetDateTime(JsonConfig.DateTime mode, ZoneId zoneId) { super(mode, OffsetDateTime.class, false, Types.TIMESTAMP); + this.zoneId = zoneId; } @Override @@ -46,7 +49,7 @@ public class ScalarTypeOffsetDateTime extends ScalarTypeBaseDateTime { @Override public boolean isDirty(Object value) { - return CheckMarkedDirty.isDirty(value); + return TypeJsonManager.checkIsDirty(value); } @SuppressWarnings("unchecked") diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTime.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTime.java index 52b777b6b..12e13cdfa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTime.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTime.java @@ -13,8 +13,11 @@ import java.time.ZonedDateTime; */ public class ScalarTypeZonedDateTime extends ScalarTypeBaseDateTime { - public ScalarTypeZonedDateTime(JsonConfig.DateTime mode) { + private final ZoneId zoneId; + + public ScalarTypeZonedDateTime(JsonConfig.DateTime mode, ZoneId zoneId) { super(mode, ZonedDateTime.class, false, Types.TIMESTAMP); + this.zoneId = zoneId; } @Override @@ -44,7 +47,7 @@ public class ScalarTypeZonedDateTime extends ScalarTypeBaseDateTime cls, Class rawType) { + if (!Object.class.equals(cls) || ModifyAwareType.class.isAssignableFrom(rawType)) { + // Set, List and Map are modify aware + return modifyAwareHandler; + } + return defaultHandler; + } + + /** + * Return true if the value should be considered dirty (and included in an update). + */ + static boolean checkIsDirty(Object value) { + if (value instanceof ModifyAwareType) { + return checkModifyAware(value); + } + return true; + } + + private static boolean checkModifyAware(Object value) { + ModifyAwareType modifyAware = (ModifyAwareType) value; + if (modifyAware.isMarkedDirty()) { + // reset the dirty state (consider not dirty after update) + modifyAware.setMarkedDirty(false); + return true; + } else { + return false; + } + } + + static final class ModifyAwareHandler implements DirtyHandler { + @Override + public boolean isDirty(Object value) { + return checkModifyAware(value); + } + } + + /** + * Effectively constant based on {@link DatabaseConfig#isJsonDirtyByDefault()} + */ + static final class DefaultHandler implements DirtyHandler { + + private final boolean dirty; + + DefaultHandler(boolean dirty) { + this.dirty = dirty; + } + + @Override + public boolean isDirty(Object value) { + return dirty; + } + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/lib/Str.java b/ebean-core/src/main/java/io/ebeaninternal/server/util/Str.java similarity index 96% rename from ebean-core/src/main/java/io/ebeaninternal/server/lib/Str.java rename to ebean-core/src/main/java/io/ebeaninternal/server/util/Str.java index 02b3d9e37..56788dc98 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/lib/Str.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/util/Str.java @@ -1,4 +1,4 @@ -package io.ebeaninternal.server.lib; +package io.ebeaninternal.server.util; /** * String utility for adding strings together. @@ -11,7 +11,6 @@ public class Str { * Append strings together. */ public static String add(String s0, String s1, String... args) { - // determine a decent buffer size int len = 16 + s0.length(); if (s1 != null) { diff --git a/ebean-core/src/main/resources/META-INF/ebean-version.mf b/ebean-core/src/main/resources/META-INF/ebean-version.mf new file mode 100644 index 000000000..6bcacf1d6 --- /dev/null +++ b/ebean-core/src/main/resources/META-INF/ebean-version.mf @@ -0,0 +1 @@ +ebean-version: 129 diff --git a/ebean-core/src/test/java/io/ebean/DtoQuery2Test.java b/ebean-core/src/test/java/io/ebean/DtoQuery2Test.java index 679fddd63..ba30e9a5c 100644 --- a/ebean-core/src/test/java/io/ebean/DtoQuery2Test.java +++ b/ebean-core/src/test/java/io/ebean/DtoQuery2Test.java @@ -6,10 +6,13 @@ import org.ebeantest.LoggedSqlCollector; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +33,73 @@ public class DtoQuery2Test extends BaseTestCase { assertThat(list).isNotEmpty(); } + @Test + public void dto_findIterator_closeWithResources() { + ResetBasicData.reset(); + + int counter = 0; + try (QueryIterator iterator = server() + .findDto(DCust.class, "select id, name from o_customer where id > ?") + .setParameter(0) + .findIterate()) { + + if (iterator.hasNext()) { + counter++; + } + } + + assertThat(counter).isEqualTo(1); + } + + @Test + public void dto_findIterator() { + ResetBasicData.reset(); + final int expectedCount = server().find(Customer.class).findCount(); + + LoggedSqlCollector.start(); + int counter = 0; + try (final QueryIterator iterator = server().findDto(DCust.class, "select id, name from o_customer where id > :id") + .setParameter("id", 0) + .findIterate()) { + + while (iterator.hasNext()) { + final DCust cust = iterator.next(); + counter++; + assertThat(cust).isNotNull(); + assertThat(cust.getName()).isNotNull(); + } + } + + assertThat(counter).isEqualTo(expectedCount); + + List sql = LoggedSqlCollector.stop(); + assertSql(sql.get(0)).contains("select id, name from o_customer where id > ?"); + } + + @Test + public void dto_findStream() { + ResetBasicData.reset(); + final int expectedCount = server().find(Customer.class).findCount(); + + LoggedSqlCollector.start(); + + try (final Stream stream = + server() + .findDto(DCust.class, "select id, name from o_customer where id > ?") + .setParameter(0) + .findStream()) { + + final List names = stream + .map(DCust::getName) + .collect(Collectors.toList()); + + assertThat(names.size()).isEqualTo(expectedCount); + } + + List sql = LoggedSqlCollector.stop(); + assertSql(sql.get(0)).contains("select id, name from o_customer where id > ?"); + } + @Test public void dto_findEach_constructorMatch() { diff --git a/ebean-core/src/test/java/io/ebean/common/BeanMapTest.java b/ebean-core/src/test/java/io/ebean/common/BeanMapTest.java index 08a57e05a..003539a72 100644 --- a/ebean-core/src/test/java/io/ebean/common/BeanMapTest.java +++ b/ebean-core/src/test/java/io/ebean/common/BeanMapTest.java @@ -2,18 +2,21 @@ package io.ebean.common; import io.ebean.bean.BeanCollection; import org.junit.Test; +import org.tests.model.basic.EBasic; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; public class BeanMapTest { - Object object1 = new Object(); - Object object2 = new Object(); - Object object3 = new Object(); + private final EBasic object1 = new EBasic("o1"); + private final EBasic object2 = new EBasic("o2"); + private final EBasic object3 = new EBasic("o3"); + private final EBasic object4 = new EBasic("o4"); + private final EBasic object5 = new EBasic("o5"); private Map all() { Map all = new LinkedHashMap<>(); @@ -174,9 +177,7 @@ public class BeanMapTest { @Test public void testClear_given_someBeansInAdditions() throws Exception { - BeanMap map = new BeanMap<>(); - map.put("1", object1); - map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + BeanMap map = newModifyListeningMap(); map.put("2", object2); map.put("3", object3); @@ -188,4 +189,252 @@ public class BeanMapTest { assertThat(map.getModifyAdditions()).isEmpty(); } + @Test(expected = UnsupportedOperationException.class) + public void keySet_add_whenModifyListening() { + BeanMap map = newModifyListeningMap(); + map.keySet().add("3"); + } + + @Test(expected = UnsupportedOperationException.class) + public void keySet_add() { + BeanMap map = new BeanMap<>(); + map.keySet().add("3"); + } + + @Test(expected = UnsupportedOperationException.class) + public void keySet_addAll_whenModifyListening() { + BeanMap map = newModifyListeningMap(); + map.keySet().addAll(asList("3", "4")); + } + + @Test(expected = UnsupportedOperationException.class) + public void keySet_addAll() { + BeanMap map = new BeanMap<>(); + map.keySet().addAll(asList("3", "4")); + } + + @Test + public void keySet_remove() { + + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.put("2", object2); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + + final Set keySet = map.keySet(); + keySet.remove("1"); + + assertThat(keySet.contains("1")).isFalse(); + assertThat(map).doesNotContainKeys("1"); + assertThat(map.get("1")).isNull(); + + assertThat(map.getModifyRemovals()).containsExactly(object1); + } + + @Test + public void keySet_clear() { + + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.put("2", object2); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + + final Set keySet = map.keySet(); + keySet.clear(); + + assertThat(map).isEmpty(); + assertThat(keySet).isEmpty(); + + assertThat(map.getModifyRemovals()).containsExactly(object1, object2); + } + + @Test + public void keySet_iterator_remove() { + + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.put("2", object2); + map.put("3", object3); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + + final Set keySet = map.keySet(); + final Iterator iterator = keySet.iterator(); + while (iterator.hasNext()) { + final String key = iterator.next(); + if (key.equals("2")) { + iterator.remove(); + } + } + + assertThat(map).hasSize(2); + assertThat(keySet).hasSize(2); + assertThat(keySet).containsExactly("1", "3"); + assertThat(map).containsKeys("1", "3"); + + assertThat(map.getModifyRemovals()).containsExactly(object2); + } + + @Test + public void keySet_removeAll() { + + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.put("2", object2); + map.put("3", object3); + map.put("4", object4); + map.put("5", object5); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + + final Set keySet = map.keySet(); + final boolean changed = keySet.removeAll(asList("2", "3", "5")); + + assertThat(changed).isTrue(); + assertThat(map).hasSize(2); + assertThat(keySet).hasSize(2); + assertThat(keySet).containsExactly("1", "4"); + assertThat(map).containsKeys("1", "4"); + + assertThat(map.getModifyRemovals()).containsExactly(object2, object3, object5); + } + + + @Test + public void keySet_retainAll() { + + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.put("2", object2); + map.put("3", object3); + map.put("4", object4); + map.put("5", object5); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + + final Set keySet = map.keySet(); + final boolean changed = keySet.retainAll(asList("2", "3", "5")); + + assertThat(changed).isTrue(); + assertThat(map).hasSize(3); + assertThat(keySet).hasSize(3); + assertThat(keySet).containsExactly("2", "3", "5"); + assertThat(map).containsKeys("2", "3", "5"); + + assertThat(map.getModifyRemovals()).containsExactly(object1, object4); + } + + @Test(expected = UnsupportedOperationException.class) + public void values_add() { + BeanMap map = new BeanMap<>(); + map.values().add(object3); + } + + @Test(expected = UnsupportedOperationException.class) + public void values_addAll() { + BeanMap map = new BeanMap<>(); + map.values().addAll(asList(object3, object5)); + } + + @Test(expected = UnsupportedOperationException.class) + public void entrySet_add() { + newModifyListeningMap() + .entrySet() + .add(new AbstractMap.SimpleEntry<>("3", object3)); + } + + @Test + public void entrySet_clear() { + final BeanMap map = newModifyListeningMap(); + final Set> entries = map.entrySet(); + entries.clear(); + + assertThat(entries).isEmpty(); + assertThat(map).isEmpty(); + assertThat(map.getModifyRemovals()).containsExactly(object1); + } + + @Test + public void entrySet_remove() { + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); + + assertThat(map).hasSize(5); + + final boolean existed1 = entries.remove(new AbstractMap.SimpleEntry<>("1", object1)); + assertThat(existed1).isTrue(); + + final boolean existed22 = entries.remove(new AbstractMap.SimpleEntry<>("22", object1)); + assertThat(existed22).isFalse(); + + assertThat(map).hasSize(4); + assertThat(map.getModifyRemovals()).containsExactly(object1); + } + + @Test + public void entrySet_remove_whenNotEqualValue() { + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); + + assertThat(map).hasSize(5); + + final boolean modified = entries.remove(new AbstractMap.SimpleEntry<>("1", object2)); + assertThat(modified).isFalse(); + + assertThat(map).hasSize(5); + assertThat(map.getModifyRemovals()).isNull(); + } + + @Test + public void entrySet_iterator_remove() { + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); + final Iterator> iterator = entries.iterator(); + while (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + if (entry.getKey().equals("2") || entry.getKey().equals("5")) { + iterator.remove(); + } + } + assertThat(map).hasSize(3); + assertThat(entries).hasSize(3); + assertThat(map.getModifyRemovals()).containsExactly(object2, object5); + } + + @Test + public void entrySet_removeAll() { + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); + + entries.removeAll(asList(new AbstractMap.SimpleEntry<>("1", object1), new AbstractMap.SimpleEntry<>("3", object4), new AbstractMap.SimpleEntry<>("4", object4))); + assertThat(map).hasSize(3); + assertThat(entries).hasSize(3); + assertThat(map.getModifyRemovals()).containsExactly(object1, object4); + } + + @Test + public void entrySet_retainAll() { + final BeanMap map = newModifyListeningMap5(); + final Set> entries = map.entrySet(); + + entries.retainAll(asList(new AbstractMap.SimpleEntry<>("1", object1), new AbstractMap.SimpleEntry<>("3", object4), new AbstractMap.SimpleEntry<>("4", object4))); + assertThat(map).hasSize(2); + assertThat(entries).hasSize(2); + assertThat(map.getModifyRemovals()).containsExactly(object2, object3, object5); + } + + private BeanMap newModifyListeningMap() { + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + return map; + } + + private BeanMap newModifyListeningMap5() { + BeanMap map = new BeanMap<>(); + map.put("1", object1); + map.put("2", object2); + map.put("3", object3); + map.put("4", object4); + map.put("5", object5); + map.setModifyListening(BeanCollection.ModifyListenMode.ALL); + return map; + } } diff --git a/ebean-core/src/test/java/io/ebean/config/ServerConfigTest.java b/ebean-core/src/test/java/io/ebean/config/ServerConfigTest.java index 4e92798d6..dde2cb216 100644 --- a/ebean-core/src/test/java/io/ebean/config/ServerConfigTest.java +++ b/ebean-core/src/test/java/io/ebean/config/ServerConfigTest.java @@ -62,6 +62,7 @@ public class ServerConfigTest { props.setProperty("dbOffline", "true"); props.setProperty("jsonDateTime", "MILLIS"); props.setProperty("jsonDate", "MILLIS"); + props.setProperty("jsonDirtyByDefault", "false"); props.setProperty("autoReadOnlyDataSource", "true"); props.setProperty("disableL2Cache", "true"); props.setProperty("notifyL2CacheInForeground", "true"); @@ -103,6 +104,9 @@ public class ServerConfigTest { assertEquals(PlatformConfig.DbUuid.BINARY, serverConfig.getPlatformConfig().getDbUuid()); assertEquals(JsonConfig.DateTime.MILLIS, serverConfig.getJsonDateTime()); assertEquals(JsonConfig.Date.MILLIS, serverConfig.getJsonDate()); + assertFalse(serverConfig.isJsonDirtyByDefault()); + serverConfig.setJsonDirtyByDefault(true); + assertTrue(serverConfig.isJsonDirtyByDefault()); assertEquals("r0,users,orgs", serverConfig.getEnabledL2Regions()); @@ -155,6 +159,7 @@ public class ServerConfigTest { assertFalse(serverConfig.isIdGeneratorAutomatic()); assertEquals(JsonConfig.DateTime.ISO8601, serverConfig.getJsonDateTime()); assertEquals(JsonConfig.Date.ISO8601, serverConfig.getJsonDate()); + assertTrue(serverConfig.isJsonDirtyByDefault()); assertTrue(serverConfig.getPlatformConfig().isCaseSensitiveCollation()); assertTrue(serverConfig.isAutoLoadModuleInfo()); diff --git a/ebean-core/src/test/java/io/ebean/config/dbplatform/OraclePlatformTest.java b/ebean-core/src/test/java/io/ebean/config/dbplatform/OraclePlatformTest.java index 667a5960b..15e9dda00 100644 --- a/ebean-core/src/test/java/io/ebean/config/dbplatform/OraclePlatformTest.java +++ b/ebean-core/src/test/java/io/ebean/config/dbplatform/OraclePlatformTest.java @@ -1,6 +1,7 @@ package io.ebean.config.dbplatform; import io.ebean.config.PlatformConfig; +import io.ebean.config.dbplatform.oracle.Oracle11Platform; import io.ebean.config.dbplatform.oracle.OraclePlatform; import org.junit.Test; @@ -8,17 +9,27 @@ import static org.assertj.core.api.Assertions.assertThat; public class OraclePlatformTest { + @Test + public void columnAliasPrefix_Oracle11Platform() { + Oracle11Platform platform11 = new Oracle11Platform(); + assertThat(platform11.columnAliasPrefix).isEqualTo("c"); + } + + @Test + public void columnAliasPrefix_OraclePlatform() { + OraclePlatform platform = new OraclePlatform(); + assertThat(platform.columnAliasPrefix).isEqualTo("c"); + } + @Test public void uuid_default() { OraclePlatform platform = new OraclePlatform(); platform.configure(new PlatformConfig(), false); DbPlatformType dbType = platform.getDbTypeMap().get(DbPlatformType.UUID); - assertThat(dbType.renderType(0, 0)).isEqualTo("varchar2(40)"); } - @Test public void uuid_as_binary() { diff --git a/ebean-core/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java b/ebean-core/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java index 03ed3362f..f0f0c0779 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java +++ b/ebean-core/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java @@ -453,6 +453,16 @@ public class TDSpiEbeanServer implements SpiEbeanServer { public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { } + @Override + public QueryIterator findDtoIterate(SpiDtoQuery query) { + return null; + } + + @Override + public Stream findDtoStream(SpiDtoQuery query) { + return null; + } + @Override public List findDtoList(SpiDtoQuery query) { return null; @@ -493,6 +503,10 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public void findSingleAttributeEach(SpiSqlQuery query, Class cls, Consumer consumer) { + } + @Override public T findOneMapper(SpiSqlQuery query, RowMapper mapper) { return null; diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java index 12c731115..1a5484f16 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java @@ -288,7 +288,7 @@ public class DefaultServer_createOrmQueryRequestTest extends BaseTestCase { } @Test - public void test_removeJoinToMany_when_filterMany() { + public void test_filterMany_included() { Query query = Ebean.find(Order.class) .fetch("details") @@ -301,6 +301,40 @@ public class DefaultServer_createOrmQueryRequestTest extends BaseTestCase { OrmQueryRequest queryRequest = queryRequest(query); OrmQueryDetail detail = queryRequest.getQuery().getDetail(); + assertThat(detail.getFetchPaths()).containsExactly("details", "details.product", "customer"); + } + + @Test + public void test_filterMany_excludedByOrdering() { + + Query query = Ebean.find(Order.class) + .fetch("customer") + .fetch("customer.contacts") + .fetch("details") + .fetch("details.product") + .filterMany("details").eq("orderQuantity", 10) + .query(); + + OrmQueryRequest queryRequest = queryRequest(query); + OrmQueryDetail detail = queryRequest.getQuery().getDetail(); + + assertThat(detail.getFetchPaths()).containsExactly("customer", "customer.contacts"); + } + + @Test + public void test_filterMany_excludedExplicitly() { + + Query query = Ebean.find(Order.class) + .fetchQuery("details") + .fetch("details.product") + .fetch("customer") + .fetch("customer.contacts") + .filterMany("details").eq("orderQuantity", 10) + .query(); + + OrmQueryRequest queryRequest = queryRequest(query); + OrmQueryDetail detail = queryRequest.getQuery().getDetail(); + assertThat(detail.getFetchPaths()).containsExactly("customer", "customer.contacts"); } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java index 88803da74..65423e012 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeLocalDateTimeTest.java @@ -1,25 +1,28 @@ package io.ebeaninternal.server.type; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; import io.ebean.config.JsonConfig; import org.junit.Test; +import java.io.StringWriter; import java.sql.Timestamp; import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class ScalarTypeLocalDateTimeTest { - ScalarTypeLocalDateTime type = new ScalarTypeLocalDateTime(JsonConfig.DateTime.MILLIS); + private final ScalarTypeLocalDateTime type = new ScalarTypeLocalDateTime(JsonConfig.DateTime.MILLIS); + + private final JsonFactory factory = new JsonFactory(); // warm up - LocalDateTime warmUp = LocalDateTime.now(); + private final LocalDateTime warmUp = LocalDateTime.now(); @Test - public void testNowToMillis() throws Exception { + public void testNowToMillis() { warmUp.hashCode(); @@ -29,7 +32,7 @@ public class ScalarTypeLocalDateTimeTest { } @Test - public void testConvertToMillis() throws Exception { + public void testConvertToMillis() { LocalDateTime now = LocalDateTime.now().withNano(123_000_000); // jdk11 workaround long asMillis = type.convertToMillis(now); @@ -39,7 +42,7 @@ public class ScalarTypeLocalDateTimeTest { } @Test - public void testConvertFromTimestamp() throws Exception { + public void testConvertFromTimestamp() { Timestamp now = new Timestamp(System.currentTimeMillis()); @@ -74,6 +77,24 @@ public class ScalarTypeLocalDateTimeTest { assertEquals(timestamp, timestamp1); } + + @Test + public void testJsonRaw() throws Exception { + + final LocalDateTime of = LocalDateTime.of(2020, 5, 4, 13, 20, 40); + + ScalarTypeLocalDateTime typeIso = new ScalarTypeLocalDateTime(JsonConfig.DateTime.ISO8601); + + StringWriter writer = new StringWriter(); + JsonGenerator generator = factory.createGenerator(writer); + + typeIso.jsonWrite(generator, of); + generator.flush(); + + assertThat(of.toString()).isEqualTo("2020-05-04T13:20:40"); + assertThat(writer.toString()).isEqualTo("\"2020-05-04T13:20:40\""); + } + @Test public void testJson() throws Exception { diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeOffsetDateTimeTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeOffsetDateTimeTest.java index ba12fcd05..b0b2b1eb0 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeOffsetDateTimeTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeOffsetDateTimeTest.java @@ -5,6 +5,8 @@ import org.junit.Test; import java.sql.Timestamp; import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.TimeZone; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -14,12 +16,12 @@ import static org.junit.Assert.assertTrue; public class ScalarTypeOffsetDateTimeTest { - ScalarTypeOffsetDateTime type = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.MILLIS); + ScalarTypeOffsetDateTime type = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.MILLIS, ZoneOffset.systemDefault()); OffsetDateTime warmUp = OffsetDateTime.now(); @Test - public void testConvertToMillis() throws Exception { + public void testConvertToMillis() { warmUp.hashCode(); @@ -30,7 +32,43 @@ public class ScalarTypeOffsetDateTimeTest { } @Test - public void testConvertFromTimestamp() throws Exception { + public void convertFromInstant_with_UTC_expect_matchingZoneOffset() { + final TimeZone timeZoneToUse = TimeZone.getTimeZone("UTC"); + final ZoneOffset expectedZoneOffset = ZoneOffset.UTC; + + convertFromInstantWithConfiguredTimeZone(timeZoneToUse, expectedZoneOffset); + } + + @Test + public void convertFromInstant_with_EST_expect_matchingZoneOffset() { + final TimeZone timeZoneToUse = TimeZone.getTimeZone("EST"); + final ZoneOffset expectedOffset = OffsetDateTime.now(timeZoneToUse.toZoneId()).getOffset(); + + convertFromInstantWithConfiguredTimeZone(timeZoneToUse, expectedOffset); + } + + private void convertFromInstantWithConfiguredTimeZone(TimeZone timeZoneToUse, ZoneOffset expectedZoneOffset) { + TimeZone previous = TimeZone.getDefault(); + try { + OffsetDateTime dateTime = OffsetDateTime.parse("2021-01-01T00:00:00+11:00"); + + // test ScalarTypeOffsetDateTime with the configured timeZone to use + ScalarTypeOffsetDateTime type = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.MILLIS, timeZoneToUse.toZoneId()); + + // effectively we desire to ignore the system timezone and use the configured one + TimeZone.setDefault(timeZoneToUse); + + final OffsetDateTime offsetDateTime = type.convertFromInstant(dateTime.toInstant()); + + assertEquals(expectedZoneOffset, offsetDateTime.getOffset()); + + } finally { + TimeZone.setDefault(previous); + } + } + + @Test + public void testConvertFromTimestamp() { Timestamp now = new Timestamp(System.currentTimeMillis()); @@ -69,11 +107,11 @@ public class ScalarTypeOffsetDateTimeTest { JsonTester jsonTester = new JsonTester<>(type); jsonTester.test(now); - ScalarTypeOffsetDateTime typeNanos = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.NANOS); + ScalarTypeOffsetDateTime typeNanos = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.NANOS, ZoneOffset.systemDefault()); jsonTester = new JsonTester<>(typeNanos); jsonTester.test(now); - ScalarTypeOffsetDateTime typeIso = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.ISO8601); + ScalarTypeOffsetDateTime typeIso = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.ISO8601, ZoneOffset.systemDefault()); jsonTester = new JsonTester<>(typeIso); jsonTester.test(now); } @@ -81,7 +119,7 @@ public class ScalarTypeOffsetDateTimeTest { @Test public void isoJsonFormatParse() { - ScalarTypeOffsetDateTime typeIso = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.ISO8601); + ScalarTypeOffsetDateTime typeIso = new ScalarTypeOffsetDateTime(JsonConfig.DateTime.ISO8601, ZoneOffset.systemDefault()); OffsetDateTime now = OffsetDateTime.now(); String asJson = typeIso.toJsonISO8601(now); diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTimeTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTimeTest.java index ea501b273..168041723 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTimeTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeZonedDateTimeTest.java @@ -4,7 +4,11 @@ import io.ebean.config.JsonConfig; import org.junit.Test; import java.sql.Timestamp; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.TimeZone; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; @@ -12,12 +16,12 @@ import static org.junit.Assert.*; public class ScalarTypeZonedDateTimeTest { - ScalarTypeZonedDateTime type = new ScalarTypeZonedDateTime(JsonConfig.DateTime.MILLIS); + ScalarTypeZonedDateTime type = new ScalarTypeZonedDateTime(JsonConfig.DateTime.MILLIS, ZoneId.systemDefault()); ZonedDateTime warmUp = ZonedDateTime.now(); @Test - public void testConvertToMillis() throws Exception { + public void testConvertToMillis() { warmUp.hashCode(); @@ -29,7 +33,7 @@ public class ScalarTypeZonedDateTimeTest { } @Test - public void testConvertFromTimestamp() throws Exception { + public void testConvertFromTimestamp() { Timestamp now = new Timestamp(System.currentTimeMillis()); @@ -39,6 +43,41 @@ public class ScalarTypeZonedDateTimeTest { assertEquals(now, timestamp); } + @Test + public void convertFromInstant_with_UTC_expect_matchingZoneOffset() { + final TimeZone timeZoneToUse = TimeZone.getTimeZone("UTC"); + final ZoneOffset expectedZoneOffset = ZoneOffset.UTC; + + convertFromInstantWithConfiguredTimeZone(timeZoneToUse, expectedZoneOffset); + } + + @Test + public void convertFromInstant_with_EST_expect_matchingZoneOffset() { + final TimeZone timeZoneToUse = TimeZone.getTimeZone("EST"); + final ZoneOffset expectedOffset = OffsetDateTime.now(timeZoneToUse.toZoneId()).getOffset(); + + convertFromInstantWithConfiguredTimeZone(timeZoneToUse, expectedOffset); + } + + private void convertFromInstantWithConfiguredTimeZone(TimeZone timeZoneToUse, ZoneOffset expectedZoneOffset) { + TimeZone previous = TimeZone.getDefault(); + try { + OffsetDateTime dateTime = OffsetDateTime.parse("2021-01-01T00:00:00+11:00"); + + // test ScalarTypeOffsetDateTime with the configured timeZone to use + ScalarTypeZonedDateTime type = new ScalarTypeZonedDateTime(JsonConfig.DateTime.MILLIS, timeZoneToUse.toZoneId()); + + // effectively we desire to ignore the system timezone and use the configured one + TimeZone.setDefault(timeZoneToUse); + + final ZonedDateTime zonedDateTime = type.convertFromInstant(dateTime.toInstant()); + + assertEquals(expectedZoneOffset, zonedDateTime.getOffset()); + + } finally { + TimeZone.setDefault(previous); + } + } @Test public void testToJdbcType() throws Exception { @@ -68,11 +107,11 @@ public class ScalarTypeZonedDateTimeTest { JsonTester jsonTester = new JsonTester<>(type); jsonTester.test(now); - ScalarTypeZonedDateTime typeNanos = new ScalarTypeZonedDateTime(JsonConfig.DateTime.NANOS); + ScalarTypeZonedDateTime typeNanos = new ScalarTypeZonedDateTime(JsonConfig.DateTime.NANOS, ZoneId.systemDefault()); jsonTester = new JsonTester<>(typeNanos); jsonTester.test(now); - ScalarTypeZonedDateTime typeIso = new ScalarTypeZonedDateTime(JsonConfig.DateTime.ISO8601); + ScalarTypeZonedDateTime typeIso = new ScalarTypeZonedDateTime(JsonConfig.DateTime.ISO8601, ZoneId.systemDefault()); jsonTester = new JsonTester<>(typeIso); jsonTester.test(now); } @@ -80,7 +119,7 @@ public class ScalarTypeZonedDateTimeTest { @Test public void toJsonISO8601() { - ScalarTypeZonedDateTime typeIso = new ScalarTypeZonedDateTime(JsonConfig.DateTime.ISO8601); + ScalarTypeZonedDateTime typeIso = new ScalarTypeZonedDateTime(JsonConfig.DateTime.ISO8601, ZoneId.systemDefault()); ZonedDateTime now = ZonedDateTime.now(); String asJson = typeIso.toJsonISO8601(now); diff --git a/ebean-core/src/test/java/org/tests/basic/TestFetchId.java b/ebean-core/src/test/java/org/tests/basic/TestFetchId.java index cf207995c..c830208d5 100644 --- a/ebean-core/src/test/java/org/tests/basic/TestFetchId.java +++ b/ebean-core/src/test/java/org/tests/basic/TestFetchId.java @@ -1,10 +1,14 @@ package org.tests.basic; import io.ebean.BaseTestCase; +import io.ebean.DB; import io.ebean.Ebean; import io.ebean.FutureIds; import io.ebean.Query; +import io.ebeantest.LoggedSql; + import org.tests.model.basic.Order; +import org.tests.model.basic.OrderDetail; import org.tests.model.basic.ResetBasicData; import org.junit.Test; @@ -36,4 +40,36 @@ public class TestFetchId extends BaseTestCase { List idList = futureIds.get(); assertThat(idList).isNotEmpty(); } + + @Test + public void testFetchIdWithExists() throws InterruptedException, ExecutionException { + + ResetBasicData.reset(); + + Query subQuery = Ebean.find(OrderDetail.class) + .alias("sq") + .where().raw("details.id = sq.id").query(); + Query query = Ebean.find(Order.class) + .where().exists(subQuery) + .orderBy("orderDate").query(); + + List ids = query.findIds(); + // TODO: assert(query.getGeneratedSql()) + assertThat(ids).isNotEmpty(); + FutureIds futureIds = query.findFutureIds(); + + // wait for all the id's to be fetched + List idList = futureIds.get(); + assertThat(idList).isNotEmpty(); + } + + @Test + public void testFetchIdWithOrderFormula() throws InterruptedException, ExecutionException { + + ResetBasicData.reset(); + + Query query = DB.find(Order.class).orderBy("totalItems"); + query.findIds(); + // TODO: assert(query.getGeneratedSql()) + } } diff --git a/ebean-core/src/test/java/org/tests/basic/encrypt/TestEncrypt.java b/ebean-core/src/test/java/org/tests/basic/encrypt/TestEncrypt.java index 80b4c9061..fb80fcabb 100644 --- a/ebean-core/src/test/java/org/tests/basic/encrypt/TestEncrypt.java +++ b/ebean-core/src/test/java/org/tests/basic/encrypt/TestEncrypt.java @@ -22,7 +22,7 @@ public class TestEncrypt extends BaseTestCase { @Test - @ForPlatform(Platform.H2) // only run this on H2 - PGCrypto not happy on CI server + @ForPlatform({Platform.H2, Platform.SQLSERVER}) // only run this on H2 - PGCrypto not happy on CI server public void testQueryBind() { LoggedSqlCollector.start(); @@ -36,7 +36,7 @@ public class TestEncrypt extends BaseTestCase { } @Test - @ForPlatform(Platform.H2) // only run this on H2 - PGCrypto not happy on CI server + @ForPlatform({Platform.H2, Platform.SQLSERVER}) // only run this on H2 - PGCrypto not happy on CI server public void testQueryJoin() { LoggedSqlCollector.start(); @@ -99,7 +99,7 @@ public class TestEncrypt extends BaseTestCase { } @Test - @ForPlatform(Platform.H2) + @ForPlatform({Platform.H2, Platform.SQLSERVER}) public void test() { DB.find(EBasicEncrypt.class).delete(); diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 74f004bca..106beb9f2 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -9,6 +9,7 @@ import org.tests.model.json.EBasicJsonList; import org.tests.model.json.PlainBean; import org.tests.model.json.PlainBeanDirtyAware; +import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -59,18 +60,20 @@ public class TestDbJson_Jackson3 extends BaseTestCase { EBasicJsonList bean = new EBasicJsonList(); bean.setName("p1"); bean.setPlainBean(contentBean); + bean.setBeanList(Arrays.asList(contentBean)); DB.save(bean); final EBasicJsonList found = DB.find(EBasicJsonList.class, bean.getId()); // json bean not modified but not aware // ideally don't load the json content if we are not going to modify it found.setName("p1-mod"); + found.setBeanList(null); LoggedSql.start(); DB.save(found); final List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, beans=?, bean_list=?, plain_bean=?, version=? where id=?"); + assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, plain_bean=?, version=? where id=?"); } } diff --git a/ebean-core/src/test/java/org/tests/lazyloadconf/AppConfig.java b/ebean-core/src/test/java/org/tests/lazyloadconf/AppConfig.java new file mode 100644 index 000000000..999ef1aa0 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/lazyloadconf/AppConfig.java @@ -0,0 +1,36 @@ +package org.tests.lazyloadconf; + +import io.ebean.annotation.Cache; + +import javax.persistence.*; +import java.util.List; + +@Entity +@Cache +@Table(name = "app_config") +public class AppConfig { + + @Id + @Column(name = "id") + private Integer id; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "appConfig") + //@JoinColumn(name = "id", referencedColumnName = "id") + private List items; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/ebean-core/src/test/java/org/tests/lazyloadconf/AppConfigControl.java b/ebean-core/src/test/java/org/tests/lazyloadconf/AppConfigControl.java new file mode 100644 index 000000000..8da38cd59 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/lazyloadconf/AppConfigControl.java @@ -0,0 +1,42 @@ +package org.tests.lazyloadconf; + +import javax.persistence.*; + +@Entity +@Table(name = "app_config_control") +public class AppConfigControl { + + @Id + private Integer id; + + private String name; + + @ManyToOne + @JoinColumn(name = "config_id", referencedColumnName = "id") + private AppConfig appConfig; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public AppConfig getAppConfig() { + return appConfig; + } + + public void setAppConfig(AppConfig appConfig) { + this.appConfig = appConfig; + } +} + diff --git a/ebean-core/src/test/java/org/tests/lazyloadconf/BeanCollectionLazyLoadingTest.java b/ebean-core/src/test/java/org/tests/lazyloadconf/BeanCollectionLazyLoadingTest.java new file mode 100644 index 000000000..b0ef7592b --- /dev/null +++ b/ebean-core/src/test/java/org/tests/lazyloadconf/BeanCollectionLazyLoadingTest.java @@ -0,0 +1,92 @@ +package org.tests.lazyloadconf; + +import io.ebean.DB; +import io.ebean.Query; +import org.junit.Test; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BeanCollectionLazyLoadingTest { + + @Test + public void test() { + + AppConfig globalAppConfig = new AppConfig(); + globalAppConfig.setId(1); + globalAppConfig.setItems(new ArrayList<>()); + AppConfigControl global = new AppConfigControl(); + global.setId(1); + global.setName("global"); + global.setAppConfig(globalAppConfig); + globalAppConfig.getItems().add(global); + DB.save(globalAppConfig); + + AppConfig userAppConfig = new AppConfig(); + userAppConfig.setId(2); + userAppConfig.setItems(new ArrayList<>()); + AppConfigControl user = new AppConfigControl(); + user.setId(2); + user.setName("user"); + user.setAppConfig(userAppConfig); + userAppConfig.getItems().add(user); + DB.save(userAppConfig); + + AppConfig otherAppConfig = new AppConfig(); + otherAppConfig.setId(3); + otherAppConfig.setItems(new ArrayList<>()); + DB.save(otherAppConfig); + + Relationship globalRe = new Relationship(); + globalRe.setId(1); + globalRe.setAppConfig(globalAppConfig); + DB.save(globalRe); + + Relationship userRe = new Relationship(); + userRe.setId(2); + userRe.setAppConfig(userAppConfig); + DB.save(userRe); + + Relationship otherRe = new Relationship(); + otherRe.setId(3); + otherRe.setAppConfig(otherAppConfig); + DB.save(otherRe); + + + // Start business processing + Query relationshipQuery = DB.find(Relationship.class); + List relationshipList = relationshipQuery.where().idIn(1, 2, 3).findList(); + + assertThat(relationshipList.size()).isEqualTo(3); + + Map map = relationshipList.stream() + .map(Relationship::getAppConfig) + .map((ac) -> new AbstractMap.SimpleImmutableEntry<>(ac.getId(), ac)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + AppConfig g = map.get(1); + AppConfig u = map.get(2); + AppConfig o = map.get(3); + if (!u.getItems().isEmpty()) { + // a source of the problem came from invoking lazy loading here + // with the setItems call which is unnecessary due to being a ToMany + g.setItems(u.getItems()); + } + + assertThat(g.getItems().size()).isEqualTo(1); + + // If this line of code is commented out, this test case will be successfully passed + assertThat(o.getItems().size()).isEqualTo(0); + + // org.junit.ComparisonFailure: Expected :1 Actual :2 + assertThat(g.getItems().size()).isEqualTo(1); + + assertThat(g.getItems().get(0).getName()).isEqualTo("user"); + } + +} diff --git a/ebean-core/src/test/java/org/tests/lazyloadconf/Relationship.java b/ebean-core/src/test/java/org/tests/lazyloadconf/Relationship.java new file mode 100644 index 000000000..f59e7677a --- /dev/null +++ b/ebean-core/src/test/java/org/tests/lazyloadconf/Relationship.java @@ -0,0 +1,35 @@ +package org.tests.lazyloadconf; + +import io.ebean.annotation.Cache; + +import javax.persistence.*; + +@Entity +@Cache(enableBeanCache = true) +@Table(name="app_config_re") +public class Relationship { + + @Id + private Integer id; + + @ManyToOne + @JoinColumn(name="config_id",referencedColumnName="id") + private AppConfig appConfig; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public AppConfig getAppConfig() { + return appConfig; + } + + public void setAppConfig(AppConfig appConfig) { + this.appConfig = appConfig; + } +} + diff --git a/ebean-core/src/test/java/org/tests/model/basic/EBasicEncrypt.java b/ebean-core/src/test/java/org/tests/model/basic/EBasicEncrypt.java index 859212781..31cbcab7a 100644 --- a/ebean-core/src/test/java/org/tests/model/basic/EBasicEncrypt.java +++ b/ebean-core/src/test/java/org/tests/model/basic/EBasicEncrypt.java @@ -28,7 +28,7 @@ public class EBasicEncrypt { @Encrypted(dbLength = 80) String description; - @Encrypted(dbLength = 20) + @Encrypted(dbLength = 80) Date dob; @Enumerated(EnumType.ORDINAL) diff --git a/ebean-core/src/test/java/org/tests/model/basic/MyEBasicConfigStartup.java b/ebean-core/src/test/java/org/tests/model/basic/MyEBasicConfigStartup.java index 72cb3e689..c27c5e79b 100644 --- a/ebean-core/src/test/java/org/tests/model/basic/MyEBasicConfigStartup.java +++ b/ebean-core/src/test/java/org/tests/model/basic/MyEBasicConfigStartup.java @@ -59,19 +59,19 @@ public class MyEBasicConfigStartup implements ServerConfigStartup { @Override public void inserted(Object bean) { insertCount.incrementAndGet(); - System.out.println("-- EBasic inserted " + ((EBasic) bean).getId()); + // System.out.println("-- EBasic inserted " + ((EBasic) bean).getId()); } @Override public void updated(Object bean, Set updatedProperties) { updateCount.incrementAndGet(); - System.out.println("-- EBasic updated " + ((EBasic) bean).getId() + " updatedProperties: " + updatedProperties); + // System.out.println("-- EBasic updated " + ((EBasic) bean).getId() + " updatedProperties: " + updatedProperties); } @Override public void deleted(Object bean) { deleteCount.incrementAndGet(); - System.out.println("-- EBasic deleted " + ((EBasic) bean).getId()); + // System.out.println("-- EBasic deleted " + ((EBasic) bean).getId()); } } diff --git a/ebean-core/src/test/java/org/tests/model/basic/Order.java b/ebean-core/src/test/java/org/tests/model/basic/Order.java index b15765b51..cea0a6e00 100644 --- a/ebean-core/src/test/java/org/tests/model/basic/Order.java +++ b/ebean-core/src/test/java/org/tests/model/basic/Order.java @@ -101,7 +101,7 @@ public class Order implements Serializable { @DocEmbedded List details; - @OneToMany(cascade = CascadeType.ALL, mappedBy = "order") + @OneToMany(cascade = CascadeType.ALL, mappedBy = "order", orphanRemoval = true) List shipments; @Override diff --git a/ebean-core/src/test/java/org/tests/model/bridge/TestIdClassScalar.java b/ebean-core/src/test/java/org/tests/model/bridge/TestIdClassScalar.java index 06b2ee143..a06c7e173 100644 --- a/ebean-core/src/test/java/org/tests/model/bridge/TestIdClassScalar.java +++ b/ebean-core/src/test/java/org/tests/model/bridge/TestIdClassScalar.java @@ -1,7 +1,9 @@ package org.tests.model.bridge; import io.ebean.BaseTestCase; +import io.ebean.DB; import io.ebean.Ebean; +import io.ebean.Transaction; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; @@ -39,6 +41,28 @@ public class TestIdClassScalar extends BaseTestCase { } + @Test + public void insertBatch() { + UUID siteId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + try (final Transaction transaction = DB.beginTransaction()) { + transaction.setBatchMode(true); + + + BSiteUserD access = new BSiteUserD(BAccessLevel.ONE, siteId, userId); + DB.save(access); + + final UUID siteId1 = access.getSiteId(); // ArrayIndexOutOfBoundsException here + assertThat(siteId1).isNotNull(); + assertThat(access.getUserId()).isEqualTo(userId); + + transaction.commit(); + } + + DB.delete(BSiteUserD.class, new BEmbId(siteId, userId)); + } + @Test public void test() { diff --git a/ebean-core/src/test/java/org/tests/model/m2m/MnyEdge.java b/ebean-core/src/test/java/org/tests/model/m2m/MnyEdge.java new file mode 100644 index 000000000..2e86cb9df --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/m2m/MnyEdge.java @@ -0,0 +1,57 @@ +package org.tests.model.m2m; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +import io.ebean.annotation.Index; + +@Entity +@Index(unique = true, columnNames = { "from_id", "to_id" }) +@Index(unique = true, columnNames = { "to_id", "from_id" }) +public class MnyEdge { + + @Id + private Integer id; + + @ManyToOne + private MnyNode from; + + @ManyToOne + private MnyNode to; + + private int flags; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public MnyNode getFrom() { + return from; + } + + public void setFrom(MnyNode from) { + this.from = from; + } + + public MnyNode getTo() { + return to; + } + + public void setTo(MnyNode to) { + this.to = to; + } + + public int getFlags() { + return flags; + } + + public void setFlags(int flags) { + this.flags = flags; + } + +} diff --git a/ebean-core/src/test/java/org/tests/model/m2m/MnyNode.java b/ebean-core/src/test/java/org/tests/model/m2m/MnyNode.java new file mode 100644 index 000000000..e51c5f417 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/m2m/MnyNode.java @@ -0,0 +1,129 @@ +package org.tests.model.m2m; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; + +import io.ebean.annotation.Platform; +import io.ebean.annotation.Where; + +import java.util.List; + +@Entity +public class MnyNode { + + @Id + Integer id; + + String name; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id")) + List allRelations; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) + List allReverseRelations; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id")) + @Where(clause = "${mta}.flags & 1 != 0") + @Where(clause = "BITAND(${mta}.flags, 1) != 0", platforms = Platform.H2) + List bit1Relations; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) + @Where(clause = "${mta}.flags & 1 != 0") + @Where(clause = "BITAND(${mta}.flags, 1) != 0", platforms = Platform.H2) + List bit1ReverseRelations; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id")) + @Where(clause = "${mta}.flags & 2 != 0") + @Where(clause = "BITAND(${mta}.flags, 2) != 0", platforms = Platform.H2) + List bit2Relations; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) + @Where(clause = "${mta}.flags & 2 != 0") + @Where(clause = "BITAND(${mta}.flags, 2) != 0", platforms = Platform.H2) + List bit2ReverseRelations; + + @ManyToMany + @JoinTable(name = "mny_edge", + joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) + @Where(clause = "'${dbTableName}' = ${ta}.name") + List withDbTableName; + + public MnyNode() { + + } + + public MnyNode(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getAllRelations() { + return allRelations; + } + + public List getAllReverseRelations() { + return allReverseRelations; + } + + public List getBit1Relations() { + return bit1Relations; + } + + public List getBit1ReverseRelations() { + return bit1ReverseRelations; + } + + public List getBit2Relations() { + return bit2Relations; + } + + public List getBit2ReverseRelations() { + return bit2ReverseRelations; + } + + public List getWithDbTableName() { + return withDbTableName; + } + + public void setWithDbTableName(List withDbTableName) { + this.withDbTableName = withDbTableName; + } + +} diff --git a/ebean-core/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java b/ebean-core/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java new file mode 100644 index 000000000..da8d83dd5 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java @@ -0,0 +1,159 @@ +package org.tests.model.m2m; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.Test; +import org.tests.model.m2m.MnyEdge; +import org.tests.model.m2m.MnyNode; + +import io.ebean.BaseTestCase; +import io.ebean.DB; +import io.ebeantest.LoggedSql; + +/** + * Tests M2M with complex where queries. + * + * @author Roland Praml, FOCONIS AG + * + */ +public class TestM2MWithWhere extends BaseTestCase { + + @Test + public void testQuery() throws Exception { + createTestData(); + MnyNode node = DB.find(MnyNode.class, 1); + + + List result = DB.find(MnyNode.class).where().eq("allRelations", node).findList(); + assertThat(result).extracting(MnyNode::getId).containsExactly(1, 2, 3, 4, 5); + + result = DB.find(MnyNode.class).where().eq("allReverseRelations", node).findList(); + assertThat(result).extracting(MnyNode::getId).containsExactly(1, 2, 3, 4, 5); + + result = DB.find(MnyNode.class).where().eq("bit1Relations", node).findList(); + assertThat(result).isEmpty(); // -> to = 1 column: 2 0 2 0 2 + + result = DB.find(MnyNode.class).where().eq("bit2Relations", node).findList(); + assertThat(result).extracting(MnyNode::getId).containsExactly(1, 3, 5); + + result = DB.find(MnyNode.class).where().eq("bit1ReverseRelations", node).findList(); + // -> from = 1 column: 2 1 3 1 3 + assertThat(result).extracting(MnyNode::getId).containsExactly(2, 3, 4, 5); + + result = DB.find(MnyNode.class).where().eq("bit2ReverseRelations", node).findList(); + assertThat(result).hasSize(3).extracting(MnyNode::getId).containsExactly(1, 3, 5); + + result = DB.find(MnyNode.class).where().eq("bit2ReverseRelations", node).findList(); + assertThat(result).hasSize(3).extracting(MnyNode::getId).containsExactly(1, 3, 5); + } + + @Test + public void testGetter() throws Exception { + createTestData(); + MnyNode node = DB.find(MnyNode.class, 3); + + assertThat(node.getAllRelations()).extracting(MnyNode::getId).containsExactly(1, 2, 3, 4, 5); + + assertThat(node.getAllReverseRelations()).extracting(MnyNode::getId).containsExactly(1, 2, 3, 4, 5); + + assertThat(node.getBit1Relations()).extracting(MnyNode::getId).containsExactly(4, 5); + + assertThat(node.getBit1ReverseRelations()).extracting(MnyNode::getId).containsExactly(1, 2); + + assertThat(node.getBit2Relations()).extracting(MnyNode::getId).containsExactly(1, 3, 5); + + LoggedSql.start(); + assertThat(node.getBit2ReverseRelations()).extracting(MnyNode::getId).containsExactly(1, 3, 5); + List sqls = LoggedSql.stop(); + assertThat(sqls).hasSize(1); // lazy load + + // prefetch everything + LoggedSql.start(); + node = DB.find(MnyNode.class) + .fetch("bit1Relations","*") + .fetch("bit1ReverseRelations","*") + .where().idEq(3).findOne(); + sqls = LoggedSql.stop(); + assertThat(sqls).hasSize(2); + + // no lazyLoad expected + LoggedSql.start(); + assertThat(node.getBit1Relations()).extracting(MnyNode::getId).containsExactly(4, 5); + assertThat(node.getBit1ReverseRelations()).extracting(MnyNode::getId).containsExactly(1, 2); + sqls = LoggedSql.stop(); + assertThat(sqls).hasSize(0); + + } + + // to = | 1 2 3 4 5 + // ---------+--------------- + // from = 1 | 2 1 3 1 3 + // from = 2 | 0 2 1 3 1 + // from = 3 | 2 0 2 1 3 + // from = 4 | 0 2 0 2 1 + // from = 5 | 2 0 2 0 2 + private void createTestData() { + DB.find(MnyEdge.class).delete(); + DB.find(MnyNode.class).delete(); + for (int i = 1; i <= 5; i++) { + MnyNode node = new MnyNode(); + node.setId(i); + node.setName("Node #" + i); + DB.save(node); + } + StringBuilder sb = new StringBuilder(); + for (int from = 1; from <= 5; from++) { + sb.append("from = ").append(from).append(" |"); + for (int to = 1; to <= 5; to++) { + MnyEdge edge = new MnyEdge(); + edge.setFrom(DB.getReference(MnyNode.class, from)); + edge.setTo(DB.getReference(MnyNode.class, to)); + int flags = 0; + if (from < to) { + flags |= 1; + } + if ((from + to) % 2 == 0) { + flags |= 2; + } + edge.setFlags(flags); + DB.save(edge); + sb.append(" ").append(flags); + } + sb.append('\n'); + } + // System.out.println(sb); dump the table + } + + + @Test + public void testWithDbTableName() { + LoggedSql.start(); + DB.find(MnyNode.class).where().isNotNull("withDbTableName.name").findList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("'mny_node' = u1.name"); + + LoggedSql.start(); + DB.find(MnyNode.class).where().isNotEmpty("withDbTableName").findList(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("'mny_node' = x2.name"); + } + + @Test + public void testLazyLoad() throws Exception { + MnyNode el = new MnyNode("testLazyLoad"); + DB.save(el); + LoggedSql.start(); + el = DB.find(MnyNode.class).select("name").where().eq("name", "testLazyLoad").findOne(); + el.getWithDbTableName().size(); // trigger Lazy load + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id, t0.name from mny_node"); + assertThat(sql.get(1)).contains("where 'mny_node' = t0.name"); + DB.delete(el); + } + +} diff --git a/ebean-core/src/test/java/org/tests/model/map/BeanMapOrphanRemovalTest.java b/ebean-core/src/test/java/org/tests/model/map/BeanMapOrphanRemovalTest.java new file mode 100644 index 000000000..48288e7a3 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/map/BeanMapOrphanRemovalTest.java @@ -0,0 +1,50 @@ +package org.tests.model.map; + +import io.ebean.DB; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class BeanMapOrphanRemovalTest { + + @Test + public void keySet_retainAll() { + + MpUser user = new MpUser(); + user.setName("u1"); + addRoles(user, "r1", "r2", "r3", "r4"); + DB.save(user); + + final MpUser user1 = DB.find(MpUser.class, user.getId()); + final Map roles = user1.getRoles(); + assertThat(roles).hasSize(4); + + + final Set keySet = roles.keySet(); + keySet.retainAll(Arrays.asList("r2", "r3")); + + DB.save(user1); + + final MpUser user2 = DB.find(MpUser.class, user.getId()); + final Map roles2 = user2.getRoles(); + assertThat(roles2).hasSize(2); + } + + private void addRoles(MpUser user, String... roles){ + for (String code : roles) { + MpRole role = newRole(code); + user.getRoles().put(role.getCode(), role); + } + } + + private MpRole newRole(String code) { + MpRole role = new MpRole(); + role.setCode(code); + return role; + } +} diff --git a/ebean-core/src/test/java/org/tests/model/map/MpUser.java b/ebean-core/src/test/java/org/tests/model/map/MpUser.java index 100e1e6a7..94edddb19 100644 --- a/ebean-core/src/test/java/org/tests/model/map/MpUser.java +++ b/ebean-core/src/test/java/org/tests/model/map/MpUser.java @@ -1,11 +1,7 @@ package org.tests.model.map; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.MapKey; -import javax.persistence.OneToMany; -import java.util.HashMap; +import javax.persistence.*; +import java.util.LinkedHashMap; import java.util.Map; @Entity @@ -16,9 +12,9 @@ public class MpUser { private String name; - @OneToMany(cascade = CascadeType.ALL) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @MapKey(name = "code") - public Map roles = new HashMap<>(); + private Map roles = new LinkedHashMap<>(); public Long getId() { return id; diff --git a/ebean-core/src/test/java/org/tests/model/onetoone/calcd/CalcDData.java b/ebean-core/src/test/java/org/tests/model/onetoone/calcd/CalcDData.java new file mode 100644 index 000000000..eef657be5 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/onetoone/calcd/CalcDData.java @@ -0,0 +1,46 @@ +package org.tests.model.onetoone.calcd; + +import io.ebean.Model; +import io.ebean.annotation.ConstraintMode; +import io.ebean.annotation.DbForeignKey; + +import javax.persistence.*; + +@Entity +@Table(name = "calcd_data") +public class CalcDData extends Model { + + @Id + private Integer id; + + @OneToOne(optional = false, cascade = CascadeType.ALL) + @DbForeignKey(onDelete = ConstraintMode.CASCADE) + @PrimaryKeyJoinColumn//(name = "Id", referencedColumnName = "Id") + private CalcDInput input; + + private final String name; + + public CalcDData(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public CalcDInput getInput() { + return input; + } + + public void setInput(CalcDInput input) { + this.input = input; + } + + public String getName() { + return name; + } +} diff --git a/ebean-core/src/test/java/org/tests/model/onetoone/calcd/CalcDInput.java b/ebean-core/src/test/java/org/tests/model/onetoone/calcd/CalcDInput.java new file mode 100644 index 000000000..00704eb27 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/onetoone/calcd/CalcDInput.java @@ -0,0 +1,42 @@ +package org.tests.model.onetoone.calcd; + +import io.ebean.Model; + +import javax.persistence.*; + +@Entity +@Table(name = "calcd_input") +public class CalcDInput extends Model { + + @Id + private Integer id; + + @OneToOne(optional = false, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy = "input") + private CalcDData data; + + private final String name; + + public CalcDInput(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public CalcDData getData() { + return data; + } + + public void setData(CalcDData data) { + this.data = data; + } +} diff --git a/ebean-core/src/test/java/org/tests/model/onetoone/calcd/TestOneToOnePrimaryKeyJoinMapping.java b/ebean-core/src/test/java/org/tests/model/onetoone/calcd/TestOneToOnePrimaryKeyJoinMapping.java new file mode 100644 index 000000000..9e1aa68f0 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/onetoone/calcd/TestOneToOnePrimaryKeyJoinMapping.java @@ -0,0 +1,26 @@ +package org.tests.model.onetoone.calcd; + +import io.ebean.DB; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestOneToOnePrimaryKeyJoinMapping { + + @Test + public void test() { + + CalcDInput inputs = new CalcDInput("input0"); + + CalcDData data = new CalcDData("data0"); + inputs.setData(data); + inputs.save(); + + CalcDInput found = DB.find(CalcDInput.class, inputs.getId()); + + assertThat(found).isNotNull(); + assertThat(found.getName()).isEqualTo("input0"); + assertThat(found.getData().getName()).isEqualTo("data0"); + + } +} diff --git a/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelX.java b/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelX.java new file mode 100644 index 000000000..fbc3f9964 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelX.java @@ -0,0 +1,54 @@ +package org.tests.model.softdelete; + +import io.ebean.annotation.SoftDelete; + +import javax.persistence.*; +import java.util.UUID; + +@Entity +public class ESoftDelX { + + @Id + private UUID id; + + @OneToOne + private ESoftDelY y; + + @ManyToOne + private ESoftDelZ organization; + + @SoftDelete + boolean deleted; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public ESoftDelY getY() { + return y; + } + + public void setY(ESoftDelY y) { + this.y = y; + } + + public ESoftDelZ getOrganization() { + return organization; + } + + public void setOrganization(ESoftDelZ organization) { + this.organization = organization; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } +} diff --git a/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelY.java b/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelY.java new file mode 100644 index 000000000..a65cc75c1 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelY.java @@ -0,0 +1,53 @@ +package org.tests.model.softdelete; + +import io.ebean.annotation.SoftDelete; + +import javax.persistence.*; + +@Entity +public class ESoftDelY { + + @Id + private Long id; + + @ManyToOne + private ESoftDelZ organization; + + @OneToOne(mappedBy = "y") + private ESoftDelX x; + + @SoftDelete + boolean deleted; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ESoftDelZ getOrganization() { + return organization; + } + + public void setOrganization(ESoftDelZ organization) { + this.organization = organization; + } + + public ESoftDelX getX() { + return x; + } + + public void setX(ESoftDelX x) { + this.x = x; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } +} diff --git a/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelZ.java b/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelZ.java new file mode 100644 index 000000000..5ce11c175 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/softdelete/ESoftDelZ.java @@ -0,0 +1,43 @@ +package org.tests.model.softdelete; + +import io.ebean.annotation.SoftDelete; + +import javax.persistence.Entity; +import javax.persistence.Id; +import java.util.UUID; + +@Entity +public class ESoftDelZ { + + @Id + private Long id; + + @SoftDelete + private boolean deleted; + + private UUID uuid; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } +} diff --git a/ebean-core/src/test/java/org/tests/o2m/OmBasicParent.java b/ebean-core/src/test/java/org/tests/o2m/OmBasicParent.java index 76faeca0e..15b117bd0 100644 --- a/ebean-core/src/test/java/org/tests/o2m/OmBasicParent.java +++ b/ebean-core/src/test/java/org/tests/o2m/OmBasicParent.java @@ -4,6 +4,9 @@ import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Version; + +import io.ebean.annotation.Where; + import java.util.List; import static javax.persistence.CascadeType.ALL; @@ -26,6 +29,10 @@ public class OmBasicParent { @OneToMany(cascade = ALL, mappedBy = "parent") private List children; + @OneToMany(cascade = ALL, mappedBy = "parent") + @Where(clause = "'${dbTableName}' = ${ta}.name") + private List childrenWithWhere; + public OmBasicParent(String name) { this.name = name; } @@ -57,4 +64,13 @@ public class OmBasicParent { public void setChildren(List children) { this.children = children; } + + public List getChildrenWithWhere() { + return childrenWithWhere; + } + + public void setChildrenWithWhere(List childrenWithWhere) { + this.childrenWithWhere = childrenWithWhere; + } + } diff --git a/ebean-core/src/test/java/org/tests/o2m/TestOneToManyWhere.java b/ebean-core/src/test/java/org/tests/o2m/TestOneToManyWhere.java new file mode 100644 index 000000000..e2dca664a --- /dev/null +++ b/ebean-core/src/test/java/org/tests/o2m/TestOneToManyWhere.java @@ -0,0 +1,44 @@ +package org.tests.o2m; + +import io.ebean.BaseTestCase; +import io.ebean.DB; +import io.ebeantest.LoggedSql; + +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestOneToManyWhere extends BaseTestCase { + + @Test + public void testWithDbTableName() { + LoggedSql.start(); + DB.find(OmBasicParent.class).where().isNotNull("childrenWithWhere.name").findList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("'om_basic_parent' = u1.name"); + + LoggedSql.start(); + DB.find(OmBasicParent.class).where().isNotEmpty("childrenWithWhere").findList(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("'om_basic_parent' = x.name"); + } + + @Test + public void testLazyLoad() throws Exception { + OmBasicParent el = new OmBasicParent("testLazyLoad"); + DB.save(el); + LoggedSql.start(); + el = DB.find(OmBasicParent.class).select("name").where().eq("name", "testLazyLoad").findOne(); + el.getChildrenWithWhere().size(); // trigger Lazy load + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id, t0.name from om_basic_parent"); + assertThat(sql.get(1)).contains("where 'om_basic_parent' = t0.name"); + DB.delete(el); + } + +} diff --git a/ebean-core/src/test/java/org/tests/query/TestImplicitJoinOnParentRelationship.java b/ebean-core/src/test/java/org/tests/query/TestImplicitJoinOnParentRelationship.java index c4aa242eb..eac3303fb 100644 --- a/ebean-core/src/test/java/org/tests/query/TestImplicitJoinOnParentRelationship.java +++ b/ebean-core/src/test/java/org/tests/query/TestImplicitJoinOnParentRelationship.java @@ -24,11 +24,11 @@ public class TestImplicitJoinOnParentRelationship extends BaseTestCase { query.findList(); if (isPostgres()) { - String expectedSql = "select distinct on (t0.id) t0.id, t0.name from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id join o_order_detail u2 on u2.order_id = u1.id join o_product u3 on u3.id = u2.product_id where u3.name = ?"; + String expectedSql = "select distinct on (t0.id) t0.id, t0.name from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null join o_order_detail u2 on u2.order_id = u1.id and u2.id > 0 join o_product u3 on u3.id = u2.product_id where u3.name = ?"; assertThat(sqlOf(query, 1)).contains(expectedSql); } else { - String expectedSql = "select distinct t0.id, t0.name from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id join o_order_detail u2 on u2.order_id = u1.id join o_product u3 on u3.id = u2.product_id where u3.name = ?"; + String expectedSql = "select distinct t0.id, t0.name from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null join o_order_detail u2 on u2.order_id = u1.id and u2.id > 0 join o_product u3 on u3.id = u2.product_id where u3.name = ?"; assertThat(sqlOf(query, 1)).contains(expectedSql); } @@ -55,10 +55,10 @@ public class TestImplicitJoinOnParentRelationship extends BaseTestCase { query.findList(); if (isPostgres()) { - String expectedSql = "select distinct on (t0.id) t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id left join o_order_detail u2 on u2.order_id = u1.id left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; + String expectedSql = "select distinct on (t0.id) t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null left join o_order_detail u2 on u2.order_id = u1.id and u2.id > 0 left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; assertThat(sqlOf(query, 1)).contains(expectedSql); } else { - String expectedSql = "select distinct t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id left join o_order_detail u2 on u2.order_id = u1.id left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; + String expectedSql = "select distinct t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null left join o_order_detail u2 on u2.order_id = u1.id and u2.id > 0 left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; assertThat(sqlOf(query, 1)).contains(expectedSql); } } @@ -76,11 +76,11 @@ public class TestImplicitJoinOnParentRelationship extends BaseTestCase { query.findList(); if (isPostgres()) { - String expectedSql = "select distinct on (t0.id) t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id left join o_order_detail u2 on u2.order_id = u1.id left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; + String expectedSql = "select distinct on (t0.id) t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null left join o_order_detail u2 on u2.order_id = u1.id and u2.id > 0 left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; assertThat(sqlOf(query, 1)).contains(expectedSql); } else { - String expectedSql = "select distinct t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id left join o_order_detail u2 on u2.order_id = u1.id left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; + String expectedSql = "select distinct t0.id, t0.name from o_customer t0 left join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null left join o_order_detail u2 on u2.order_id = u1.id and u2.id > 0 left join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ?)"; assertThat(sqlOf(query, 1)).contains(expectedSql); } } diff --git a/ebean-core/src/test/java/org/tests/query/TestManyWhereJoin.java b/ebean-core/src/test/java/org/tests/query/TestManyWhereJoin.java index 250a32709..b90751c83 100644 --- a/ebean-core/src/test/java/org/tests/query/TestManyWhereJoin.java +++ b/ebean-core/src/test/java/org/tests/query/TestManyWhereJoin.java @@ -41,7 +41,7 @@ public class TestManyWhereJoin extends BaseTestCase { } assertThat(sql).contains("join o_order "); assertThat(sql).contains(".status = ?"); - assertThat(sql).contains("t0.id, t0.status from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id where u1.status = ?"); + assertThat(sql).contains("t0.id, t0.status from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id and u1.order_date is not null where u1.status = ?"); } @Test diff --git a/ebean-core/src/test/java/org/tests/query/TestQueryFilterMany.java b/ebean-core/src/test/java/org/tests/query/TestQueryFilterMany.java index 976dac580..01275cfa0 100644 --- a/ebean-core/src/test/java/org/tests/query/TestQueryFilterMany.java +++ b/ebean-core/src/test/java/org/tests/query/TestQueryFilterMany.java @@ -172,7 +172,6 @@ public class TestQueryFilterMany extends BaseTestCase { LoggedSqlCollector.start(); Query query = Ebean.find(Customer.class) - .fetch("orders") .filterMany("orders").raw("1=0") .where().isNotEmpty("orders") .query(); @@ -183,12 +182,10 @@ public class TestQueryFilterMany extends BaseTestCase { } List sqlList = LoggedSqlCollector.stop(); - assertEquals(2, sqlList.size()); - assertThat(sqlList.get(0)).contains("where exists (select 1 from o_order x where x.kcustomer_id = t0.id)"); - assertThat(sqlList.get(1)).contains("and 1=0"); + assertEquals(1, sqlList.size()); + assertThat(sqlList.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where exists (select 1 from o_order x where x.kcustomer_id = t0.id and x.order_date is not null) and 1=0 order by t0.id"); } - @Test public void test_filterMany_in_findCount() { @@ -212,7 +209,6 @@ public class TestQueryFilterMany extends BaseTestCase { public void test_filterMany_copy_findList() { ResetBasicData.reset(); - LoggedSqlCollector.start(); Query query = Ebean.find(Customer.class) @@ -222,17 +218,35 @@ public class TestQueryFilterMany extends BaseTestCase { query.copy().findList(); + List sqlList = LoggedSqlCollector.stop(); + assertEquals(1, sqlList.size()); + assertThat(sqlList.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where t1.status in (?) order by t0.id"); + } + + @Test + public void test_filterMany_fetchQuery() { + + ResetBasicData.reset(); + LoggedSqlCollector.start(); + + Query query = Ebean.find(Customer.class) + .fetchQuery("orders") // explicitly fetch orders separately + .filterMany("orders").in("status", Order.Status.NEW) + .order().asc("id"); + + query.findList(); + List sqlList = LoggedSqlCollector.stop(); assertEquals(2, sqlList.size()); assertThat(sqlList.get(0)).contains("from o_customer t0"); - assertThat(sqlList.get(1)).contains("from o_order t0 join o_customer t1"); + assertThat(sqlList.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) in "); + assertThat(sqlList.get(1)).contains(" and t0.status in "); } @Test public void testDisjunction() { ResetBasicData.reset(); - LoggedSqlCollector.start(); Ebean.find(Customer.class) @@ -243,8 +257,8 @@ public class TestQueryFilterMany extends BaseTestCase { .findList(); List sql = LoggedSqlCollector.stop(); - assertEquals(2, sql.size()); - assertSql(sql.get(1)).contains("and (t0.status = ? or t0.order_date = ?"); + assertEquals(1, sql.size()); + assertSql(sql.get(0)).contains(" from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where (t1.status = ? or t1.order_date = ?) order by t0.id"); } @Test @@ -259,12 +273,10 @@ public class TestQueryFilterMany extends BaseTestCase { List sql = LoggedSqlCollector.stop(); - assertThat(sql).hasSize(3); - assertSql(sql.get(0)).contains(" from o_customer t0; --bind()"); - platformAssertIn(sql.get(1), " from contact t0 where (t0.customer_id)"); - assertSql(sql.get(1)).contains(" and t0.first_name is not null"); - platformAssertIn(sql.get(2), " from contact_note t0 where (t0.contact_id)"); - assertSql(sql.get(2)).contains(" and lower(t0.title) like"); + assertThat(sql).hasSize(2); + assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id where t1.first_name is not null order by t0.id; --bind()"); + platformAssertIn(sql.get(1), " from contact_note t0 where (t0.contact_id)"); + assertSql(sql.get(1)).contains(" and lower(t0.title) like"); } @Test @@ -280,9 +292,7 @@ public class TestQueryFilterMany extends BaseTestCase { List sql = LoggedSqlCollector.stop(); - assertThat(sql).hasSize(2); - assertSql(sql.get(0)).contains(" from o_customer t0"); - assertSql(sql.get(1)).contains("from contact t0 where "); - assertSql(sql.get(1)).contains("and (t0.first_name is not null and lower(t0.email) like ?"); + assertThat(sql).hasSize(1); + assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id where (t1.first_name is not null and lower(t1.email) like ? escape'|' ) order by t0.id; --bind(rob%)"); } } diff --git a/ebean-core/src/test/java/org/tests/query/TestQueryFindStream.java b/ebean-core/src/test/java/org/tests/query/TestQueryFindStream.java index b05ada969..c4a852f38 100644 --- a/ebean-core/src/test/java/org/tests/query/TestQueryFindStream.java +++ b/ebean-core/src/test/java/org/tests/query/TestQueryFindStream.java @@ -39,9 +39,7 @@ public class TestQueryFindStream extends BaseTestCase { @Test public void findLargeStream_basic() { - ResetBasicData.reset(); - try (Stream stream = DB.find(Customer.class) .findLargeStream()) { @@ -61,8 +59,16 @@ public class TestQueryFindStream extends BaseTestCase { } @Test - public void manualTest_findSteam_when_streamNotClosed_connectionLeak() { + public void manualTest_findSteam_when_closeWithResources() { + // confirm manually the stream is closed via try with resources block + try (Stream stream = DB.find(Customer.class).findStream()) { + // silly assert + assertThat(stream.hashCode()).isNotZero(); + } + } + @Test + public void manualTest_findSteam_when_streamNotClosed_connectionLeak() { Stream stream = DB.find(Customer.class).findStream(); // remember a steam MUST be closed or we leak resources // comment out the close(); below to leak a connection diff --git a/ebean-core/src/test/java/org/tests/query/TestQueryIsNull.java b/ebean-core/src/test/java/org/tests/query/TestQueryIsNull.java index d2689337b..f80aee9f5 100644 --- a/ebean-core/src/test/java/org/tests/query/TestQueryIsNull.java +++ b/ebean-core/src/test/java/org/tests/query/TestQueryIsNull.java @@ -1,14 +1,15 @@ package org.tests.query; import io.ebean.BaseTestCase; -import io.ebean.Ebean; +import io.ebean.DB; import io.ebean.Query; import org.tests.model.basic.Order; import org.tests.model.basic.ResetBasicData; import org.tests.model.m2m.Role; import org.junit.Test; -import static org.junit.Assert.assertTrue; + +import static org.assertj.core.api.Assertions.assertThat; public class TestQueryIsNull extends BaseTestCase { @@ -17,90 +18,90 @@ public class TestQueryIsNull extends BaseTestCase { public void queryShouldContainIsNullOnColumn() { ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isNull("customerName").query(); + Query query = DB.find(Order.class).where().isNull("customerName").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains("name is null")); + assertThat(query.getGeneratedSql()).contains("name is null"); } @Test public void isNotNull_when_OneToMany_expect_existsSubquery() { ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isNotNull("details").query(); + Query query = DB.find(Order.class).where().isNotNull("details").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains(" where exists (select 1 from o_order_detail x where x.order_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains(" where exists (select 1 from o_order_detail x where x.order_id = t0.id and x.id > 0)"); } @Test public void isNotEmpty_when_OneToMany_expect_existsSubquery() { ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isNotEmpty("details").query(); + Query query = DB.find(Order.class).where().isNotEmpty("details").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains(" where exists (select 1 from o_order_detail x where x.order_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains(" where exists (select 1 from o_order_detail x where x.order_id = t0.id and x.id > 0)"); } @Test public void isNull_when_OneToMany_expect_notExistsSubquery() { ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isNull("details").query(); + Query query = DB.find(Order.class).where().isNull("details").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains(" where not exists (select 1 from o_order_detail x where x.order_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains(" where not exists (select 1 from o_order_detail x where x.order_id = t0.id and x.id > 0)"); } @Test public void isEmpty_when_OneToMany_expect_notExistsSubquery() { ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isEmpty("details").query(); + Query query = DB.find(Order.class).where().isEmpty("details").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains(" where not exists (select 1 from o_order_detail x where x.order_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains(" where not exists (select 1 from o_order_detail x where x.order_id = t0.id and x.id > 0)"); } @Test public void isEmpty_when_ManyToMany_expect_notExistsSubqueryAndNoJoin() { ResetBasicData.reset(); - Query query = Ebean.find(Role.class).where().isEmpty("permissions").query(); + Query query = DB.find(Role.class).where().isEmpty("permissions").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains("from mt_role t0 where not exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains("from mt_role t0 where not exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)"); } @Test public void isNull_when_ManyToMany_expect_notExistsSubqueryAndNoJoin() { ResetBasicData.reset(); - Query query = Ebean.find(Role.class).where().isNull("permissions").query(); + Query query = DB.find(Role.class).where().isNull("permissions").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains("from mt_role t0 where not exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains("from mt_role t0 where not exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)"); } @Test public void isNotEmpty_when_ManyToMany_expect_existsSubqueryAndNoJoin() { ResetBasicData.reset(); - Query query = Ebean.find(Role.class).where().isNotEmpty("permissions").query(); + Query query = DB.find(Role.class).where().isNotEmpty("permissions").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains("from mt_role t0 where exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains("from mt_role t0 where exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)"); } @Test public void isNotNull_when_ManyToMany_expect_existsSubqueryAndNoJoin() { ResetBasicData.reset(); - Query query = Ebean.find(Role.class).where().isNotNull("permissions").query(); + Query query = DB.find(Role.class).where().isNotNull("permissions").query(); query.findList(); - assertTrue(query.getGeneratedSql().contains("from mt_role t0 where exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)")); + assertThat(query.getGeneratedSql()).contains("from mt_role t0 where exists (select 1 from mt_role_permission x where x.mt_role_id = t0.id)"); } } diff --git a/ebean-core/src/test/java/org/tests/query/cancel/EBasicDto.java b/ebean-core/src/test/java/org/tests/query/cancel/EBasicDto.java new file mode 100644 index 000000000..49b6b157c --- /dev/null +++ b/ebean-core/src/test/java/org/tests/query/cancel/EBasicDto.java @@ -0,0 +1,28 @@ +package org.tests.query.cancel; + +import org.tests.model.basic.EBasic.Status; + +/** + * DTO for Ebasic Queries. + */ +public class EBasicDto { + private Integer id; + + private Status status; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } +} \ No newline at end of file diff --git a/ebean-core/src/test/java/org/tests/query/cancel/SlowDownEBasic.java b/ebean-core/src/test/java/org/tests/query/cancel/SlowDownEBasic.java new file mode 100644 index 000000000..68d6c038b --- /dev/null +++ b/ebean-core/src/test/java/org/tests/query/cancel/SlowDownEBasic.java @@ -0,0 +1,57 @@ +package org.tests.query.cancel; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.h2.api.Trigger; + +import io.ebean.DB; +import io.ebean.Transaction; + +/** + * Class to artificially slow down selects on 'e_basic' table + */ +public class SlowDownEBasic implements Trigger { + + private static int wait; + + private static boolean triggerInstalled; + + @Override + public void init(final Connection conn, final String schemaName, final String triggerName, final String tableName, + final boolean before, final int type) { + } + + @Override + public void fire(final Connection conn, final Object[] oldRow, final Object[] newRow) { + try { + Thread.sleep(wait); + } catch (InterruptedException e) { + // nop + } + } + + @Override + public void close() { + } + + @Override + public void remove() { + } + + + public static void setSelectWaitMillis(final int wait) throws SQLException { + SlowDownEBasic.wait = wait; + if (triggerInstalled) { + return; + } + triggerInstalled = true; + try (Transaction txn = DB.beginTransaction(); Statement stmt = txn.getConnection().createStatement()) { + + stmt.execute("CREATE TRIGGER SLOW_DOWN_E_BASIC BEFORE SELECT ON e_basic " + "CALL \"" + + SlowDownEBasic.class.getName() + "\""); + txn.commit(); + } + } +} diff --git a/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java b/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java new file mode 100644 index 000000000..d3dda7443 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java @@ -0,0 +1,362 @@ +package org.tests.query.cancel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.SQLException; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.persistence.PersistenceException; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.tests.model.basic.EBasic; + +import io.ebean.BaseTestCase; +import io.ebean.DB; +import io.ebean.DtoQuery; +import io.ebean.Query; +import io.ebean.QueryIterator; +import io.ebean.SqlQuery; +import io.ebean.annotation.ForPlatform; +import io.ebean.annotation.Platform; + +/** + * Tests, if all kind of queries are cancelable. There are two ways how to + * cancel a query:
+ * At begin: + * + *
+ * query = DB.find(...)
+ * query.cancel();
+ * query.findList();
+ * 
+ * + * The query was caneled before executing. In this case we do hit the DB driver + *
+ *
+ * During run: + * + *
+ * // Thread 1:              Thread 2
+ * query = DB.find(...)
+ * query.findList();
+ *    ...finding
+ *     ...finding            query.cancel();
+ *      ...JDBC-Exception
+ * 
+ * + * The test tries to simulate a slow query by installing the + * {@link SlowDownEBasic} 'SELECT' trigger. The trigger can be configured to + * wait 3 * timing ms and a second thread will cancel the query in + * timing ms. + * + * in this case, we expect a JDBC exception from the driver.
+ *
+ * NOTE:
+ * H2 checks the cancel flag in org.h2.command.Prepared::setCurrentRowNumber + * only every 128th row. So we need at least 128 models and we cannot check + * queries like findCount or findOne, because they only return one row. + * + * @author Roland Praml, FOCONIS AG + * + */ +public class SqlQueryCancelTest extends BaseTestCase { + + private int timing = 10; + + @BeforeClass + public static void setupTestData() throws SQLException { + for (int i = 0; i < 128; i++) { + EBasic model = new EBasic("Basic " + i); + DB.save(model); + } + SlowDownEBasic.setSelectWaitMillis(0); + } + + @Test + public void cancelSqlQueryAtBegin() throws SQLException { + doCancelSqlAtBegin(SqlQuery::findList); + doCancelSqlAtBegin(SqlQuery::findOne); + doCancelSqlAtBegin(q -> q.findEach(e -> {})); + doCancelSqlAtBegin(q -> q.findEachWhile(e -> true)); + } + + @ForPlatform(Platform.H2) + @Test + public void cancelSqlDuringRun() throws SQLException { + + doCancelSqlDuringRun(SqlQuery::findList); + // doCancelSqlDuringRun(q -> q.setMaxRows(1).findOne()); + // findOne cannot be tested, as H2 does the cancel check every 128 rows only + doCancelSqlDuringRun(q -> q.findEach(e -> {})); + doCancelSqlDuringRun(q -> q.findEachWhile(e -> true)); + } + + + @Test + public void cancelOrmQueryAtBegin() throws SQLException { + doCancelOrmAtBegin(Query::findCount); + doCancelOrmAtBegin(Query::findFutureCount); + // We cannot test 'findCount' due H2 restrictions + doCancelOrmAtBegin(Query::findFutureIds); + doCancelOrmAtBegin(Query::findFutureList); + doCancelOrmAtBegin(Query::findIds); + doCancelOrmAtBegin(Query::findIterate); + doCancelOrmAtBegin(Query::findList); + doCancelOrmAtBegin(Query::findMap); + doCancelOrmAtBegin(Query::findOne); + doCancelOrmAtBegin(q -> q.setMaxRows(1000).findPagedList().getList()); // untested + doCancelOrmAtBegin(Query::findSet); + doCancelOrmAtBegin(Query::findSingleAttribute); + doCancelOrmAtBegin(Query::findSingleAttributeList); + doCancelOrmAtBegin(Query::findStream); + // testDuringRun(Query::findVersions); + // EBasic has no history support, but it should work if @History is added + doCancelOrmAtBegin(q -> q.findEach(e -> {})); + doCancelOrmAtBegin(q -> q.findEachWhile(e -> true)); + } + + @ForPlatform(Platform.H2) + @Test + public void cancelOrmDuringRun() throws Throwable { + // doCancelOrmDuringRun(Query::findCount); + // testDuringRunFuture(Query::findFutureCount); + // We cannot test 'findCount' due H2 restrictions + doCancelOrmFutureDuringRun(Query::findFutureIds); + doCancelOrmFutureDuringRun(Query::findFutureList); + doCancelOrmDuringRun(Query::findIds); + doCancelOrmDuringRun(Query::findIterate); + doCancelOrmDuringRun(Query::findList); + doCancelOrmDuringRun(Query::findMap); + // doCancelOrmDuringRun(q -> q.setMaxRows(1).findOne()); + // findOne cannot be tested, as H2 does the cancel check every 128 rows only + doCancelOrmDuringRun(q -> q.setMaxRows(1000).findPagedList().getList()); // untested + doCancelOrmDuringRun(Query::findSet); + doCancelOrmDuringRun(Query::findSingleAttribute); + doCancelOrmDuringRun(Query::findSingleAttributeList); + doCancelOrmDuringRun(Query::findStream); + // testDuringRun(Query::findVersions); + // EBasic has no history support, but it should work if @History is added + doCancelOrmDuringRun(q -> q.findEach(e -> {})); + doCancelOrmDuringRun(q -> q.findEachWhile(e -> true)); + } + + @Test + public void cancelOrmDuringIterate() throws SQLException { + + Query query = DB.find(EBasic.class); + + QueryIterator iter = query.findIterate(); + assertThat(iter.hasNext()).isTrue(); + query.cancel(); + assertThat(iter.next()).isNotNull(); + + // We might have 100 entities in a buffer. So we must iterate through all. + assertThatThrownBy(() -> { + while(iter.hasNext()) iter.next(); + }) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + @Test + public void cancelOrmDtoQueryAtBegin() throws SQLException { + + doCancelOrmDtoAtBegin(DtoQuery::findIterate); + doCancelOrmDtoAtBegin(DtoQuery::findList); + doCancelOrmDtoAtBegin(DtoQuery::findOne); + doCancelOrmDtoAtBegin(DtoQuery::findStream); + doCancelOrmDtoAtBegin(q -> q.findEach(e -> {})); + doCancelOrmDtoAtBegin(q -> q.findEachWhile(e -> true)); + } + + @ForPlatform(Platform.H2) + @Test + public void cancelOrmDtoDuringRun() throws SQLException { + + doCancelOrmDtoDuringRun(DtoQuery::findIterate); + doCancelOrmDtoDuringRun(DtoQuery::findList); + // doCancelOrmDtoDuringRun(q -> q.setMaxRows(1).findOne()); + // findOne cannot be tested, as H2 does the cancel check every 128 rows only + doCancelOrmDtoDuringRun(DtoQuery::findStream); + doCancelOrmDtoDuringRun(q -> q.findEach(e -> {})); + doCancelOrmDtoDuringRun(q -> q.findEachWhile(e -> true)); + } + + @Test + public void cancelOrmDtoDuringIterate() throws SQLException { + + DtoQuery query = DB.find(EBasic.class).select("id,status").asDto(EBasicDto.class); + + QueryIterator iter = query.findIterate(); + assertThat(iter.hasNext()).isTrue(); + query.cancel(); + assertThat(iter.next()).isNotNull(); + + // We might have 100 entities in a buffer. So we must iterate through all. + assertThatThrownBy(() -> { + while(iter.hasNext()) iter.next(); + }) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + @Test + public void cancelSqlDtoQueryAtBegin() throws SQLException { + + doCancelSqlDtoAtBegin(DtoQuery::findIterate); + doCancelSqlDtoAtBegin(DtoQuery::findList); + doCancelSqlDtoAtBegin(DtoQuery::findOne); + doCancelSqlDtoAtBegin(DtoQuery::findStream); + doCancelSqlDtoAtBegin(q -> q.findEach(e -> {})); + doCancelSqlDtoAtBegin(q -> q.findEachWhile(e -> true)); + } + + @ForPlatform(Platform.H2) + @Test + public void cancelSqlDtoDuringRun() throws SQLException { + + //doCancelSqlDtoDuringRun(DtoQuery::findIterate); + doCancelSqlDtoDuringRun(DtoQuery::findList); + // doCancelSqlDtoDuringRun(q -> q.setMaxRows(1).findOne()); + // findOne cannot be tested, as H2 does the cancel check every 128 rows only + doCancelSqlDtoDuringRun(DtoQuery::findStream); + doCancelSqlDtoDuringRun(q -> q.findEach(e -> {})); + doCancelSqlDtoDuringRun(q -> q.findEachWhile(e -> true)); + } + + @Test + public void cancelSqlDtoDuringIterate() throws SQLException { + + DtoQuery query = DB.findDto(EBasicDto.class, "select id, status from e_basic"); + + QueryIterator iter = query.findIterate(); + assertThat(iter.hasNext()).isTrue(); + query.cancel(); + assertThat(iter.next()).isNotNull(); + + // We might have 100 entities in a buffer. So we must iterate through all. + assertThatThrownBy(() -> { + while(iter.hasNext()) iter.next(); + }) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + private void doCancelSqlAtBegin(Consumer test) throws SQLException { + SqlQuery query = DB.sqlQuery("select * from e_basic"); + query.cancel(); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + private void doCancelSqlDuringRun(Consumer test) throws SQLException { + SqlQuery warmup = DB.sqlQuery("select * from e_basic"); + test.accept(warmup); + SqlQuery query = DB.sqlQuery("select * from e_basic"); + executeDelayed(query::cancel); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); + } + + private void doCancelOrmAtBegin(Consumer> test) throws SQLException { + Query query = DB.find(EBasic.class); + query.cancel(); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + private void doCancelOrmDuringRun(Consumer> test) throws SQLException { + Query warmup = DB.find(EBasic.class); + test.accept(warmup); + Query warmup2 = DB.find(EBasic.class); + test.accept(warmup2); + Query query = DB.find(EBasic.class); + executeDelayed(query::cancel); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); + } + + private void doCancelOrmFutureDuringRun(Function, Future> test) throws SQLException, InterruptedException, ExecutionException { + Query warmup = DB.find(EBasic.class); + test.apply(warmup).get(); + + Query query = DB.find(EBasic.class); + executeDelayed(query::cancel); + assertThatThrownBy(() -> { + try { + test.apply(query).get(); + } catch (ExecutionException ee) { + throw ee.getCause(); + } + }) + .isInstanceOf(PersistenceException.class) + .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); + } + + private void doCancelOrmDtoAtBegin(Consumer> test) throws SQLException { + DtoQuery query = DB.find(EBasic.class).select("id,status").asDto(EBasicDto.class); + query.cancel(); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + private void doCancelOrmDtoDuringRun(Consumer> test) throws SQLException { + DtoQuery warmup = DB.find(EBasic.class).select("id,status").asDto(EBasicDto.class); + test.accept(warmup); + + DtoQuery query = DB.find(EBasic.class).select("id,status").asDto(EBasicDto.class); + executeDelayed(query::cancel); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); + } + + private void doCancelSqlDtoAtBegin(Consumer> test) throws SQLException { + DtoQuery query = DB.findDto(EBasicDto.class, "select id, status from e_basic"); + query.cancel(); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Query was cancelled"); + } + + private void doCancelSqlDtoDuringRun(Consumer> test) throws SQLException { + DtoQuery warmup = DB.findDto(EBasicDto.class, "select id, status from e_basic"); + test.accept(warmup); + + DtoQuery query = DB.findDto(EBasicDto.class, "select id, status from e_basic"); + executeDelayed(query::cancel); + assertThatThrownBy(() -> test.accept(query)) + .isInstanceOf(PersistenceException.class) + .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); + } + + private void executeDelayed(Runnable r) throws SQLException { + // We modify the DB here. Otherwise we may hit an internal H2 cache, if the + // same query is performed. Queries from the cache cannot be canceled. + EBasic makeDbDirty = new EBasic("Basic " + UUID.randomUUID()); + DB.save(makeDbDirty); + SlowDownEBasic.setSelectWaitMillis(timing * 3); + new Thread(() -> { + try { + Thread.sleep(timing); + r.run(); + SlowDownEBasic.setSelectWaitMillis(0); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } + +} diff --git a/ebean-core/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterJoin.java b/ebean-core/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterJoin.java index 28b8b0dbd..5d895adbc 100644 --- a/ebean-core/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterJoin.java +++ b/ebean-core/src/test/java/org/tests/query/joins/TestDisjunctWhereOuterJoin.java @@ -5,6 +5,7 @@ import io.ebean.Ebean; import io.ebean.Expr; import io.ebean.Query; import org.junit.Test; +import org.tests.basic.one2one.Wheel; import org.tests.model.basic.MRole; import org.tests.model.basic.MUser; @@ -82,6 +83,24 @@ public class TestDisjunctWhereOuterJoin extends BaseTestCase { assertThat(sql).contains("where (t0.user_name = ? or u1.roleid = ?)"); } + @Test + public void testSelectOneToOneDisjunction() { + Ebean.beginTransaction(); + try { + Query query = Ebean.find(Wheel.class) + .select("id") + .where().or() + .ge("tire.id", 100) + .lt("tire.id", 100) + .endOr().query(); + query.findList(); + String sql = sqlOf(query); + assertThat(sql).contains("join"); + } finally { + Ebean.rollbackTransaction(); + } + } + private void queryOrExpression(Integer roleid) { Query query = Ebean.find(MUser.class) diff --git a/ebean-core/src/test/java/org/tests/query/lazy/TestQueryDefaultBatchSize.java b/ebean-core/src/test/java/org/tests/query/lazy/TestQueryDefaultBatchSize.java index a8685f985..e1d9afd02 100644 --- a/ebean-core/src/test/java/org/tests/query/lazy/TestQueryDefaultBatchSize.java +++ b/ebean-core/src/test/java/org/tests/query/lazy/TestQueryDefaultBatchSize.java @@ -2,6 +2,7 @@ package org.tests.query.lazy; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import io.ebean.ProfileLocation; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; import org.tests.model.basic.OrderDetail; @@ -13,12 +14,18 @@ import java.util.List; public class TestQueryDefaultBatchSize extends BaseTestCase { + private static final ProfileLocation loc0 = ProfileLocation.create(); + private static final ProfileLocation loc1 = ProfileLocation.create(); + private static final ProfileLocation loc2 = ProfileLocation.create(); + private static final ProfileLocation loc3 = ProfileLocation.create(); + @Test public void test_findEach() { ResetBasicData.reset(); Ebean.find(Order.class) + .setProfileLocation(loc0) .setLazyLoadBatchSize(2) .findEach(bean -> doStuff(bean)); } @@ -29,6 +36,7 @@ public class TestQueryDefaultBatchSize extends BaseTestCase { ResetBasicData.reset(); Ebean.find(Order.class) + .setProfileLocation(loc1) .fetch("details", "id") .fetch("details.product", "sku") .fetch("customer") @@ -44,6 +52,7 @@ public class TestQueryDefaultBatchSize extends BaseTestCase { List orders = Ebean.find(Order.class) + .setProfileLocation(loc2) .setLazyLoadBatchSize(2) .findList(); @@ -59,6 +68,7 @@ public class TestQueryDefaultBatchSize extends BaseTestCase { List orders = Ebean.find(Order.class) + .setProfileLocation(loc3) .fetch("details", "id") .fetch("details.product", "sku") .fetch("customer") diff --git a/ebean-core/src/test/java/org/tests/query/other/TestQueryRawExpressionMany.java b/ebean-core/src/test/java/org/tests/query/other/TestQueryRawExpressionMany.java index af52302d2..55901a720 100644 --- a/ebean-core/src/test/java/org/tests/query/other/TestQueryRawExpressionMany.java +++ b/ebean-core/src/test/java/org/tests/query/other/TestQueryRawExpressionMany.java @@ -30,6 +30,6 @@ public class TestQueryRawExpressionMany extends BaseTestCase { query.findCount(); List sql = LoggedSqlCollector.stop(); - assertThat(trimSql(sql.get(0), 1)).contains("select count(*) from ( select distinct t0.id from o_order t0 left join o_order_detail t1 on t1.order_id = t0.id where t1.order_qty = ?)"); + assertThat(trimSql(sql.get(0), 1)).contains("select count(*) from ( select distinct t0.id from o_order t0 left join o_order_detail t1 on t1.order_id = t0.id and t1.id > 0 where t1.order_qty = ?)"); } } diff --git a/ebean-core/src/test/java/org/tests/query/other/TestQueryRowCountWithMany.java b/ebean-core/src/test/java/org/tests/query/other/TestQueryRowCountWithMany.java index a9d18476b..44c8a78f0 100644 --- a/ebean-core/src/test/java/org/tests/query/other/TestQueryRowCountWithMany.java +++ b/ebean-core/src/test/java/org/tests/query/other/TestQueryRowCountWithMany.java @@ -69,7 +69,7 @@ public class TestQueryRowCountWithMany extends BaseTestCase { assertEquals(list.size(), rowCount); assertEquals(2, sqlLogged.size()); assertThat(trimSql(sqlLogged.get(1), 1)).contains( - "select count(*) from ( select distinct t0.id from o_order t0 join o_order_detail u1 on u1.order_id = t0.id where u1.product_id = ?)"); + "select count(*) from ( select distinct t0.id from o_order t0 join o_order_detail u1 on u1.order_id = t0.id and u1.id > 0 where u1.product_id = ?)"); } @@ -95,7 +95,7 @@ public class TestQueryRowCountWithMany extends BaseTestCase { List sqlLogged = LoggedSqlCollector.stop(); assertEquals(1, sqlLogged.size()); - assertThat(trimSql(sqlLogged.get(0), 1)).contains("select count(*) from ( select distinct t0.id from o_order t0 join o_order_detail u1 on u1.order_id = t0.id where u1.product_id = ?)"); + assertThat(trimSql(sqlLogged.get(0), 1)).contains("select count(*) from ( select distinct t0.id from o_order t0 join o_order_detail u1 on u1.order_id = t0.id and u1.id > 0 where u1.product_id = ?)"); query.findList(); } diff --git a/ebean-core/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java b/ebean-core/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java index f476ed084..dd230b866 100644 --- a/ebean-core/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java +++ b/ebean-core/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java @@ -64,7 +64,7 @@ public class TestQuerySingleAttribute extends BaseTestCase { assertThat(sqlOf(query)).contains("select r1.attribute_, count(*) " + "from (select distinct t0.id, t0.name as attribute_ " + - "from o_customer t0 left join contact u1 on u1.customer_id = t0.id left join o_order u2 on u2.kcustomer_id = t0.id " + + "from o_customer t0 left join contact u1 on u1.customer_id = t0.id left join o_order u2 on u2.kcustomer_id = t0.id and u2.order_date is not null " + "where t0.name = ? and (u2.status = ? or u1.first_name = ?)) r1 " + "group by r1.attribute_ " + "order by count(*) desc, r1.attribute_"); diff --git a/ebean-core/src/test/java/org/tests/query/sqlquery/SqlQueryTests.java b/ebean-core/src/test/java/org/tests/query/sqlquery/SqlQueryTests.java index c79f07d21..996f08bbe 100644 --- a/ebean-core/src/test/java/org/tests/query/sqlquery/SqlQueryTests.java +++ b/ebean-core/src/test/java/org/tests/query/sqlquery/SqlQueryTests.java @@ -2,10 +2,11 @@ package org.tests.query.sqlquery; import io.ebean.BaseTestCase; import io.ebean.DB; -import io.ebean.Ebean; import io.ebean.RowMapper; import io.ebean.SqlQuery; import io.ebean.SqlRow; +import io.ebean.annotation.ForPlatform; +import io.ebean.annotation.Platform; import io.ebean.meta.MetaTimedMetric; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; @@ -25,6 +26,19 @@ import static org.junit.Assert.assertEquals; public class SqlQueryTests extends BaseTestCase { + @ForPlatform(Platform.H2) + @Test + public void selectFunction() { + + String sql = "select length(?)"; + final Long val = DB.sqlQuery(sql).setParameter("NotVeryLong").mapToScalar(Long.class).findOne(); + assertThat(val).isEqualTo(11); + + String sql2 = "select length(:val)"; + final Long val2 = DB.sqlQuery(sql2).setParameter("val", "NotVeryLong").mapToScalar(Long.class).findOne(); + assertThat(val2).isEqualTo(11); + } + @Test public void findSingleAttributeList_decimal() { @@ -40,6 +54,28 @@ public class SqlQueryTests extends BaseTestCase { assertThat(lineAmounts).isNotEmpty(); } + @Test + public void findSingleAttributeEach_decimal() { + + ResetBasicData.reset(); + + String sql = "select (unit_price * order_qty) from o_order_detail where unit_price > ? order by (unit_price * order_qty) desc"; + + AtomicLong counter = new AtomicLong(); + AtomicLong inc = new AtomicLong(); + + DB.sqlQuery(sql) + .setParameter(3) + .mapToScalar(BigDecimal.class) + .findEach(val -> { + counter.incrementAndGet(); + inc.addAndGet(val.longValue()); + }); + + assertThat(inc.get()).isGreaterThan(counter.get()); + assertThat(counter.get()).isGreaterThan(0); + } + @Test public void findSingleDecimal() { @@ -142,6 +178,24 @@ public class SqlQueryTests extends BaseTestCase { private static final CustMapper CUST_MAPPER = new CustMapper(); + @Test + public void findEach_mapper() { + + ResetBasicData.reset(); + + String sql = "select id, name, status from o_customer where name is not null"; + + AtomicInteger counter = new AtomicInteger(); + DB.sqlQuery(sql) + .mapTo(CUST_MAPPER) + .findEach(custDto -> { + counter.incrementAndGet(); + assertThat(custDto.name).isNotNull(); + }); + + assertThat(counter.get()).isGreaterThan(0); + } + @Test public void findOne_mapper() { @@ -361,7 +415,7 @@ public class SqlQueryTests extends BaseTestCase { ResetBasicData.reset(); - int expectedRows = Ebean.find(Order.class).findCount(); + int expectedRows = DB.find(Order.class).findCount(); final AtomicInteger count = new AtomicInteger(); diff --git a/ebean-core/src/test/java/org/tests/softdelete/TestSoftDeleteOptionalRelationship.java b/ebean-core/src/test/java/org/tests/softdelete/TestSoftDeleteOptionalRelationship.java index fc925f62c..b4e320b21 100644 --- a/ebean-core/src/test/java/org/tests/softdelete/TestSoftDeleteOptionalRelationship.java +++ b/ebean-core/src/test/java/org/tests/softdelete/TestSoftDeleteOptionalRelationship.java @@ -1,9 +1,17 @@ package org.tests.softdelete; import io.ebean.BaseTestCase; +import io.ebean.DB; import io.ebean.Ebean; +import org.ebeantest.LoggedSqlCollector; import org.tests.model.softdelete.ESoftDelMid; +import io.ebean.Finder; import org.junit.Test; +import org.tests.model.softdelete.ESoftDelY; +import org.tests.model.softdelete.ESoftDelZ; + +import java.util.List; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -24,5 +32,35 @@ public class TestSoftDeleteOptionalRelationship extends BaseTestCase { assertThat(bean.getTop()).isNull(); } + @Test + public void testFindNullWhenMultiple() { + UUID uuid = UUID.randomUUID(); + { + ESoftDelZ z = new ESoftDelZ(); + z.setUuid(uuid); + DB.save(z); + + ESoftDelY y = new ESoftDelY(); + y.setOrganization(z); + y.setX(null); + DB.save(y); + } + + LoggedSqlCollector.start(); + + Finder finder = new Finder<>(ESoftDelY.class); + ESoftDelY bean = finder + .query() + .where() + .eq("organization.uuid", uuid) + .isNull("x") + .findOne(); + + assertThat(bean).isNotNull(); + + final List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("left join esoft_del_z t1 on t1.id = t0.organization_id and t1.deleted = false where"); + } } diff --git a/ebean-core/src/test/java/org/tests/transaction/TestTransactionCallback.java b/ebean-core/src/test/java/org/tests/transaction/TestTransactionCallback.java index 44e91f601..d926b8067 100644 --- a/ebean-core/src/test/java/org/tests/transaction/TestTransactionCallback.java +++ b/ebean-core/src/test/java/org/tests/transaction/TestTransactionCallback.java @@ -1,8 +1,7 @@ package org.tests.transaction; import io.ebean.BaseTestCase; -import io.ebean.Ebean; -import io.ebean.EbeanServer; +import io.ebean.DB; import io.ebean.Transaction; import io.ebean.TransactionCallbackAdapter; import org.junit.Test; @@ -18,20 +17,12 @@ public class TestTransactionCallback extends BaseTestCase { int countPreRollback; int countPostRollback; - @Test(expected = PersistenceException.class) - public void test_noActiveTransaction() { - - Ebean.register(new MyCallback()); - } - @Test public void test_commitAndRollback() { - - - try (Transaction txn = Ebean.beginTransaction()) { - Ebean.register(new MyCallback()); - txn.getConnection(); - Ebean.commitTransaction(); + try (Transaction txn = DB.beginTransaction()) { + DB.register(new MyCallback()); + txn.getConnection(); // Ebean assumes writes have occurred + txn.commit(); } assertEquals(1, countPreCommit); @@ -39,28 +30,54 @@ public class TestTransactionCallback extends BaseTestCase { assertEquals(0, countPreRollback); assertEquals(0, countPostRollback); - Ebean.beginTransaction(); + DB.beginTransaction(); try { - Ebean.register(new MyCallback()); + DB.register(new MyCallback()); } finally { - Ebean.rollbackTransaction(); + DB.rollbackTransaction(); } assertEquals(1, countPreCommit); assertEquals(1, countPostCommit); assertEquals(1, countPreRollback); assertEquals(1, countPostRollback); + } + @Test + public void test_commit_whenNoDbWrite() { + try (Transaction txn = DB.beginTransaction()) { + DB.register(new MyCallback()); + txn.commit(); + } + assertEquals(1, countPreCommit); + assertEquals(1, countPostCommit); + assertEquals(0, countPreRollback); + assertEquals(0, countPostRollback); + } + + @Test + public void test_rollback_whenNoDbWrite() { + try (Transaction txn = DB.beginTransaction()) { + DB.register(new MyCallback()); + txn.rollback(); + } + + assertEquals(0, countPreCommit); + assertEquals(0, countPostCommit); + assertEquals(1, countPreRollback); + assertEquals(1, countPostRollback); } @Test(expected = PersistenceException.class) - public void test_withEbeanserver() { - - EbeanServer server = Ebean.getServer(null); - server.register(new MyCallback()); + public void test_noActiveTransaction() { + DB.register(new MyCallback()); } + @Test(expected = PersistenceException.class) + public void test_noActiveTransaction_withDatabase() { + DB.getDefault().register(new MyCallback()); + } class MyCallback extends TransactionCallbackAdapter { diff --git a/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java b/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java index 75d767b3b..8a870f8ab 100644 --- a/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java +++ b/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java @@ -2,12 +2,15 @@ package org.tests.transparentpersist; import io.ebean.*; import io.ebean.annotation.*; +import io.ebeaninternal.api.SpiBeanTypeManager; import io.ebeaninternal.api.SpiTransaction; import io.ebeantest.LoggedSql; import org.junit.Test; +import org.mockito.Mockito; import org.tests.model.basic.Customer; import org.tests.model.basic.EBasicVer; import org.tests.model.basic.Order; +import org.tests.model.basic.OrderShipment; import java.util.List; @@ -181,6 +184,8 @@ public class TestTransparentPersist extends BaseTestCase { Order order = new Order(); order.setStatus(Order.Status.NEW); order.setCustomer(c0); + OrderShipment osh1 = new OrderShipment(); + order.addShipment(osh1); DB.save(order); LoggedSql.start(); @@ -194,7 +199,9 @@ public class TestTransparentPersist extends BaseTestCase { Customer c1 = new Customer(); c1.setName("newCust CascadePersist"); foundOrder.setCustomer(c1); - + foundOrder.getShipments().remove(0); + OrderShipment osh2 = new OrderShipment(); + foundOrder.addShipment(osh2); transaction.commit(); } @@ -204,13 +211,19 @@ public class TestTransparentPersist extends BaseTestCase { assertThat(checkOrder.getStatus()).isEqualTo(Order.Status.NEW); assertThat(checkOrder.getCustomer().getName()).isEqualTo("newCust CascadePersist"); + assertThat(checkOrder.getShipments().size()).isEqualTo(1); - assertThat(sql).hasSize(5); + assertThat(sql).hasSize(10); assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.order_date"); assertThat(sql.get(1)).contains("insert into o_customer"); assertThat(sql.get(2)).contains(" -- bind("); assertThat(sql.get(3)).contains("update o_order set updtime=?, kcustomer_id=? where id=? and updtime=?"); assertThat(sql.get(4)).contains(" -- bind("); + assertThat(sql.get(5)).contains("select t0.order_id, t0.id, t0.ship_time, t0.cretime, t0.updtime, t0.version, t0.order_id from or_order_ship"); + assertThat(sql.get(6)).contains("delete from or_order_ship"); + assertThat(sql.get(7)).contains(" -- bind("); + assertThat(sql.get(8)).contains("insert into or_order_ship"); + assertThat(sql.get(9)).contains(" -- bind("); DB.delete(checkOrder); DB.delete(Customer.class, checkOrder.getCustomer().getId()); @@ -292,7 +305,8 @@ public class TestTransparentPersist extends BaseTestCase { } private List getDirtyBeansFromPersistenceContext(Transaction transaction) { - return ((SpiTransaction)transaction).getPersistenceContext().dirtyBeans(); + final SpiBeanTypeManager mgr = Mockito.mock(SpiBeanTypeManager.class); + return ((SpiTransaction)transaction).getPersistenceContext().dirtyBeans(mgr); } } diff --git a/ebean-core/src/test/resources/ebean.properties b/ebean-core/src/test/resources/ebean.properties index aa1210739..49aae2a31 100644 --- a/ebean-core/src/test/resources/ebean.properties +++ b/ebean-core/src/test/resources/ebean.properties @@ -26,7 +26,7 @@ datasource.default=h2 #datasource.h2.capturestacktrace=true ebean.dumpMetricsOnShutdown=true -ebean.dumpMetricsOptions=sql,hash +ebean.dumpMetricsOptions=sql,hash,loc #ebean.queryPlan.thresholdMicros=1000 ebean.queryPlan.enable=true diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index f94924830..2c7df7a5d 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean ddl generation @@ -28,14 +28,14 @@ io.ebean ebean-core-type - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided @@ -76,7 +76,7 @@ io.ebean ebean-maven-plugin - 12.8.2 + 12.9.1 test diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/H2HistoryDdl.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/H2HistoryDdl.java index bb474c1e4..5bd58adeb 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/H2HistoryDdl.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/ddlgeneration/platform/H2HistoryDdl.java @@ -1,6 +1,5 @@ package io.ebeaninternal.dbmigration.ddlgeneration.platform; -import io.ebean.config.dbplatform.h2.H2HistoryTrigger; import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer; import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite; import io.ebeaninternal.dbmigration.model.MTable; @@ -12,7 +11,7 @@ import java.io.IOException; */ public class H2HistoryDdl extends DbTriggerBasedHistoryDdl { - private static final String TRIGGER_CLASS = H2HistoryTrigger.class.getName(); + private static final String TRIGGER_CLASS = "io.ebean.config.dbplatform.h2.H2HistoryTrigger"; H2HistoryDdl() { this.sysPeriodType = "timestamp"; @@ -22,31 +21,25 @@ public class H2HistoryDdl extends DbTriggerBasedHistoryDdl { @Override protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException { - buffer.append("drop trigger ").append(updateTriggerName(baseTable)).endOfStatement(); } @Override protected void createTriggers(DdlWrite writer, MTable table) throws IOException { - String baseTableName = table.getName(); DdlBuffer apply = writer.applyHistoryTrigger(); - addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName); } @Override protected void updateHistoryTriggers(DbTriggerUpdate update) throws IOException { - recreateHistoryView(update); - DdlBuffer buffer = update.historyTriggerBuffer(); dropTriggers(buffer, update.getBaseTable()); addCreateTrigger(buffer, updateTriggerName(update.getBaseTable()), update.getBaseTable()); } private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable) throws IOException { - // Note that this does not take into account the historyTable name (excepts _history suffix) and // does not take into account excluded columns (all columns included in history) apply diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java index 3a7457a56..6530611dd 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/model/build/ModelBuildPropertyVisitor.java @@ -149,7 +149,7 @@ public class ModelBuildPropertyVisitor extends BaseTablePropertyVisitor { @Override public void visitMany(BeanPropertyAssocMany p) { - if (p.hasJoinTable() && p.getMappedBy() == null) { + if (p.createJoinTable()) { // only create on other 'owning' side // build the create table and fkey constraints diff --git a/ebean-externalmapping-api/pom.xml b/ebean-externalmapping-api/pom.xml index 079eebec9..5eed791a6 100644 --- a/ebean-externalmapping-api/pom.xml +++ b/ebean-externalmapping-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean external mapping api diff --git a/ebean-externalmapping-xml/pom.xml b/ebean-externalmapping-xml/pom.xml index bcf9f10ff..76e183c67 100644 --- a/ebean-externalmapping-xml/pom.xml +++ b/ebean-externalmapping-xml/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT @@ -33,7 +33,7 @@ io.ebean ebean-externalmapping-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT @@ -59,14 +59,14 @@ io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test io.ebean ebean-ddl-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test diff --git a/ebean-postgis/pom.xml b/ebean-postgis/pom.xml index 4e1b6063b..9a7cb3948 100644 --- a/ebean-postgis/pom.xml +++ b/ebean-postgis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean postgis @@ -23,7 +23,7 @@ io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided @@ -74,7 +74,7 @@ io.ebean ebean-test - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index ff6dd8754..3d0600c83 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean querybean @@ -17,7 +17,7 @@ io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided @@ -57,21 +57,21 @@ io.ebean ebean-ddl-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test io.ebean querybean-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test io.ebean ebean-test - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index 573fbcf3b..13e5695b8 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean-redis @@ -16,41 +16,41 @@ redis.clients jedis - 3.6.0 + 3.6.1 io.ebean ebean-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided io.ebean ebean-querybean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test io.ebean querybean-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test io.ebean ebean-test - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index c3ed1f57f..fa0b249da 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean test @@ -29,14 +29,14 @@ io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT provided io.ebean ebean-ddl-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT diff --git a/ebean-test/src/test/resources/application-test.yaml b/ebean-test/src/test/resources/application-test.yaml index 7ad9e2dd5..d0d3fc81a 100644 --- a/ebean-test/src/test/resources/application-test.yaml +++ b/ebean-test/src/test/resources/application-test.yaml @@ -24,7 +24,7 @@ ebean: test: redis: latest - platform: mariadb #, postgres, mysql, mariadb, oracle, sqlserver, hana + platform: h2 #, postgres, mysql, mariadb, oracle, sqlserver, hana # useDocker: false ddlMode: dropCreate # none | dropCreate | create | migrations dbName: junk diff --git a/ebean/pom.xml b/ebean/pom.xml index ba312953f..c9c24cb7b 100644 --- a/ebean/pom.xml +++ b/ebean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT ebean composite @@ -22,20 +22,20 @@ io.ebean ebean-api - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT io.ebean ebean-querybean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml index a556671bb..a4b14ff53 100644 --- a/kotlin-querybean-generator/pom.xml +++ b/kotlin-querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT kotlin querybean generator @@ -12,7 +12,7 @@ kotlin-querybean-generator - 1.4.31 + 1.5.0 @@ -29,7 +29,7 @@ io.ebean ebean-querybean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test @@ -43,7 +43,7 @@ io.ebean ebean-core - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test @@ -64,7 +64,7 @@ io.ebean ebean-ddl-generator - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT test @@ -81,43 +81,42 @@ src/test/kotlin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + test-compile + test-compile + + test-compile + + + + test-kapt + + test-kapt + + + + src/test/kotlin + + + + io.ebean + kotlin-querybean-generator + 12.8.2 + + + + + + + 1.8 + + + org.apache.maven.plugins maven-compiler-plugin @@ -146,7 +145,7 @@ io.ebean ebean-maven-plugin - 12.8.2 + 12.9.1 test diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java index 04d1c270c..a5683165d 100644 --- a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java @@ -24,7 +24,7 @@ class SimpleModuleInfoWriter { SimpleModuleInfoWriter(ProcessingContext processingContext) { this.processingContext = processingContext; this.factoryPackage = processingContext.getFactoryPackage(); - this.factoryShortName = "_ebean$ModuleInfo"; + this.factoryShortName = "_Ebean$ModuleInfo"; this.factoryFullName = factoryPackage + "." + factoryShortName; } diff --git a/pom.xml b/pom.xml index fb82113fa..da28cb2ab 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ io.ebean ebean-parent - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT pom ebean parent diff --git a/querybean-generator/pom.xml b/querybean-generator/pom.xml index 1ea632096..b75980c9e 100644 --- a/querybean-generator/pom.xml +++ b/querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 12.8.4-SNAPSHOT + 12.10.0-SNAPSHOT querybean generator diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java index e7e2826eb..e9f8c30f0 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java @@ -24,7 +24,7 @@ class SimpleModuleInfoWriter { SimpleModuleInfoWriter(ProcessingContext processingContext) { this.processingContext = processingContext; this.factoryPackage = processingContext.getFactoryPackage(); - this.factoryShortName = "_ebean$ModuleInfo"; + this.factoryShortName = "_Ebean$ModuleInfo"; this.factoryFullName = factoryPackage + "." + factoryShortName; }