Skip to content

Commit

Permalink
PR ebean-orm#3041 - IMPROVED: checkUniqueness supports queryCache and…
Browse files Browse the repository at this point in the history
… skipClean
  • Loading branch information
rPraml committed Aug 10, 2023
1 parent a7db1f6 commit 924f611
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 27 deletions.
4 changes: 2 additions & 2 deletions ebean-api/src/main/java/io/ebean/DB.java
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,8 @@ public static Set<Property> checkUniqueness(Object bean) {
/**
* Same as {@link #checkUniqueness(Object)} but with given transaction.
*/
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
return getDefault().checkUniqueness(bean, transaction);
public static Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) {
return getDefault().checkUniqueness(bean, transaction, useQueryCache, skipClean);
}

/**
Expand Down
13 changes: 11 additions & 2 deletions ebean-api/src/main/java/io/ebean/Database.java
Original file line number Diff line number Diff line change
Expand Up @@ -1105,12 +1105,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2182,12 +2182,7 @@ public void slowQueryCheck(long timeMicros, int rowCount, SpiQuery<?> query) {
}

@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();
Expand All @@ -2199,27 +2194,49 @@ public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transact
if (entityBean._ebean_getIntercept().isNew() && id != null) {
// Primary Key is changeable only on new models - so skip check if we are not new
Query<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
query.setUseQueryCache(useQueryCache);
query.setId(id);
if (findCount(query, transaction) > 0) {
if (exists(query, transaction)) {
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;
}
}
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();
Query<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
query.setUseQueryCache(useQueryCache);
ExpressionList<?> exprList = query.where();
if (!entityBean._ebean_getIntercept().isNew()) {
// if model is not new, exclude ourself.
Expand All @@ -2232,7 +2249,7 @@ private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> b
}
exprList.eq(prop.name(), value);
}
if (findCount(query, transaction) > 0) {
if (exists(query, transaction)) {
Set<Property> ret = new LinkedHashSet<>();
Collections.addAll(ret, props);
return ret;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -958,12 +958,7 @@ public void slowQueryCheck(long executionTimeMicros, int rowCount, SpiQuery<?> q
}

@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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,12 +399,7 @@ public int saveAll(Collection<?> beans, Transaction transaction) throws Optimist
}

@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;
}

Expand Down
119 changes: 118 additions & 1 deletion ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -102,9 +105,123 @@ public void example() {

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);
assertThat(sql.get(0)).contains("select t0.id 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.checkUniqueness(basic, null, true, false)).isEmpty();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("select t0.id from e_basicverucon t0 where t0.name = ?");
assertThat(sql.get(1)).contains("select t0.id 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.checkUniqueness(basic, null, true, false)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("select t0.id from e_basicverucon t0 where t0.id <> ? and t0.name = ?");
assertThat(sql.get(1)).contains("select t0.id 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.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.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.checkUniqueness(basic, null, false, true)).isEmpty();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("select t0.id from e_basicverucon t0 where t0.name = ?");
assertThat(sql.get(1)).contains("select t0.id 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.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.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.checkUniqueness(basic, txn, false, true)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(1);

// enable also query cache
assertThat(DB.checkUniqueness(basic, txn, true, true)).isEmpty();
LoggedSql.start();
assertThat(DB.checkUniqueness(basic, txn, true, true)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).isEmpty();
} finally {
DB.delete(EBasicWithUniqueCon.class, basic.getId());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package org.tests.model.basic;

import io.ebean.annotation.Cache;

import javax.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 {
Expand Down

0 comments on commit 924f611

Please sign in to comment.