introduce @View annotation

This commit is contained in:
Gavin 2023-05-22 09:05:23 +02:00 committed by Gavin King
parent a4e7b7e482
commit 7b3c77c0c3
8 changed files with 255 additions and 45 deletions

View File

@ -0,0 +1,72 @@
/*
* 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 java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Maps an entity to a database view. The name of the view is
* determined according to the usual rules regarding table
* mappings, and may be customized using the JPA-standard
* {@link jakarta.persistence.Table @Table} annotation. This
* annotation specifies the query which defines the view,
* allowing the view to be exported by the schema management
* tooling.
* <p>
* For example, this mapping:
* <pre>
* &#64;Immutable &#64;Entity
* &#64;Table(name="summary")
* &#64;View(query="select type, sum(amount) as total, avg(amount) as average from details group by type")
* &#64;Synchronize("details")
* public class Summary {
* &#64;Id String type;
* Double total;
* Double average;
* }
* </pre>
* <p>
* results in the following generated DDL:
* <pre>
* create view summary
* as select type, sum(amount) as total, avg(amount) as average from details group by type
* </pre>
* <p>
* If a view is not updatable, we recommend annotating the
* entity {@link Immutable @Immutable}.
* <p>
* It's possible to have an entity class which maps a table,
* and another entity which maps a view defined as a query
* against that table. In this case, a stateful session is
* vulnerable to data aliasing effects, and it is the
* responsibility of client code to ensure that changes to
* the first entity are flushed to the database before
* reading the same data via the second entity. The
* {@link Synchronize @Synchronize} annotation can help
* alleviate this problem, but it's an incomplete solution.
* <p>
* Therefore, we recommend the use of {@linkplain
* org.hibernate.StatelessSession stateless sessions}
* when interacting with entities mapped to views.
*
* @since 6.3
*
* @author Gavin King
*/
@Target(TYPE)
@Retention(RUNTIME)
public @interface View {
/**
* The SQL query that defines the view.
*/
String query();
}

View File

@ -61,6 +61,7 @@ import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize;
import org.hibernate.annotations.Tables;
import org.hibernate.annotations.TypeBinderType;
import org.hibernate.annotations.View;
import org.hibernate.annotations.Where;
import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.annotations.common.reflection.XAnnotatedElement;
@ -679,12 +680,14 @@ public class EntityBinder {
List<UniqueConstraintHolder> uniqueConstraints,
InFlightMetadataCollector collector) {
final RowId rowId = annotatedClass.getAnnotation( RowId.class );
final View view = annotatedClass.getAnnotation( View.class );
bindTable(
schema,
catalog,
table,
uniqueConstraints,
rowId == null ? null : rowId.value(),
view == null ? null : view.query(),
inheritanceState.hasDenormalizedTable()
? collector.getEntityTableXref( superEntity.getEntityName() )
: null
@ -1728,6 +1731,7 @@ public class EntityBinder {
String tableName,
List<UniqueConstraintHolder> uniqueConstraints,
String rowId,
String viewQuery,
InFlightMetadataCollector.EntityTableXref denormalizedSuperTableXref) {
final EntityTableNamingStrategyHelper namingStrategyHelper = new EntityTableNamingStrategyHelper(
@ -1751,6 +1755,7 @@ public class EntityBinder {
);
table.setRowId( rowId );
table.setViewQuery( viewQuery );
// final Comment comment = annotatedClass.getAnnotation( Comment.class );
// if ( comment != null ) {

View File

@ -71,6 +71,7 @@ public class Table implements Serializable, ContributableDatabaseObject {
private boolean isAbstract;
private boolean hasDenormalizedTables;
private String comment;
private String viewQuery;
private List<Function<SqlStringGenerationContext, InitCommand>> initCommandProducers;
@ -665,6 +666,10 @@ public class Table implements Serializable, ContributableDatabaseObject {
return !isSubselect() && !isAbstractUnionTable();
}
public boolean isView() {
return viewQuery != null;
}
public String getComment() {
return comment;
}
@ -705,6 +710,14 @@ public class Table implements Serializable, ContributableDatabaseObject {
}
}
public String getViewQuery() {
return viewQuery;
}
public void setViewQuery(String viewQuery) {
this.viewQuery = viewQuery;
}
public static class ForeignKeyKey implements Serializable {
private final String referencedClassName;
private final Column[] columns;

View File

@ -413,6 +413,21 @@ public class SchemaCreatorImpl implements SchemaCreator {
Namespace namespace) {
for ( Table table : namespace.getTables() ) {
if ( table.isPhysicalTable()
&& !table.isView()
&& options.getSchemaFilter().includeTable( table )
&& contributableInclusionMatcher.matches( table ) ) {
checkExportIdentifier( table, exportIdentifiers );
applySqlStrings(
dialect.getTableExporter().getSqlCreateStrings( table, metadata, context ),
formatter,
options,
targets
);
}
}
for ( Table table : namespace.getTables() ) {
if ( table.isPhysicalTable()
&& table.isView()
&& options.getSchemaFilter().includeTable( table )
&& contributableInclusionMatcher.matches( table ) ) {
checkExportIdentifier( table, exportIdentifiers );

View File

@ -356,6 +356,21 @@ public class SchemaDropperImpl implements SchemaDropper {
GenerationTarget[] targets) {
for ( Table table : namespace.getTables() ) {
if ( table.isPhysicalTable()
&& table.isView()
&& options.getSchemaFilter().includeTable( table )
&& inclusionFilter.matches( table ) ) {
checkExportIdentifier( table, exportIdentifiers);
applySqlStrings(
dialect.getTableExporter().getSqlDropStrings( table, metadata, context),
formatter,
options,
targets
);
}
}
for ( Table table : namespace.getTables() ) {
if ( table.isPhysicalTable()
&& !table.isView()
&& options.getSchemaFilter().includeTable( table )
&& inclusionFilter.matches( table ) ) {
checkExportIdentifier( table, exportIdentifiers);

View File

@ -56,10 +56,19 @@ public class StandardTableExporter implements Exporter<Table> {
try {
final String formattedTableName = context.format( tableName );
final StringBuilder createTable = new StringBuilder();
final String viewQuery = table.getViewQuery();
if ( viewQuery != null ) {
createTable.append("create view ")
.append( formattedTableName )
.append(" as ")
.append( viewQuery );
}
else {
final StringBuilder extra = new StringBuilder();
final StringBuilder createTable =
new StringBuilder( tableCreateString( table.hasPrimaryKey() ) )
createTable.append( tableCreateString( table.hasPrimaryKey() ) )
.append( ' ' )
.append( formattedTableName )
.append( " (" );
@ -99,6 +108,7 @@ public class StandardTableExporter implements Exporter<Table> {
}
applyTableTypeString( createTable );
}
final List<String> sqlStrings = new ArrayList<>();
sqlStrings.add( createTable.toString() );
@ -301,16 +311,22 @@ public class StandardTableExporter implements Exporter<Table> {
@Override
public String[] getSqlDropStrings(Table table, Metadata metadata, SqlStringGenerationContext context) {
StringBuilder buf = new StringBuilder( "drop table " );
if ( dialect.supportsIfExistsBeforeTableName() ) {
buf.append( "if exists " );
final StringBuilder dropTable = new StringBuilder();
if ( table.getViewQuery() == null ) {
dropTable.append( "drop table " );
}
buf.append( context.format( getTableName( table ) ) )
else {
dropTable.append( "drop view " );
}
if ( dialect.supportsIfExistsBeforeTableName() ) {
dropTable.append( "if exists " );
}
dropTable.append( context.format( getTableName( table ) ) )
.append( dialect.getCascadeConstraintsString() );
if ( dialect.supportsIfExistsAfterTableName() ) {
buf.append( " if exists" );
dropTable.append( " if exists" );
}
return new String[] { buf.toString() };
return new String[] { dropTable.toString() };
}
private static QualifiedName getTableName(Table table) {

View File

@ -51,9 +51,15 @@ public class StandardTableMigrator implements TableMigrator {
Metadata metadata,
TableInformation tableInfo,
SqlStringGenerationContext context) {
if ( table.isView() ) {
// perhaps we could execute a 'create or replace view'
return EMPTY_STRING_ARRAY;
}
else {
return sqlAlterStrings( table, dialect, metadata, tableInfo, context )
.toArray( EMPTY_STRING_ARRAY );
}
}
@Internal
public static List<String> sqlAlterStrings(

View File

@ -0,0 +1,68 @@
package org.hibernate.orm.test.view;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
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.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SessionFactory
@DomainModel(annotatedClasses = {ViewTest.Table.class, ViewTest.View.class, ViewTest.Summary.class})
public class ViewTest {
@Test void test(SessionFactoryScope scope) {
UUID id = scope.fromTransaction( s -> {
Table t = new Table();
t.quantity = 69.0;
t.name = "Trompon";
s.persist(t);
return t.id;
});
scope.inSession( s -> {
View v = s.find(View.class, id);
assertNotNull(v);
assertEquals("TROMPON", v.name);
});
scope.inSession( s -> {
Summary summary = s.find(Summary.class, 1);
assertNotNull(summary);
});
}
@Entity(name = "Table")
@jakarta.persistence.Table(name = "MyTable")
static class Table {
@Id @GeneratedValue
UUID id;
String name;
double quantity;
}
@Entity(name = "View")
@jakarta.persistence.Table(name = "MyView")
@org.hibernate.annotations.View(query
= "select id as uuid, upper(name) as name, quantity as amount from MyTable")
static class View {
@Id
UUID uuid;
String name;
double amount;
}
@Entity(name = "Summary")
@org.hibernate.annotations.View(query
= "select 1 as id, sum(quantity) as total from MyTable")
static class Summary {
@Id
int id;
double total;
}
}