JPA 存储过程结果集映射和 NonUniqueResultException
JPA Stored Procedure result set mappings and NonUniqueResultException
我是 JPA 的新手,我正在尝试使用存储过程来 运行 查询并将其结果映射到我的 java 类.
这是table
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 个 table 以查找一个或所有品牌时,我得到一个生成的查询,该查询在单个查询中检索所有 3 个 table。
我现在正在尝试扩展它以允许我使用我按以下方式配置的命名存储过程来执行相同类型的结果集映射:
@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 的一对多关系,存储过程为品牌 table 中的每一行返回重复行。
使用 Spring Data JPA 存储库时的结果集映射及其生成的查询以与我的过程相同的方式具有重复行,并且在映射到对象时能够完美地处理此问题。但是,当使用命名存储过程时,出现以下异常:
javax.persistence.NonUniqueResultException: Call to stored procedure [spGetBrandingByHost] returned multiple results
我知道我可能需要包含更多结果集映射才能使其正常工作,但找不到演示任何类似内容的示例。我所追求的可能吗?
提前致谢
在回答我自己的问题时,不,你不能。这确实有道理。自动生成查询时,休眠结果集中期望的列名,包括从一对 many/many 到一个关系中重复的任何列名。存储过程可以 return hibernate 不知道期望的任何列,因此需要显式设置它们。
经过大量挖掘,我确实找到了 class org.hibernate.cfg.annotations.ResultsetMappingSecondPass
,它被调用以将 JPA 注释映射到本机休眠 org.hibernate.engine.ResultSetMappingDefinition
并且阅读源代码,我可以完全看到它忽略列和连接的大部分注释。
如果@NamedStoredProcedureQuery
能支持一对一many/many对一连接就好了。现在我已经创建了自己的解决方案:
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
向休眠注册的
代码有点乱,但确实能用。它基本上查找 @EntityResultSet
,其中定义了特定结果集的实体。 EntityResultSetSecondPass
查看这些给定的实体并生成一个 ResultSetMappingDefinition
,其中包括用于集合映射的所有连接元数据。它从所有标准列注释运行,但可以用 @EntityResultSet
中定义的 FieldResult
覆盖
看起来有点恶心,但效果很好。
我是 JPA 的新手,我正在尝试使用存储过程来 运行 查询并将其结果映射到我的 java 类.
这是table
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 个 table 以查找一个或所有品牌时,我得到一个生成的查询,该查询在单个查询中检索所有 3 个 table。
我现在正在尝试扩展它以允许我使用我按以下方式配置的命名存储过程来执行相同类型的结果集映射:
@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 的一对多关系,存储过程为品牌 table 中的每一行返回重复行。
使用 Spring Data JPA 存储库时的结果集映射及其生成的查询以与我的过程相同的方式具有重复行,并且在映射到对象时能够完美地处理此问题。但是,当使用命名存储过程时,出现以下异常:
javax.persistence.NonUniqueResultException: Call to stored procedure [spGetBrandingByHost] returned multiple results
我知道我可能需要包含更多结果集映射才能使其正常工作,但找不到演示任何类似内容的示例。我所追求的可能吗?
提前致谢
在回答我自己的问题时,不,你不能。这确实有道理。自动生成查询时,休眠结果集中期望的列名,包括从一对 many/many 到一个关系中重复的任何列名。存储过程可以 return hibernate 不知道期望的任何列,因此需要显式设置它们。
经过大量挖掘,我确实找到了 class org.hibernate.cfg.annotations.ResultsetMappingSecondPass
,它被调用以将 JPA 注释映射到本机休眠 org.hibernate.engine.ResultSetMappingDefinition
并且阅读源代码,我可以完全看到它忽略列和连接的大部分注释。
如果@NamedStoredProcedureQuery
能支持一对一many/many对一连接就好了。现在我已经创建了自己的解决方案:
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
代码有点乱,但确实能用。它基本上查找 @EntityResultSet
,其中定义了特定结果集的实体。 EntityResultSetSecondPass
查看这些给定的实体并生成一个 ResultSetMappingDefinition
,其中包括用于集合映射的所有连接元数据。它从所有标准列注释运行,但可以用 @EntityResultSet
FieldResult
覆盖
看起来有点恶心,但效果很好。