Last active
November 22, 2019 08:11
-
-
Save Sam-Kruglov/cf46c96392b7a2c4cddf5450174b6715 to your computer and use it in GitHub Desktop.
Validate persistent collection on insert. https://vladmihalcea.com/hibernate-facts-favoring-sets-vs-bags/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mypackage; | |
import mypackage.Identifiable; | |
import java.util.Objects; | |
import java.util.Set; | |
public final class EntitySetUtil { | |
private EntitySetUtil() { | |
// not meant to be initialized | |
} | |
/** | |
* Adds an entity to the set specified in the following {@link AddDetachedBuilder#to to} method. | |
* | |
* @param item an {@code @Entity} that is not null, which id is null (detached), and which does not yet exists in the set. | |
*/ | |
public static <T extends Identifiable> AddDetachedBuilder<T> addDetachedEntity(T item) { | |
return new AddDetachedBuilder<>(item); | |
} | |
/** | |
* Adds an entity to the set specified in the following {@link AddManagedBuilder#to to} method. | |
* | |
* @param item an {@code @Entity} that is not null, which id is not null (managed), and which does not yet exists | |
* in the set. | |
*/ | |
public static <T extends Identifiable> AddManagedBuilder<T> addManagedEntity(T item) { | |
return new AddManagedBuilder<>(item); | |
} | |
/** | |
* Removes an entity from the set specified in the following {@link RemoveManagedBuilder#from from} method. | |
* | |
* @param item an {@code @Entity} that is not null, which id is not null (managed), and which already exists in the | |
* set. | |
*/ | |
public static <T extends Identifiable> RemoveManagedBuilder<T> removeManagedEntity(T item) { | |
return new RemoveManagedBuilder<>(item); | |
} | |
public static class AddDetachedBuilder<T extends Identifiable> extends BaseWithItemAndExtraCode<T> { | |
private AddDetachedBuilder(final T item) { | |
super(item); | |
} | |
@Override | |
public AddDetachedBuilder<T> withCode(Runnable check) { | |
setExtraCode(check); | |
return this; | |
} | |
/** | |
* Adds the entity assigned in {@link #addDetachedEntity} to the specified set. | |
* | |
* @throws IllegalArgumentException id is not null (managed) or already exists in the set. | |
* @throws NullPointerException is null. | |
*/ | |
public void to(Set<T> items) { | |
assertItemIsNotNull(); | |
assertTrue(item.getId() == null, | |
"Trying to persist an already managed " + item.getClass().getSimpleName() + "."); | |
runExtraCodeIfPresent(); | |
addItemTo(items); | |
} | |
void addItemTo(Set<T> items) { | |
assertTrue(items.add(item), item.getClass().getSimpleName() + " already exists."); | |
} | |
} | |
public static class AddManagedBuilder<T extends Identifiable> extends AddDetachedBuilder<T> { | |
private AddManagedBuilder(final T item) { | |
super(item); | |
} | |
/** | |
* Adds the entity assigned in {@link #addManagedEntity} to the specified set. | |
* | |
* @throws IllegalArgumentException id is null (detached) or already exists in the set. | |
* @throws NullPointerException is null. | |
*/ | |
@Override | |
public void to(Set<T> items) { | |
assertItemIsNotNull(); | |
assertTrue(item.getId() != null, | |
"Trying to add a detached " + item.getClass().getSimpleName() + "."); | |
runExtraCodeIfPresent(); | |
addItemTo(items); | |
} | |
} | |
public static class RemoveManagedBuilder<T extends Identifiable> extends BaseWithItemAndExtraCode<T> { | |
RemoveManagedBuilder(final T item) { | |
super(item); | |
} | |
/** | |
* Executes the supplied code after removing from the set if entity is present there. | |
*/ | |
@Override | |
public RemoveManagedBuilder<T> withCode(Runnable check) { | |
setExtraCode(check); | |
return this; | |
} | |
/** | |
* Removes the entity assigned in the {@link #removeManagedEntity} from the specified set. | |
* | |
* @return true if this set contained the specified entity. | |
* @throws IllegalArgumentException id is null (detached). | |
* @throws NullPointerException is null. | |
*/ | |
public boolean from(Set<T> items) { | |
assertItemIsNotNull(); | |
assertTrue(item.getId() != null, | |
"Trying to remove detached " + item.getClass().getSimpleName() + "."); | |
boolean removed = removeItemFrom(items); | |
if (removed) { | |
runExtraCodeIfPresent(); | |
} | |
return removed; | |
} | |
boolean removeItemFrom(final Set<T> items) { | |
return items.remove(item); | |
} | |
} | |
private abstract static class BaseWithItemAndExtraCode<T extends Identifiable> { | |
final T item; | |
private Runnable extraCode; | |
private BaseWithItemAndExtraCode(final T item) { | |
this.item = item; | |
} | |
/** | |
* Executes the supplied code after validation of the entity. | |
*/ | |
public abstract BaseWithItemAndExtraCode<T> withCode(Runnable check); | |
void setExtraCode(Runnable code) { | |
extraCode = code; | |
} | |
void runExtraCodeIfPresent() { | |
Optional.ofNullable(extraCode).ifPresent(Runnable::run); | |
} | |
void assertItemIsNotNull() { | |
Objects.requireNonNull(item, "Persistent collection cannot work with null elements."); | |
} | |
} | |
private static void assertTrue(Boolean expression, String message) { | |
if (!expression) { | |
throw new IllegalArgumentException(message); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mypackage; | |
import mypackage.Identifiable; | |
import org.junit.jupiter.api.AfterEach; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.api.DisplayName; | |
import org.junit.jupiter.api.Nested; | |
import org.junit.jupiter.api.Tag; | |
import org.junit.jupiter.api.Test; | |
import org.junit.jupiter.api.TestInstance; | |
import java.util.HashSet; | |
import java.util.Set; | |
import static package.EntitySetUtil.addDetachedEntity; | |
import static package.EntitySetUtil.addManagedEntity; | |
import static package.EntitySetUtil.removeManagedEntity; | |
import static org.assertj.core.api.Assertions.assertThat; | |
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; | |
import static org.assertj.core.api.Assertions.assertThatNullPointerException; | |
import static org.assertj.core.api.Assertions.catchThrowable; | |
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; | |
import static org.mockito.Mockito.clearInvocations; | |
import static org.mockito.Mockito.mock; | |
import static org.mockito.Mockito.never; | |
import static org.mockito.Mockito.spy; | |
import static org.mockito.Mockito.verify; | |
import static org.mockito.Mockito.when; | |
@SuppressWarnings("unchecked") | |
@DisplayName("Set of entities") | |
@TestInstance(PER_CLASS) | |
class EntitySetUtilTest { | |
final Set<Identifiable> entities = new HashSet<>(); | |
@AfterEach | |
void clearSet() { | |
entities.clear(); | |
} | |
@DisplayName("supplying managed entity") | |
@Nested | |
class ManagedEntity implements TestSingleton { | |
final Identifiable managedEntity; | |
ManagedEntity() { | |
managedEntity = mock(Identifiable.class); | |
int id = 1; | |
when(managedEntity.getId()).thenReturn(id); | |
} | |
@DisplayName("addDetachedEntity throws 'managed'") | |
@Test | |
void addDetachedEntity_exception() { | |
//act & assert | |
assertThatIllegalArgumentException() | |
.isThrownBy(() -> addDetachedEntity(managedEntity).to(entities)) | |
.withMessageContaining("managed"); | |
} | |
@DisplayName("addManagedEntity adds") | |
@Test | |
void addManagedEntity_added() { | |
//act | |
addManagedEntity(managedEntity).to(entities); | |
//assert | |
assertThat(entities).containsExactly(managedEntity); | |
} | |
@DisplayName("removeManagedEntity returns false") | |
@Test | |
void removeManagedEntity_false() { | |
//act & assert | |
assertThat(removeManagedEntity(managedEntity).from(entities)).isFalse(); | |
} | |
@DisplayName("supplying extra code") | |
@Nested | |
class ExtraCode extends BaseExtraCode { | |
@DisplayName("addManagedEntity executes") | |
@Test | |
void addManagedEntity_executed() { | |
//act | |
addManagedEntity(managedEntity).withCode(extraCode).to(entities); | |
//assert | |
verify(extraCode).run(); | |
} | |
@DisplayName("addDetachedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void addDetachedEntity_exception() { | |
//act | |
catchThrowable(() -> addDetachedEntity(managedEntity).withCode(extraCode).to(entities)); | |
//assert | |
verify(extraCode, never()).run(); | |
} | |
@DisplayName("removeManagedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void removeManagedEntity_exception() { | |
//entity does not exist so we do not execute the code because it may influence hashcode & equals | |
//act | |
catchThrowable(() -> removeManagedEntity(managedEntity).withCode(extraCode).from(entities)); | |
//act & assert | |
verify(extraCode, never()).run(); | |
} | |
} | |
@DisplayName("the entity is already inside the set") | |
@Nested | |
class InsideSet implements TestSingleton { | |
@BeforeEach | |
void addEntityToSet() { | |
entities.add(managedEntity); | |
} | |
@Test | |
@DisplayName("addManagedEntity throws 'exists'") | |
void addManagedEntity_exception() { | |
//act & assert | |
assertThatIllegalArgumentException() | |
.isThrownBy(() -> addManagedEntity(managedEntity).to(entities)) | |
.withMessageContaining("exists"); | |
} | |
@Test | |
@DisplayName("removeManagedEntity removes") | |
void removeManagedEntity_removed() { | |
//act | |
removeManagedEntity(managedEntity).from(entities); | |
//assert | |
assertThat(entities).isEmpty(); | |
} | |
@DisplayName("supplying extra code") | |
@Nested | |
class ExtraCode extends BaseExtraCode { | |
@Test | |
@DisplayName("removeManagedEntity executes") | |
void removeManagedEntity_executed() { | |
//act | |
removeManagedEntity(managedEntity).withCode(extraCode).from(entities); | |
//assert | |
verify(extraCode).run(); | |
} | |
@DisplayName("addManagedEntity executes") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void addManagedEntity_exception() { | |
//entity exists but we still execute the code because it may influence hashcode & equals | |
//act | |
catchThrowable(() -> addManagedEntity(managedEntity).withCode(extraCode).to(entities)); | |
//assert | |
verify(extraCode).run(); | |
} | |
} | |
} | |
} | |
@DisplayName("supplying detached entity") | |
@Nested | |
class DetachedEntity implements TestSingleton { | |
final Identifiable detachedEntity; | |
DetachedEntity() { | |
detachedEntity = mock(Identifiable.class); | |
when(detachedEntity.getId()).thenReturn(null); | |
} | |
@DisplayName("addDetachedEntity adds") | |
@Test | |
void addDetachedEntity_added() { | |
//act | |
addDetachedEntity(detachedEntity).to(entities); | |
//assert | |
assertThat(entities).containsExactly(detachedEntity); | |
} | |
@DisplayName("addManagedEntity throws 'detached'") | |
@Test | |
void addManagedEntity_exception() { | |
//act & assert | |
assertThatIllegalArgumentException() | |
.isThrownBy(() -> addManagedEntity(detachedEntity).to(entities)) | |
.withMessageContaining("detached"); | |
} | |
@DisplayName("removeManagedEntity throws 'detached'") | |
@Test | |
void removeManagedEntity_exception() { | |
//act & assert | |
assertThatIllegalArgumentException() | |
.isThrownBy(() -> removeManagedEntity(detachedEntity).from(entities)) | |
.withMessageContaining("detached"); | |
} | |
@DisplayName("supplying extra code") | |
@Nested | |
class ExtraCode extends BaseExtraCode { | |
@DisplayName("addDetachedEntity executes") | |
@Test | |
void addDetachedEntity_executed() { | |
//act | |
addDetachedEntity(detachedEntity).withCode(extraCode).to(entities); | |
//assert | |
verify(extraCode).run(); | |
} | |
@DisplayName("addManagedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void addManagedEntity_exception() { | |
//act | |
catchThrowable(() -> addManagedEntity(detachedEntity).withCode(extraCode).to(entities)); | |
//assert | |
verify(extraCode, never()).run(); | |
} | |
@DisplayName("removeManagedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void removeManagedEntity_exception() { | |
//act | |
catchThrowable(() -> removeManagedEntity(detachedEntity).withCode(extraCode).from(entities)); | |
//assert | |
verify(extraCode, never()).run(); | |
} | |
} | |
@DisplayName("the entity is already inside the set") | |
@Nested | |
class InsideSet implements TestSingleton { | |
@BeforeEach | |
void addEntityToSet() { | |
entities.add(detachedEntity); | |
} | |
@DisplayName("addDetachedEntity throws 'exists'") | |
@Test | |
void addDetachedEntity_exception() { | |
//act & assert | |
assertThatIllegalArgumentException() | |
.isThrownBy(() -> addDetachedEntity(detachedEntity).to(entities)) | |
.withMessageContaining("exists"); | |
} | |
@DisplayName("supplying extra code") | |
@Nested | |
class ExtraCode extends BaseExtraCode { | |
@DisplayName("addDetachedEntity executes") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void addDetachedEntity_executedBeforeException() { | |
//entity exists but we still execute the code because it may influence hashcode & equals | |
//act | |
catchThrowable(() -> addDetachedEntity(detachedEntity).withCode(extraCode).to(entities)); | |
//assert | |
verify(extraCode).run(); | |
} | |
} | |
} | |
} | |
@DisplayName("supplying null entity") | |
@Nested | |
class NullEntity implements TestSingleton { | |
final Identifiable nullEntity = null; | |
@DisplayName("addDetachedEntity throws 'null'") | |
@Test | |
void addDetachedEntity_exception() { | |
//act & assert | |
assertThatNullPointerException() | |
.isThrownBy(() -> addDetachedEntity(nullEntity).to(entities)) | |
.withMessageContaining("null"); | |
} | |
@DisplayName("addManagedEntity throws 'null'") | |
@Test | |
void addManagedEntity_exception() { | |
//act & assert | |
assertThatNullPointerException() | |
.isThrownBy(() -> addManagedEntity(nullEntity).to(entities)) | |
.withMessageContaining("null"); | |
} | |
@DisplayName("removeManagedEntity throws 'null'") | |
@Test | |
void removeManagedEntity_exception() { | |
//act & assert | |
assertThatNullPointerException() | |
.isThrownBy(() -> removeManagedEntity(nullEntity).from(entities)) | |
.withMessageContaining("null"); | |
} | |
@DisplayName("supplying extra code") | |
@Nested | |
class ExtraCode extends BaseExtraCode { | |
@DisplayName("addDetachedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void addDetachedEntity_exception() { | |
//act | |
catchThrowable(() -> addDetachedEntity(nullEntity).withCode(extraCode).to(entities)); | |
//assert | |
verify(extraCode, never()).run(); | |
} | |
@DisplayName("addManagedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void addManagedEntity_exception() { | |
//act | |
catchThrowable(() -> addManagedEntity(nullEntity).withCode(extraCode).to(entities)); | |
//assert | |
verify(extraCode, never()).run(); | |
} | |
@DisplayName("removeManagedEntity does not execute") | |
@Test | |
@SuppressWarnings("ThrowableNotThrown") | |
void removeManagedEntity_exception() { | |
//act | |
catchThrowable(() -> removeManagedEntity(nullEntity).withCode(extraCode).from(entities)); | |
//assert | |
verify(extraCode, never()).run(); | |
} | |
} | |
} | |
class BaseExtraCode implements TestSingleton { | |
final Runnable extraCode = spy(Runnable.class); | |
@AfterEach | |
void clearSpy() { | |
clearInvocations(extraCode); | |
} | |
} | |
@TestInstance(PER_CLASS) | |
interface TestSingleton { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mypackage; | |
import java.io.Serializable; | |
public interface Identifiable<T extends Serializable> { | |
T getId(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment