[英]JPA Stored Procedure result set mappings and NonUniqueResultException
我對JPA還是很陌生,並且正在嘗試使用存儲過程來運行查詢並將其結果映射到我的java類。
這是表格
CREATE TABLE dbo.Branding
(
Branding_ID INT IDENTITY NOT NULL
CONSTRAINT PK_Branding PRIMARY KEY CLUSTERED,
BrandingType_ID INT,
Reseller_ID INT NULL,
Host VARCHAR(MAX) NULL
)
CREATE TABLE dbo.BrandingResource
(
BrandingResource_ID INT IDENTITY NOT NULL
CONSTRAINT PK_BrandingResource PRIMARY KEY CLUSTERED,
Branding_ID INT NOT NULL,
Name VARCHAR(255) NOT NULL,
[Value] VARCHAR(MAX) NOT NULL
)
CREATE TABLE dbo.BrandingType
(
BrandingType_ID INT IDENTITY NOT NULL
CONSTRAINT PK_BrandingType PRIMARY KEY CLUSTERED,
Description VARCHAR(255)
)
以下是實體:
@Table(name = "[Branding]")
@Entity
public class Branding extends CommonDomainBase
{
@Id
@Column(name = "branding_id")
private int id;
@OneToOne(optional = false)
@JoinColumn(name = "brandingtype_id", nullable = false)
private BrandingType type;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "branding_id", referencedColumnName = "branding_id")
private Set<BrandingResource> brandingResources;
@Column(name = "reseller_id", nullable = true)
private Integer resellerId;
@Column(name = "host", nullable = true)
private String host;
}
@Table(name = "[BrandingResource]")
@Entity
public class BrandingResource extends CommonDomainBase
{
@Id
@Column(name = "BrandingResource_Id")
private int id;
@Column(name = "Name")
private String name;
@Column(name = "Value")
private String value;
}
@Table(name = "[BrandingType]")
@Entity
public class BrandingType extends CommonDomainBase
{
@Id
@Column(name = "brandingtype_id")
private int id;
@Column(name = "description")
private String description;
}
我已經知道實體上的注釋可以正常工作。 當我使用Spring Data JPA存儲庫查詢3個表以查找一個表或查找所有Branding時,我得到了一個生成的查詢,該查詢可以在單個查詢中檢索所有3個表。
我現在正在嘗試擴展它,以允許我使用以以下方式配置的命名存儲過程執行相同類型的結果集映射:
@NamedStoredProcedureQuery(name = "Branding.getBrandingByHost", procedureName = "spGetBrandingByHost", parameters =
{ @StoredProcedureParameter(mode = ParameterMode.IN, name = "host", type = String.class) }, resultSetMappings =
{ "BrandingResults" })
@SqlResultSetMapping(name = "BrandingResults", entities =
{ @EntityResult(entityClass = Branding.class) })
由於與BrandingResource的一對多關系,存儲過程將為品牌表中的每一行返回重復的行。
使用Spring Data JPA存儲庫及其生成的查詢時,結果集映射具有與我的過程相同的重復行,並且在映射到對象時能夠完美地處理此行。 但是,當使用命名存儲過程時,出現以下異常:
javax.persistence.NonUniqueResultException: Call to stored procedure [spGetBrandingByHost] returned multiple results
我知道我可能需要包括更多結果集映射才能起作用,但是找不到能說明類似內容的示例。 我什至有可能嗎?
提前致謝
回答我自己的問題,不,你不能。 哪個有道理。 自動生成查詢時,請休眠結果集中需要哪些列名,包括從一對多/多對多關系中重復的列名。 存儲過程可以返回休眠不期望的任何列,因此需要顯式設置它們。
經過大量的挖掘,我確實找到了org.hibernate.cfg.annotations.ResultsetMappingSecondPass
類,該類被調用以將JPA注釋映射到本地休眠的org.hibernate.engine.ResultSetMappingDefinition
並且在閱讀源代碼時,我可以看到它完全忽略了大部分列和連接的注釋。
如果@NamedStoredProcedureQuery
可以支持一對多/多對一聯接,那就太好了。 現在,我已經創建了自己的解決方案:
public class EntityResultSetSecondPass implements QuerySecondPass
{
private static final String ALIAS = EntityResultSetSecondPass.class.getName() + "_alias";
private final InFlightMetadataCollector metadataCollector;
private int entityAliasIndex;
private final Map<Class<?>, String> aliasMap = new ConcurrentHashMap<>();
public EntityResultSetSecondPass(final InFlightMetadataCollector metadataCollector)
{
this.metadataCollector = metadataCollector;
}
@Override
public void doSecondPass(final Map persistentClasses) throws MappingException
{
for (final Object key : persistentClasses.keySet())
{
final String className = key.toString();
try
{
final Class<?> clazz = Class.forName(className);
final EntityResultSet entityResultSet = clazz.getDeclaredAnnotation(EntityResultSet.class);
if (entityResultSet == null)
{
continue;
}
else
{
createEntityResultDefinition(entityResultSet, clazz);
}
}
catch (final ClassNotFoundException e)
{
throw new HibernateException(e);
}
}
}
private void createEntityResultDefinition(final EntityResultSet entityResultSet, final Class<?> entityClass)
throws ClassNotFoundException
{
final List<NativeSQLQueryReturn> mappedReturns = new ArrayList<>();
final ResultSetMappingDefinition definition = new ResultSetMappingDefinition(entityResultSet.name());
final Map<Class<?>, FieldResult[]> returnedEntities = new ConcurrentHashMap<>();
returnedEntities.put(entityClass, entityResultSet.fields());
for (final EntityResult entityResult : entityResultSet.relatedEntities())
{
returnedEntities.put(entityResult.entityClass(), entityResultSet.fields());
}
definition.addQueryReturn(new NativeSQLQueryRootReturn(getOrCreateAlias(entityClass), entityClass.getName(),
getPropertyResults(entityClass, entityResultSet.fields(), returnedEntities, mappedReturns, ""),
LockMode.READ));
for (final EntityResult entityResult : entityResultSet.relatedEntities())
{
definition
.addQueryReturn(
new NativeSQLQueryRootReturn(getOrCreateAlias(entityResult.entityClass()),
entityResult.entityClass().getName(), getPropertyResults(entityResult.entityClass(),
entityResult.fields(), returnedEntities, mappedReturns, ""),
LockMode.READ));
}
for (final NativeSQLQueryReturn mappedReturn : mappedReturns)
{
definition.addQueryReturn(mappedReturn);
}
metadataCollector.addResultSetMapping(definition);
}
private Map<String, String[]> getPropertyResults(final Class<?> entityClass, final FieldResult[] fields,
final Map<Class<?>, FieldResult[]> returnedEntities, final List<NativeSQLQueryReturn> mappedReturns,
final String prefix) throws ClassNotFoundException
{
final Map<String, String[]> properties = new ConcurrentHashMap<>();
for (final Field field : entityClass.getDeclaredFields())
{
final Column column = field.getAnnotation(Column.class);
if (column != null)
{
properties.put(prefix + field.getName(), new String[]
{ column.name() });
}
final JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);
if (joinColumn != null)
{
properties.putAll(handleJoinColumn(entityClass, field, joinColumn, returnedEntities, mappedReturns));
}
}
if (entityClass.getSuperclass() != null)
{
properties.putAll(
getPropertyResults(entityClass.getSuperclass(), fields, returnedEntities, mappedReturns, prefix));
}
return properties;
}
private Map<String, String[]> handleJoinColumn(final Class<?> sourceEntity, final Field field,
final JoinColumn joinColumn, final Map<Class<?>, FieldResult[]> returnedEntities,
final List<NativeSQLQueryReturn> mappedReturns) throws ClassNotFoundException
{
final Map<String, String[]> properties = new ConcurrentHashMap<>();
final OneToOne oneToOne = field.getAnnotation(OneToOne.class);
if (oneToOne != null)
{
properties.put(field.getName(), new String[]
{ joinColumn.name() });
}
final OneToMany oneToMany = field.getAnnotation(OneToMany.class);
if (oneToMany != null)
{
Class<?> fieldType;
if (field.getType().isArray())
{
fieldType = field.getType();
}
else if (Collection.class.isAssignableFrom(field.getType()))
{
fieldType = Class.forName(
ParameterizedType.class.cast(field.getGenericType()).getActualTypeArguments()[0].getTypeName());
}
else
{
throw new UnsupportedOperationException("One to many only supports collection and array types");
}
if (returnedEntities.keySet().contains(fieldType))
{
properties.put(field.getName(), new String[]
{ joinColumn.name() });
final Map<String, String[]> resolvedProperties = getPropertyResults(fieldType,
returnedEntities.get(fieldType), returnedEntities, mappedReturns, "element.");
resolvedProperties.put("key", new String[]
{ joinColumn.referencedColumnName() });
resolvedProperties.put("element", new String[]
{ joinColumn.name() });
mappedReturns.add(new NativeSQLQueryCollectionReturn(getOrCreateAlias(fieldType),
sourceEntity.getName(), field.getName(), resolvedProperties, LockMode.READ));
mappedReturns
.add(new NativeSQLQueryJoinReturn(getOrCreateAlias(fieldType),
getOrCreateAlias(sourceEntity), field.getName(), getPropertyResults(fieldType,
returnedEntities.get(fieldType), returnedEntities, mappedReturns, ""),
LockMode.READ));
}
}
return properties;
}
private String getOrCreateAlias(final Class<?> entityClass)
{
if (!aliasMap.containsKey(entityClass))
{
aliasMap.put(entityClass, ALIAS + entityAliasIndex++);
}
return aliasMap.get(entityClass);
}
}
以及隨附的注釋:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EntityResultSet
{
/**
* The name of the result set
*
* @return
*/
String name();
/**
* The {@link FieldResult} to override those of the {@link Column}s on the
* current {@link Entity}
*
* @return
*/
FieldResult[] fields() default {};
/**
* The {@link EntityResult} that define related {@link Entity}s that are
* included in this result set.
*
* </p>Note: discriminatorColumn has no impact in this usage
*
* @return
*/
EntityResult[] relatedEntities() default {};
}
所有這些都通過MetadataContributor
在hibernate中注冊。
該代碼有點混亂,但實際上可以正常工作。 它基本上查找@EntityResultSet
,其中定義了特定結果集的實體。 EntityResultSetSecondPass
查看這些給定的實體,並生成一個ResultSetMappingDefinition
,其中包括用於集合映射的所有加入的元數據。 它從所有標准列注釋運行,但可以用FieldResult
定義的@EntityResultSet
覆蓋
看起來有點討厭,但效果很好。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.