JPA 查询可以将结果作为 Java 映射返回吗?
Posted
技术标签:
【中文标题】JPA 查询可以将结果作为 Java 映射返回吗?【英文标题】:Can a JPA Query return results as a Java Map? 【发布时间】:2011-05-21 06:14:26 【问题描述】:我们目前正在根据命名 JPA 查询返回的两个字段手动构建 Map
,因为 JPA 2.1 仅提供 getResultList()
方法:
@NamedQueryname="myQuery",query="select c.name, c.number from Client c"
HashMap<Long,String> myMap = new HashMap<Long,String>();
for(Client c: em.createNamedQuery("myQuery").getResultList() )
myMap.put(c.getNumber, c.getName);
但是,我觉得自定义映射器或类似的映射器会更高效,因为此列表很容易包含 30,000 多个结果。
在不手动迭代的情况下构建地图的任何想法。
(我使用的是 OpenJPA,不是休眠)
【问题讨论】:
什么将用作您的 Map 键? 如代码所示,数字字段 (Long),返回两个值之一。但是我可以使用任何类型,只要键是数字,值是名称。我添加了声明以获取更多详细信息。 【参考方案1】:使用 JPA Query getResultStream 返回地图结果
从JPA 2.2版本开始,可以使用getResultStream
Query
方法将List<Tuple>
结果转化为Map<Integer, Integer>
:
Map<Integer, Integer> postCountByYearMap = entityManager.createQuery("""
select
YEAR(p.createdOn) as year,
count(p) as postCount
from
Post p
group by
YEAR(p.createdOn)
""", Tuple.class)
.getResultStream()
.collect(
Collectors.toMap(
tuple -> ((Number) tuple.get("year")).intValue(),
tuple -> ((Number) tuple.get("postCount")).intValue()
)
);
使用 JPA Query getResultList 和 Java 流返回 Map 结果
如果您使用的是 JPA 2.1 或更早版本,但您的应用程序在 Java 8 或更高版本上运行,那么您可以使用 getResultList
并将 List<Tuple>
转换为 Java 8 流:
Map<Integer, Integer> postCountByYearMap = entityManager.createQuery("""
select
YEAR(p.createdOn) as year,
count(p) as postCount
from
Post p
group by
YEAR(p.createdOn)
""", Tuple.class)
.getResultList()
.stream()
.collect(
Collectors.toMap(
tuple -> ((Number) tuple.get("year")).intValue(),
tuple -> ((Number) tuple.get("postCount")).intValue()
)
);
使用特定于 Hibernate 的 ResultTransformer 返回 Map 结果
另一种选择是使用 Hibernate Types 开源项目提供的 MapResultTransformer
类:
Map<Number, Number> postCountByYearMap = (Map<Number, Number>) entityManager.createQuery("""
select
YEAR(p.createdOn) as year,
count(p) as postCount
from
Post p
group by
YEAR(p.createdOn)
""")
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(
new MapResultTransformer<Number, Number>()
)
.getSingleResult();
MapResultTransformer
适用于仍在 Java 6 上运行或使用旧 Hibernate 版本的项目。
避免返回大型结果集
OP 说:
但是,我觉得自定义映射器或类似的会更高效 因为这个列表很容易有 30,000 多个结果。
这是一个糟糕的主意。您永远不需要选择 30k 条记录。这将如何适应 UI?或者,为什么要对这么大批量的记录进行操作?
您应该使用查询分页,因为这将帮助您减少事务响应时间并提供更好的并发性。
【讨论】:
EclipseLink 2.7 拒绝返回Tuple
s,我总是得到 Vector
的 Object
s 而不是关于别名的信息。
我虽然这是 JPA 功能,但不是特定于 Hibernate 的。你也没有把它放在上面的“Hibernate specific”下。你用 EclipseLink 试过这个吗?
这是一个 JPA 特性,这就是接口被称为javax.persistence.Tuple
的原因。任何 JPA 提供者都应该支持它。如果 EclipseLink 不支持它,您应该为此打开一个问题。
JPA 似乎只是在规范中将 Tuple 添加到标准查询中。 JSR 添加了脚注 26+27,明确指出 JPQL 查询不支持元组等其他类型:“指定其他结果类型(例如 Tuple.class)的应用程序将不可移植。” EclipseLink 没有理由不支持它,但它不是规范的一部分。
实际上,JPA 可移植性是一个神话。就像 SQL 标准一样。我个人从未遇到过更换 JPA 提供商的人。即使在 Hibernate 论坛上,我也不记得有超过 2 或 3 个与提供程序迁移相关的问题。因此,实际上,可移植性并不是真正的问题。在这种情况下,我认为createQuery
应该允许Tuple
,就像它允许通过构造函数表达式进行 DTO 投影一样。【参考方案2】:
JPA v2.2
虽然我来晚了,但如果有人来这里寻求解决方案,这是我为多行选择多列的自定义工作解决方案:
Query query = this.entityManager.createNativeQuery("SELECT abc, xyz, pqr,...FROM...", Tuple.class);
.
.
.
List<Tuple> lst = query.getResultList();
List<Map<String, Object>> result = convertTuplesToMap(lst);
convertTuplesToMap()
的实现:
public static List<Map<String, Object>> convertTuplesToMap(List<Tuple> tuples)
List<Map<String, Object>> result = new ArrayList<>();
for (Tuple single : tuples)
Map<String, Object> tempMap = new HashMap<>();
for (TupleElement<?> key : single.getElements())
tempMap.put(key.getAlias(), single.get(key));
result.add(tempMap);
return result;
【讨论】:
【参考方案3】:如果是 java 8 有内置条目“CustomEntryClass”
既然return是流,那么调用函数(repoistory层)必须有@Transactional(readonly=true|false)注解,否则会抛出异常
确保您将使用 CustomEntryClass 类的完整限定名...
@Query("select new CustomEntryClass(config.propertyName, config.propertyValue) " + "from ClientConfigBO config where config.clientCode =:clientCode ") Stream<CustomEntryClass<String, String>> getByClientCodeMap(@Param("clientCode") String clientCode);
【讨论】:
【参考方案4】:使用 java 8 (+),您可以通过 hibernate 实体管理器将结果作为数组对象列表(选择的每一列将在结果数组上具有相同的索引),然后从结果列表到流,将结果映射到条目(key, value),然后将它们收集到相同类型的map中。
final String sql = "SELECT ID, MODE FROM MODES";
List<Object[]> result = entityManager.createNativeQuery(sql).getResultList();
return result.stream()
.map(o -> new AbstractMap.SimpleEntry<>(Long.valueOf(o[0].toString()), String.valueOf(o[1])))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
【讨论】:
【参考方案5】:Map<String,Object> map = null;
try
EntityManager entityManager = getEntityManager();
Query query = entityManager.createNativeQuery(sql);
query.setHint(QueryHints.RESULT_TYPE, ResultType.Map);
map = (Map<String,Object>) query.getSingleResult();
catch (Exception e)
List<Map<String,Object>> list = null;
try
EntityManager entityManager = getEntityManager();
Query query = entityManager.createNativeQuery(sql);
query.setHint(QueryHints.RESULT_TYPE, ResultType.Map);
list = query.getResultList();
catch (Exception e)
【讨论】:
【参考方案6】:这很好用。 仓库代码:
@Repository
public interface BookRepository extends CrudRepository<Book,Id>
@Query("SELECT b.name, b.author from Book b")
List<Object[]> findBooks();
service.java
List<Object[]> list = bookRepository.findBooks();
for (Object[] ob : list)
String key = (String)ob[0];
String value = (String)ob[1];
链接https://codereview.stackexchange.com/questions/1409/jpa-query-to-return-a-map
【讨论】:
这正是 OP 想要避免的 @KamilBęben 客户制图师也会在幕后做同样的事情。 嘿伙计,这还不错,如果您的查询返回多于一列,那您就很好了。【参考方案7】:请参考JPA 2.0 native query results as map
在你的Postgres
中,它会是这样的,
List<String> list = em.createNativeQuery("select cast(json_object_agg(c.number, c.name) as text) from schema.client c")
.getResultList();
//handle exception here, this is just sample
Map map = new ObjectMapper().readValue(list.get(0), Map.class);
请注意,我只是在与 Postgres 分享我的解决方法。
【讨论】:
【参考方案8】:您可以改为检索 java.util.Map.Entry 列表。 因此,您的实体中的集合应建模为 Map:
@OneToMany
@MapKeyEnumerated(EnumType.STRING)
public Map<PhoneType, PhoneNumber> phones;
在示例中,PhoneType 是一个简单的枚举,PhoneNumber 是一个实体。在您的查询中,使用 JPA 2.0 中引入的 ENTRY
关键字进行映射操作:
public List<Entry> getPersonPhones()
return em.createQuery("SELECT ENTRY(pn) FROM Person p JOIN p.phones pn",java.util.Map.Entry.class).getResultList();
您现在已准备好检索条目并开始使用它:
List<java.util.Map.Entry> phoneEntries = personDao.getPersonPhoneNumbers();
for (java.util.Map.Entry<PhoneType, PhoneNumber> entry: phoneEntries)
//entry.key(), entry.value()
如果您仍然需要地图中的条目,但不想手动遍历条目列表,请查看适用于 Java 8 的这篇帖子 Convert Set<Map.Entry<K, V>> to HashMap<K, V>。
【讨论】:
【参考方案9】:使用自定义结果class
和一点番石榴,这是我的方法,效果很好:
public static class SlugPair
String canonicalSlug;
String slug;
public SlugPair(String canonicalSlug, String slug)
super();
this.canonicalSlug = canonicalSlug;
this.slug = slug;
...
final TypedQuery<SlugPair> query = em.createQuery(
"SELECT NEW com.quikdo.core.impl.JpaPlaceRepository$SlugPair(e.canonicalSlug, e.slug) FROM "
+ entityClass.getName() + " e WHERE e.canonicalSlug IN :canonicalSlugs",
SlugPair.class);
query.setParameter("canonicalSlugs", canonicalSlugs);
final Map<String, SlugPair> existingSlugs =
FluentIterable.from(query.getResultList()).uniqueIndex(
new Function<SlugPair, String>()
@Override @Nullable
public String apply(@Nullable SlugPair input)
return input.canonicalSlug;
);
【讨论】:
【参考方案10】:没有让 JPA 返回地图的标准方法。
查看相关问题:JPA 2.0 native query results as map
手动迭代应该没问题。相对于执行/返回查询结果的时间,在内存中迭代列表/映射的时间会很短。除非有确凿证据表明手动迭代不可行,否则我不会尝试使用 JPA 内部或自定义。
另外,如果您在其他地方将查询结果列表转换为地图,您可能希望将其重构为一个实用方法,并带有一个参数来指示地图键属性。
【讨论】:
【参考方案11】:这个怎么样?
@NamedNativeQueries(
@NamedNativeQuery(
name="myQuery",
query="select c.name, c.number from Client c",
resultClass=RegularClient.class
)
)
和
public static List<RegularClient> runMyQuery()
return entityManager().createNamedQuery("myQuery").getResultList();
【讨论】:
@zawhut - 谢谢,但这仍然返回一个列表。问题是我需要在另一个方法中使用结果并且需要一个快速查找方法。我认为地图是最好的方法来做到这一点。 OP 要求提供地图以上是关于JPA 查询可以将结果作为 Java 映射返回吗?的主要内容,如果未能解决你的问题,请参考以下文章
两个不同的查询返回相同的对象 Spring Boot JPA