HHH-6054 introduce AttributeBinder and @AttributeBinderType

and introduce TenantIdBinder on top of this stuff
also make @TenantId imply non-null, immutable
This commit is contained in:
Gavin King 2021-09-11 16:00:24 +02:00 committed by Steve Ebersole
parent ea0dd35362
commit 5837a60e71
10 changed files with 241 additions and 75 deletions

View File

@ -0,0 +1,31 @@
/*
* 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.annotations;
import org.hibernate.Incubating;
import org.hibernate.tuple.AttributeBinder;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Associates an annotation with an {@link AttributeBinder}.
*
* @author Gavin King
*/
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
@Incubating
public @interface AttributeBinderType {
/**
* @return a type which implements {@link AttributeBinder}
*/
Class<? extends AttributeBinder> binder();
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.annotations;
import org.hibernate.tuple.TenantIdBinder;
import org.hibernate.tuple.TenantIdGeneration;
import java.lang.annotation.Retention;
@ -13,7 +14,6 @@
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
@ -23,6 +23,7 @@
* @author Gavin King
*/
@ValueGenerationType(generatedBy = TenantIdGeneration.class)
@AttributeBinderType(binder = TenantIdBinder.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface TenantId {}

View File

@ -15,17 +15,15 @@
import org.hibernate.AnnotationException;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.annotations.AttributeAccessor;
import org.hibernate.annotations.AttributeBinderType;
import org.hibernate.annotations.Generated;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.OptimisticLock;
import org.hibernate.annotations.TenantId;
import org.hibernate.annotations.ValueGenerationType;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.cfg.AccessType;
import org.hibernate.cfg.AnnotationBinder;
@ -34,7 +32,6 @@
import org.hibernate.cfg.InheritanceState;
import org.hibernate.cfg.PropertyHolder;
import org.hibernate.cfg.PropertyPreloadedData;
import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.Collection;
@ -48,18 +45,13 @@
import org.hibernate.metamodel.EmbeddableInstantiator;
import org.hibernate.property.access.spi.PropertyAccessStrategy;
import org.hibernate.tuple.AnnotationValueGeneration;
import org.hibernate.tuple.AttributeBinder;
import org.hibernate.tuple.GenerationTiming;
import org.hibernate.tuple.TenantIdGeneration;
import org.hibernate.tuple.ValueGeneration;
import org.hibernate.tuple.ValueGenerator;
import org.hibernate.type.BasicType;
import org.hibernate.type.Type;
import org.jboss.logging.Logger;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
/**
* @author Emmanuel Bernard
*/
@ -100,9 +92,9 @@ public void setEntityBinder(EntityBinder entityBinder) {
}
/*
* property can be null
* prefer propertyName to property.getName() since some are overloaded
*/
* property can be null
* prefer propertyName to property.getName() since some are overloaded
*/
private XProperty property;
private XClass returnedClass;
private boolean isId;
@ -205,62 +197,25 @@ private Property makePropertyAndValue() {
SimpleValue propertyValue = basicValueBinder.make();
setValue( propertyValue );
Property prop = makeProperty();
makeTenantIdFilter();
return prop;
return makeProperty();
}
private void makeTenantIdFilter() {
if ( property.isAnnotationPresent(TenantId.class) ) {
InFlightMetadataCollector collector = buildingContext.getMetadataCollector();
BasicType<Object> tenantIdType =
collector.getTypeConfiguration().getBasicTypeRegistry()
.getRegisteredType(returnedClassName);
FilterDefinition filterDefinition = collector.getFilterDefinition(TenantIdGeneration.FILTER_NAME);
if ( filterDefinition == null ) {
collector.addFilterDefinition(
new FilterDefinition(
TenantIdGeneration.FILTER_NAME,
"",
singletonMap( TenantIdGeneration.PARAMETER_NAME, tenantIdType )
)
);
}
else {
Type parameterType = filterDefinition.getParameterTypes().get(TenantIdGeneration.PARAMETER_NAME);
if ( !parameterType.getName().equals( tenantIdType.getName() ) ) {
throw new MappingException(
"all @TenantId fields must have the same type: "
+ parameterType.getName()
+ " differs from "
+ tenantIdType.getName()
);
@SuppressWarnings({"rawtypes", "unchecked"})
private void callAttributeBinders(Property prop) {
for ( Annotation annotation: property.getAnnotations() ) {
if ( annotation.annotationType().isAnnotationPresent(AttributeBinderType.class) ) {
AttributeBinder binder;
try {
binder = annotation.annotationType().getAnnotation(AttributeBinderType.class).binder().newInstance();
}
catch (Exception e) {
throw new AnnotationException("error processing @AttributeBinderType annotation", e);
}
binder.bind( annotation, buildingContext, entityBinder.getPersistentClass(), prop );
}
entityBinder.getPersistentClass()
.addFilter(
TenantIdGeneration.FILTER_NAME,
getColumnNameOrFormula()
+ " = :"
+ TenantIdGeneration.PARAMETER_NAME,
true,
emptyMap(),
emptyMap()
);
}
}
private String getColumnNameOrFormula() {
if ( columns.length!=1 ) {
throw new MappingException("@TenantId field must be mapped to a single column or formula");
}
return columns[0].isFormula()
? columns[0].getFormulaString()
: columns[0].getName();
}
//used when value is provided
public Property makePropertyAndBind() {
return bind( makeProperty() );
@ -324,6 +279,9 @@ private Property bind(Property prop) {
else {
holder.addProperty( prop, columns, declaringClass );
}
callAttributeBinders(prop);
return prop;
}
@ -352,6 +310,7 @@ public Property makeProperty() {
prop.setLazyGroup( lazyGroup );
prop.setCascade( cascade );
prop.setPropertyAccessorName( accessType.getType() );
prop.setReturnedClassName( returnedClassName );
if ( property != null ) {
prop.setValueGenerationStrategy( determineValueGenerationStrategy( property ) );

View File

@ -129,7 +129,7 @@
import org.hibernate.stat.SessionStatistics;
import org.hibernate.stat.internal.SessionStatisticsImpl;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.tuple.TenantIdGeneration;
import org.hibernate.tuple.TenantIdBinder;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
@ -243,15 +243,15 @@ public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) {
setHibernateFlushMode( initialMode );
}
if ( factory.getDefinedFilterNames().contains( TenantIdGeneration.FILTER_NAME ) ) {
if ( factory.getDefinedFilterNames().contains( TenantIdBinder.FILTER_NAME ) ) {
String tenantIdentifier = getTenantIdentifier();
if ( tenantIdentifier == null ) {
throw new HibernateException( "SessionFactory configured for multi-tenancy, but no tenant identifier specified" );
}
else {
getLoadQueryInfluencers()
.enableFilter( TenantIdGeneration.FILTER_NAME )
.setParameter( TenantIdGeneration.PARAMETER_NAME, tenantIdentifier );
.enableFilter( TenantIdBinder.FILTER_NAME )
.setParameter( TenantIdBinder.PARAMETER_NAME, tenantIdentifier );
}
}

View File

@ -53,6 +53,7 @@ public class Property implements Serializable, MetaAttributable {
private boolean naturalIdentifier;
private boolean lob;
private java.util.List<CallbackDefinition> callbackDefinitions;
private String returnedClassName;
public boolean isBackRef() {
return false;
@ -92,7 +93,26 @@ public boolean isComposite() {
public Value getValue() {
return value;
}
public void resetUpdateable(boolean updateable) {
setUpdateable(updateable);
boolean[] columnUpdateability = getValue().getColumnUpdateability();
for (int i=0; i<getColumnSpan(); i++ ) {
columnUpdateability[i] = updateable;
}
}
public void resetOptional(boolean optional) {
setOptional(optional);
Iterator<Selectable> columnIterator = getValue().getColumnIterator();
while ( columnIterator.hasNext() ) {
Selectable column = columnIterator.next();
if (column instanceof Column) {
( (Column) column ).setNullable(optional);
}
}
}
public boolean isPrimitive(Class clazz) {
return getGetter(clazz).getReturnTypeClass().isPrimitive();
}
@ -381,4 +401,11 @@ public java.util.List<CallbackDefinition> getCallbackDefinitions() {
return Collections.unmodifiableList( callbackDefinitions );
}
public String getReturnedClassName() {
return returnedClassName;
}
public void setReturnedClassName(String returnedClassName) {
this.returnedClassName = returnedClassName;
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.tuple;
import org.hibernate.Incubating;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import java.lang.annotation.Annotation;
/**
* Allows a user-written annotation to drive some customized model binding.
*
* @see org.hibernate.annotations.AttributeBinderType
*
* @author Gavin King
*/
@Incubating
public interface AttributeBinder<A extends Annotation> {
/**
* Perform some custom configuration of the model relating to the given {@link Property}
* of the given {@link PersistentClass}.
*/
void bind(A annotation, MetadataBuildingContext buildingContext, PersistentClass persistentClass, Property property);
}

View File

@ -0,0 +1,90 @@
/*
* 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.tuple;
import org.hibernate.MappingException;
import org.hibernate.annotations.TenantId;
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Formula;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Selectable;
import org.hibernate.type.BasicType;
import org.hibernate.type.Type;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
/**
* Sets up filters associated with a @TenantId field
*
* @author Gavin King
*/
public class TenantIdBinder implements AttributeBinder<TenantId> {
public static final String FILTER_NAME = "_tenantId";
public static final String PARAMETER_NAME = "tenantId";
@Override
public void bind(
TenantId tenantId,
MetadataBuildingContext buildingContext,
PersistentClass persistentClass,
Property property) {
InFlightMetadataCollector collector = buildingContext.getMetadataCollector();
BasicType<Object> tenantIdType =
collector.getTypeConfiguration().getBasicTypeRegistry()
.getRegisteredType( property.getReturnedClassName() );
FilterDefinition filterDefinition = collector.getFilterDefinition(FILTER_NAME);
if ( filterDefinition == null ) {
collector.addFilterDefinition(
new FilterDefinition(
FILTER_NAME,
"",
singletonMap( PARAMETER_NAME, tenantIdType )
)
);
}
else {
Type parameterType = filterDefinition.getParameterTypes().get(PARAMETER_NAME);
if ( !parameterType.getName().equals( tenantIdType.getName() ) ) {
throw new MappingException(
"all @TenantId fields must have the same type: "
+ parameterType.getName()
+ " differs from "
+ tenantIdType.getName()
);
}
}
persistentClass.addFilter(
FILTER_NAME,
columnNameOrFormula(property)
+ " = :"
+ PARAMETER_NAME,
true,
emptyMap(),
emptyMap()
);
property.resetUpdateable(false);
property.resetOptional(false);
}
private String columnNameOrFormula(Property property) {
if ( property.getColumnSpan()!=1 ) {
throw new MappingException("@TenantId attribute must be mapped to a single column or formula");
}
Selectable column = property.getColumnIterator().next();
return column.isFormula()
? ((Formula) column).getFormula()
: ((Column) column).getName();
}
}

View File

@ -17,9 +17,6 @@
*/
public class TenantIdGeneration implements AnnotationValueGeneration<TenantId>, ValueGenerator<Object> {
public static final String FILTER_NAME = "_tenantId";
public static final String PARAMETER_NAME = "tenantId";
private String entityName;
private String propertyName;

View File

@ -20,7 +20,7 @@ public class Account {
@TenantId String tenantId;
@ManyToOne Client client;
@ManyToOne(optional = false) Client client;
public Account(Client client) {
this.client = client;

View File

@ -17,11 +17,12 @@
import org.hibernate.testing.orm.junit.SessionFactoryProducer;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.hibernate.cfg.AvailableSettings.HBM2DDL_DATABASE_ACTION;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SessionFactory
@DomainModel(annotatedClasses = { Account.class, Client.class })
@ -34,6 +35,14 @@ public class TenantIdTest implements SessionFactoryProducer {
String currentTenant;
@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createQuery("delete from Account").executeUpdate();
session.createQuery("delete from Client").executeUpdate();
});
}
@Override
public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) {
final SessionFactoryBuilder sessionFactoryBuilder = model.getSessionFactoryBuilder();
@ -73,10 +82,10 @@ public void test(SessionFactoryScope scope) {
}
@Test
public void testError(SessionFactoryScope scope) {
public void testErrorOnInsert(SessionFactoryScope scope) {
currentTenant = "mine";
Client client = new Client("Gavin");
Account acc = new Account();
Account acc = new Account(client);
acc.tenantId = "yours";
try {
scope.inTransaction( session -> {
@ -89,4 +98,26 @@ public void testError(SessionFactoryScope scope) {
assertTrue( e.getCause() instanceof PropertyValueException );
}
}
@Test
public void testErrorOnUpdate(SessionFactoryScope scope) {
currentTenant = "mine";
Client client = new Client("Gavin");
Account acc = new Account(client);
scope.inTransaction( session -> {
session.persist(client);
session.persist(acc);
acc.tenantId = "yours";
client.tenantId = "yours";
client.name = "Steve";
} );
//TODO: it would be better if this were an error
scope.inTransaction( session -> {
Account account = session.find(Account.class, acc.id);
assertNotNull(account);
assertEquals( "mine", acc.tenantId );
assertEquals( "Steve", acc.client.name );
assertEquals( "mine", acc.client.tenantId );
} );
}
}