mirror of
https://github.com/hibernate/hibernate-orm
synced 2025-02-16 16:15:06 +00:00
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:
parent
ea0dd35362
commit
5837a60e71
@ -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();
|
||||
}
|
@ -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 {}
|
||||
|
@ -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 ) );
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user