JPA存储过程结果集映射和NonUniqueResultException

时间:2016-02-27 22:50:10

标签: java spring hibernate jpa spring-data-jpa

我是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

据我所知,我可能需要包含更多的结果集映射才能正常工作,但无法找到一个演示类似内容的示例。我甚至可能追求什么?

提前致谢

1 个答案:

答案 0 :(得分:0)

回答我自己的问题,不,你不能。哪个有意义。在自动生成查询时,请在结果集中休眠所期望的列名,包括从一对多/多对一关系复制的任何列名。存储过程可以返回hibernate不知道的任何列,因此需要明确设置它们。

经过多次挖掘后,我确实找到了一个类org.hibernate.cfg.annotations.ResultsetMappingSecondPass,它被调用来将JPA注释映射到本地hibernate a 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覆盖

看起来有点讨厌,但它运作得很好。