HHH-17164 - Proper, first-class soft-delete support
HHH-17311 - Reversed soft delete support https://hibernate.atlassian.net/browse/HHH-17164 https://hibernate.atlassian.net/browse/HHH-17311
This commit is contained in:
parent
2cb4652688
commit
51f2f4f75d
|
@ -33,6 +33,7 @@ include::associations.adoc[]
|
|||
include::collections.adoc[]
|
||||
include::natural_id.adoc[]
|
||||
include::partitioning.adoc[]
|
||||
include::soft_delete.adoc[]
|
||||
include::dynamic_model.adoc[]
|
||||
include::inheritance.adoc[]
|
||||
include::immutability.adoc[]
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
[[soft-delete]]
|
||||
=== Soft Delete
|
||||
:root-project-dir: ../../../../../../..
|
||||
:core-project-dir: {root-project-dir}/hibernate-core
|
||||
:testing-dir: {core-project-dir}/src/test/java/org/hibernate/orm/test/softdelete
|
||||
|
||||
An occasional requirement seen in the wild is to never physically remove rows from the database, but to
|
||||
instead perform a "soft delete" where a column is updated to indicate that the row is no longer active.
|
||||
Hibernate offers first-class support for this behavior through its `@SoftDelete` annotation.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `@SoftDelete` annotation is new in 6.4.
|
||||
|
||||
It was possible to hack together support for soft deletes in previous versions using a combination of filters,
|
||||
`@Where` and custom delete event handling. However, that approach was tedious and did not work in
|
||||
all cases. `@SoftDelete` should be highly preferred.
|
||||
====
|
||||
|
||||
Hibernate supports soft delete for both <<soft-delete-entity,entities>> and <<soft-delete-collection,collections>>.
|
||||
|
||||
Soft delete support is defined by 3 main parts -
|
||||
|
||||
1. The <<soft-delete-column,column>> which contains the indicator.
|
||||
2. A conversion from `Boolean` indicator value to the proper database type
|
||||
3. Whether to <<soft-delete-reverse,reverse>> the indicator values
|
||||
|
||||
|
||||
[[soft-delete-column]]
|
||||
==== Indicator column
|
||||
|
||||
The column where the indicator value is stored is defined using `@SoftDelete#columnName` attribute. This
|
||||
defaults to the name `deleted`.
|
||||
|
||||
See <<soft-delete-basic-example>> for an example of customizing the column name.
|
||||
|
||||
Depending on the conversion type, an appropriate check constraint may be applied to the column.
|
||||
|
||||
|
||||
[[soft-delete-conversion]]
|
||||
==== Indicator conversion
|
||||
|
||||
The conversion is defined using a JPA <<basic-jpa-convert,AttributeConverter>>. The "domain type" is always
|
||||
boolean. The "relational type" can be any type, as defined by the converter; though generally speaking,
|
||||
numerics and characters work best.
|
||||
|
||||
An explicit conversion can be specified using `@SoftDelete#converter`. See <<soft-delete-basic-example>>
|
||||
for an example of specifying an explicit conversion. Explicit conversions can leverage the 3
|
||||
Hibernate-provided converters for the 3 most common cases -
|
||||
|
||||
`NumericBooleanConverter`:: Defines conversion using `0` for `false` and `1` for `true`
|
||||
`YesNoConverter`:: Defines conversion using `'N'` for `false` and `'Y'` for `true`
|
||||
`TrueFalseConverter`:: Defines conversion using `'F'` for `false` and `'T'` for `true`
|
||||
|
||||
If an explicit converter is not specified, Hibernate will follow the same resolution steps defined in
|
||||
<<basic-boolean>> to determine the proper database type. This breaks down into 3 categories -
|
||||
|
||||
boolean (and bit):: the underlying type is boolean / bit and no conversion is applied
|
||||
numeric:: the underlying type is integer and values are converted according to `NumericBooleanConverter`
|
||||
character:: the underlying type is char and values are converted according to `TrueFalseConverter`
|
||||
|
||||
|
||||
[[soft-delete-entity]]
|
||||
==== Entity soft delete
|
||||
|
||||
Hibernate supports the soft delete of entities, with the indicator column defined on the primary table.
|
||||
|
||||
[[soft-delete-basic-example]]
|
||||
.Basic entity soft-delete
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{testing-dir}/SimpleEntity.java[tag=example-soft-delete-basic, indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
For entity hierarchies, the soft delete applies to all inheritance types.
|
||||
|
||||
[[soft-delete-secondary-example]]
|
||||
.Inherited entity soft-delete
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{testing-dir}/secondary/JoinedRoot.java[tag=example-soft-delete-secondary, indent=0]
|
||||
include::{testing-dir}/secondary/JoinedSub.java[tag=example-soft-delete-secondary, indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
See also <<soft-delete-package>>.
|
||||
|
||||
|
||||
[[soft-delete-collection]]
|
||||
==== Collection soft delete
|
||||
|
||||
Soft delete may be applied to collection mapped with a "collection table", aka `@ElementCollection`
|
||||
and `@ManyToMany`. The soft delete applies to the collection table row.
|
||||
|
||||
Annotating a `@OneToMany` association with `@SoftDelete` will throw an exception.
|
||||
|
||||
In the case of `@OneToMany` and `@ManyToMany`, the mapped entity may itself be soft deletable which is
|
||||
handled transparently.
|
||||
|
||||
[[soft-delete-element-collection-example]]
|
||||
.Soft delete for @ElementCollection
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{testing-dir}/collections/CollectionOwner.java[tag=example-soft-delete-element-collection, indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Given this `@ElementCollection` mapping, rows in the `elements` table will be soft deleted using an indicator column named `deleted`.
|
||||
|
||||
[[soft-delete-many2many-example]]
|
||||
.Soft delete for @ManyToMany
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{testing-dir}/collections/CollectionOwner.java[tag=example-soft-delete-many-to-many, indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Given this `@ManyToMany` mapping, rows in the `m2m` table will be soft deleted using an indicator column named `gone`.
|
||||
|
||||
See also <<soft-delete-package>>.
|
||||
|
||||
|
||||
[[soft-delete-package]]
|
||||
==== Package-level soft delete
|
||||
|
||||
The `@SoftDelete` annotation may also be placed at the package level, in which case it applies to all
|
||||
entities and collections defined within the package.
|
||||
|
||||
|
||||
[[soft-delete-reverse]]
|
||||
==== Reversed soft delete
|
||||
|
||||
A common requirement in applications using soft delete is to track rows which are active as opposed to removed,
|
||||
reversing the boolean value. For example:
|
||||
|
||||
[[soft-delete-reverse-example]]
|
||||
.Reversed soft-delete
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{testing-dir}/converter/reversed/TheEntity.java[tag=example-soft-delete-reverse, indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
When an instance of `TheEntity` is persisted, the value `'Y'` will be inserted into the
|
||||
`active` column. When an instance of `TheEntity` is removed, the column's value is updated to `'N'`.
|
||||
|
||||
This example explicitly specifies the built-in `YesNoConverter`, but reversal works with any conversion
|
||||
even implicit conversions -
|
||||
|
||||
[[soft-delete-reverse-example-2]]
|
||||
.Reversed soft-delete with implicit conversion
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{testing-dir}/converter/reversed/TheEntity2.java[tag=example-soft-delete-reverse, indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The important thing to remember is that the stored values are reversed from the "normal" soft delete state.
|
||||
`active == true` is the same as `deleted == false` - both describe the same state.
|
|
@ -12,7 +12,6 @@ import java.lang.annotation.Retention;
|
|||
import java.lang.annotation.Target;
|
||||
|
||||
import org.hibernate.dialect.Dialect;
|
||||
import org.hibernate.type.BooleanAsBooleanConverter;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
|
||||
|
@ -65,19 +64,6 @@ public @interface SoftDelete {
|
|||
*/
|
||||
String columnName() default "deleted";
|
||||
|
||||
/**
|
||||
* (Optional) The Java type used for values when dealing with JDBC.
|
||||
* This type should match the "relational type" of the specified
|
||||
* {@linkplain #converter() converter}.
|
||||
* <p/>
|
||||
* By default, Hibernate will inspect the {@linkplain #converter() converter}
|
||||
* and determine the proper type from its signature.
|
||||
*
|
||||
* @apiNote Sometimes useful since {@linkplain #converter() converter}
|
||||
* signatures are not required to be parameterized.
|
||||
*/
|
||||
Class<?> jdbcType() default void.class;
|
||||
|
||||
/**
|
||||
* (Optional) Conversion to apply to determine the appropriate value to
|
||||
* store in the database. The "domain representation" can be: <dl>
|
||||
|
@ -94,5 +80,19 @@ public @interface SoftDelete {
|
|||
*
|
||||
* @apiNote The converter should never return {@code null}
|
||||
*/
|
||||
Class<? extends AttributeConverter<Boolean,?>> converter() default BooleanAsBooleanConverter.class;
|
||||
Class<? extends AttributeConverter<Boolean,?>> converter() default UnspecifiedConversion.class;
|
||||
|
||||
/**
|
||||
* Whether the stored values should be reversed. This is used when the application tracks
|
||||
* rows that are active as opposed to rows that are deleted.
|
||||
*/
|
||||
boolean reversed() default false;
|
||||
|
||||
/**
|
||||
* Used as the default for {@linkplain SoftDelete#converter()}, indicating that
|
||||
* {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} and
|
||||
* {@linkplain org.hibernate.cfg.MappingSettings#PREFERRED_BOOLEAN_JDBC_TYPE settings}
|
||||
* resolution should be used.
|
||||
*/
|
||||
interface UnspecifiedConversion extends AttributeConverter<Boolean,Object> {}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ public class SoftDeleteHelper {
|
|||
);
|
||||
|
||||
final BasicValue softDeleteIndicatorValue = new BasicValue( context, table );
|
||||
softDeleteIndicatorValue.makeSoftDelete( softDelete.reversed() );
|
||||
softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor );
|
||||
softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> {
|
||||
return converterDescriptor.getRelationalValueResolvedType().getErasedType();
|
||||
|
@ -149,8 +150,17 @@ public class SoftDeleteHelper {
|
|||
//noinspection unchecked
|
||||
final JdbcLiteralFormatter<Object> literalFormatter = resolution.getJdbcMapping().getJdbcLiteralFormatter();
|
||||
|
||||
final Object deletedLiteralValue = converter.toRelationalValue( true );
|
||||
final Object nonDeletedLiteralValue = converter.toRelationalValue( false );
|
||||
final Object deletedLiteralValue;
|
||||
final Object nonDeletedLiteralValue;
|
||||
if ( converter == null ) {
|
||||
// the database column is BIT or BOOLEAN : pass-thru
|
||||
deletedLiteralValue = true;
|
||||
nonDeletedLiteralValue = false;
|
||||
}
|
||||
else {
|
||||
deletedLiteralValue = converter.toRelationalValue( true );
|
||||
nonDeletedLiteralValue = converter.toRelationalValue( false );
|
||||
}
|
||||
|
||||
return new SoftDeleteMappingImpl(
|
||||
softDeletableModelPart,
|
||||
|
|
|
@ -16,9 +16,13 @@ import org.hibernate.Incubating;
|
|||
import org.hibernate.Internal;
|
||||
import org.hibernate.MappingException;
|
||||
import org.hibernate.TimeZoneStorageStrategy;
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.annotations.TimeZoneStorageType;
|
||||
import org.hibernate.boot.model.TypeDefinition;
|
||||
import org.hibernate.boot.model.convert.internal.AutoApplicableConverterDescriptorBypassedImpl;
|
||||
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
|
||||
import org.hibernate.boot.model.convert.internal.InstanceBasedConverterDescriptor;
|
||||
import org.hibernate.boot.model.convert.spi.AutoApplicableConverterDescriptor;
|
||||
import org.hibernate.boot.model.convert.spi.ConverterDescriptor;
|
||||
import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext;
|
||||
import org.hibernate.boot.model.process.internal.InferredBasicValueResolution;
|
||||
|
@ -47,13 +51,17 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
|
|||
import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation;
|
||||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.CustomType;
|
||||
import org.hibernate.type.NumericBooleanConverter;
|
||||
import org.hibernate.type.TrueFalseConverter;
|
||||
import org.hibernate.type.Type;
|
||||
import org.hibernate.type.WrapperArrayHandling;
|
||||
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
|
||||
import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter;
|
||||
import org.hibernate.type.descriptor.java.BasicJavaType;
|
||||
import org.hibernate.type.descriptor.java.BasicPluralJavaType;
|
||||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
import org.hibernate.type.descriptor.java.MutabilityPlan;
|
||||
import org.hibernate.type.descriptor.jdbc.BooleanJdbcType;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcType;
|
||||
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
|
||||
import org.hibernate.type.internal.BasicTypeImpl;
|
||||
|
@ -62,6 +70,7 @@ import org.hibernate.type.spi.TypeConfigurationAware;
|
|||
import org.hibernate.usertype.DynamicParameterizedType;
|
||||
import org.hibernate.usertype.UserType;
|
||||
|
||||
import com.fasterxml.classmate.ResolvedType;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.TemporalType;
|
||||
|
@ -73,7 +82,7 @@ import static org.hibernate.mapping.MappingHelper.injectParameters;
|
|||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resolvable {
|
||||
public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resolvable, JpaAttributeConverterCreationContext {
|
||||
private static final CoreMessageLogger log = CoreLogging.messageLogger( BasicValue.class );
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -90,6 +99,8 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
private EnumType enumerationStyle;
|
||||
private TemporalType temporalPrecision;
|
||||
private TimeZoneStorageType timeZoneStorageType;
|
||||
private boolean isSoftDelete;
|
||||
private boolean isSoftDeleteReversed;
|
||||
|
||||
private java.lang.reflect.Type resolvedJavaType;
|
||||
|
||||
|
@ -135,6 +146,19 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
return new BasicValue( this );
|
||||
}
|
||||
|
||||
public boolean isSoftDelete() {
|
||||
return isSoftDelete;
|
||||
}
|
||||
|
||||
public boolean isSoftDeleteReversed() {
|
||||
return isSoftDeleteReversed;
|
||||
}
|
||||
|
||||
public void makeSoftDelete(boolean reversed) {
|
||||
isSoftDelete = true;
|
||||
isSoftDeleteReversed = reversed;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Setters - in preparation of resolution
|
||||
|
||||
|
@ -417,16 +441,168 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
}
|
||||
|
||||
// determine JavaType if we can
|
||||
final BasicJavaType<?> explicitJavaType =
|
||||
explicitJavaTypeAccess == null ? null : explicitJavaTypeAccess.apply( getTypeConfiguration() );
|
||||
final JavaType<?> javaType = determineJavaType( explicitJavaType );
|
||||
final BasicJavaType<?> explicitJavaType = explicitJavaTypeAccess == null
|
||||
? null
|
||||
: explicitJavaTypeAccess.apply( getTypeConfiguration() );
|
||||
|
||||
JavaType<?> javaType = determineJavaType( explicitJavaType );
|
||||
ConverterDescriptor attributeConverterDescriptor = getAttributeConverterDescriptor();
|
||||
|
||||
if ( isSoftDelete() ) {
|
||||
assert attributeConverterDescriptor != null;
|
||||
final boolean conversionWasUnspecified = SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() );
|
||||
if ( conversionWasUnspecified ) {
|
||||
final JdbcType jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType );
|
||||
if ( jdbcType.isNumber() ) {
|
||||
attributeConverterDescriptor = new InstanceBasedConverterDescriptor(
|
||||
NumericBooleanConverter.INSTANCE,
|
||||
getBuildingContext().getBootstrapContext().getClassmateContext()
|
||||
);
|
||||
}
|
||||
else if ( jdbcType.isString() ) {
|
||||
// here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50
|
||||
attributeConverterDescriptor = new InstanceBasedConverterDescriptor(
|
||||
TrueFalseConverter.INSTANCE,
|
||||
getBuildingContext().getBootstrapContext().getClassmateContext()
|
||||
);
|
||||
}
|
||||
else {
|
||||
// should indicate BIT or BOOLEAN == no conversion needed
|
||||
// - we still create the converter to properly set up JDBC type, etc
|
||||
attributeConverterDescriptor = new InstanceBasedConverterDescriptor(
|
||||
PassThruSoftDeleteConverter.INSTANCE,
|
||||
getBuildingContext().getBootstrapContext().getClassmateContext()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( isSoftDeleteReversed() ) {
|
||||
attributeConverterDescriptor = new ReversedConverterDescriptor<>( attributeConverterDescriptor );
|
||||
}
|
||||
}
|
||||
|
||||
final ConverterDescriptor attributeConverterDescriptor = getAttributeConverterDescriptor();
|
||||
return attributeConverterDescriptor != null
|
||||
? converterResolution( javaType, attributeConverterDescriptor )
|
||||
: resolution( explicitJavaType, javaType );
|
||||
}
|
||||
|
||||
private static class ReversedConverterDescriptor<R> implements ConverterDescriptor {
|
||||
private final ConverterDescriptor underlyingDescriptor;
|
||||
|
||||
public ReversedConverterDescriptor(ConverterDescriptor underlyingDescriptor) {
|
||||
this.underlyingDescriptor = underlyingDescriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends AttributeConverter<Boolean,R>> getAttributeConverterClass() {
|
||||
//noinspection unchecked
|
||||
return (Class<? extends AttributeConverter<Boolean, R>>) getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedType getDomainValueResolvedType() {
|
||||
return underlyingDescriptor.getDomainValueResolvedType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedType getRelationalValueResolvedType() {
|
||||
return underlyingDescriptor.getRelationalValueResolvedType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoApplicableConverterDescriptor getAutoApplyDescriptor() {
|
||||
return AutoApplicableConverterDescriptorBypassedImpl.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaAttributeConverter<Boolean,R> createJpaAttributeConverter(JpaAttributeConverterCreationContext context) {
|
||||
//noinspection unchecked
|
||||
return new ReversedJpaAttributeConverter<>(
|
||||
(JpaAttributeConverter<Boolean, R>) underlyingDescriptor.createJpaAttributeConverter( context ),
|
||||
context.getJavaTypeRegistry().getDescriptor( ReversedJpaAttributeConverter.class )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReversedJpaAttributeConverter<R, B extends AttributeConverter<Boolean, R>>
|
||||
implements JpaAttributeConverter<Boolean,R>, AttributeConverter<Boolean,R>, ManagedBean<B> {
|
||||
private final JpaAttributeConverter<Boolean,R> underlyingJpaConverter;
|
||||
private final JavaType<ReversedJpaAttributeConverter<R,B>> converterJavaType;
|
||||
|
||||
public ReversedJpaAttributeConverter(
|
||||
JpaAttributeConverter<Boolean, R> underlyingJpaConverter,
|
||||
JavaType<ReversedJpaAttributeConverter<R,B>> converterJavaType) {
|
||||
this.underlyingJpaConverter = underlyingJpaConverter;
|
||||
this.converterJavaType = converterJavaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean toDomainValue(R relationalValue) {
|
||||
return !underlyingJpaConverter.toDomainValue( relationalValue );
|
||||
}
|
||||
|
||||
@Override
|
||||
public R toRelationalValue(Boolean domainValue) {
|
||||
return underlyingJpaConverter.toRelationalValue( !domainValue );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean convertToEntityAttribute(R relationalValue) {
|
||||
return toDomainValue( relationalValue );
|
||||
}
|
||||
|
||||
@Override
|
||||
public R convertToDatabaseColumn(Boolean domainValue) {
|
||||
return toRelationalValue( domainValue );
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<Boolean> getDomainJavaType() {
|
||||
return underlyingJpaConverter.getDomainJavaType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<R> getRelationalJavaType() {
|
||||
return underlyingJpaConverter.getRelationalJavaType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<? extends AttributeConverter<Boolean, R>> getConverterJavaType() {
|
||||
return converterJavaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedBean<? extends AttributeConverter<Boolean, R>> getConverterBean() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<B> getBeanClass() {
|
||||
//noinspection unchecked
|
||||
return (Class<B>) getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public B getBeanInstance() {
|
||||
//noinspection unchecked
|
||||
return (B) this;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PassThruSoftDeleteConverter implements AttributeConverter<Boolean,Boolean> {
|
||||
private static final PassThruSoftDeleteConverter INSTANCE = new PassThruSoftDeleteConverter();
|
||||
|
||||
@Override
|
||||
public Boolean convertToDatabaseColumn(Boolean domainValue) {
|
||||
return domainValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean convertToEntityAttribute(Boolean relationalValue) {
|
||||
return relationalValue;
|
||||
}
|
||||
}
|
||||
|
||||
private Resolution<?> resolution(BasicJavaType explicitJavaType, JavaType<?> javaType) {
|
||||
final JavaType<?> basicJavaType;
|
||||
final JdbcType jdbcType;
|
||||
|
@ -471,24 +647,19 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
|
|||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedBeanRegistry getManagedBeanRegistry() {
|
||||
return getServiceRegistry().getService( ManagedBeanRegistry.class );
|
||||
}
|
||||
|
||||
private Resolution<?> converterResolution(JavaType<?> javaType, ConverterDescriptor attributeConverterDescriptor) {
|
||||
final ManagedBeanRegistry managedBeanRegistry = getServiceRegistry().getService( ManagedBeanRegistry.class );
|
||||
final NamedConverterResolution<?> converterResolution = NamedConverterResolution.from(
|
||||
attributeConverterDescriptor,
|
||||
explicitJavaTypeAccess,
|
||||
explicitJdbcTypeAccess,
|
||||
explicitMutabilityPlanAccess,
|
||||
this,
|
||||
new JpaAttributeConverterCreationContext() {
|
||||
@Override
|
||||
public ManagedBeanRegistry getManagedBeanRegistry() {
|
||||
return managedBeanRegistry;
|
||||
}
|
||||
@Override
|
||||
public TypeConfiguration getTypeConfiguration() {
|
||||
return BasicValue.this.getTypeConfiguration();
|
||||
}
|
||||
},
|
||||
this,
|
||||
getBuildingContext()
|
||||
);
|
||||
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||
*/
|
||||
package org.hibernate.type;
|
||||
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.type.descriptor.java.BooleanJavaType;
|
||||
import org.hibernate.type.descriptor.java.JavaType;
|
||||
|
||||
/**
|
||||
* Simple pass-through boolean value converter.
|
||||
* Useful in {@linkplain SoftDelete#converter() certain scenarios}.
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class BooleanAsBooleanConverter implements StandardBooleanConverter<Boolean> {
|
||||
public static final BooleanAsBooleanConverter INSTANCE = new BooleanAsBooleanConverter();
|
||||
|
||||
@Override
|
||||
public Boolean convertToDatabaseColumn(Boolean attribute) {
|
||||
return toRelationalValue( attribute );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean convertToEntityAttribute(Boolean dbData) {
|
||||
return toDomainValue( dbData );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean toDomainValue(Boolean relationalForm) {
|
||||
return relationalForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean toRelationalValue(Boolean domainForm) {
|
||||
return domainForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<Boolean> getDomainJavaType() {
|
||||
return BooleanJavaType.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaType<Boolean> getRelationalJavaType() {
|
||||
return BooleanJavaType.INSTANCE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||
*/
|
||||
package org.hibernate.orm.test.softdelete;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.ObjectNotFoundException;
|
||||
import org.hibernate.annotations.NaturalId;
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@DomainModel(annotatedClasses = ImplicitSoftDeleteTests.ImplicitEntity.class)
|
||||
@SessionFactory
|
||||
public class ImplicitSoftDeleteTests {
|
||||
@BeforeEach
|
||||
void createTestData(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
session.persist( new ImplicitEntity( 1, "first" ) );
|
||||
session.persist( new ImplicitEntity( 2, "second" ) );
|
||||
session.persist( new ImplicitEntity( 3, "third" ) );
|
||||
} );
|
||||
|
||||
scope.inTransaction( (session) -> {
|
||||
final ImplicitEntity first = session.getReference( ImplicitEntity.class, 1 );
|
||||
session.remove( first );
|
||||
|
||||
session.flush();
|
||||
|
||||
// make sure all 3 are still physically there
|
||||
session.doWork( (connection) -> {
|
||||
final Statement statement = connection.createStatement();
|
||||
final ResultSet resultSet = statement.executeQuery( "select count(1) from implicit_entities" );
|
||||
resultSet.next();
|
||||
final int count = resultSet.getInt( 1 );
|
||||
assertThat( count ).isEqualTo( 3 );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> session.doWork( (connection) -> {
|
||||
final Statement statement = connection.createStatement();
|
||||
statement.execute( "delete from implicit_entities" );
|
||||
} ) );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSelectionQuery(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
// should not return #1
|
||||
assertThat( session.createQuery( "from ImplicitEntity" ).list() ).hasSize( 2 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoading(SessionFactoryScope scope) {
|
||||
// Load
|
||||
scope.inTransaction( (session) -> {
|
||||
assertThat( session.get( ImplicitEntity.class, 1 ) ).isNull();
|
||||
assertThat( session.get( ImplicitEntity.class, 2 ) ).isNotNull();
|
||||
assertThat( session.get( ImplicitEntity.class, 3 ) ).isNotNull();
|
||||
} );
|
||||
|
||||
// Proxy
|
||||
scope.inTransaction( (session) -> {
|
||||
final ImplicitEntity reference = session.getReference( ImplicitEntity.class, 1 );
|
||||
try {
|
||||
reference.getName();
|
||||
fail( "Expecting to fail" );
|
||||
}
|
||||
catch (ObjectNotFoundException expected) {
|
||||
}
|
||||
|
||||
final ImplicitEntity reference2 = session.getReference( ImplicitEntity.class, 2 );
|
||||
reference2.getName();
|
||||
|
||||
final ImplicitEntity reference3 = session.getReference( ImplicitEntity.class, 3 );
|
||||
reference3.getName();
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiLoading(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final List<ImplicitEntity> results = session
|
||||
.byMultipleIds( ImplicitEntity.class )
|
||||
// otherwise the first position would contain a null for #1
|
||||
.enableOrderedReturn( false )
|
||||
.multiLoad( 1, 2, 3 );
|
||||
assertThat( results ).hasSize( 2 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNaturalIdLoading(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final ImplicitEntity first = session.bySimpleNaturalId( ImplicitEntity.class ).load( "first" );
|
||||
assertThat( first ).isNull();
|
||||
|
||||
final ImplicitEntity second = session.bySimpleNaturalId( ImplicitEntity.class ).load( "second" );
|
||||
assertThat( second ).isNotNull();
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeletion(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final ImplicitEntity reference = session.getReference( ImplicitEntity.class, 2 );
|
||||
session.remove( reference );
|
||||
session.flush();
|
||||
|
||||
final List<ImplicitEntity> active = session
|
||||
.createSelectionQuery( "from ImplicitEntity", ImplicitEntity.class )
|
||||
.list();
|
||||
// #1 was "deleted" up front and we just "deleted" #2... only #3 should be active
|
||||
assertThat( active ).hasSize( 1 );
|
||||
assertThat( active.get(0).getId() ).isEqualTo( 3 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFullUpdateMutationQuery(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final int affected = session.createMutationQuery( "update ImplicitEntity set name = null" ).executeUpdate();
|
||||
assertThat( affected ).isEqualTo( 2 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestrictedUpdateMutationQuery(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final int affected = session
|
||||
.createMutationQuery( "update ImplicitEntity set name = null where name = 'second'" )
|
||||
.executeUpdate();
|
||||
assertThat( affected ).isEqualTo( 1 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFullDeleteMutationQuery(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final int affected = session.createMutationQuery( "delete ImplicitEntity" ).executeUpdate();
|
||||
// only #2 and #3
|
||||
assertThat( affected ).isEqualTo( 2 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRestrictedDeleteMutationQuery(SessionFactoryScope scope) {
|
||||
scope.inTransaction( (session) -> {
|
||||
final int affected = session
|
||||
.createMutationQuery( "delete ImplicitEntity where name = 'second'" )
|
||||
.executeUpdate();
|
||||
// only #2
|
||||
assertThat( affected ).isEqualTo( 1 );
|
||||
} );
|
||||
}
|
||||
|
||||
@Entity(name="ImplicitEntity")
|
||||
@Table(name="implicit_entities")
|
||||
@SoftDelete
|
||||
public static class ImplicitEntity {
|
||||
@Id
|
||||
private Integer id;
|
||||
@NaturalId
|
||||
private String name;
|
||||
|
||||
public ImplicitEntity() {
|
||||
}
|
||||
|
||||
public ImplicitEntity(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ package org.hibernate.orm.test.softdelete;
|
|||
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
|
||||
import org.hibernate.type.BooleanAsBooleanConverter;
|
||||
import org.hibernate.type.NumericBooleanConverter;
|
||||
import org.hibernate.type.TrueFalseConverter;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
@ -80,7 +79,7 @@ public class MappingTests {
|
|||
|
||||
@Entity(name="BooleanEntity")
|
||||
@Table(name="boolean_entity")
|
||||
@SoftDelete(converter = BooleanAsBooleanConverter.class)
|
||||
@SoftDelete()
|
||||
public static class BooleanEntity {
|
||||
@Id
|
||||
private Integer id;
|
||||
|
@ -116,7 +115,7 @@ public class MappingTests {
|
|||
|
||||
@Entity(name="ReversedYesNoEntity")
|
||||
@Table(name="reversed_yes_no_entity")
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
public static class ReversedYesNoEntity {
|
||||
@Id
|
||||
private Integer id;
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||
*/
|
||||
package org.hibernate.orm.test.softdelete;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class ReverseYesNoConverter implements AttributeConverter<Boolean,Character> {
|
||||
@Override
|
||||
public Character convertToDatabaseColumn(Boolean attribute) {
|
||||
return attribute ? 'N' : 'Y';
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean convertToEntityAttribute(Character dbData) {
|
||||
if ( dbData == 'Y' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( dbData == 'N' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException( "Illegal value [" + dbData + "]; expecting 'Y' or 'N'" );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||
*/
|
||||
package org.hibernate.orm.test.softdelete;
|
||||
|
||||
import org.hibernate.annotations.NaturalId;
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Table(name = "simple")
|
||||
//tag::example-soft-delete-basic[]
|
||||
@Entity(name = "SimpleEntity")
|
||||
@SoftDelete(columnName = "removed", converter = YesNoConverter.class)
|
||||
public class SimpleEntity {
|
||||
// ...
|
||||
//end::example-soft-delete-basic[]
|
||||
@Id
|
||||
private Integer id;
|
||||
@NaturalId
|
||||
private String name;
|
||||
|
||||
public SimpleEntity() {
|
||||
}
|
||||
|
||||
public SimpleEntity(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
//tag::example-soft-delete-basic[]
|
||||
}
|
||||
//end::example-soft-delete-basic[]
|
|
@ -12,7 +12,6 @@ import java.util.List;
|
|||
import org.hibernate.Hibernate;
|
||||
import org.hibernate.ObjectNotFoundException;
|
||||
import org.hibernate.annotations.BatchSize;
|
||||
import org.hibernate.annotations.NaturalId;
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
|
@ -34,7 +33,7 @@ import static org.assertj.core.api.Assertions.fail;
|
|||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@DomainModel(annotatedClasses = { SimpleSoftDeleteTests.SimpleEntity.class, SimpleSoftDeleteTests.BatchLoadable.class })
|
||||
@DomainModel(annotatedClasses = { SimpleEntity.class, SimpleSoftDeleteTests.BatchLoadable.class })
|
||||
@SessionFactory(useCollectingStatementInspector = true)
|
||||
public class SimpleSoftDeleteTests {
|
||||
@BeforeEach
|
||||
|
@ -212,40 +211,10 @@ public class SimpleSoftDeleteTests {
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* @implNote Uses YesNoConverter to work across all databases, even those
|
||||
* not supporting an actual BOOLEAN datatype
|
||||
*/
|
||||
@Entity(name="SimpleEntity")
|
||||
@Table(name="simple")
|
||||
@SoftDelete(columnName = "removed", converter = YesNoConverter.class)
|
||||
public static class SimpleEntity {
|
||||
@Id
|
||||
private Integer id;
|
||||
@NaturalId
|
||||
private String name;
|
||||
|
||||
public SimpleEntity() {
|
||||
}
|
||||
|
||||
public SimpleEntity(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(name="BatchLoadable")
|
||||
@Table(name="batch_loadable")
|
||||
@BatchSize(size = 5)
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
public static class BatchLoadable {
|
||||
@Id
|
||||
private Integer id;
|
||||
|
|
|
@ -11,18 +11,17 @@ import java.sql.Statement;
|
|||
import org.hibernate.annotations.Fetch;
|
||||
import org.hibernate.annotations.FetchMode;
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
import org.hibernate.testing.jdbc.SQLStatementInspector;
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
|
@ -145,7 +144,7 @@ public class ToOneTests {
|
|||
|
||||
@Entity(name="User")
|
||||
@Table(name="users")
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
public static class User {
|
||||
@Id
|
||||
private Integer id;
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.hibernate.annotations.SoftDelete;
|
|||
import org.hibernate.boot.Metadata;
|
||||
import org.hibernate.boot.MetadataSources;
|
||||
import org.hibernate.metamodel.UnsupportedMappingException;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
@ -64,7 +65,7 @@ public class ValidationTests {
|
|||
|
||||
@Entity(name="Address")
|
||||
@Table(name="addresses")
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
public static class Address {
|
||||
@Id
|
||||
private Integer id;
|
||||
|
@ -73,7 +74,7 @@ public class ValidationTests {
|
|||
|
||||
@Entity(name="NoNo")
|
||||
@Table(name="nonos")
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
@SQLDelete( sql = "delete from nonos" )
|
||||
public static class NoNo {
|
||||
@Id
|
||||
|
|
|
@ -35,12 +35,15 @@ public class CollectionOwner {
|
|||
@Basic
|
||||
private String name;
|
||||
|
||||
//tag::example-soft-delete-element-collection[]
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "elements", joinColumns = @JoinColumn(name = "owner_fk"))
|
||||
@Column(name = "txt")
|
||||
@SoftDelete(converter = YesNoConverter.class)
|
||||
private Collection<String> elements;
|
||||
//end::example-soft-delete-element-collection[]
|
||||
|
||||
//tag::example-soft-delete-many-to-many[]
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "m2m",
|
||||
|
@ -49,6 +52,7 @@ public class CollectionOwner {
|
|||
)
|
||||
@SoftDelete(columnName = "gone", converter = NumericBooleanConverter.class)
|
||||
private Collection<CollectionOwned> manyToMany;
|
||||
//end::example-soft-delete-many-to-many[]
|
||||
|
||||
protected CollectionOwner() {
|
||||
// for Hibernate use
|
||||
|
|
|
@ -12,7 +12,8 @@ import org.hibernate.annotations.BatchSize;
|
|||
import org.hibernate.annotations.Fetch;
|
||||
import org.hibernate.annotations.FetchMode;
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.orm.test.softdelete.ReverseYesNoConverter;
|
||||
import org.hibernate.type.NumericBooleanConverter;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
import jakarta.persistence.CollectionTable;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
|
@ -34,13 +35,13 @@ public class CollectionOwner2 {
|
|||
@ElementCollection
|
||||
@CollectionTable(name="batch_loadables", joinColumns = @JoinColumn(name="owner_fk"))
|
||||
@BatchSize(size = 5)
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
private Set<String> batchLoadable;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name="subselect_loadables", joinColumns = @JoinColumn(name="owner_fk"))
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
@SoftDelete(columnName = "active", converter = NumericBooleanConverter.class, reversed = true)
|
||||
private Set<String> subSelectLoadable;
|
||||
|
||||
public CollectionOwner2() {
|
||||
|
|
|
@ -121,7 +121,7 @@ public class FetchLoadableTests {
|
|||
// trigger loading one of the subselect-loadable collections
|
||||
first.getSubSelectLoadable().size();
|
||||
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
|
||||
assertThat( statementInspector.getSqlQueries().get( 0 ) ).containsAnyOf( "active='Y'", "active=N'Y'" );
|
||||
assertThat( statementInspector.getSqlQueries().get( 0 ) ).containsAnyOf( "active=1", "active=N'Y'" );
|
||||
assertThat( Hibernate.isInitialized( first.getSubSelectLoadable() ) ).isTrue();
|
||||
assertThat( Hibernate.isInitialized( second.getSubSelectLoadable() ) ).isTrue();
|
||||
} );
|
||||
|
|
|
@ -6,22 +6,27 @@
|
|||
*/
|
||||
package org.hibernate.orm.test.softdelete.converter.reversed;
|
||||
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
|
||||
import org.hibernate.orm.test.softdelete.MappingVerifier;
|
||||
import org.hibernate.orm.test.softdelete.ReverseYesNoConverter;
|
||||
|
||||
import org.hibernate.testing.jdbc.SQLStatementInspector;
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.ServiceRegistry;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.hibernate.testing.orm.junit.Setting;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @implNote {@code preferred_boolean_jdbc_type=CHAR} will use T/F as the default (Entity2)
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@DomainModel(annotatedClasses = { ReverseYesNoConverter.class, TheEntity.class })
|
||||
@ServiceRegistry(settings = @Setting(name= AvailableSettings.PREFERRED_BOOLEAN_JDBC_TYPE, value = "CHAR"))
|
||||
@DomainModel(annotatedClasses = {TheEntity.class, TheEntity2.class})
|
||||
@SessionFactory( useCollectingStatementInspector = true)
|
||||
public class ReversedSoftDeleteTests {
|
||||
@Test
|
||||
|
@ -33,6 +38,12 @@ public class ReversedSoftDeleteTests {
|
|||
"the_entity",
|
||||
'N'
|
||||
);
|
||||
MappingVerifier.verifyMapping(
|
||||
metamodel.getEntityDescriptor( TheEntity2.class ).getSoftDeleteMapping(),
|
||||
"active",
|
||||
"the_entity2",
|
||||
'F'
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -60,4 +71,30 @@ public class ReversedSoftDeleteTests {
|
|||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "update " );
|
||||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsAnyOf( "active='N'", "active=N'N'" );
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUsage2(SessionFactoryScope scope) {
|
||||
final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector();
|
||||
sqlInspector.clear();
|
||||
|
||||
scope.inTransaction( (session) -> {
|
||||
session.persist( new TheEntity2( 1, "it" ) );
|
||||
} );
|
||||
|
||||
assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 );
|
||||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "'T'" );
|
||||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContain( "'F'" );
|
||||
|
||||
sqlInspector.clear();
|
||||
|
||||
scope.inTransaction( (session) -> {
|
||||
final TheEntity2 reference = session.getReference( TheEntity2.class, 1 );
|
||||
session.remove( reference );
|
||||
} );
|
||||
|
||||
assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 );
|
||||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( "delete " );
|
||||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "update " );
|
||||
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "active='F'" );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,20 +7,23 @@
|
|||
package org.hibernate.orm.test.softdelete.converter.reversed;
|
||||
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.orm.test.softdelete.ReverseYesNoConverter;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
import jakarta.persistence.Basic;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Basic;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "the_entity")
|
||||
@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class)
|
||||
//tag::example-soft-delete-reverse[]
|
||||
@Entity
|
||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
||||
public class TheEntity {
|
||||
// ...
|
||||
//end::example-soft-delete-reverse[]
|
||||
@Id
|
||||
private Integer id;
|
||||
@Basic
|
||||
|
@ -46,4 +49,7 @@ public class TheEntity {
|
|||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
//tag::example-soft-delete-reverse[]
|
||||
}
|
||||
//end::example-soft-delete-reverse[]
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Hibernate, Relational Persistence for Idiomatic Java
|
||||
*
|
||||
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||
*/
|
||||
package org.hibernate.orm.test.softdelete.converter.reversed;
|
||||
|
||||
import org.hibernate.annotations.SoftDelete;
|
||||
import org.hibernate.type.YesNoConverter;
|
||||
|
||||
import jakarta.persistence.Basic;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@Table(name = "the_entity2")
|
||||
//tag::example-soft-delete-reverse[]
|
||||
@Entity
|
||||
@SoftDelete(columnName = "active", reversed = true)
|
||||
public class TheEntity2 {
|
||||
// ...
|
||||
//end::example-soft-delete-reverse[]
|
||||
@Id
|
||||
private Integer id;
|
||||
@Basic
|
||||
private String name;
|
||||
|
||||
protected TheEntity2() {
|
||||
// for Hibernate use
|
||||
}
|
||||
|
||||
public TheEntity2(Integer id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
//tag::example-soft-delete-reverse[]
|
||||
}
|
||||
//end::example-soft-delete-reverse[]
|
|
@ -22,11 +22,14 @@ import jakarta.persistence.Table;
|
|||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@Table(name = "joined_root")
|
||||
//tag::example-soft-delete-secondary[]
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
@Table(name = "joined_root")
|
||||
@SoftDelete(columnName = "removed", converter = YesNoConverter.class)
|
||||
public abstract class JoinedRoot {
|
||||
// ...
|
||||
//end::example-soft-delete-secondary[]
|
||||
@Id
|
||||
private Integer id;
|
||||
@Basic
|
||||
|
@ -52,4 +55,6 @@ public abstract class JoinedRoot {
|
|||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
//tag::example-soft-delete-secondary[]
|
||||
}
|
||||
//end::example-soft-delete-secondary[]
|
||||
|
|
|
@ -14,10 +14,13 @@ import jakarta.persistence.Table;
|
|||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
//tag::example-soft-delete-secondary[]
|
||||
@Entity
|
||||
@Table(name = "joined_sub")
|
||||
@PrimaryKeyJoinColumn(name = "joined_fk")
|
||||
public class JoinedSub extends JoinedRoot {
|
||||
// ...
|
||||
//end::example-soft-delete-secondary[]
|
||||
@Basic
|
||||
String subDetails;
|
||||
|
||||
|
@ -28,4 +31,6 @@ public class JoinedSub extends JoinedRoot {
|
|||
super( id, name );
|
||||
this.subDetails = subDetails;
|
||||
}
|
||||
//tag::example-soft-delete-secondary[]
|
||||
}
|
||||
//end::example-soft-delete-secondary[]
|
||||
|
|
|
@ -16,3 +16,6 @@ earlier versions, see any other pertinent migration guides as well.
|
|||
* link:{docsBase}/6.0/migration-guide/migration-guide.html[6.0 Migration guide]
|
||||
|
||||
|
||||
[[soft-delete]]
|
||||
== Soft Delete
|
||||
|
||||
|
|
Loading…
Reference in New Issue