mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-21 19:21:00 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56894400f4 | ||
|
|
32931d6db0 | ||
|
|
73c9ff8d80 | ||
|
|
bdbe743f56 | ||
|
|
ed2026b858 | ||
|
|
8d21f91f62 | ||
|
|
6d1d58b8c2 | ||
|
|
a943ee9225 | ||
|
|
94f252f423 | ||
|
|
275f8ad9f9 | ||
|
|
9ac3bd71f6 | ||
|
|
bb46c88db1 | ||
|
|
28e6108315 | ||
|
|
c20d1cdf9b | ||
|
|
007409d21c | ||
|
|
8b61069b3f | ||
|
|
a82dcb2509 | ||
|
|
08bb170cb2 |
@@ -14,7 +14,7 @@
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -101,7 +101,7 @@ Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.8</version>
|
||||
<version>42.7.11</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -457,6 +457,10 @@ public final class DB {
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)} but with given transaction.
|
||||
* <p>
|
||||
* For control over query cache use and whether to skip the check when the bean's unique
|
||||
* properties are unchanged, use {@link Database#checkUniqueness(Object, Transaction, boolean, boolean)}
|
||||
* via {@link #getDefault()} instead.
|
||||
*/
|
||||
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
return getDefault().checkUniqueness(bean, transaction);
|
||||
|
||||
@@ -1078,12 +1078,21 @@ public interface Database {
|
||||
* @param bean The entity bean to check uniqueness on
|
||||
* @return a set of Properties if constraint validation was detected or empty list.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean);
|
||||
default Set<Property> checkUniqueness(Object bean) {
|
||||
return checkUniqueness(bean, null, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean, Transaction transaction);
|
||||
default Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
return checkUniqueness(bean, transaction, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean);
|
||||
|
||||
/**
|
||||
* Marks the entity bean as dirty.
|
||||
|
||||
@@ -62,9 +62,10 @@ public interface DatabaseBuilder {
|
||||
/**
|
||||
* Build and return the Database instance.
|
||||
* <p>
|
||||
* When {@link #setRegister(boolean)} is set to true, and a database with the same
|
||||
* name is already registered, this may return the existing registered database
|
||||
* rather than creating a new one.
|
||||
* When {@link #setRegister(boolean)} is set to true (the default), and a database
|
||||
* with the same name is already registered, this throws an {@link IllegalStateException}.
|
||||
* Use a unique name, or use {@link #setRegister(boolean)} with {@code false} if the
|
||||
* Database instance is not intended to be registered/looked up by name.
|
||||
*/
|
||||
Database build();
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
|
||||
/**
|
||||
* Low-level factory for creating {@link Database} instances.
|
||||
* <p>
|
||||
@@ -81,10 +79,10 @@ public final class DatabaseFactory {
|
||||
// We're explicitly creating a database to be registered, so avoid
|
||||
// triggering DbContext static initialisation to auto-create a default one.
|
||||
DbPrimary.setSkip(true);
|
||||
Database existing = DbContext.getInstance().getRegistered(name);
|
||||
if (existing != null) {
|
||||
EbeanVersion.log.log(WARNING, "Using existing database with name:{0}", name);
|
||||
return existing;
|
||||
if (DbContext.getInstance().contains(name)) {
|
||||
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
|
||||
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
|
||||
+ " if this Database instance is not intended to be registered/looked up by name.");
|
||||
}
|
||||
}
|
||||
Database server = createInternal(config);
|
||||
@@ -123,6 +121,24 @@ public final class DatabaseFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the registration of this Database.
|
||||
* <p>
|
||||
* This is invoked when a Database is shutdown so that its registered name
|
||||
* becomes available again for a subsequently created Database with the same name.
|
||||
*/
|
||||
public static void unregister(Database server) {
|
||||
lock.lock();
|
||||
try {
|
||||
DbContext.getInstance().deregister(server);
|
||||
if (server.name().equals(defaultServerName)) {
|
||||
defaultServerName = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown gracefully all Database instances cleaning up any resources as required.
|
||||
* <p>
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
|
||||
import io.ebean.datasource.DataSourceConfigurationException;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -77,9 +76,8 @@ final class DbContext {
|
||||
return defaultDatabase;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Database getRegistered(String name) {
|
||||
return concMap.get(name);
|
||||
boolean contains(String name) {
|
||||
return concMap.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +120,27 @@ final class DbContext {
|
||||
registerWithName(server.name(), server, isDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the registration for this Database (typically on shutdown) so that
|
||||
* its name becomes available again for a subsequently created Database.
|
||||
* <p>
|
||||
* Only removes the registration if it currently maps to this exact instance
|
||||
* (avoids removing a different Database subsequently registered with the same name).
|
||||
*/
|
||||
void deregister(Database server) {
|
||||
lock.lock();
|
||||
try {
|
||||
String name = server.name();
|
||||
concMap.remove(name, server);
|
||||
syncMap.remove(name, server);
|
||||
if (defaultDatabase == server) {
|
||||
defaultDatabase = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerWithName(String name, Database server, boolean isDefault) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -246,4 +246,41 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
*/
|
||||
DtoQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
* The benefit of using this over findList() is that it provides functionality to get the
|
||||
* total row count etc.
|
||||
* <p>
|
||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
||||
* PersistenceException is thrown.
|
||||
* <p>
|
||||
* This is only supported for a DtoQuery that is derived from an ORM query via
|
||||
* {@link Query#asDto(Class)} / {@link ExpressionList#asDto(Class)}. It is not supported
|
||||
* for a DtoQuery based on raw SQL (e.g. via {@link Database#findDto(Class, String)}) as
|
||||
* there is no query structure available from which to derive a matching row count query -
|
||||
* a PersistenceException is thrown in that case.
|
||||
* <pre>{@code
|
||||
*
|
||||
* PagedList<OrderDto> pagedList =
|
||||
* DB.find(Order.class)
|
||||
* .where().eq("status", Order.Status.NEW)
|
||||
* .orderBy().asc("id")
|
||||
* .setFirstRow(50)
|
||||
* .setMaxRows(20)
|
||||
* .asDto(OrderDto.class)
|
||||
* .findPagedList();
|
||||
*
|
||||
* // fetch the total row count in the background
|
||||
* pagedList.loadCount();
|
||||
*
|
||||
* List<OrderDto> orders = pagedList.getList();
|
||||
* int totalRowCount = pagedList.getTotalCount();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
}
|
||||
|
||||
@@ -162,6 +162,18 @@ public class DatabasePlatform {
|
||||
protected boolean selectCountWithAlias;
|
||||
protected boolean selectCountWithColumnAlias;
|
||||
|
||||
/**
|
||||
* Set true for platforms where {@code exists(...)} can only be used as a predicate
|
||||
* and not as a directly selectable scalar boolean expression (e.g. SQL Server, Oracle).
|
||||
*/
|
||||
protected boolean existsWithCaseWhen;
|
||||
|
||||
/**
|
||||
* Clause appended after the {@code case when exists(...) then 1 else 0 end} exists query
|
||||
* for platforms that require a FROM clause on every select (e.g. {@code from dual} on Oracle).
|
||||
*/
|
||||
protected String existsFromClause = "";
|
||||
|
||||
/**
|
||||
* If set then use the FORWARD ONLY hint when creating ResultSets for
|
||||
* findIterate() and findVisit().
|
||||
@@ -660,6 +672,21 @@ public class DatabasePlatform {
|
||||
return selectCountWithColumnAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a scalar boolean {@code exists(...)} expression is not supported
|
||||
* as a select expression and needs to be wrapped as {@code case when exists(...) then 1 else 0 end}.
|
||||
*/
|
||||
public boolean existsWithCaseWhen() {
|
||||
return existsWithCaseWhen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the clause to append after the exists case-when wrapping (e.g. {@code from dual} on Oracle).
|
||||
*/
|
||||
public String existsFromClause() {
|
||||
return existsFromClause;
|
||||
}
|
||||
|
||||
|
||||
public String completeSql(String sql, Query<?> query) {
|
||||
if (query.isForUpdate()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.ebean.event;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.DatabaseFactory;
|
||||
import io.ebean.EbeanVersion;
|
||||
import io.ebean.service.SpiContainer;
|
||||
|
||||
@@ -188,6 +189,7 @@ public final class ShutdownManager {
|
||||
*/
|
||||
public static void unregisterDatabase(Database server) {
|
||||
databases.remove(server);
|
||||
DatabaseFactory.unregister(server);
|
||||
}
|
||||
|
||||
private static class ShutdownHook extends Thread {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -15,4 +15,19 @@ public interface SpiQueryManyJoin {
|
||||
*/
|
||||
String fetchOrderBy();
|
||||
|
||||
/**
|
||||
* Return true if this many relationship has an order column stored on the
|
||||
* ManyToMany intersection table (rather than a target descriptor property).
|
||||
*/
|
||||
default boolean hasIntersectionOrderColumn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the db column name of the ManyToMany intersection table order column (or null).
|
||||
*/
|
||||
default String intersectionOrderColumn() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1242,9 +1242,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
}
|
||||
|
||||
private <T> int delete(SpiQuery<T> query, boolean permanent) {
|
||||
if (query.getId() != null && query.whereExpressions() == null && query.descriptor().isSoftDelete()) {
|
||||
return executeInTrans((txn) -> persister.delete(query.getBeanType(), query.getId(), txn, permanent), query.transaction());
|
||||
}
|
||||
SpiOrmQueryRequest<T> request = createQueryRequest(Type.DELETE, query);
|
||||
try {
|
||||
request.initTransIfRequired();
|
||||
@@ -2199,12 +2196,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean) {
|
||||
return checkUniqueness(bean, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transaction) {
|
||||
public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) {
|
||||
EntityBean entityBean = checkEntityBean(bean);
|
||||
BeanDescriptor<?> beanDesc = descriptor(entityBean.getClass());
|
||||
BeanProperty idProperty = beanDesc.idProperty();
|
||||
@@ -2216,14 +2208,15 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
if (entityBean._ebean_getIntercept().isNew() && id != null) {
|
||||
// Primary Key is changeable only on new models - so skip check if we are not new
|
||||
SpiQuery<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
|
||||
query.setUseQueryCache(useQueryCache);
|
||||
query.usingTransaction(transaction);
|
||||
query.setId(id);
|
||||
if (findCount(query) > 0) {
|
||||
if (exists(query)) {
|
||||
return Collections.singleton(idProperty);
|
||||
}
|
||||
}
|
||||
for (BeanProperty[] props : beanDesc.uniqueProps()) {
|
||||
Set<Property> ret = checkUniqueness(entityBean, beanDesc, props, transaction);
|
||||
Set<Property> ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean);
|
||||
if (ret != null) {
|
||||
return ret;
|
||||
}
|
||||
@@ -2231,13 +2224,34 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if any property is dirty.
|
||||
*/
|
||||
private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) {
|
||||
if (entityBean._ebean_getIntercept().isNew()) {
|
||||
return true;
|
||||
}
|
||||
for (BeanProperty prop : props) {
|
||||
if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties).
|
||||
*/
|
||||
@Nullable
|
||||
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, @Nullable Transaction transaction) {
|
||||
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, @Nullable Transaction transaction,
|
||||
boolean useQueryCache, boolean skipClean) {
|
||||
if (skipClean && !isAnyPropertyDirty(entityBean, props)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BeanProperty idProperty = beanDesc.idProperty();
|
||||
SpiQuery<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
|
||||
query.setUseQueryCache(useQueryCache);
|
||||
query.usingTransaction(transaction);
|
||||
ExpressionList<?> exprList = query.where();
|
||||
if (!entityBean._ebean_getIntercept().isNew()) {
|
||||
@@ -2251,7 +2265,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
}
|
||||
exprList.eq(prop.name(), value);
|
||||
}
|
||||
if (findCount(query) > 0) {
|
||||
if (exists(query)) {
|
||||
Set<Property> ret = new LinkedHashSet<>();
|
||||
Collections.addAll(ret, props);
|
||||
return ret;
|
||||
|
||||
@@ -43,6 +43,8 @@ import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyLists;
|
||||
import io.ebeaninternal.server.el.*;
|
||||
import io.ebeaninternal.server.persist.DeleteMode;
|
||||
import io.ebeaninternal.server.persist.MultiValueWrapper;
|
||||
import io.ebeaninternal.server.type.ScalarTypeArray;
|
||||
import io.ebeaninternal.server.query.*;
|
||||
import io.ebeaninternal.server.querydefn.DefaultOrmQuery;
|
||||
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
|
||||
@@ -753,7 +755,16 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
public void bindElementValue(SqlUpdate insert, Object value) {
|
||||
EntityBean bean = (EntityBean) value;
|
||||
for (BeanProperty property : propertiesBaseScalar) {
|
||||
insert.setParameter(property.getValue(bean));
|
||||
Object propertyValue = property.getValue(bean);
|
||||
if (property.isArrayType() && propertyValue instanceof Collection) {
|
||||
// Bind with the declared element type rather than relying on MultiValueWrapper's default
|
||||
// constructor which infers the element type from the first value - this fails with a
|
||||
// NoSuchElementException for an empty collection. See #2477.
|
||||
Class<?> elementType = ((ScalarTypeArray) property.scalarType()).elementType();
|
||||
insert.setParameter(new MultiValueWrapper((Collection<?>) propertyValue, elementType));
|
||||
} else {
|
||||
insert.setParameter(propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -372,14 +372,15 @@ abstract class BeanDescriptorCacheHelp<T> {
|
||||
* Hit the bean cache with the given ids returning the hits.
|
||||
*/
|
||||
BeanCacheResult<T> cacheIdLookup(PersistenceContext context, boolean unmodifiable, Collection<?> ids) {
|
||||
Set<Object> keys = new HashSet<>(ids.size());
|
||||
for (Object id : ids) {
|
||||
keys.add(desc.cacheKey(id));
|
||||
}
|
||||
if (ids.isEmpty()) {
|
||||
return new BeanCacheResult<>();
|
||||
}
|
||||
Map<Object, Object> beanDataMap = beanCache().getAll(keys);
|
||||
// map cacheKey -> original id to support type coercion
|
||||
Map<Object, Object> keyToOriginalId = new HashMap<>(ids.size());
|
||||
for (Object id : ids) {
|
||||
keyToOriginalId.put(desc.cacheKey(id), id);
|
||||
}
|
||||
Map<Object, Object> beanDataMap = beanCache().getAll(keyToOriginalId.keySet());
|
||||
if (beanLog.isLoggable(TRACE)) {
|
||||
beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet());
|
||||
}
|
||||
@@ -387,7 +388,8 @@ abstract class BeanDescriptorCacheHelp<T> {
|
||||
for (Map.Entry<Object, Object> entry : beanDataMap.entrySet()) {
|
||||
CachedBeanData cachedBeanData = (CachedBeanData) entry.getValue();
|
||||
T bean = convertToBean(entry.getKey(), unmodifiable, context, cachedBeanData);
|
||||
result.add(bean, desc.id(bean));
|
||||
Object originalId = keyToOriginalId.get(entry.getKey());
|
||||
result.add(bean, originalId != null ? originalId : desc.id(bean));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -907,8 +907,18 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
|
||||
}
|
||||
|
||||
private void makeOrderColumn(DeployBeanPropertyAssocMany<?> oneToMany) {
|
||||
DeployBeanDescriptor<?> targetDesc = targetDescriptor(oneToMany);
|
||||
DeployOrderColumn orderColumn = oneToMany.getOrderColumn();
|
||||
makeOrderColumn(oneToMany, targetDescriptor(oneToMany));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and assign the synthetic order column property onto the given target descriptor.
|
||||
* <p>
|
||||
* Used for both {@code @OneToMany} (targetDesc looked up via the target entity type) and
|
||||
* {@code @ElementCollection} (targetDesc is the synthetic element descriptor which is not
|
||||
* registered in {@code deployInfoMap} and so must be passed in directly).
|
||||
*/
|
||||
public void makeOrderColumn(DeployBeanPropertyAssocMany<?> many, DeployBeanDescriptor<?> targetDesc) {
|
||||
DeployOrderColumn orderColumn = many.getOrderColumn();
|
||||
final ScalarType<?> scalarType = typeManager.type(Integer.class);
|
||||
DeployBeanProperty orderProperty = new DeployBeanProperty(targetDesc, Integer.class, scalarType, null);
|
||||
orderProperty.setName(DeployOrderColumn.LOGICAL_NAME);
|
||||
|
||||
@@ -671,6 +671,35 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
|
||||
setValue(entityBean, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, getIntercept and setIntercept will check if the passed bean is an instance of the descriptor type.
|
||||
* <p>
|
||||
* If the property is not part of the type hierarchy (i.e. is not this property from this descriptor) an
|
||||
* IllegalArgumentException is thrown.
|
||||
* <p>
|
||||
* If inheritance is involved, this method returns false instead of throwing an exception, if the property might
|
||||
* exist on one of the sibling child beans. This is necessary for getIntercept, as it returns <code>null</code>
|
||||
* in this case.
|
||||
*
|
||||
* @return true if the property can be accessed on the given bean, false if it should be treated as unloaded.
|
||||
*/
|
||||
private boolean checkPropertyAccess(EntityBean bean) {
|
||||
if (bean == null || descriptor.type().isInstance(bean)) { // null = fall through - NPE is caught later.
|
||||
return true;
|
||||
}
|
||||
InheritInfo inheritInfo = descriptor.inheritInfo();
|
||||
if (inheritInfo == null || inheritInfo.isRoot() || !inheritInfo.getRoot().getType().isInstance(bean)) {
|
||||
throw new IllegalArgumentException(propertyIncompatibleMsg(bean));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String propertyIncompatibleMsg(EntityBean bean) {
|
||||
String beanType = bean == null ? "null" : bean.getClass().getName();
|
||||
return "Property " + name + " on [" + descriptor + "] is incompatible with type[" + beanType + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the property without interception or
|
||||
* PropertyChangeSupport.
|
||||
@@ -687,6 +716,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
|
||||
* Set the value of the property.
|
||||
*/
|
||||
public void setValueIntercept(EntityBean bean, Object value) {
|
||||
if (!checkPropertyAccess(bean)) {
|
||||
throw new IllegalArgumentException(propertyIncompatibleMsg(bean));
|
||||
}
|
||||
try {
|
||||
setter.setIntercept(bean, value);
|
||||
} catch (Exception ex) {
|
||||
@@ -798,6 +830,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
|
||||
}
|
||||
|
||||
public Object getValueIntercept(EntityBean bean) {
|
||||
if (!checkPropertyAccess(bean)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return getter.getIntercept(bean);
|
||||
} catch (Exception ex) {
|
||||
|
||||
@@ -57,6 +57,12 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
* Flag to indicate that the target has a order column to auto populate.
|
||||
*/
|
||||
private final boolean hasOrderColumn;
|
||||
/**
|
||||
* For ManyToMany, the db column name of the order column stored on the intersection
|
||||
* table (null for OneToMany/ElementCollection which use a target descriptor property instead).
|
||||
*/
|
||||
private final String intersectionOrderColumn;
|
||||
private final boolean intersectionOrderColumnNullable;
|
||||
/**
|
||||
* Flag to indicate manyToMany relationship.
|
||||
*/
|
||||
@@ -95,6 +101,8 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
this.o2mJoinTable = deploy.isO2mJoinTable();
|
||||
this.hasOrderColumn = deploy.hasOrderColumn();
|
||||
this.manyToMany = deploy.isManyToMany();
|
||||
this.intersectionOrderColumn = (manyToMany && hasOrderColumn) ? deploy.getOrderColumn().getName() : null;
|
||||
this.intersectionOrderColumnNullable = (manyToMany && hasOrderColumn) && deploy.getOrderColumn().isNullable();
|
||||
this.elementCollection = deploy.isElementCollection();
|
||||
this.elementDescriptor = deploy.getElementDescriptor();
|
||||
this.manyType = deploy.getManyType();
|
||||
@@ -154,6 +162,9 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
embeddedExportedProperties = exportedProperties[0].isEmbedded();
|
||||
if (fetchOrderBy != null) {
|
||||
lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy(fetchOrderBy);
|
||||
} else if (intersectionOrderColumn != null) {
|
||||
// ManyToMany @OrderColumn - the intersection table is always aliased "int_"
|
||||
lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy("int_." + intersectionOrderColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,6 +522,29 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
|
||||
return hasOrderColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a ManyToMany with an order column stored on the intersection table.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasIntersectionOrderColumn() {
|
||||
return intersectionOrderColumn != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the db column name of the ManyToMany intersection table order column (or null).
|
||||
*/
|
||||
@Override
|
||||
public String intersectionOrderColumn() {
|
||||
return intersectionOrderColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the intersection table order column is nullable.
|
||||
*/
|
||||
public boolean isIntersectionOrderColumnNullable() {
|
||||
return intersectionOrderColumnNullable;
|
||||
}
|
||||
|
||||
public boolean isOrphanRemoval() {
|
||||
return orphanRemoval;
|
||||
}
|
||||
|
||||
@@ -234,6 +234,10 @@ public final class IdBinderSimple implements IdBinder {
|
||||
@Override
|
||||
public Object convertId(Object idValue) {
|
||||
if (!idValue.getClass().equals(expectedType)) {
|
||||
if (idValue instanceof String) {
|
||||
// for cacheKey() formatted values
|
||||
return scalarType.parse((String) idValue);
|
||||
}
|
||||
return scalarType.toBeanType(idValue);
|
||||
}
|
||||
return idValue;
|
||||
|
||||
+15
@@ -89,6 +89,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
ManyToMany manyToMany = get(prop, ManyToMany.class);
|
||||
if (manyToMany != null) {
|
||||
readToMany(manyToMany, prop);
|
||||
OrderColumn orderColumn = get(prop, OrderColumn.class);
|
||||
if (orderColumn != null) {
|
||||
// ManyToMany order value is stored on the intersection table (not the target bean)
|
||||
prop.setOrderColumn(new DeployOrderColumn(orderColumn));
|
||||
}
|
||||
}
|
||||
ElementCollection elementCollection = get(prop, ElementCollection.class);
|
||||
if (elementCollection != null) {
|
||||
@@ -177,6 +182,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
if (!elementCollection.targetClass().equals(void.class)) {
|
||||
prop.setTargetType(elementCollection.targetClass());
|
||||
}
|
||||
OrderColumn orderColumn = get(prop, OrderColumn.class);
|
||||
if (orderColumn != null) {
|
||||
prop.setOrderColumn(new DeployOrderColumn(orderColumn));
|
||||
prop.setFetchOrderBy(DeployOrderColumn.LOGICAL_NAME);
|
||||
}
|
||||
Column column = prop.getMetaAnnotation(Column.class);
|
||||
if (column != null) {
|
||||
prop.setDbColumn(column.name());
|
||||
@@ -268,6 +278,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
|
||||
|
||||
elementDescriptor.setName(prop.toString());
|
||||
factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin());
|
||||
if (prop.hasOrderColumn()) {
|
||||
// create the synthetic order property on the element descriptor - the element descriptor
|
||||
// is not registered in deployInfoMap so this can't go through the usual OneToMany path
|
||||
factory.makeOrderColumn(prop, elementDescriptor);
|
||||
}
|
||||
prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType(), scalar));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,4 +91,16 @@ public interface ElPropertyDeploy extends SpiQueryManyJoin {
|
||||
default String fetchOrderBy() {
|
||||
return beanProperty().fetchOrderBy();
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean hasIntersectionOrderColumn() {
|
||||
BeanProperty prop = beanProperty();
|
||||
return prop instanceof io.ebeaninternal.server.deploy.BeanPropertyAssocMany
|
||||
&& prop.hasIntersectionOrderColumn();
|
||||
}
|
||||
|
||||
@Override
|
||||
default String intersectionOrderColumn() {
|
||||
return beanProperty().intersectionOrderColumn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +256,12 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
}
|
||||
|
||||
private void saveAssocManyIntersection(boolean queue) {
|
||||
if (many.hasIntersectionOrderColumn()) {
|
||||
// With @OrderColumn the position of every row can change on any add/remove/reorder so
|
||||
// we always delete all intersection rows and reinsert them in the current list order.
|
||||
saveAssocManyIntersectionOrdered(queue);
|
||||
return;
|
||||
}
|
||||
final boolean vanillaCollection = !(value instanceof BeanCollection<?>);
|
||||
if (vanillaCollection || forcedUpdate) {
|
||||
// delete all intersection rows and then treat all
|
||||
@@ -340,6 +346,53 @@ final class SaveManyBeans extends SaveManyBase {
|
||||
transaction.depth(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the ManyToMany intersection rows for a property with an {@code @OrderColumn}.
|
||||
* <p>
|
||||
* Unlike the standard diff based save (additions/removals), this always deletes all existing
|
||||
* intersection rows for the parent and reinserts every current entry in list order, binding
|
||||
* the sequential order index. This is required because a pure reorder (no add/remove) would
|
||||
* not otherwise be detected/persisted, and there is no per-row place (unlike OneToMany/
|
||||
* ElementCollection) to compare an existing 'loaded' order against - the order value lives on
|
||||
* the intersection row, not on the target bean.
|
||||
*/
|
||||
private void saveAssocManyIntersectionOrdered(boolean queue) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
Collection<?> current;
|
||||
if (value instanceof Map<?, ?>) {
|
||||
current = ((Map<?, ?>) value).values();
|
||||
} else if (value instanceof Collection<?>) {
|
||||
current = (Collection<?>) value;
|
||||
} else {
|
||||
throw new PersistenceException("Unhandled ManyToMany type " + value.getClass().getName() + " for " + many.fullName());
|
||||
}
|
||||
if (value instanceof BeanCollection<?>) {
|
||||
BeanCollection<?> manyValue = (BeanCollection<?>) value;
|
||||
setListenMode(manyValue, many);
|
||||
manyValue.modifyReset();
|
||||
}
|
||||
if (!insertedParent) {
|
||||
request.preManyToManyUpdate();
|
||||
persister.deleteManyIntersection(parentBean, many, transaction, publish, queue);
|
||||
}
|
||||
String orderColumn = many.intersectionOrderColumn();
|
||||
transaction.depth(+1);
|
||||
int position = 0;
|
||||
for (Object other : current) {
|
||||
EntityBean otherBean = (EntityBean) other;
|
||||
if (!many.hasImportedId(otherBean)) {
|
||||
throw new PersistenceException("ManyToMany bean does not have an Id value? " + otherBean);
|
||||
}
|
||||
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish);
|
||||
intRow.put(orderColumn, position++);
|
||||
SpiSqlUpdate sqlInsert = intRow.createInsert(server);
|
||||
persister.executeOrQueue(sqlInsert, transaction, queue, BatchControl.INSERT_QUEUE);
|
||||
}
|
||||
transaction.depth(-1);
|
||||
}
|
||||
|
||||
private boolean isChangedProperty() {
|
||||
return request.isChangedProperty(many.propertyIndex());
|
||||
}
|
||||
|
||||
+5
@@ -44,10 +44,15 @@ final class SaveManyElementCollection extends SaveManyBase {
|
||||
private void saveCollection() {
|
||||
SpiSqlUpdate proto = many.insertElementCollection();
|
||||
Object parentId = request.beanId();
|
||||
boolean hasOrderColumn = many.hasOrderColumn();
|
||||
int position = 0;
|
||||
for (Object value : collection) {
|
||||
final SpiSqlUpdate sqlInsert = proto.copy();
|
||||
sqlInsert.setParameter(parentId);
|
||||
many.bindElementValue(sqlInsert, value);
|
||||
if (hasOrderColumn) {
|
||||
sqlInsert.setParameter(position++);
|
||||
}
|
||||
persister.addToFlushQueue(sqlInsert, transaction, BatchControl.INSERT_QUEUE);
|
||||
}
|
||||
resetModifyState();
|
||||
|
||||
+14
-2
@@ -60,6 +60,10 @@ final class BindableIdEmbedded implements BindableId {
|
||||
@Override
|
||||
public void dmlBind(BindableRequest request, EntityBean bean) throws SQLException {
|
||||
EntityBean idValue = (EntityBean) embId.getValue(bean);
|
||||
if (idValue == null && matches != null) {
|
||||
// The id (e.g. IdClass) hasn't been derived/cached on this bean yet
|
||||
idValue = deriveId(bean);
|
||||
}
|
||||
for (BeanProperty prop : props) {
|
||||
Object value = prop.getValue(idValue);
|
||||
request.bind(value, prop);
|
||||
@@ -83,13 +87,21 @@ final class BindableIdEmbedded implements BindableId {
|
||||
|
||||
@Override
|
||||
public boolean deriveConcatenatedId(PersistRequestBean<?> persist) {
|
||||
deriveId(persist.entityBean());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive/build the concatenated id (e.g. IdClass) from the entity's own matching id
|
||||
* properties, caching it on the bean (via setValueIntercept) for subsequent use.
|
||||
*/
|
||||
private EntityBean deriveId(EntityBean bean) {
|
||||
if (matches == null) {
|
||||
String m = "No matches for " + embId.fullName() + " the concatenated key columns where not found?"
|
||||
+ " I expect that the concatenated key was null, and this bean does"
|
||||
+ " not have ManyToOne assoc beans matching the primary key columns?";
|
||||
throw new PersistenceException(m);
|
||||
}
|
||||
EntityBean bean = persist.entityBean();
|
||||
// create the new id
|
||||
EntityBean newId = (EntityBean) embId.createEmbeddedId();
|
||||
// populate it from the assoc one id values...
|
||||
@@ -97,7 +109,7 @@ final class BindableIdEmbedded implements BindableId {
|
||||
match.populate(bean, newId);
|
||||
}
|
||||
embId.setValueIntercept(bean, newId);
|
||||
return true;
|
||||
return newId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -336,7 +336,10 @@ final class CQueryBuilder {
|
||||
return sql;
|
||||
}
|
||||
|
||||
private String wrapSelectExists(String sql) {
|
||||
static String wrapSelectExists(String sql, boolean existsWithCaseWhen, String existsFromClause) {
|
||||
if (existsWithCaseWhen) {
|
||||
return "select case when exists(" + sql + ") then 1 else 0 end" + existsFromClause;
|
||||
}
|
||||
return "select exists(" + sql + ")";
|
||||
}
|
||||
|
||||
@@ -366,7 +369,7 @@ final class CQueryBuilder {
|
||||
}
|
||||
|
||||
SqlLimitResponse s = buildSql("select 1", request, predicates, sqlTree);
|
||||
String sql = wrapSelectExists(s.getSql());
|
||||
String sql = wrapSelectExists(s.getSql(), dbPlatform.existsWithCaseWhen(), dbPlatform.existsFromClause());
|
||||
|
||||
queryPlan = new CQueryPlan(request, sql, sqlTree.plan(), predicates.logWhereSql());
|
||||
request.putQueryPlan(queryPlan);
|
||||
|
||||
@@ -33,6 +33,13 @@ import static java.lang.System.Logger.Level.WARNING;
|
||||
*/
|
||||
public final class CQueryPredicates {
|
||||
|
||||
/**
|
||||
* Default lower bound used for findVersions() (no explicit start/end) on sql2011
|
||||
* standards based platforms that require actual bind values for the root table's
|
||||
* 'for system_time between ? and ?' clause.
|
||||
*/
|
||||
private static final Timestamp EPOCH = Timestamp.valueOf("1970-01-01 00:00:00");
|
||||
|
||||
private final Binder binder;
|
||||
private final OrmQueryRequest<?> request;
|
||||
private final SpiQuery<?> query;
|
||||
@@ -60,6 +67,12 @@ public final class CQueryPredicates {
|
||||
private String dbOrderBy;
|
||||
private String dbDistinctOn;
|
||||
private String dbUpdateClause;
|
||||
/**
|
||||
* Set when the many join is a ManyToMany with an intersection table order column - used to
|
||||
* resolve the literal "${path}dbColumn" marker appended to dbOrderBy in parseTableAlias().
|
||||
*/
|
||||
private String intersectionOrderPath;
|
||||
private String intersectionOrderColumn;
|
||||
/**
|
||||
* Includes from where and order by clauses.
|
||||
*/
|
||||
@@ -84,10 +97,16 @@ public final class CQueryPredicates {
|
||||
// bind the update set clause
|
||||
updateProperties.bind(binder, dataBind);
|
||||
}
|
||||
if (query.isVersionsBetween() && binder.isAsOfStandardsBased()) {
|
||||
if (binder.isAsOfStandardsBased() && query.temporalMode() == SpiQuery.TemporalMode.VERSIONS) {
|
||||
// sql2011 based versions between timestamp syntax
|
||||
Timestamp start = query.versionStart();
|
||||
Timestamp end = query.versionEnd();
|
||||
if (start == null) {
|
||||
start = EPOCH;
|
||||
}
|
||||
if (end == null) {
|
||||
end = new Timestamp(System.currentTimeMillis());
|
||||
}
|
||||
dataBind.append("between ").append(start).append(" and ").append(end);
|
||||
binder.bindObject(dataBind, start);
|
||||
binder.bindObject(dataBind, end);
|
||||
@@ -239,6 +258,14 @@ public final class CQueryPredicates {
|
||||
dbHaving = alias.parseWhere(dbHaving);
|
||||
}
|
||||
if (dbOrderBy != null) {
|
||||
if (intersectionOrderColumn != null) {
|
||||
// resolve the ManyToMany intersection table order column BEFORE the generic
|
||||
// alias substitution runs, as it needs the "z_" suffixed intersection alias
|
||||
// rather than the plain target table alias.
|
||||
String marker = "${" + intersectionOrderPath + "}" + intersectionOrderColumn;
|
||||
String targetAlias = alias.tableAlias(intersectionOrderPath);
|
||||
dbOrderBy = dbOrderBy.replace(marker, targetAlias + "z_." + intersectionOrderColumn);
|
||||
}
|
||||
dbOrderBy = alias.parse(dbOrderBy);
|
||||
}
|
||||
if (dbDistinctOn != null) {
|
||||
@@ -268,9 +295,18 @@ public final class CQueryPredicates {
|
||||
}
|
||||
// check for default ordering on the many property...
|
||||
SpiQueryManyJoin manyProp = request.manyJoin();
|
||||
String manyOrderBy = manyProp.fetchOrderBy();
|
||||
if (manyOrderBy != null) {
|
||||
orderBy = orderBy + ", " + parser.parse(CQueryBuilder.prefixOrderByFields(manyProp.path(), manyOrderBy));
|
||||
if (manyProp.hasIntersectionOrderColumn()) {
|
||||
// ManyToMany with @OrderColumn on the intersection table - the column lives on the
|
||||
// intersection table (alias "<targetAlias>z_") rather than a normal property path,
|
||||
// so we append a literal marker that parseTableAlias() resolves directly.
|
||||
intersectionOrderPath = manyProp.path();
|
||||
intersectionOrderColumn = manyProp.intersectionOrderColumn();
|
||||
orderBy = orderBy + ", ${" + intersectionOrderPath + "}" + intersectionOrderColumn;
|
||||
} else {
|
||||
String manyOrderBy = manyProp.fetchOrderBy();
|
||||
if (manyOrderBy != null) {
|
||||
orderBy = orderBy + ", " + parser.parse(CQueryBuilder.prefixOrderByFields(manyProp.path(), manyOrderBy));
|
||||
}
|
||||
}
|
||||
if (request.isFindById()) {
|
||||
// only one master bean so should be fine...
|
||||
|
||||
@@ -43,6 +43,16 @@ public interface STreePropertyAssocMany extends STreePropertyAssoc {
|
||||
*/
|
||||
boolean hasJoinTable();
|
||||
|
||||
/**
|
||||
* Return true if this is a ManyToMany with an order column stored on the intersection table.
|
||||
*/
|
||||
boolean hasIntersectionOrderColumn();
|
||||
|
||||
/**
|
||||
* Return the db column name of the ManyToMany intersection table order column (or null).
|
||||
*/
|
||||
String intersectionOrderColumn();
|
||||
|
||||
/**
|
||||
* Return the intersection table join.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.ebean.DtoQuery;
|
||||
import io.ebean.PagedList;
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebean.QueryIterator;
|
||||
import io.ebean.Transaction;
|
||||
@@ -11,6 +12,7 @@ import io.ebeaninternal.server.dto.DtoMappingRequest;
|
||||
import io.ebeaninternal.server.dto.DtoQueryPlan;
|
||||
import io.ebeaninternal.server.transaction.ExternalJdbcTransaction;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import javax.annotation.Nullable;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
@@ -50,6 +52,8 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
|
||||
this.useMaster = ormQuery.isUseMaster();
|
||||
this.label = ormQuery.label();
|
||||
this.profileLocation = ormQuery.profileLocation();
|
||||
this.firstRow = ormQuery.getFirstRow();
|
||||
this.maxRows = ormQuery.getMaxRows();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +139,24 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
|
||||
return server.findDtoList(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PagedList<T> findPagedList() {
|
||||
if (ormQuery == null) {
|
||||
throw new PersistenceException("findPagedList() is only supported for a DtoQuery derived from an ORM query");
|
||||
}
|
||||
if (maxRows == 0) {
|
||||
throw new PersistenceException("maxRows must be specified for findPagedList()");
|
||||
}
|
||||
// Use an independent copy for the row count query. The ormQuery instance is mutated with
|
||||
// a (potentially since ended/inactive) implicit transaction when the DTO list is executed,
|
||||
// so the count query must not share that transaction reference - instead it uses the
|
||||
// transaction explicitly bound to this DtoQuery (if any), consistent with a plain
|
||||
// Query.findPagedList().
|
||||
SpiQuery<?> countQuery = ormQuery.copy();
|
||||
countQuery.usingTransaction(transaction);
|
||||
return new DtoPagedList<>(server, this, countQuery);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public T findOne() {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package io.ebeaninternal.server.querydefn;
|
||||
|
||||
import io.ebean.PagedList;
|
||||
import io.ebeaninternal.api.SpiDtoQuery;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* PagedList implementation for a DtoQuery that is derived from an underlying ORM query
|
||||
* (created via {@code Query.asDto()}).
|
||||
* <p>
|
||||
* The page of DTO beans is fetched via the DtoQuery while the total row count is derived
|
||||
* from the underlying ORM query (which has the matching where clause, joins etc).
|
||||
*/
|
||||
final class DtoPagedList<T> implements PagedList<T> {
|
||||
|
||||
private final transient SpiEbeanServer server;
|
||||
private final transient ReentrantLock lock = new ReentrantLock();
|
||||
private final SpiDtoQuery<T> dtoQuery;
|
||||
private final SpiQuery<?> countQuery;
|
||||
private final int firstRow;
|
||||
private final int maxRows;
|
||||
|
||||
private int totalRowCount = -1;
|
||||
private Future<Integer> futureRowCount;
|
||||
private List<T> list;
|
||||
|
||||
/**
|
||||
* Construct with the dto query and the underlying ORM query used to derive the row count.
|
||||
*/
|
||||
DtoPagedList(SpiEbeanServer server, SpiDtoQuery<T> dtoQuery, SpiQuery<?> countQuery) {
|
||||
this.server = server;
|
||||
this.dtoQuery = dtoQuery;
|
||||
this.countQuery = countQuery;
|
||||
this.maxRows = countQuery.getMaxRows();
|
||||
this.firstRow = countQuery.getFirstRow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadCount() {
|
||||
getFutureCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<Integer> getFutureCount() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (futureRowCount == null) {
|
||||
futureRowCount = server.findFutureCount(countQuery);
|
||||
}
|
||||
return futureRowCount;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getList() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (list == null) {
|
||||
if (totalRowCount == 0) {
|
||||
// already count and no rows
|
||||
list = Collections.emptyList();
|
||||
} else {
|
||||
list = server.findDtoList(dtoQuery);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPageIndex() {
|
||||
if (firstRow == 0) {
|
||||
return 0;
|
||||
}
|
||||
return ((firstRow - 1) / maxRows) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalPageCount() {
|
||||
int rowCount = getTotalCount();
|
||||
if (rowCount == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return ((rowCount - 1) / maxRows) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalCount() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (totalRowCount > -1) {
|
||||
return totalRowCount;
|
||||
}
|
||||
if (futureRowCount != null) {
|
||||
try {
|
||||
// background query already initiated so get it with a wait
|
||||
totalRowCount = futureRowCount.get();
|
||||
return totalRowCount;
|
||||
} catch (Exception e) {
|
||||
throw new PersistenceException(e);
|
||||
}
|
||||
}
|
||||
// just using foreground thread
|
||||
totalRowCount = server.findCount(countQuery);
|
||||
return totalRowCount;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return (firstRow + maxRows) < getTotalCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPrev() {
|
||||
return firstRow > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPageSize() {
|
||||
return maxRows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayXtoYofZ(String to, String of) {
|
||||
int first = firstRow + 1;
|
||||
int last = firstRow + getList().size();
|
||||
int total = getTotalCount();
|
||||
return first + to + last + of + total;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -265,7 +265,7 @@ public final class DefaultTypeManager implements TypeManager {
|
||||
|
||||
@Override
|
||||
public ScalarType<?> dbMapType() {
|
||||
return hstoreSupport() ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, false);
|
||||
return hstoreSupport() ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, MutationDetection.DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -322,17 +322,17 @@ public final class DefaultTypeManager implements TypeManager {
|
||||
}
|
||||
Type genericType = prop.genericType();
|
||||
if (type.equals(List.class) && isValueTypeSimple(genericType)) {
|
||||
return ScalarTypeJsonList.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop));
|
||||
return ScalarTypeJsonList.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), collectionMutationDetection(prop));
|
||||
}
|
||||
if (type.equals(Set.class) && isValueTypeSimple(genericType)) {
|
||||
return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop));
|
||||
return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), collectionMutationDetection(prop));
|
||||
}
|
||||
if (type.equals(Map.class) && isBuiltinJsonMap(genericType)) {
|
||||
Type keyType = TypeReflectHelper.getMapKeyTypeRaw(genericType);
|
||||
if (isEnumType(keyType)) {
|
||||
return enumJsonMapType(postgres, dbType, keyType, keepSource(prop));
|
||||
return enumJsonMapType(postgres, dbType, keyType, collectionMutationDetection(prop));
|
||||
}
|
||||
return ScalarTypeJsonMap.typeFor(postgres, dbType, keepSource(prop));
|
||||
return ScalarTypeJsonMap.typeFor(postgres, dbType, collectionMutationDetection(prop));
|
||||
}
|
||||
if (objectMapperPresent && prop.mutationDetection() == MutationDetection.DEFAULT) {
|
||||
ScalarTypeSet<?> typeSet = typeSets.get(type);
|
||||
@@ -343,18 +343,25 @@ public final class DefaultTypeManager implements TypeManager {
|
||||
return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT);
|
||||
}
|
||||
|
||||
private boolean keepSource(DeployProperty prop) {
|
||||
if (prop.mutationDetection() == MutationDetection.DEFAULT) {
|
||||
prop.setMutationDetection(jsonManager != null ? jsonManager.mutationDetection() : MutationDetection.NONE);
|
||||
}
|
||||
return prop.mutationDetection() == MutationDetection.SOURCE;
|
||||
/**
|
||||
* Return the mutation detection mode to use for the built-in JSON collection types
|
||||
* (Map, List, Set).
|
||||
* <p>
|
||||
* Unlike {@code @DbJson} properties handled via the Jackson ObjectMapper, {@code DEFAULT}
|
||||
* on these collection types is <em>not</em> resolved against the DatabaseConfig wide
|
||||
* default - it always uses the legacy ModifyAware wrapper based dirty checking. Only an
|
||||
* explicit {@code NONE}, {@code HASH} or {@code SOURCE} on the property itself switches
|
||||
* these types away from ModifyAware based checking.
|
||||
*/
|
||||
private MutationDetection collectionMutationDetection(DeployProperty prop) {
|
||||
return prop.mutationDetection();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ScalarType<?> enumJsonMapType(boolean postgres, int dbType, Type keyType, boolean keepSource) {
|
||||
private ScalarType<?> enumJsonMapType(boolean postgres, int dbType, Type keyType, MutationDetection mutationDetection) {
|
||||
Class<? extends Enum<?>> enumClass = asEnumClass(keyType);
|
||||
ScalarType<? extends Enum<?>> enumScalarType = (ScalarType<? extends Enum<?>>) enumType(enumClass, null);
|
||||
return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, keepSource);
|
||||
return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, mutationDetection);
|
||||
}
|
||||
|
||||
private DocPropertyType docPropertyType(DeployProperty prop, Class<?> type) {
|
||||
|
||||
+4
-4
@@ -1,5 +1,6 @@
|
||||
package io.ebeaninternal.server.type;
|
||||
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import io.ebean.core.type.ScalarType;
|
||||
|
||||
@@ -27,15 +28,14 @@ class PlatformArrayTypeJsonList implements PlatformArrayTypeFactory {
|
||||
@Override
|
||||
public ScalarType<?> typeFor(Type valueType, boolean nullable) {
|
||||
if (valueType.equals(UUID.class)) {
|
||||
// TODO: keepSource for @DbArray?
|
||||
return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID);
|
||||
return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, ArrayElementConverter.UUID);
|
||||
}
|
||||
return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false);
|
||||
return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, MutationDetection.DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
|
||||
final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType);
|
||||
return new ScalarTypeJsonList.VarcharWithConverter(scalarType.docType(), nullable, false, converter);
|
||||
return new ScalarTypeJsonList.VarcharWithConverter(scalarType.docType(), nullable, converter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.ebeaninternal.server.type;
|
||||
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import io.ebean.core.type.ScalarType;
|
||||
|
||||
@@ -27,15 +28,14 @@ class PlatformArrayTypeJsonSet implements PlatformArrayTypeFactory {
|
||||
@Override
|
||||
public ScalarType<?> typeFor(Type valueType, boolean nullable) {
|
||||
if (valueType.equals(UUID.class)) {
|
||||
// TODO: keepSource for @DbArray?
|
||||
return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID);
|
||||
return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, ArrayElementConverter.UUID);
|
||||
}
|
||||
return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false);
|
||||
return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, MutationDetection.DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
|
||||
final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType);
|
||||
return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.docType(), nullable, false, converter);
|
||||
return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.docType(), nullable, converter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,12 @@ public interface ScalarTypeArray {
|
||||
*/
|
||||
String getDbColumnDefn();
|
||||
|
||||
/**
|
||||
* Return the Java type of the individual array elements.
|
||||
* <p>
|
||||
* Used to bind the array correctly when the collection value is empty
|
||||
* and so the element type can't be determined from the collection content.
|
||||
*/
|
||||
Class<?> elementType();
|
||||
|
||||
}
|
||||
|
||||
@@ -45,31 +45,31 @@ class ScalarTypeArrayList extends ScalarTypeArrayBase<List> implements ScalarTyp
|
||||
try {
|
||||
String key = valueType + ":" + nullable;
|
||||
if (valueType.equals(UUID.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
|
||||
}
|
||||
if (valueType.equals(Long.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
|
||||
}
|
||||
if (valueType.equals(Integer.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
|
||||
}
|
||||
if (valueType.equals(Float.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float4", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float4", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT, Float.class));
|
||||
}
|
||||
if (valueType.equals(Double.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
|
||||
}
|
||||
if (valueType.equals(BigDecimal.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "decimal", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "decimal", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL, BigDecimal.class));
|
||||
}
|
||||
if (valueType.equals(String.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
|
||||
}
|
||||
if (valueType.equals(Instant.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "timestamptz", DocPropertyType.TEXT, ArrayElementConverter.INSTANT));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "timestamptz", DocPropertyType.TEXT, ArrayElementConverter.INSTANT, Instant.class));
|
||||
}
|
||||
if (valueType.equals(LocalDate.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE, LocalDate.class));
|
||||
}
|
||||
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
|
||||
} finally {
|
||||
@@ -79,18 +79,24 @@ class ScalarTypeArrayList extends ScalarTypeArrayBase<List> implements ScalarTyp
|
||||
|
||||
@Override
|
||||
public ScalarTypeArrayList typeForEnum(ScalarType<?> scalarType, boolean nullable) {
|
||||
return new ScalarTypeArrayList(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType));
|
||||
return new ScalarTypeArrayList(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
|
||||
}
|
||||
}
|
||||
|
||||
private final String arrayType;
|
||||
|
||||
private final ArrayElementConverter converter;
|
||||
private final Class<?> elementType;
|
||||
|
||||
public ScalarTypeArrayList(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
|
||||
public ScalarTypeArrayList(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
|
||||
super(List.class, Types.ARRAY, docPropertyType, nullable);
|
||||
this.arrayType = arrayType;
|
||||
this.converter = converter;
|
||||
this.elementType = elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> elementType() {
|
||||
return elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -40,31 +40,31 @@ final class ScalarTypeArrayListH2 extends ScalarTypeArrayList {
|
||||
try {
|
||||
String key = valueType + ":" + nullable;
|
||||
if (valueType.equals(UUID.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
|
||||
}
|
||||
if (valueType.equals(Long.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
|
||||
}
|
||||
if (valueType.equals(Integer.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
|
||||
}
|
||||
if (valueType.equals(Float.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "real", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "real", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT, Float.class));
|
||||
}
|
||||
if (valueType.equals(Double.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
|
||||
}
|
||||
if (valueType.equals(BigDecimal.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL, BigDecimal.class));
|
||||
}
|
||||
if (valueType.equals(String.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
|
||||
}
|
||||
if (valueType.equals(Instant.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "timestamp", DocPropertyType.TEXT, ArrayElementConverter.INSTANT));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "timestamp", DocPropertyType.TEXT, ArrayElementConverter.INSTANT, Instant.class));
|
||||
}
|
||||
if (valueType.equals(LocalDate.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE, LocalDate.class));
|
||||
}
|
||||
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
|
||||
} finally {
|
||||
@@ -74,12 +74,12 @@ final class ScalarTypeArrayListH2 extends ScalarTypeArrayList {
|
||||
|
||||
@Override
|
||||
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
|
||||
return new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType));
|
||||
return new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
|
||||
}
|
||||
}
|
||||
|
||||
private ScalarTypeArrayListH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
|
||||
super(nullable, arrayType, docPropertyType, converter);
|
||||
private ScalarTypeArrayListH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
|
||||
super(nullable, arrayType, docPropertyType, converter, elementType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,19 +42,19 @@ class ScalarTypeArraySet extends ScalarTypeArrayBase<Set> implements ScalarTypeA
|
||||
try {
|
||||
String key = valueType + ":" + nullable;
|
||||
if (valueType.equals(UUID.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
|
||||
}
|
||||
if (valueType.equals(Long.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
|
||||
}
|
||||
if (valueType.equals(Integer.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
|
||||
}
|
||||
if (valueType.equals(Double.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
|
||||
}
|
||||
if (valueType.equals(String.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
|
||||
}
|
||||
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
|
||||
} finally {
|
||||
@@ -64,18 +64,24 @@ class ScalarTypeArraySet extends ScalarTypeArrayBase<Set> implements ScalarTypeA
|
||||
|
||||
@Override
|
||||
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
|
||||
return new ScalarTypeArraySet(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType));
|
||||
return new ScalarTypeArraySet(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
|
||||
}
|
||||
}
|
||||
|
||||
private final String arrayType;
|
||||
|
||||
private final ArrayElementConverter converter;
|
||||
private final Class<?> elementType;
|
||||
|
||||
public ScalarTypeArraySet(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
|
||||
public ScalarTypeArraySet(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
|
||||
super(Set.class, Types.ARRAY, docPropertyType, nullable);
|
||||
this.arrayType = arrayType;
|
||||
this.converter = converter;
|
||||
this.elementType = elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> elementType() {
|
||||
return elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,19 +37,19 @@ final class ScalarTypeArraySetH2 extends ScalarTypeArraySet {
|
||||
try {
|
||||
String key = valueType + ":" + nullable;
|
||||
if (valueType.equals(UUID.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
|
||||
}
|
||||
if (valueType.equals(Long.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
|
||||
}
|
||||
if (valueType.equals(Integer.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
|
||||
}
|
||||
if (valueType.equals(Double.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
|
||||
}
|
||||
if (valueType.equals(String.class)) {
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
|
||||
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
|
||||
}
|
||||
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
|
||||
} finally {
|
||||
@@ -59,13 +59,13 @@ final class ScalarTypeArraySetH2 extends ScalarTypeArraySet {
|
||||
|
||||
@Override
|
||||
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
|
||||
return new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType));
|
||||
return new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private ScalarTypeArraySetH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
|
||||
super(nullable, arrayType, docPropertyType, converter);
|
||||
private ScalarTypeArraySetH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
|
||||
super(nullable, arrayType, docPropertyType, converter, elementType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+28
-2
@@ -1,7 +1,10 @@
|
||||
package io.ebeaninternal.server.type;
|
||||
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Base for the JSON collection value types (List, Set).
|
||||
* <p>
|
||||
@@ -10,9 +13,9 @@ import io.ebean.core.type.DocPropertyType;
|
||||
*/
|
||||
abstract class ScalarTypeJsonCollectionValue<T> extends ScalarTypeJsonValue<T> implements ScalarTypeArray {
|
||||
|
||||
ScalarTypeJsonCollectionValue(Class<T> type, int jdbcType, JsonStorage storage, boolean keepSource,
|
||||
ScalarTypeJsonCollectionValue(Class<T> type, int jdbcType, JsonStorage storage, MutationDetection mutationDetection,
|
||||
boolean nullable, DocPropertyType docType) {
|
||||
super(type, jdbcType, storage, keepSource, nullable, "[]", docType);
|
||||
super(type, jdbcType, storage, mutationDetection, nullable, "[]", docType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -29,4 +32,27 @@ abstract class ScalarTypeJsonCollectionValue<T> extends ScalarTypeJsonValue<T> i
|
||||
return "varchar[]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the element type from the docType - used only to determine the ScalarType to use
|
||||
* when binding an element (e.g. for an empty collection where the element type can't be
|
||||
* determined from the collection content).
|
||||
*/
|
||||
@Override
|
||||
public Class<?> elementType() {
|
||||
switch (docType()) {
|
||||
case UUID:
|
||||
return UUID.class;
|
||||
case SHORT:
|
||||
case INTEGER:
|
||||
return Integer.class;
|
||||
case LONG:
|
||||
return Long.class;
|
||||
case FLOAT:
|
||||
case DOUBLE:
|
||||
return Double.class;
|
||||
default:
|
||||
return String.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
|
||||
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonWriter;
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.config.dbplatform.DbPlatformType;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import io.ebean.core.type.PostgresHelper;
|
||||
@@ -25,20 +26,20 @@ class ScalarTypeJsonList extends ScalarTypeJsonCollectionValue<List> {
|
||||
/**
|
||||
* Return the appropriate ScalarType for the requested dbType and platform.
|
||||
*/
|
||||
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) {
|
||||
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
|
||||
if (postgres) {
|
||||
switch (dbType) {
|
||||
case DbPlatformType.JSONB:
|
||||
return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource);
|
||||
return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, mutationDetection);
|
||||
case DbPlatformType.JSON:
|
||||
return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource);
|
||||
return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, mutationDetection);
|
||||
}
|
||||
}
|
||||
return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
|
||||
return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, mutationDetection);
|
||||
}
|
||||
|
||||
ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) {
|
||||
super(List.class, jdbcType, storage, keepSource, nullable, docType);
|
||||
ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
|
||||
super(List.class, jdbcType, storage, mutationDetection, nullable, docType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,8 +88,8 @@ class ScalarTypeJsonList extends ScalarTypeJsonCollectionValue<List> {
|
||||
|
||||
private final ArrayElementConverter converter;
|
||||
|
||||
VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) {
|
||||
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
|
||||
VarcharWithConverter(DocPropertyType docType, boolean nullable, ArrayElementConverter converter) {
|
||||
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, MutationDetection.DEFAULT);
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
|
||||
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonWriter;
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.config.dbplatform.DbPlatformType;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import io.ebean.core.type.PostgresHelper;
|
||||
@@ -22,8 +23,8 @@ class ScalarTypeJsonMap extends ScalarTypeJsonValue<Map> {
|
||||
/**
|
||||
* Return the ScalarType for the requested dbType and platform.
|
||||
*/
|
||||
static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, boolean keepSource) {
|
||||
return new ScalarTypeJsonMap(storageFor(postgres, dbType), keepSource);
|
||||
static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, MutationDetection mutationDetection) {
|
||||
return new ScalarTypeJsonMap(storageFor(postgres, dbType), mutationDetection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,8 +48,8 @@ class ScalarTypeJsonMap extends ScalarTypeJsonValue<Map> {
|
||||
}
|
||||
}
|
||||
|
||||
ScalarTypeJsonMap(JsonStorage storage, boolean keepSource) {
|
||||
super(Map.class, storage.jdbcType(), storage, keepSource, true, null, DocPropertyType.OBJECT);
|
||||
ScalarTypeJsonMap(JsonStorage storage, MutationDetection mutationDetection) {
|
||||
super(Map.class, storage.jdbcType(), storage, mutationDetection, true, null, DocPropertyType.OBJECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
|
||||
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonWriter;
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.core.type.ScalarType;
|
||||
import io.ebean.text.TextException;
|
||||
import io.ebean.text.json.EJson;
|
||||
@@ -23,13 +24,13 @@ final class ScalarTypeJsonMapEnum<T extends Enum<T>> extends ScalarTypeJsonMap {
|
||||
|
||||
private final ScalarType<T> enumType;
|
||||
|
||||
static ScalarType<?> typeFor(boolean postgres, int dbType, ScalarType<? extends Enum<?>> enumType, boolean keepSource) {
|
||||
return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, keepSource);
|
||||
static ScalarType<?> typeFor(boolean postgres, int dbType, ScalarType<? extends Enum<?>> enumType, MutationDetection mutationDetection) {
|
||||
return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, mutationDetection);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType<? extends Enum> enumType, boolean keepSource) {
|
||||
super(storage, keepSource);
|
||||
private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType<? extends Enum> enumType, MutationDetection mutationDetection) {
|
||||
super(storage, mutationDetection);
|
||||
this.enumType = (ScalarType<T>) enumType;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
|
||||
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonWriter;
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.config.dbplatform.DbPlatformType;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import io.ebean.core.type.PostgresHelper;
|
||||
@@ -25,20 +26,20 @@ class ScalarTypeJsonSet extends ScalarTypeJsonCollectionValue<Set> {
|
||||
/**
|
||||
* Return the appropriate ScalarType for the requested dbType and platform.
|
||||
*/
|
||||
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) {
|
||||
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
|
||||
if (postgres) {
|
||||
switch (dbType) {
|
||||
case DbPlatformType.JSONB:
|
||||
return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource);
|
||||
return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, mutationDetection);
|
||||
case DbPlatformType.JSON:
|
||||
return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource);
|
||||
return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, mutationDetection);
|
||||
}
|
||||
}
|
||||
return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
|
||||
return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, mutationDetection);
|
||||
}
|
||||
|
||||
ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) {
|
||||
super(Set.class, jdbcType, storage, keepSource, nullable, docType);
|
||||
ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
|
||||
super(Set.class, jdbcType, storage, mutationDetection, nullable, docType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,8 +90,8 @@ class ScalarTypeJsonSet extends ScalarTypeJsonCollectionValue<Set> {
|
||||
|
||||
private final ArrayElementConverter converter;
|
||||
|
||||
VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) {
|
||||
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
|
||||
VarcharWithConverter(DocPropertyType docType, boolean nullable, ArrayElementConverter converter) {
|
||||
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, MutationDetection.DEFAULT);
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.ebeaninternal.server.type;
|
||||
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.core.type.DataBinder;
|
||||
import io.ebean.core.type.DataReader;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
@@ -20,11 +21,18 @@ import java.sql.SQLException;
|
||||
* backed {@code EJson} facade)</li>
|
||||
* </ul>
|
||||
* This removes the previous explosion of per-storage and per-platform subclasses.
|
||||
* <p>
|
||||
* Mutation detection: {@code DEFAULT} uses the legacy ModifyAware wrapper based dirty
|
||||
* checking. {@code NONE} disables dirty checking entirely (mutable() is false and the
|
||||
* property is only included in an update when explicitly set). {@code HASH} and
|
||||
* {@code SOURCE} are handled via {@code BeanPropertyJsonMapper} (json content is kept
|
||||
* for the read/bind round trip so that a checksum or the source content can be compared).
|
||||
*/
|
||||
abstract class ScalarTypeJsonValue<T> extends ScalarTypeBase<T> {
|
||||
|
||||
protected final JsonStorage storage;
|
||||
protected final boolean keepSource;
|
||||
private final boolean mutable;
|
||||
private final boolean nullable;
|
||||
private final String emptyJson;
|
||||
private final DocPropertyType docType;
|
||||
@@ -33,11 +41,12 @@ abstract class ScalarTypeJsonValue<T> extends ScalarTypeBase<T> {
|
||||
* @param emptyJson JSON bound when the value is null and the property is not nullable
|
||||
* (e.g. {@code "[]"} for collections), or null to always bind SQL null.
|
||||
*/
|
||||
ScalarTypeJsonValue(Class<T> type, int jdbcType, JsonStorage storage, boolean keepSource,
|
||||
ScalarTypeJsonValue(Class<T> type, int jdbcType, JsonStorage storage, MutationDetection mutationDetection,
|
||||
boolean nullable, String emptyJson, DocPropertyType docType) {
|
||||
super(type, false, jdbcType);
|
||||
this.storage = storage;
|
||||
this.keepSource = keepSource;
|
||||
this.keepSource = mutationDetection == MutationDetection.HASH || mutationDetection == MutationDetection.SOURCE;
|
||||
this.mutable = mutationDetection != MutationDetection.NONE;
|
||||
this.nullable = nullable;
|
||||
this.emptyJson = emptyJson;
|
||||
this.docType = docType;
|
||||
@@ -51,7 +60,7 @@ abstract class ScalarTypeJsonValue<T> extends ScalarTypeBase<T> {
|
||||
|
||||
@Override
|
||||
public final boolean mutable() {
|
||||
return true;
|
||||
return mutable;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -117,6 +117,31 @@ class CQueryBuilderTest {
|
||||
assertThat(countSql).isEqualTo("select count(*) from ( select t0.id from ad t0) as c");
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapSelectExists_default_usesScalarExists() {
|
||||
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", false, "");
|
||||
assertThat(sql).isEqualTo("select exists(select 1 from o_order t0 where t0.id > ?)");
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL Server does not support exists(...) as a directly selectable scalar
|
||||
* boolean expression - see https://github.com/ebean-orm/ebean/issues/3848
|
||||
*/
|
||||
@Test
|
||||
void wrapSelectExists_existsWithCaseWhen_wrapsAsCaseWhen() {
|
||||
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", true, "");
|
||||
assertThat(sql).isEqualTo("select case when exists(select 1 from o_order t0 where t0.id > ?) then 1 else 0 end");
|
||||
}
|
||||
|
||||
/**
|
||||
* Oracle also requires a FROM clause on every select (from dual) - see https://github.com/ebean-orm/ebean/issues/3848
|
||||
*/
|
||||
@Test
|
||||
void wrapSelectExists_existsWithCaseWhenAndFromClause_appendsFromClause() {
|
||||
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", true, " from dual");
|
||||
assertThat(sql).isEqualTo("select case when exists(select 1 from o_order t0 where t0.id > ?) then 1 else 0 end from dual");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inlineSqlCommentLabel_rootExplicitLabel_prefixesBeanType() {
|
||||
String label = CQueryBuilder.inlineSqlCommentLabel("fetchMachineFleets", null, false, "COrganisationMachine");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.ebeaninternal.server.type;
|
||||
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.config.dbplatform.ExtraDbTypes;
|
||||
import io.ebean.core.type.DocPropertyType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -11,19 +12,19 @@ public class ScalarTypeJsonListTest extends BasePlatformArrayTypeFactoryTest {
|
||||
@Test
|
||||
public void typeFor_expect_nullToEmpty_when_postgresNonNull() throws SQLException {
|
||||
|
||||
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, false));
|
||||
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, false));
|
||||
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT));
|
||||
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT));
|
||||
|
||||
assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, false));
|
||||
assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typeFor_expect_nullToNull_when_nullable() throws SQLException {
|
||||
|
||||
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, false));
|
||||
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, false));
|
||||
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT));
|
||||
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT));
|
||||
|
||||
assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, false));
|
||||
assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-3
@@ -686,7 +686,8 @@ public class BaseTableDdl implements TableDdl {
|
||||
if (hasValue(alterColumn.getUniqueOneToOne())) {
|
||||
alterColumnAddUniqueOneToOneConstraint(writer, alterColumn);
|
||||
}
|
||||
if (hasValue(alterColumn.getComment())) {
|
||||
if (hasValue(alterColumn.getComment()) && !platformDdl.isInlineComments()) {
|
||||
// platform supports comments as a separate statement (e.g. postgres)
|
||||
alterColumnComment(writer, alterColumn);
|
||||
}
|
||||
if (hasValue(alterColumn.getDropCheckConstraint())) {
|
||||
@@ -700,7 +701,10 @@ public class BaseTableDdl implements TableDdl {
|
||||
}
|
||||
if (typeChange(alterColumn)
|
||||
|| hasValue(alterColumn.getDefaultValue())
|
||||
|| alterColumn.isNotnull() != null) {
|
||||
|| alterColumn.isNotnull() != null
|
||||
|| (hasValue(alterColumn.getComment()) && platformDdl.isInlineComments())) {
|
||||
// platforms with inline comments (e.g. mysql) must restate the whole column
|
||||
// definition (including comment) even when only the comment is changing
|
||||
alterColumn(writer, alterColumn);
|
||||
}
|
||||
if (alterCheckConstraint) {
|
||||
@@ -824,7 +828,9 @@ public class BaseTableDdl implements TableDdl {
|
||||
|
||||
platformDdl.alterTableAddColumn(writer, tableName, column, onHistoryTable, help.getDefaultValue());
|
||||
final String comment = column.getComment();
|
||||
if (comment != null && !comment.isEmpty()) {
|
||||
if (comment != null && !comment.isEmpty() && !platformDdl.isInlineComments()) {
|
||||
// platforms with inline comments (e.g. mysql) embed the comment directly into the
|
||||
// "add column" statement itself rather than as a separate statement
|
||||
platformDdl.addColumnComment(writer.applyPostAlter(), tableName, column.getName(), comment);
|
||||
}
|
||||
|
||||
|
||||
+41
-3
@@ -94,9 +94,10 @@ public class MySqlDdl extends PlatformDdl {
|
||||
public void alterColumn(DdlWrite writer, AlterColumn alter) {
|
||||
String tableName = alter.getTableName();
|
||||
String columnName = alter.getColumnName();
|
||||
boolean commentChange = hasValue(alter.getComment());
|
||||
|
||||
if (alter.getType() == null && alter.isNotnull() == null) {
|
||||
// No type change or notNull change -> handle default value change
|
||||
if (alter.getType() == null && alter.isNotnull() == null && !commentChange) {
|
||||
// No type change, notNull change or comment change -> handle default value change
|
||||
if (hasValue(alter.getDefaultValue())) {
|
||||
alterColumnDefault(writer, alter);
|
||||
}
|
||||
@@ -115,13 +116,50 @@ public class MySqlDdl extends PlatformDdl {
|
||||
if (hasValue(defaultValue) && !DdlHelp.isDropDefault(defaultValue)) {
|
||||
buffer.append(" default ").append(convertDefaultValue(defaultValue));
|
||||
}
|
||||
// restate the comment (new, existing, or none) as mysql requires the whole column
|
||||
// definition to be repeated - otherwise a comment could be silently dropped
|
||||
String comment = alter.getComment() != null ? alter.getComment() : alter.getCurrentComment();
|
||||
if (DdlHelp.isDropComment(comment)) {
|
||||
comment = null;
|
||||
}
|
||||
appendColumnComment(buffer, comment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void alterTableAddColumn(DdlWrite writer, String tableName, Column column, boolean onHistoryTable, String defaultValue) {
|
||||
String convertedType = convert(column.getType());
|
||||
DdlBuffer buffer = alterTable(writer, tableName).append(addColumn, column.getName());
|
||||
buffer.append(convertedType);
|
||||
|
||||
// Add default value also to history table if it is not excluded
|
||||
if (defaultValue != null) {
|
||||
if (!onHistoryTable || !isTrue(column.isHistoryExclude())) {
|
||||
buffer.append(" default ");
|
||||
buffer.append(defaultValue);
|
||||
}
|
||||
}
|
||||
if (!onHistoryTable) {
|
||||
if (isTrue(column.isNotnull())) {
|
||||
buffer.appendWithSpace(columnNotNull);
|
||||
}
|
||||
// check constraints cannot be added in one statement for h2
|
||||
if (!StringHelper.isNull(column.getCheckConstraint())) {
|
||||
String ddl = alterTableAddCheckConstraint(tableName, column.getCheckConstraintName(), column.getCheckConstraint());
|
||||
writer.applyPostAlter().appendStatement(ddl);
|
||||
}
|
||||
// comment must be inline as part of the column definition for mysql
|
||||
appendColumnComment(buffer, column.getComment());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeColumnDefinition(DdlBuffer buffer, Column column, DdlIdentity identity) {
|
||||
super.writeColumnDefinition(buffer, column, identity);
|
||||
String comment = column.getComment();
|
||||
appendColumnComment(buffer, column.getComment());
|
||||
}
|
||||
|
||||
private void appendColumnComment(DdlBuffer buffer, String comment) {
|
||||
if (!StringHelper.isNull(comment)) {
|
||||
// in mysql 5.5 column comment save in information_schema.COLUMNS.COLUMN_COMMENT(VARCHAR 1024)
|
||||
if (comment.length() > 500) {
|
||||
|
||||
+27
@@ -28,6 +28,7 @@ import java.util.List;
|
||||
* <attribute name="notnull" type="{http://www.w3.org/2001/XMLSchema}boolean" />
|
||||
* <attribute name="currentNotnull" type="{http://www.w3.org/2001/XMLSchema}boolean" />
|
||||
* <attribute name="comment" type="{http://www.w3.org/2001/XMLSchema}string" />
|
||||
* <attribute name="currentComment" type="{http://www.w3.org/2001/XMLSchema}string" />
|
||||
* <attribute name="historyExclude" type="{http://www.w3.org/2001/XMLSchema}boolean" />
|
||||
* <attribute name="checkConstraint" type="{http://www.w3.org/2001/XMLSchema}string" />
|
||||
* <attribute name="checkConstraintName" type="{http://www.w3.org/2001/XMLSchema}string" />
|
||||
@@ -77,6 +78,8 @@ public class AlterColumn {
|
||||
protected Boolean currentNotnull;
|
||||
@XmlAttribute(name = "comment")
|
||||
protected String comment;
|
||||
@XmlAttribute(name = "currentComment")
|
||||
protected String currentComment;
|
||||
@XmlAttribute(name = "historyExclude")
|
||||
protected Boolean historyExclude;
|
||||
@XmlAttribute(name = "checkConstraint")
|
||||
@@ -360,6 +363,30 @@ public class AlterColumn {
|
||||
this.comment = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the currentComment property.
|
||||
* <p>
|
||||
* This is the pre-existing comment value on the column, only populated when a platform
|
||||
* needs to fully restate the column definition (e.g. mysql) so that the existing comment
|
||||
* is not lost when another attribute (type, notnull, or the comment itself) changes.
|
||||
*
|
||||
* @return possible object is
|
||||
* {@link String }
|
||||
*/
|
||||
public String getCurrentComment() {
|
||||
return currentComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the currentComment property.
|
||||
*
|
||||
* @param value allowed object is
|
||||
* {@link String }
|
||||
*/
|
||||
public void setCurrentComment(String value) {
|
||||
this.currentComment = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the historyExclude property.
|
||||
*
|
||||
|
||||
@@ -374,6 +374,7 @@ public class MColumn {
|
||||
this.alterColumn = null;
|
||||
|
||||
boolean changeBaseAttribute = false;
|
||||
boolean changeComment = false;
|
||||
|
||||
if (historyExclude != newColumn.historyExclude) {
|
||||
getAlterColumn(tableName, tableWithHistory).setHistoryExclude(newColumn.historyExclude);
|
||||
@@ -396,6 +397,7 @@ public class MColumn {
|
||||
}
|
||||
}
|
||||
if (different(comment, newColumn.comment)) {
|
||||
changeComment = true;
|
||||
AlterColumn alter = getAlterColumn(tableName, tableWithHistory);
|
||||
if (newColumn.comment == null) {
|
||||
alter.setComment(DdlHelp.DROP_COMMENT);
|
||||
@@ -459,10 +461,14 @@ public class MColumn {
|
||||
|
||||
if (alterColumn != null) {
|
||||
modelDiff.addAlterColumn(alterColumn);
|
||||
if (changeBaseAttribute) {
|
||||
// support reverting these changes
|
||||
if (changeBaseAttribute || changeComment) {
|
||||
// Support reverting these changes and let platforms that must restate the whole
|
||||
// column definition on any change (e.g. mysql) preserve unchanged attributes -
|
||||
// don't lose the existing comment when altering type/notnull, and have the
|
||||
// current type/notnull available to restate when only the comment is changing.
|
||||
alterColumn.setCurrentType(type);
|
||||
alterColumn.setCurrentNotnull(notnull);
|
||||
alterColumn.setCurrentComment(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -2,12 +2,15 @@ package io.ebeaninternal.dbmigration.model.build;
|
||||
|
||||
import io.ebeaninternal.dbmigration.model.MColumn;
|
||||
import io.ebeaninternal.dbmigration.model.MTable;
|
||||
import io.ebean.config.dbplatform.DbPlatformType;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanProperty;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
import io.ebeaninternal.server.deploy.TableJoin;
|
||||
import io.ebeaninternal.server.deploy.TableJoinColumn;
|
||||
|
||||
import java.sql.Types;
|
||||
|
||||
/**
|
||||
* Add the intersection table to the model.
|
||||
*/
|
||||
@@ -79,10 +82,23 @@ class ModelBuildIntersectionTable {
|
||||
for (TableJoinColumn otherColumn : otherColumns) {
|
||||
addColumn(table, targetDesc, otherColumn.getLocalDbColumn(), otherColumn.getForeignDbColumn());
|
||||
}
|
||||
|
||||
if (manyProp.hasIntersectionOrderColumn()) {
|
||||
addOrderColumn(table);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the extra (non PK) order column used to persist {@code @OrderColumn} position
|
||||
* for a ManyToMany relationship.
|
||||
*/
|
||||
private void addOrderColumn(MTable table) {
|
||||
DbPlatformType dbType = ctx.getDbTypeMap().get(Types.INTEGER);
|
||||
MColumn col = new MColumn(manyProp.intersectionOrderColumn(), dbType.renderType(0, 0));
|
||||
col.setNotnull(!manyProp.isIntersectionOrderColumnNullable());
|
||||
table.addColumn(col);
|
||||
}
|
||||
|
||||
private void addColumn(MTable table, BeanDescriptor<?> desc, String column, String findPropColumn) {
|
||||
|
||||
BeanProperty p = desc.idBinder().findBeanProperty(findPropColumn);
|
||||
|
||||
@@ -288,6 +288,7 @@
|
||||
<xsd:attribute name="notnull" type="xsd:boolean"/>
|
||||
<xsd:attribute name="currentNotnull" type="xsd:boolean"/>
|
||||
<xsd:attribute name="comment" type="xsd:string"/>
|
||||
<xsd:attribute name="currentComment" type="xsd:string"/>
|
||||
<xsd:attribute name="historyExclude" type="xsd:boolean"/>
|
||||
<xsd:attribute name="checkConstraint" type="xsd:string"/>
|
||||
<xsd:attribute name="checkConstraintName" type="xsd:string"/>
|
||||
|
||||
+54
@@ -180,6 +180,60 @@ public class PlatformDdl_AlterColumnTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mysql_alterColumn_commentOnly_rebuildsWithType() {
|
||||
AlterColumn alter = new AlterColumn();
|
||||
alter.setTableName("mytab");
|
||||
alter.setColumnName("acol");
|
||||
alter.setCurrentType("varchar(50)");
|
||||
alter.setCurrentNotnull(Boolean.TRUE);
|
||||
alter.setComment("new comment");
|
||||
|
||||
String sql = alterColumn(mysqlDdl, alter);
|
||||
softly.assertThat(sql).isEqualTo("-- apply alter tables\n"
|
||||
+ "alter table mytab modify acol varchar(50) not null comment 'new comment';\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mysql_alterColumn_typeChange_preservesExistingComment() {
|
||||
AlterColumn alter = new AlterColumn();
|
||||
alter.setTableName("mytab");
|
||||
alter.setColumnName("acol");
|
||||
alter.setCurrentType("varchar(20)");
|
||||
alter.setType("varchar(50)");
|
||||
alter.setCurrentComment("existing comment");
|
||||
|
||||
String sql = alterColumn(mysqlDdl, alter);
|
||||
softly.assertThat(sql).isEqualTo("-- apply alter tables\n"
|
||||
+ "alter table mytab modify acol varchar(50) comment 'existing comment';\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mysql_alterColumn_dropComment() {
|
||||
AlterColumn alter = new AlterColumn();
|
||||
alter.setTableName("mytab");
|
||||
alter.setColumnName("acol");
|
||||
alter.setCurrentType("varchar(50)");
|
||||
alter.setComment("DROP COMMENT");
|
||||
alter.setCurrentComment("existing comment");
|
||||
|
||||
String sql = alterColumn(mysqlDdl, alter);
|
||||
softly.assertThat(sql).isEqualTo("-- apply alter tables\n"
|
||||
+ "alter table mytab modify acol varchar(50);\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mysql_alterTableAddColumn_withComment() {
|
||||
Column column = simpleColumn();
|
||||
column.setComment("a comment");
|
||||
|
||||
DdlWrite writer = new DdlWrite();
|
||||
mysqlDdl.alterTableAddColumn(writer, "my_table", column, false, "1");
|
||||
softly.assertThat(writer.toString())
|
||||
.isEqualTo("-- apply alter tables\n"
|
||||
+ "alter table my_table add column my_column int default 1 not null comment 'a comment';\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlterColumnType() {
|
||||
|
||||
|
||||
@@ -174,6 +174,53 @@ class MColumnTest {
|
||||
assertThat(getAlterColumn(diff).getDefaultValue()).isEqualTo("abc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void diffComment_add() {
|
||||
ModelDiff diff = diff();
|
||||
MColumn newCol = basic();
|
||||
newCol.setComment("a comment");
|
||||
basic().compare(diff, table, newCol);
|
||||
|
||||
assertChanges(diff);
|
||||
AlterColumn alterColumn = getAlterColumn(diff);
|
||||
assertThat(alterColumn.getComment()).isEqualTo("a comment");
|
||||
// no pre-existing base attribute changed, current type/notnull/comment carried
|
||||
// through anyway so platforms that must restate the whole column (e.g. mysql)
|
||||
// have what they need to rebuild the statement
|
||||
assertThat(alterColumn.getCurrentType()).isEqualTo("integer");
|
||||
assertThat(alterColumn.getCurrentComment()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void diffComment_remove() {
|
||||
ModelDiff diff = diff();
|
||||
MColumn newCol = basic();
|
||||
MColumn oldCol = basic();
|
||||
oldCol.setComment("a comment");
|
||||
oldCol.compare(diff, table, newCol);
|
||||
|
||||
assertChanges(diff);
|
||||
assertThat(getAlterColumn(diff).getComment()).isEqualTo("DROP COMMENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void diffType_preservesCurrentComment() {
|
||||
ModelDiff diff = diff();
|
||||
MColumn oldCol = basic();
|
||||
oldCol.setComment("existing comment");
|
||||
MColumn newCol = new MColumn("col", "integer(8)");
|
||||
newCol.setComment("existing comment");
|
||||
oldCol.compare(diff, table, newCol);
|
||||
|
||||
assertChanges(diff);
|
||||
AlterColumn alterColumn = getAlterColumn(diff);
|
||||
assertThat(alterColumn.getType()).isEqualTo("integer(8)");
|
||||
assertThat(alterColumn.getComment()).isNull();
|
||||
// current comment recorded so mysql can restate it when rebuilding the full
|
||||
// column definition for the type change
|
||||
assertThat(alterColumn.getCurrentComment()).isEqualTo("existing comment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void diffReferencesAdd() {
|
||||
ModelDiff diff = diff();
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -5,17 +5,23 @@ import io.ebean.Database;
|
||||
import io.ebean.redis.DuelCache;
|
||||
import org.domain.Person;
|
||||
import org.domain.query.QPerson;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ClusterTest {
|
||||
|
||||
private Database createOther(DataSource dataSource) {
|
||||
return Database.builder()
|
||||
.dataSource(dataSource)
|
||||
private static Database db;
|
||||
private static Database other;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// ensure the default server exists first
|
||||
db = DB.getDefault();
|
||||
other = Database.builder()
|
||||
.dataSource(db.pluginApi().dataSource())
|
||||
.loadFromProperties()
|
||||
.defaultDatabase(false)
|
||||
.name("other")
|
||||
@@ -24,12 +30,13 @@ class ClusterTest {
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
other.shutdown(false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBothNear() throws InterruptedException {
|
||||
// ensure the default server exists first
|
||||
final Database db = DB.getDefault();
|
||||
Database other = createOther(db.pluginApi().dataSource());
|
||||
|
||||
new QPerson()
|
||||
.name.eq("Someone")
|
||||
.delete();
|
||||
@@ -59,10 +66,6 @@ class ClusterTest {
|
||||
|
||||
@Test
|
||||
void test() throws InterruptedException {
|
||||
// ensure the default server exists first
|
||||
final Database db = DB.getDefault();
|
||||
Database other = createOther(db.pluginApi().dataSource());
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Person foo = new Person("name " + i);
|
||||
foo.save();
|
||||
|
||||
@@ -8,3 +8,10 @@ ebean:
|
||||
platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite
|
||||
ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate
|
||||
dbName: myapp
|
||||
|
||||
# Keep the shared "ut_redis" test container running rather than removing it on JVM exit.
|
||||
# ebean-redis and ebean-redisson both reuse this fixed-name/fixed-port container - with a
|
||||
# parallel reactor build (mvn -T 1C) the module that finishes first would otherwise remove
|
||||
# the container while the other module's tests are still using it.
|
||||
redis:
|
||||
shutdownMode: none
|
||||
|
||||
@@ -5,17 +5,23 @@ import io.ebean.Database;
|
||||
import io.ebean.redisson.DuelCache;
|
||||
import org.domain.Person;
|
||||
import org.domain.query.QPerson;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ClusterTest {
|
||||
|
||||
private Database createOther(DataSource dataSource) {
|
||||
return Database.builder()
|
||||
.dataSource(dataSource)
|
||||
private static Database db;
|
||||
private static Database other;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// ensure the default server exists first
|
||||
db = DB.getDefault();
|
||||
other = Database.builder()
|
||||
.dataSource(db.pluginApi().dataSource())
|
||||
.loadFromProperties()
|
||||
.defaultDatabase(false)
|
||||
.name("other")
|
||||
@@ -24,12 +30,13 @@ class ClusterTest {
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
other.shutdown(false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBothNear() throws InterruptedException {
|
||||
// ensure the default server exists first
|
||||
final Database db = DB.getDefault();
|
||||
Database other = createOther(db.pluginApi().dataSource());
|
||||
|
||||
new QPerson()
|
||||
.name.eq("Someone")
|
||||
.delete();
|
||||
@@ -59,10 +66,6 @@ class ClusterTest {
|
||||
|
||||
@Test
|
||||
void test() throws InterruptedException {
|
||||
// ensure the default server exists first
|
||||
final Database db = DB.getDefault();
|
||||
Database other = createOther(db.pluginApi().dataSource());
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Person foo = new Person("name " + i);
|
||||
foo.save();
|
||||
|
||||
+4
-4
@@ -213,7 +213,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.checkerframework</groupId>
|
||||
@@ -231,9 +231,9 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.28</version>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.7.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.ebean.xtest.base;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebean.DB;
|
||||
import io.ebean.DtoQuery;
|
||||
import io.ebean.PagedList;
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebean.xtest.ForPlatform;
|
||||
import io.ebean.annotation.Platform;
|
||||
@@ -10,6 +11,7 @@ import io.ebean.meta.MetaQueryMetric;
|
||||
import io.ebean.meta.MetaTimedMetric;
|
||||
import io.ebean.meta.ServerMetrics;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -20,6 +22,7 @@ import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class DtoQueryFromOrmTest extends BaseTestCase {
|
||||
|
||||
@@ -411,6 +414,63 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
|
||||
assertThat(contactDtos).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findPagedList_fromOrmQuery() {
|
||||
|
||||
ResetBasicData.reset();
|
||||
|
||||
PagedList<ContactDto> pagedList = DB.find(Contact.class)
|
||||
.select("id, email, " + concat("lastName", ", ", "firstName") + " as fullName")
|
||||
.where().isNotNull("email")
|
||||
.orderBy().asc("id")
|
||||
.setFirstRow(0)
|
||||
.setMaxRows(2)
|
||||
.asDto(ContactDto.class)
|
||||
.findPagedList();
|
||||
|
||||
List<ContactDto> dtos = pagedList.getList();
|
||||
assertThat(dtos).hasSizeLessThanOrEqualTo(2);
|
||||
|
||||
int totalRowCount = pagedList.getTotalCount();
|
||||
List<ContactDto> allDtos = DB.find(Contact.class)
|
||||
.select("id, email, " + concat("lastName", ", ", "firstName") + " as fullName")
|
||||
.where().isNotNull("email")
|
||||
.orderBy().asc("id")
|
||||
.asDto(ContactDto.class)
|
||||
.findList();
|
||||
|
||||
assertThat(totalRowCount).isEqualTo(allDtos.size());
|
||||
assertThat(pagedList.getPageSize()).isEqualTo(2);
|
||||
assertThat(pagedList.getPageIndex()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findPagedList_noMaxRows_throws() {
|
||||
|
||||
ResetBasicData.reset();
|
||||
|
||||
DtoQuery<ContactDto> query = DB.find(Contact.class)
|
||||
.select("id, email")
|
||||
.where().isNotNull("email")
|
||||
.asDto(ContactDto.class);
|
||||
|
||||
assertThatThrownBy(query::findPagedList)
|
||||
.isInstanceOf(PersistenceException.class)
|
||||
.hasMessageContaining("maxRows must be specified");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findPagedList_rawSql_throws() {
|
||||
|
||||
DtoQuery<ContactDto> query = DB.findDto(ContactDto.class,
|
||||
"select id, email from contact where email is not null")
|
||||
.setMaxRows(10);
|
||||
|
||||
assertThatThrownBy(query::findPagedList)
|
||||
.isInstanceOf(PersistenceException.class)
|
||||
.hasMessageContaining("only supported for a DtoQuery derived");
|
||||
}
|
||||
|
||||
public static class ContactTotals {
|
||||
|
||||
String lastName;
|
||||
|
||||
+10
-6
@@ -15,6 +15,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class EbeanServerFactory_ServerConfigStart_Test {
|
||||
|
||||
@@ -83,7 +84,7 @@ public class EbeanServerFactory_ServerConfigStart_Test {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_registeredDatabase_twice_returnsExistingInstance() {
|
||||
public void create_registeredDatabase_twice_throwsException() {
|
||||
|
||||
DatabaseBuilder config = new DatabaseConfig();
|
||||
config.setName("h2");
|
||||
@@ -100,12 +101,15 @@ public class EbeanServerFactory_ServerConfigStart_Test {
|
||||
config.addServerConfigStartup(serverConfig -> startupCount.incrementAndGet());
|
||||
|
||||
Database db = DatabaseFactory.create(config);
|
||||
Database existing = DatabaseFactory.create(config);
|
||||
try {
|
||||
assertThatThrownBy(() -> DatabaseFactory.create(config))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("already registered");
|
||||
|
||||
assertThat(existing).isSameAs(db);
|
||||
assertThat(startupCount.get()).isEqualTo(1);
|
||||
|
||||
db.shutdown(true, false);
|
||||
assertThat(startupCount.get()).isEqualTo(1);
|
||||
} finally {
|
||||
db.shutdown(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -962,12 +962,7 @@ public class TDSpiEbeanServer extends TDSpiServer implements SpiEbeanServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
public Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
|
||||
@@ -389,12 +389,7 @@ public class TDSpiServer implements SpiServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
public Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,24 @@ public class TestBeanCache extends BaseTestCase {
|
||||
assertThat(sql).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findList_withInIdAsString_when_idTypeConverted_expect_noDuplicates() {
|
||||
OCachedBean bean = new OCachedBean();
|
||||
bean.setName("findList_withInIdAsString");
|
||||
DB.save(bean);
|
||||
|
||||
// warm the bean cache
|
||||
DB.find(OCachedBean.class, bean.getId());
|
||||
|
||||
// id passed as String rather than the bean's actual Long id type
|
||||
List<OCachedBean> list = DB.find(OCachedBean.class)
|
||||
.where().in("id", String.valueOf(bean.getId()))
|
||||
.findList();
|
||||
|
||||
assertThat(list).hasSize(1);
|
||||
assertThat(list.get(0).getId()).isEqualTo(bean.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void idsInFindMap() {
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.tests.cache;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.basic.ECachedEnumId;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for #3110 - Finder.byId() on a @Cache entity with an Enum @Id (mapped via
|
||||
* @EnumValue) threw a NumberFormatException on the second call (cache hit) because
|
||||
* the bean cache key (the enum's name()) was being converted back to the bean id
|
||||
* using the db-value based toBeanType() rather than parse() (the correct inverse
|
||||
* of the cache key's format()).
|
||||
*/
|
||||
class TestCacheEnumId extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
void findById_enumId_whenCacheHit() {
|
||||
ECachedEnumId bean = new ECachedEnumId();
|
||||
bean.setStatus(ECachedEnumId.Status.APPROVED);
|
||||
bean.setName("approved");
|
||||
DB.save(bean);
|
||||
|
||||
// first find - misses the bean cache, loads from DB and populates the cache
|
||||
ECachedEnumId bean0 = DB.find(ECachedEnumId.class, ECachedEnumId.Status.APPROVED);
|
||||
assertThat(bean0).isNotNull();
|
||||
assertThat(bean0.getStatus()).isEqualTo(ECachedEnumId.Status.APPROVED);
|
||||
|
||||
// second find - hits the bean cache, used to throw NumberFormatException
|
||||
ECachedEnumId bean1 = DB.find(ECachedEnumId.class, ECachedEnumId.Status.APPROVED);
|
||||
assertThat(bean1).isNotNull();
|
||||
assertThat(bean1.getStatus()).isEqualTo(ECachedEnumId.Status.APPROVED);
|
||||
assertThat(bean1.getName()).isEqualTo("approved");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.tests.inheritance;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.plugin.ExpressionPath;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.basic.*;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* @author Roland Praml, FOCONIS AG
|
||||
*/
|
||||
public class TestPropertyAccess {
|
||||
|
||||
|
||||
private ExpressionPath custCretime = DB.getDefault().pluginApi().beanType(Customer.class).expressionPath("cretime");
|
||||
private ExpressionPath custName = DB.getDefault().pluginApi().beanType(Customer.class).expressionPath("name");
|
||||
// Note: Animal.name is only present in "Cat" not in "Dog"
|
||||
private ExpressionPath animalName = DB.getDefault().pluginApi().beanType(Animal.class).expressionPath("name");
|
||||
private ExpressionPath productName = DB.getDefault().pluginApi().beanType(Product.class).expressionPath("name");
|
||||
private ExpressionPath animalSpecies = DB.getDefault().pluginApi().beanType(Animal.class).expressionPath("species");
|
||||
|
||||
@Test
|
||||
void testOnInheritance() {
|
||||
Cat cat = new Cat();
|
||||
cat.setName("Tom");
|
||||
DB.save(cat);
|
||||
|
||||
Dog dog = new Dog();
|
||||
dog.setRegistrationNumber("FOO");
|
||||
DB.save(dog);
|
||||
|
||||
|
||||
Animal animal = DB.find(Animal.class, cat.getId());
|
||||
assertThat(animalName.pathGet(animal)).isEqualTo("Tom");
|
||||
|
||||
animal = DB.find(Animal.class, dog.getId());
|
||||
assertThat(animalName.pathGet(animal)).isNull();
|
||||
|
||||
animalName.pathSet(cat, "Jerry");
|
||||
assertThat(cat.getName()).isEqualTo("Jerry");
|
||||
|
||||
assertThatThrownBy(() -> animalName.pathSet(dog, "Jerry"))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
animalSpecies.pathSet(cat, "Angora");
|
||||
animalSpecies.pathSet(dog, "Bulldog");
|
||||
|
||||
assertThat(cat.getSpecies()).isEqualTo("Angora");
|
||||
assertThat(dog.getSpecies()).isEqualTo("Bulldog");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnMappedSuperClass() {
|
||||
Customer cust = new Customer();
|
||||
|
||||
Timestamp ts = new Timestamp(123);
|
||||
custCretime.pathSet(cust, ts);
|
||||
assertThat(custCretime.pathGet(cust)).isEqualTo(ts);
|
||||
|
||||
custName.pathSet(cust, "Roland");
|
||||
assertThat(custName.pathGet(cust)).isEqualTo("Roland");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnPlainBean() {
|
||||
Product product = new Product();
|
||||
|
||||
productName.pathSet(product, "Roland");
|
||||
assertThat(productName.pathGet(product)).isEqualTo("Roland");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnContactGroup() {
|
||||
ContactGroup cg = new ContactGroup();
|
||||
|
||||
// CHECKEM: Ist it OK to us the "custCretime" on "contactGroup"
|
||||
assertThatThrownBy(() -> custCretime.pathGet(cg)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnCrossUsage() {
|
||||
Product product = new Product();
|
||||
Customer cust = new Customer();
|
||||
Cat cat = new Cat();
|
||||
Dog dog = new Dog();
|
||||
|
||||
|
||||
assertThatThrownBy(() -> custCretime.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
|
||||
custCretime.pathGet(cust);
|
||||
assertThatThrownBy(() -> custCretime.pathGet(cat)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> custCretime.pathGet(dog)).isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
assertThatThrownBy(() -> custName.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
|
||||
custName.pathGet(cust);
|
||||
assertThatThrownBy(() -> custName.pathGet(cat)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> custName.pathGet(dog)).isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
assertThatThrownBy(() -> animalName.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> animalName.pathGet(cust)).isInstanceOf(IllegalArgumentException.class);
|
||||
animalName.pathGet(cat);
|
||||
animalName.pathGet(dog);
|
||||
|
||||
productName.pathGet(product);
|
||||
assertThatThrownBy(() -> productName.pathGet(cust)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> productName.pathGet(cat)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> productName.pathGet(dog)).isInstanceOf(IllegalArgumentException.class);
|
||||
|
||||
assertThatThrownBy(() -> animalSpecies.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> animalSpecies.pathGet(cust)).isInstanceOf(IllegalArgumentException.class);
|
||||
animalSpecies.pathGet(cat);
|
||||
animalSpecies.pathGet(dog);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
package org.tests.insert;
|
||||
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.plugin.Property;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.basic.EBasicWithUniqueCon;
|
||||
import org.tests.model.draftable.Document;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -102,9 +105,131 @@ public class TestInsertCheckUnique extends BaseTestCase {
|
||||
|
||||
System.out.println("uniqueProperties > " + uniqueProperties);
|
||||
System.out.println(" custom msg > " + msg);
|
||||
|
||||
}
|
||||
|
||||
LoggedSql.start();
|
||||
assertThat(DB.checkUniqueness(doc2).toString()).contains("title");
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
if (isH2() || isPostgresCompatible()) {
|
||||
assertThat(sql.get(0)).contains("select exists(select 1 from document t0 where t0.title = ?)");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled
|
||||
*/
|
||||
@Test
|
||||
public void testUseQueryCache() {
|
||||
DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test)
|
||||
|
||||
EBasicWithUniqueCon basic = new EBasicWithUniqueCon();
|
||||
basic.setName("foo");
|
||||
basic.setOther("bar");
|
||||
basic.setOtherOne("baz");
|
||||
|
||||
// create a new bean
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(2);
|
||||
if (isH2() || isPostgresCompatible()) {
|
||||
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
|
||||
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
|
||||
}
|
||||
DB.save(basic);
|
||||
try {
|
||||
// reload from database
|
||||
basic = DB.find(EBasicWithUniqueCon.class, basic.getId());
|
||||
|
||||
// and check again
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(2);
|
||||
if (isH2() || isPostgresCompatible()) {
|
||||
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.name = ?)");
|
||||
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.other = ? and t0.other_one = ?)");
|
||||
}
|
||||
|
||||
// and check again - expect to hit query cache
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).as("Expected to hit query cache").hasSize(0);
|
||||
|
||||
// and check again, where only one value is changed
|
||||
basic.setOther("fooo");
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
assertThat(sql.get(0)).contains("fooo,baz)");
|
||||
|
||||
} finally {
|
||||
DB.delete(EBasicWithUniqueCon.class, basic.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled
|
||||
*/
|
||||
@Test
|
||||
public void testSkipClean() {
|
||||
DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test)
|
||||
|
||||
EBasicWithUniqueCon basic = new EBasicWithUniqueCon();
|
||||
basic.setName("foo");
|
||||
basic.setOther("bar");
|
||||
basic.setOtherOne("baz");
|
||||
|
||||
// create a new bean
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, null, false, true)).isEmpty();
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(2);
|
||||
if (isH2() || isPostgresCompatible()) {
|
||||
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
|
||||
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
|
||||
}
|
||||
DB.save(basic);
|
||||
try (Transaction txn = DB.beginTransaction()) {
|
||||
// reload from database
|
||||
basic = DB.find(EBasicWithUniqueCon.class, basic.getId());
|
||||
|
||||
// and check again. We do not check unmodified properties
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, txn, false, true)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(0);
|
||||
|
||||
// and check again, where only one value is changed
|
||||
basic.setOther("fooo");
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, txn, false, true)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
assertThat(sql.get(0)).contains("fooo,baz)");
|
||||
|
||||
// multiple checks will hit DB
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, txn, false, true)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
|
||||
// enable also query cache
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, txn, true, true)).isEmpty();
|
||||
LoggedSql.start();
|
||||
assertThat(DB.getDefault().checkUniqueness(basic, txn, true, true)).isEmpty();
|
||||
sql = LoggedSql.stop();
|
||||
assertThat(sql).isEmpty();
|
||||
} finally {
|
||||
DB.delete(EBasicWithUniqueCon.class, basic.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package org.tests.json;
|
||||
|
||||
import io.ebean.BeanState;
|
||||
import io.ebean.DB;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebean.xtest.IgnorePlatform;
|
||||
import io.ebean.annotation.Platform;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.tests.model.json.EBasicJsonMapMutation;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Verifies that {@code Map<String,Object>} @DbJson properties honour mutationDetection
|
||||
* rather than always using ModifyAware based dirty checking - see issue #2840.
|
||||
*/
|
||||
public class TestDbJsonMapMutationDetection extends BaseTestCase {
|
||||
|
||||
private static Map<String, Object> content(String value) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("field", List.of(value));
|
||||
return map;
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void noneMode_inPlaceMutation_notDetected() {
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setNoneMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
// mutate the map content directly (without calling the setter)
|
||||
found.getNoneMap().put("field", List.of("mutated"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
// NONE means no attempt to detect mutation - so no update at all
|
||||
assertThat(LoggedSql.stop()).isEmpty();
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void noneMode_setEqualValue_noUpdate() {
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setNoneMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
// set a brand new (but equal content) map instance via the setter
|
||||
found.setNoneMap(content("a"));
|
||||
|
||||
BeanState state = DB.beanState(found);
|
||||
assertThat(state.changedProps()).isEmpty();
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
assertThat(LoggedSql.stop()).isEmpty();
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void noneMode_setDifferentValue_updates() {
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setNoneMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
found.setNoneMap(content("b"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
assertThat(sql.get(0)).contains("none_map");
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void hashMode_inPlaceMutation_detected() {
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setHashMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
found.getHashMap().put("field", List.of("mutated"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
assertThat(sql.get(0)).contains("hash_map");
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void hashMode_setEqualValue_noUpdate() {
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setHashMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
// brand new map instance with equal content - hash should match so no update
|
||||
found.setHashMap(content("a"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
assertThat(LoggedSql.stop()).isEmpty();
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void sourceMode_setEqualValue_noUpdate() {
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setSourceMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
found.setSourceMap(content("a"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
assertThat(LoggedSql.stop()).isEmpty();
|
||||
}
|
||||
|
||||
@IgnorePlatform(Platform.MYSQL)
|
||||
@Test
|
||||
public void defaultMode_inPlaceMutation_stillDetectedViaModifyAware() {
|
||||
// unchanged legacy behaviour - DEFAULT mode keeps using ModifyAware wrapper dirty checking
|
||||
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
|
||||
bean.setDefaultMap(content("a"));
|
||||
DB.save(bean);
|
||||
|
||||
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
|
||||
found.getDefaultMap().put("field", List.of("mutated"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(found);
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
assertThat(sql.get(0)).contains("default_map");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.tests.model.basic;
|
||||
|
||||
import io.ebean.annotation.Cache;
|
||||
import jakarta.persistence.*;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.sql.Timestamp;
|
||||
|
||||
@Entity
|
||||
@Cache(enableQueryCache = true)
|
||||
@Table(name = "e_basicverucon")
|
||||
@UniqueConstraint(columnNames = {"other", "other_one"})
|
||||
public class EBasicWithUniqueCon {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.tests.model.basic;
|
||||
|
||||
import io.ebean.annotation.Cache;
|
||||
import io.ebean.annotation.EnumValue;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
* Cached bean with an Enum @Id (mapped via @EnumValue) for testing #3110.
|
||||
*/
|
||||
@Cache
|
||||
@Entity
|
||||
@Table(name = "e_cached_enum_id")
|
||||
public class ECachedEnumId {
|
||||
|
||||
public enum Status {
|
||||
@EnumValue("1")
|
||||
NEW,
|
||||
|
||||
@EnumValue("2")
|
||||
APPROVED,
|
||||
|
||||
@EnumValue("3")
|
||||
DELETED,
|
||||
}
|
||||
|
||||
@Id
|
||||
Status status;
|
||||
|
||||
String name;
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -219,4 +219,36 @@ class TestIdClassScalar extends BaseTestCase {
|
||||
DB.deleteAll(Arrays.asList(user, site));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for #3275 - update/delete on a fresh bean instance that has not been read
|
||||
* from the database or previously inserted via this same bean instance (e.g. built
|
||||
* directly from external data) used to throw a NullPointerException as the IdClass
|
||||
* id had not yet been derived/cached on the bean.
|
||||
*/
|
||||
@Test
|
||||
void update_freshBeanInstance_notPreviouslyLoaded() {
|
||||
UUID siteId = UUID.randomUUID();
|
||||
UUID userId = UUID.randomUUID();
|
||||
|
||||
BSiteUserD access = new BSiteUserD(BAccessLevel.ONE, siteId, userId);
|
||||
DB.save(access);
|
||||
|
||||
// a fresh bean instance - never read from the db or saved via this instance
|
||||
BSiteUserD freshUpdate = new BSiteUserD(BAccessLevel.TWO, siteId, userId);
|
||||
freshUpdate.setVersion(access.getVersion());
|
||||
DB.update(freshUpdate);
|
||||
|
||||
BEmbId id = new BEmbId(siteId, userId);
|
||||
BSiteUserD found = DB.find(BSiteUserD.class, id);
|
||||
assertThat(found).isNotNull();
|
||||
assertThat(found.getAccessLevel()).isEqualTo(BAccessLevel.TWO);
|
||||
|
||||
// a fresh bean instance used for delete
|
||||
BSiteUserD freshDelete = new BSiteUserD(BAccessLevel.TWO, siteId, userId);
|
||||
freshDelete.setVersion(found.getVersion());
|
||||
DB.delete(freshDelete);
|
||||
|
||||
assertThat(DB.find(BSiteUserD.class, id)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.tests.model.elementcollection;
|
||||
|
||||
import jakarta.persistence.CollectionTable;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.OrderColumn;
|
||||
import jakarta.persistence.Version;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Element collection of embeddable values with an explicit {@code @OrderColumn}.
|
||||
*/
|
||||
@Entity
|
||||
public class EcolPerson {
|
||||
|
||||
@Id
|
||||
long id;
|
||||
|
||||
String name;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(joinColumns = @JoinColumn(name = "person_id"))
|
||||
@OrderColumn(name = "ordinal")
|
||||
List<EcPhone> phoneNumbers = new ArrayList<>();
|
||||
|
||||
@Version
|
||||
long version;
|
||||
|
||||
public EcolPerson(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "person id:" + id + " name:" + name + " phs:" + phoneNumbers;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<EcPhone> getPhoneNumbers() {
|
||||
return phoneNumbers;
|
||||
}
|
||||
|
||||
public void setPhoneNumbers(List<EcPhone> phoneNumbers) {
|
||||
this.phoneNumbers = phoneNumbers;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package org.tests.model.elementcollection;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for issue #2393 - {@code @OrderColumn} on an {@code @ElementCollection}.
|
||||
*/
|
||||
public class TestElementCollectionOrderColumn extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
public void insert_populatesOrderColumn() {
|
||||
LoggedSql.start();
|
||||
|
||||
EcolPerson person = new EcolPerson("OrderCol1");
|
||||
person.getPhoneNumbers().add(new EcPhone("64", "021", "1111"));
|
||||
person.getPhoneNumbers().add(new EcPhone("64", "021", "2222"));
|
||||
person.getPhoneNumbers().add(new EcPhone("64", "021", "3333"));
|
||||
DB.save(person);
|
||||
|
||||
List<String> sql = LoggedSql.stop();
|
||||
|
||||
// find the insert statements for the collection table and check ordinal column/values
|
||||
List<String> inserts = sql.stream()
|
||||
.filter(s -> s.contains("insert into ecol_person_phone_numbers"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertThat(inserts).isNotEmpty();
|
||||
assertThat(inserts.get(0)).contains("ordinal");
|
||||
|
||||
// reload and confirm ordering is maintained
|
||||
EcolPerson found = DB.find(EcolPerson.class, person.getId());
|
||||
List<EcPhone> phones = found.getPhoneNumbers();
|
||||
assertThat(phones).hasSize(3);
|
||||
assertThat(phones.get(0).getNumber()).isEqualTo("1111");
|
||||
assertThat(phones.get(1).getNumber()).isEqualTo("2222");
|
||||
assertThat(phones.get(2).getNumber()).isEqualTo("3333");
|
||||
|
||||
DB.delete(person);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetch_hasOrderByOnOrdinal() {
|
||||
EcolPerson person = new EcolPerson("OrderCol2");
|
||||
person.getPhoneNumbers().add(new EcPhone("64", "021", "1111"));
|
||||
person.getPhoneNumbers().add(new EcPhone("64", "021", "2222"));
|
||||
DB.save(person);
|
||||
|
||||
LoggedSql.start();
|
||||
|
||||
EcolPerson found = DB.find(EcolPerson.class)
|
||||
.fetch("phoneNumbers")
|
||||
.where().idEq(person.getId())
|
||||
.findOne();
|
||||
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).isNotEmpty();
|
||||
String trimmed = trimSql(sql.get(0));
|
||||
assertThat(trimmed).contains("order by").contains("ordinal");
|
||||
|
||||
assertThat(found.getPhoneNumbers()).hasSize(2);
|
||||
|
||||
DB.delete(person);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,36 @@
|
||||
package org.tests.model.embedded;
|
||||
|
||||
import io.ebean.DB;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import io.ebean.annotation.Platform;
|
||||
import io.ebean.xtest.BaseTestCase;
|
||||
import io.ebean.xtest.ForPlatform;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
class TestEmbeddedDbArray {
|
||||
class TestEmbeddedDbArray extends BaseTestCase {
|
||||
|
||||
/**
|
||||
* Failing test case for #2477
|
||||
* Test case for #2477 - inserting an empty collection into a @DbArray column
|
||||
* nested inside an @ElementCollection @Embeddable used to throw a NoSuchElementException.
|
||||
* <p>
|
||||
* Restricted to Postgres as the underlying platform - other platforms (e.g. MariaDB, SQLServer)
|
||||
* fall back to JSON storage for @DbArray and MultiValueBind doesn't support binding a single
|
||||
* array value in that case, which is a separate, pre-existing limitation.
|
||||
*/
|
||||
@Disabled
|
||||
@Test
|
||||
void testArrayInsert() {
|
||||
@ForPlatform(Platform.POSTGRES)
|
||||
void testArrayInsert_empty() {
|
||||
EmbArrayMaster t = new EmbArrayMaster(singletonList(new EmbArrayMaster.EmbArrayDetail(emptyList())));
|
||||
DB.insert(t);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ForPlatform(Platform.POSTGRES)
|
||||
void testArrayInsert_nonEmpty() {
|
||||
EmbArrayMaster t = new EmbArrayMaster(singletonList(new EmbArrayMaster.EmbArrayDetail(asList("a", "b"))));
|
||||
DB.insert(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.tests.model.json;
|
||||
|
||||
import io.ebean.annotation.DbJson;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Version;
|
||||
import java.util.Map;
|
||||
|
||||
import static io.ebean.annotation.MutationDetection.HASH;
|
||||
import static io.ebean.annotation.MutationDetection.NONE;
|
||||
import static io.ebean.annotation.MutationDetection.SOURCE;
|
||||
|
||||
/**
|
||||
* Entity with {@code Map<String,Object>} @DbJson properties covering each
|
||||
* {@code MutationDetection} mode - used to verify that the built-in Map JSON
|
||||
* type honours mutationDetection rather than always using ModifyAware checking.
|
||||
*/
|
||||
@Entity
|
||||
public class EBasicJsonMapMutation {
|
||||
|
||||
@Id
|
||||
Long id;
|
||||
|
||||
@DbJson
|
||||
Map<String, Object> defaultMap;
|
||||
|
||||
@DbJson(mutationDetection = NONE)
|
||||
Map<String, Object> noneMap;
|
||||
|
||||
@DbJson(mutationDetection = HASH)
|
||||
Map<String, Object> hashMap;
|
||||
|
||||
@DbJson(mutationDetection = SOURCE)
|
||||
Map<String, Object> sourceMap;
|
||||
|
||||
@Version
|
||||
Long version;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Map<String, Object> getDefaultMap() {
|
||||
return defaultMap;
|
||||
}
|
||||
|
||||
public void setDefaultMap(Map<String, Object> defaultMap) {
|
||||
this.defaultMap = defaultMap;
|
||||
}
|
||||
|
||||
public Map<String, Object> getNoneMap() {
|
||||
return noneMap;
|
||||
}
|
||||
|
||||
public void setNoneMap(Map<String, Object> noneMap) {
|
||||
this.noneMap = noneMap;
|
||||
}
|
||||
|
||||
public Map<String, Object> getHashMap() {
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
public void setHashMap(Map<String, Object> hashMap) {
|
||||
this.hashMap = hashMap;
|
||||
}
|
||||
|
||||
public Map<String, Object> getSourceMap() {
|
||||
return sourceMap;
|
||||
}
|
||||
|
||||
public void setSourceMap(Map<String, Object> sourceMap) {
|
||||
this.sourceMap = sourceMap;
|
||||
}
|
||||
|
||||
public Long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Long version) {
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
-29
@@ -88,35 +88,6 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase {
|
||||
cover.deletePermanent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryByIdDelete_when_softDelete() {
|
||||
|
||||
Cover cover = new Cover("q1");
|
||||
cover.save();
|
||||
|
||||
LoggedSql.start();
|
||||
|
||||
DB.find(Cover.class).setId(cover.getId()).delete();
|
||||
|
||||
List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(1);
|
||||
if (isPlatformBooleanNative()) {
|
||||
assertSql(sql.get(0)).contains("update cover set deleted=true where id = ?");
|
||||
} else {
|
||||
assertSql(sql.get(0)).contains("update cover set deleted=1 where id = ?");
|
||||
}
|
||||
|
||||
assertNull(DB.find(Cover.class, cover.getId()));
|
||||
Cover softDeleted = DB.find(Cover.class)
|
||||
.setIncludeSoftDeletes()
|
||||
.setId(cover.getId())
|
||||
.findOne();
|
||||
assertNotNull(softDeleted);
|
||||
assertThat(softDeleted.isDeleted()).isTrue();
|
||||
|
||||
cover.deletePermanent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletePermanentById_when_softDelete() {
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.tests.order;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class M2mOrderChild {
|
||||
|
||||
@Id
|
||||
long id;
|
||||
|
||||
String name;
|
||||
|
||||
public M2mOrderChild() {
|
||||
}
|
||||
|
||||
public M2mOrderChild(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "M2mOrderChild[" + id + "," + name + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.tests.order;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.OrderColumn;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ManyToMany relationship with an {@code @OrderColumn} - the order value is stored on the
|
||||
* intersection/join table (m2m_order_master_m2m_order_child) rather than on the target bean.
|
||||
*/
|
||||
@Entity
|
||||
public class M2mOrderMaster {
|
||||
|
||||
@Id
|
||||
long id;
|
||||
|
||||
String name;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.ALL)
|
||||
@OrderColumn(name = "sort_order")
|
||||
List<M2mOrderChild> children = new ArrayList<>();
|
||||
|
||||
public M2mOrderMaster() {
|
||||
}
|
||||
|
||||
public M2mOrderMaster(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<M2mOrderChild> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<M2mOrderChild> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "M2mOrderMaster[" + id + "," + name + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.tests.order;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.xtest.base.TransactionalTestCase;
|
||||
import io.ebean.test.LoggedSql;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TestM2mOrderColumn extends TransactionalTestCase {
|
||||
|
||||
@Test
|
||||
void insert_thenOrderIsPersistedAndReturnedInOrder() {
|
||||
M2mOrderMaster master = new M2mOrderMaster("m1");
|
||||
for (int i = 4; i >= 0; i--) {
|
||||
master.getChildren().add(new M2mOrderChild("c" + i));
|
||||
}
|
||||
DB.save(master);
|
||||
|
||||
// plain findOne() + lazy load of children (secondary query)
|
||||
M2mOrderMaster result = DB.find(M2mOrderMaster.class).findOneOrEmpty().orElseThrow();
|
||||
assertThat(result.getChildren()).extracting(M2mOrderChild::getName)
|
||||
.containsExactly("c4", "c3", "c2", "c1", "c0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetch_thenOrderIsRespected() {
|
||||
M2mOrderMaster master = new M2mOrderMaster("mf");
|
||||
for (int i = 4; i >= 0; i--) {
|
||||
master.getChildren().add(new M2mOrderChild("cf" + i));
|
||||
}
|
||||
DB.save(master);
|
||||
|
||||
// fetch("children") uses a SQL JOIN rather than a secondary query
|
||||
M2mOrderMaster result = DB.find(M2mOrderMaster.class)
|
||||
.fetch("children")
|
||||
.where().idEq(master.getId())
|
||||
.findOneOrEmpty()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(result.getChildren()).extracting(M2mOrderChild::getName)
|
||||
.containsExactly("cf4", "cf3", "cf2", "cf1", "cf0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fetchQuery_thenOrderIsRespected() {
|
||||
M2mOrderMaster master = new M2mOrderMaster("m2");
|
||||
for (int i = 4; i >= 0; i--) {
|
||||
master.getChildren().add(new M2mOrderChild("cq" + i));
|
||||
}
|
||||
DB.save(master);
|
||||
|
||||
M2mOrderMaster result = DB.find(M2mOrderMaster.class)
|
||||
.fetchQuery("children")
|
||||
.where().idEq(master.getId())
|
||||
.findOneOrEmpty()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(result.getChildren()).extracting(M2mOrderChild::getName)
|
||||
.containsExactly("cq4", "cq3", "cq2", "cq1", "cq0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reorder_thenNewOrderPersistedOnReload() {
|
||||
M2mOrderMaster master = new M2mOrderMaster("m3");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
master.getChildren().add(new M2mOrderChild("r" + i));
|
||||
}
|
||||
DB.save(master);
|
||||
|
||||
M2mOrderMaster result = DB.find(M2mOrderMaster.class).findOneOrEmpty().orElseThrow();
|
||||
result.getChildren().sort(Comparator.comparing(M2mOrderChild::getName).reversed());
|
||||
DB.save(result);
|
||||
|
||||
M2mOrderMaster reloaded = DB.find(M2mOrderMaster.class).findOneOrEmpty().orElseThrow();
|
||||
assertThat(reloaded.getChildren()).extracting(M2mOrderChild::getName)
|
||||
.containsExactly("r4", "r3", "r2", "r1", "r0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void insert_sqlContainsOrderColumn() {
|
||||
M2mOrderMaster master = new M2mOrderMaster("m4");
|
||||
master.getChildren().add(new M2mOrderChild("s0"));
|
||||
master.getChildren().add(new M2mOrderChild("s1"));
|
||||
|
||||
LoggedSql.start();
|
||||
DB.save(master);
|
||||
List<String> sql = LoggedSql.stop();
|
||||
|
||||
boolean foundIntersectionInsert = sql.stream().anyMatch(s -> s.contains("insert into") && s.contains("sort_order"));
|
||||
assertThat(foundIntersectionInsert)
|
||||
.as("sql: %s", sql)
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ SET @@system_versioning_alter_history = 1;
|
||||
-- NOTE: table has @History - special migration may be necessary
|
||||
update migtest_e_history6 set test_number1 = 42 where test_number1 is null;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
|
||||
alter table `table` modify textfield varchar(255);
|
||||
alter table `table` add column `select` varchar(255);
|
||||
alter table `table` add column textfield2 varchar(255);
|
||||
@@ -185,7 +186,7 @@ alter table migtest_e_basic add column new_boolean_field2 tinyint(1) default 1 n
|
||||
alter table migtest_e_basic add column progress integer default 0 not null;
|
||||
alter table migtest_e_basic add column new_integer integer default 42 not null;
|
||||
alter table migtest_e_history add system versioning;
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
|
||||
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
|
||||
alter table migtest_e_history2 add column test_string2 varchar(255);
|
||||
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
|
||||
|
||||
@@ -50,6 +50,7 @@ SET @@system_versioning_alter_history = 1;
|
||||
-- NOTE: table has @History - special migration may be necessary
|
||||
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is a comment';
|
||||
alter table migtest_e_basic modify status varchar(1);
|
||||
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
|
||||
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
|
||||
@@ -58,6 +59,7 @@ alter table migtest_e_basic add column description_file longblob;
|
||||
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
|
||||
alter table migtest_e_basic add column old_boolean2 tinyint(1);
|
||||
alter table migtest_e_basic add column eref_id integer;
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history2 modify test_string varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
-2111548334, 1.0__initial.sql
|
||||
1085383251, 1.1.sql
|
||||
-858370485, 1.1.sql
|
||||
919151678, 1.2__dropsFor_1.1.sql
|
||||
-1145514481, 1.3.sql
|
||||
1360066142, 1.3.sql
|
||||
-924292968, 1.4__dropsFor_1.3.sql
|
||||
561281075, R__order_views.sql
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ SET @@system_versioning_alter_history = 1;
|
||||
-- NOTE: table has @History - special migration may be necessary
|
||||
update migtest_e_history6 set test_number1 = 42 where test_number1 is null;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
|
||||
alter table `table` modify textfield varchar(255);
|
||||
alter table `table` add column `select` varchar(255);
|
||||
alter table `table` add column textfield2 varchar(255);
|
||||
@@ -185,7 +186,7 @@ alter table migtest_e_basic add column new_boolean_field2 tinyint(1) default 1 n
|
||||
alter table migtest_e_basic add column progress integer default 0 not null;
|
||||
alter table migtest_e_basic add column new_integer integer default 42 not null;
|
||||
alter table migtest_e_history add system versioning;
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
|
||||
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
|
||||
alter table migtest_e_history2 add column test_string2 varchar(255);
|
||||
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
|
||||
|
||||
@@ -50,6 +50,7 @@ SET @@system_versioning_alter_history = 1;
|
||||
-- NOTE: table has @History - special migration may be necessary
|
||||
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is a comment';
|
||||
alter table migtest_e_basic modify status varchar(1);
|
||||
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
|
||||
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
|
||||
@@ -58,6 +59,7 @@ alter table migtest_e_basic add column description_file longblob;
|
||||
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
|
||||
alter table migtest_e_basic add column old_boolean2 tinyint(1);
|
||||
alter table migtest_e_basic add column eref_id integer;
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history2 modify test_string varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
725100959, I__create_procs.sql
|
||||
-2111548334, 1.0__initial.sql
|
||||
1085383251, 1.1.sql
|
||||
-858370485, 1.1.sql
|
||||
1187950993, 1.2__dropsFor_1.1.sql
|
||||
-1145514481, 1.3.sql
|
||||
1360066142, 1.3.sql
|
||||
-298825700, 1.4__dropsFor_1.3.sql
|
||||
561281075, R__order_views.sql
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<addColumn tableName="migtest_e_softdelete">
|
||||
<column name="deleted" type="boolean" defaultValue="false" notnull="true"/>
|
||||
</addColumn>
|
||||
<alterColumn columnName=""index"" tableName=""table"" withHistory="true" comment="this is an other comment"/>
|
||||
<alterColumn columnName=""index"" tableName=""table"" withHistory="true" currentType="varchar" currentNotnull="false" comment="this is an other comment" currentComment="this is a comment"/>
|
||||
<alterColumn columnName="textfield" tableName=""table"" withHistory="true" currentType="varchar" notnull="false" currentNotnull="true"/>
|
||||
<addColumn tableName=""table"" withHistory="true">
|
||||
<column name=""select"" type="varchar"/>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<addUniqueConstraint constraintName="uq_migtest_e_basic_indextest6" tableName="migtest_e_basic" columnNames="indextest6" oneToOne="false" nullableColumns="indextest6"/>
|
||||
<alterTable name="migtest_e_basic" tablespace="db2;TSTABLES;" indexTablespace="db2;INDEXTS;" lobTablespace="db2;TSTABLES;"/>
|
||||
<alterColumn columnName="test_status" tableName="migtest_e_enum" checkConstraint="check ( test_status in ('N','A','I'))" checkConstraintName="ck_migtest_e_enum_test_status"/>
|
||||
<alterColumn columnName="test_string" tableName="migtest_e_history" withHistory="true" comment="DROP COMMENT"/>
|
||||
<alterColumn columnName="test_string" tableName="migtest_e_history" withHistory="true" currentType="bigint" currentNotnull="false" comment="DROP COMMENT" currentComment="Column altered to long now"/>
|
||||
<addTableComment name="migtest_e_history" comment="DROP COMMENT"/>
|
||||
<alterColumn columnName="test_string" tableName="migtest_e_history2" withHistory="true" currentType="varchar" defaultValue="DROP DEFAULT" notnull="false" currentNotnull="true"/>
|
||||
<addColumn tableName="migtest_e_history2" withHistory="true">
|
||||
@@ -47,7 +47,7 @@
|
||||
<column name="name" type="varchar(127)" notnull="true"/>
|
||||
<uniqueConstraint name="uq_migtest_e_ref_name" columnNames="name" oneToOne="false" nullableColumns=""/>
|
||||
</createTable>
|
||||
<alterColumn columnName=""index"" tableName=""table"" withHistory="true" comment="this is a comment"/>
|
||||
<alterColumn columnName=""index"" tableName=""table"" withHistory="true" currentType="varchar" currentNotnull="false" comment="this is a comment" currentComment="this is an other comment"/>
|
||||
<alterTable name="migtest_mtm_c" tablespace="$TABLESPACE_DEFAULT" indexTablespace="$TABLESPACE_DEFAULT" lobTablespace="$TABLESPACE_DEFAULT"/>
|
||||
<alterTable name="migtest_mtm_m" tablespace="$TABLESPACE_DEFAULT" indexTablespace="$TABLESPACE_DEFAULT" lobTablespace="$TABLESPACE_DEFAULT"/>
|
||||
<addUniqueConstraint constraintName="uq_m12_otoc72" tableName="migtest_oto_child" columnNames="name" oneToOne="false" nullableColumns="name" platforms="MYSQL"/>
|
||||
|
||||
@@ -186,6 +186,7 @@ drop trigger table_history_upd;
|
||||
drop trigger table_history_del;
|
||||
drop view table_with_history;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
|
||||
alter table `table` modify textfield varchar(255);
|
||||
alter table `table` add column `select` varchar(255);
|
||||
alter table `table` add column textfield2 varchar(255);
|
||||
@@ -203,7 +204,7 @@ alter table migtest_e_basic add column progress integer default 0 not null;
|
||||
alter table migtest_e_basic add column new_integer integer default 42 not null;
|
||||
alter table migtest_e_history add column sys_period_start datetime(6) default now(6);
|
||||
alter table migtest_e_history add column sys_period_end datetime(6);
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
|
||||
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
|
||||
alter table migtest_e_history2 add column test_string2 varchar(255);
|
||||
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
|
||||
|
||||
@@ -61,6 +61,7 @@ drop view migtest_e_history6_with_history;
|
||||
-- NOTE: table has @History - special migration may be necessary
|
||||
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is a comment';
|
||||
alter table migtest_e_basic modify status varchar(1);
|
||||
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
|
||||
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
|
||||
@@ -69,6 +70,7 @@ alter table migtest_e_basic add column description_file longblob;
|
||||
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
|
||||
alter table migtest_e_basic add column old_boolean2 tinyint(1);
|
||||
alter table migtest_e_basic add column eref_id integer;
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history2 modify test_string varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
725100959, I__create_procs.sql
|
||||
1980830787, 1.0__initial.sql
|
||||
-151422898, 1.1.sql
|
||||
1568294555, 1.1.sql
|
||||
-1097227916, 1.2__dropsFor_1.1.sql
|
||||
888094006, 1.3.sql
|
||||
2018541974, 1.3.sql
|
||||
261977585, 1.4__dropsFor_1.3.sql
|
||||
561281075, R__order_views.sql
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ drop trigger table_history_upd;
|
||||
drop trigger table_history_del;
|
||||
drop view table_with_history;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
|
||||
alter table `table` modify textfield varchar(255);
|
||||
alter table `table` add column `select` varchar(255);
|
||||
alter table `table` add column textfield2 varchar(255);
|
||||
@@ -203,7 +204,7 @@ alter table migtest_e_basic add column progress integer default 0 not null;
|
||||
alter table migtest_e_basic add column new_integer integer default 42 not null;
|
||||
alter table migtest_e_history add column sys_period_start datetime(6) default now(6);
|
||||
alter table migtest_e_history add column sys_period_end datetime(6);
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
|
||||
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
|
||||
alter table migtest_e_history2 add column test_string2 varchar(255);
|
||||
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
|
||||
|
||||
@@ -61,6 +61,7 @@ drop view migtest_e_history6_with_history;
|
||||
-- NOTE: table has @History - special migration may be necessary
|
||||
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
|
||||
-- apply alter tables
|
||||
alter table `table` modify `index` varchar(255) comment 'this is a comment';
|
||||
alter table migtest_e_basic modify status varchar(1);
|
||||
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
|
||||
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
|
||||
@@ -69,6 +70,7 @@ alter table migtest_e_basic add column description_file longblob;
|
||||
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
|
||||
alter table migtest_e_basic add column old_boolean2 tinyint(1);
|
||||
alter table migtest_e_basic add column eref_id integer;
|
||||
alter table migtest_e_history modify test_string bigint;
|
||||
alter table migtest_e_history2 modify test_string varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
|
||||
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
725100959, I__create_procs.sql
|
||||
369577572, 1.0__initial.sql
|
||||
-151422898, 1.1.sql
|
||||
1568294555, 1.1.sql
|
||||
-1097227916, 1.2__dropsFor_1.1.sql
|
||||
888094006, 1.3.sql
|
||||
2018541974, 1.3.sql
|
||||
261977585, 1.4__dropsFor_1.3.sql
|
||||
561281075, R__order_views.sql
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ public class OraclePlatform extends DatabasePlatform {
|
||||
this.dbDefaultValue.setTrue("1");
|
||||
this.dbDefaultValue.setNow("current_timestamp");
|
||||
this.likeClauseRaw = "like ?";
|
||||
this.existsWithCaseWhen = true;
|
||||
this.existsFromClause = " from dual";
|
||||
|
||||
this.exceptionTranslator =
|
||||
new SqlErrorCodes()
|
||||
|
||||
@@ -45,4 +45,11 @@ class OraclePlatformTest {
|
||||
DbPlatformType dbType = platform.dbTypeMap().get(DbPlatformType.UUID);
|
||||
assertThat(dbType.renderType(0, 0)).isEqualTo("raw(16)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsWithCaseWhen_trueForOracle() {
|
||||
OraclePlatform platform = new OraclePlatform();
|
||||
assertThat(platform.existsWithCaseWhen()).isTrue();
|
||||
assertThat(platform.existsFromClause()).isEqualTo(" from dual");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ abstract class SqlServerBasePlatform extends DatabasePlatform {
|
||||
this.idInExpandedForm = true;
|
||||
this.selectCountWithAlias = true;
|
||||
this.selectCountWithColumnAlias = true;
|
||||
this.existsWithCaseWhen = true;
|
||||
this.sqlLimiter = new SqlServerSqlLimiter();
|
||||
this.basicSqlLimiter = new SqlServerBasicSqlLimiter();
|
||||
this.historySupport = new SqlServerHistorySupport();
|
||||
|
||||
+6
@@ -40,6 +40,12 @@ class SqlServerPlatformTest {
|
||||
assertEquals(dbPlatform.unQuote("[firstName]"), "firstName");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void existsWithCaseWhen_trueForSqlServer() {
|
||||
SqlServer17Platform dbPlatform = new SqlServer17Platform();
|
||||
assertEquals(dbPlatform.existsWithCaseWhen(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultTypesForDecimalAndVarchar() {
|
||||
DatabasePlatform dbPlatform = new DatabasePlatform();
|
||||
|
||||
Reference in New Issue
Block a user