introduce @View annotation
This commit is contained in:
parent
a4e7b7e482
commit
7b3c77c0c3
|
@ -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>
|
||||
* @Immutable @Entity
|
||||
* @Table(name="summary")
|
||||
* @View(query="select type, sum(amount) as total, avg(amount) as average from details group by type")
|
||||
* @Synchronize("details")
|
||||
* public class Summary {
|
||||
* @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();
|
||||
}
|
|
@ -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 ) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -56,49 +56,59 @@ public class StandardTableExporter implements Exporter<Table> {
|
|||
try {
|
||||
final String formattedTableName = context.format( tableName );
|
||||
|
||||
final StringBuilder extra = new StringBuilder();
|
||||
final StringBuilder createTable = new StringBuilder();
|
||||
|
||||
final StringBuilder createTable =
|
||||
new StringBuilder( tableCreateString( table.hasPrimaryKey() ) )
|
||||
.append( ' ' )
|
||||
.append( formattedTableName )
|
||||
.append( " (" );
|
||||
final String viewQuery = table.getViewQuery();
|
||||
if ( viewQuery != null ) {
|
||||
createTable.append("create view ")
|
||||
.append( formattedTableName )
|
||||
.append(" as ")
|
||||
.append( viewQuery );
|
||||
}
|
||||
else {
|
||||
final StringBuilder extra = new StringBuilder();
|
||||
|
||||
boolean isFirst = true;
|
||||
for ( Column column : table.getColumns() ) {
|
||||
if ( isFirst ) {
|
||||
isFirst = false;
|
||||
createTable.append( tableCreateString( table.hasPrimaryKey() ) )
|
||||
.append( ' ' )
|
||||
.append( formattedTableName )
|
||||
.append( " (" );
|
||||
|
||||
boolean isFirst = true;
|
||||
for ( Column column : table.getColumns() ) {
|
||||
if ( isFirst ) {
|
||||
isFirst = false;
|
||||
}
|
||||
else {
|
||||
createTable.append( ", " );
|
||||
}
|
||||
appendColumn( createTable, column, table, metadata, dialect, context );
|
||||
|
||||
extra.append( column.getValue().getExtraCreateTableInfo() );
|
||||
}
|
||||
else {
|
||||
createTable.append( ", " );
|
||||
if ( table.getRowId() != null ) {
|
||||
String rowIdColumn = dialect.getRowIdColumnString( table.getRowId() );
|
||||
if ( rowIdColumn != null ) {
|
||||
createTable.append(", ").append( rowIdColumn );
|
||||
}
|
||||
}
|
||||
appendColumn( createTable, column, table, metadata, dialect, context );
|
||||
|
||||
extra.append( column.getValue().getExtraCreateTableInfo() );
|
||||
}
|
||||
if ( table.getRowId() != null ) {
|
||||
String rowIdColumn = dialect.getRowIdColumnString( table.getRowId() );
|
||||
if ( rowIdColumn != null ) {
|
||||
createTable.append(", ").append( rowIdColumn );
|
||||
if ( table.hasPrimaryKey() ) {
|
||||
createTable.append( ", " ).append( table.getPrimaryKey().sqlConstraintString( dialect ) );
|
||||
}
|
||||
|
||||
createTable.append( dialect.getUniqueDelegate().getTableCreationUniqueConstraintsFragment( table, context ) );
|
||||
|
||||
applyTableCheck( table, createTable );
|
||||
|
||||
createTable.append( ')' );
|
||||
|
||||
createTable.append( extra );
|
||||
|
||||
if ( table.getComment() != null ) {
|
||||
createTable.append( dialect.getTableComment( table.getComment() ) );
|
||||
}
|
||||
|
||||
applyTableTypeString( createTable );
|
||||
}
|
||||
if ( table.hasPrimaryKey() ) {
|
||||
createTable.append( ", " ).append( table.getPrimaryKey().sqlConstraintString( dialect ) );
|
||||
}
|
||||
|
||||
createTable.append( dialect.getUniqueDelegate().getTableCreationUniqueConstraintsFragment( table, context ) );
|
||||
|
||||
applyTableCheck( table, createTable );
|
||||
|
||||
createTable.append( ')' );
|
||||
|
||||
createTable.append( extra );
|
||||
|
||||
if ( table.getComment() != null ) {
|
||||
createTable.append( dialect.getTableComment( table.getComment() ) );
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -51,8 +51,14 @@ public class StandardTableMigrator implements TableMigrator {
|
|||
Metadata metadata,
|
||||
TableInformation tableInfo,
|
||||
SqlStringGenerationContext context) {
|
||||
return sqlAlterStrings( table, dialect, metadata, tableInfo, context )
|
||||
.toArray( EMPTY_STRING_ARRAY );
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue