问题描述
目前,我尝试在JPA的帮助下从Web api存储一些新闻。 我需要存储3个实体:网页,新闻发布和返回新闻发布的查询。我三个人都有一张桌子。我简化后的JPA实体如下所示:
@Entity
@Data
@Table(name = "NewsPosts",schema = "data")
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NewsPost {
@Id
@Column(name = "id")
private long id;
@Basic
@Column(name = "subject")
private String subject;
@Basic
@Column(name = "post_text")
private String postText;
@ManyToOne(fetch = FetchType.LAZY,cascade = CascadeType.MERGE)
@JoinColumn(name = "newsSite")
private NewsSite site;
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.MERGE)
@JoinTable(name = "query_news_post",joinColumns = @JoinColumn(name = "newsid"),inverseJoinColumns = @JoinColumn(name = "queryid"))
private Set<QueryEntity> queries;
}
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "queries",schema = "data")
@EqualsAndHashCode
public class QueryEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;
@EqualsAndHashCode.Exclude
@Basic
@Column(name = "query")
private String query;
// needs to be exclueded otherwise we can create stack overflow,because of circular references...
@EqualsAndHashCode.Exclude
@ToString.Exclude
@ManyToMany(mappedBy = "queries",fetch = FetchType.LAZY,cascade = CascadeType.MERGE)
Set<PostsEntity> posts;
}
@Entity
@Data
@Table(name = "sites",schema = "data")
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class newsSite {
@Id
@Column(name = "SiteId")
private long id;
@Basic
@Column(name = "SiteName")
private String site;
}
当前,我正在执行以下操作:创建查询并检索查询的。然后我开始爬行: 我从页面上以分页的方式从Web api获取了对象,页面大小为100条newsPosts,我使用对象映射器将json响应映射到我的实体类。
然后我尝试了两个不同的选项:
- 我将查询ID作为Set添加到NewsPost,并使用
EntityManager
的merge选项将其写回到DB。直到我到达要点,我再次收到NewsPost进行另一个查询,然后新的查询被旧的查询覆盖后,此方法才能很好地工作。为了解决这个问题,它尝试了2。 - 我检查NewsPost是否存在,如果我检索了该帖子,则将该新查询添加到现有查询中,然后像以前一样将其合并回数据库。当执行此操作时,我工作得很好,并且我得到了第一批的预期结果,但是随后应用程序突然开始为第三批消耗越来越多的内存。我附上了JavaVisualVM的屏幕截图。有人知道为什么会这样吗?
编辑: 由于评论中提出了一些问题,我想在此处提供问题的答案。
我认为通过爬行,一切都可以正常工作。 Webapi的返回为json。我使用杰克逊映射器将此映射到POJO,然后使用推土机映射器将POJO转换为实体。 (是的,在应用程序中,我需要先将POJO用于其他目的,这是可以的。)
关于使用EntityManager进行书写,我不确定我是否做得正确。
首先,我创建了一个JPA仓库来检查帖子是否已经存在(以获取旧的查询ID并避免在queryid,postid表中覆盖问题)。我的JPA仓库如下所示。
@Repository
public interface PostRepo extends JpaRepository<NewsPost,Long> {
NewsPost getById(long id);
}
要更新帖子,我正在执行以下操作:
private void updatePosts(List<NewsPost> posts){
posts.forEach(post->{
NewsPost foundPost = postRepo.getById(post.getId());
if(foundPost!=null){
post.getQueries().addAll(foundPost.getQueries());
}});
}
我目前正在按照以下方式编写我的实体,我有一个实体列表,其中还包含更新的帖子,并且我在处理该写作的班上有一个自动连线的EntityManagerFactory
。
EntityManager em = entityManagerFactory.createEntityManager();
try {
EntityTransaction transaction = em.getTransaction();
transaction.begin();
entities.forEach(entity->em.merge(entity))
em.flush();
transaction.commit();
} finally {
em.clear();
em.close();
}
我很确定这是写作过程。如果我保持软件逻辑不变,只是跳过合并,或者只是将实体打印或转储到文件中,那么一切正常且快速,并且没有错误出现,因此合并注释似乎有问题吗?
关于我的程序是否由于内存消耗而死的问题取决于它。如果我在Mac上运行它,则将消耗多达8 GB以上的RAM,但MAC OS正在处理此问题并将RAM交换到磁盘。如果我将其作为Docker容器von CentOS运行,则由于内存不足,进程将被终止。
现在是否相关,但是我正在使用OpenJDK 11,Springboot 2.2.6和MysqL 8数据库。
我在application.yml中按以下方式配置了jpa:
spring:
main:
allow-bean-deFinition-overriding: true
datasource:
url: "jdbc:MysqL://db"
username: user
password: secret
driver-class-name: com.MysqL.cj.jdbc.Driver
test-while-idle: true
validation-query: Select 1
jpa:
database-platform: org.hibernate.dialect.MysqL8Dialect
hibernate:
ddl-auto: none
properties:
hibernate:
event:
merge:
entity_copy_observer: allow
```
解决方法
如果合并过程出了问题,则可以在每次合并之后添加entityManager
和em.flush();
来快速降低em.clear();
中的内存消耗:
EntityTransaction transaction = em.getTransaction();
transaction.begin();
entities.forEach(entity-> {
em.merge(entity);
em.flush();
em.clear();
});
transaction.commit();
但是,我认为您应该更改模型。加载每个帖子的所有现有查询只是为了添加新的查询,效率很低。您可以将N-M关系建模为一个新实体,然后仅保留新关系。
,尝试自行解决。我为多对多关系创建了一个实体。之后,我为每个实体创建了CRUD存储库,并使用了Crud存储库中的saveAll
。内存也可以正常工作。现在,GC在内存可视化中生成了预期的电锯模式。但是我仍然不知道为什么我之前使用批注中的联接表创建的多对多关系会产生有关内存管理的问题。有人可以解释为什么这解决了我的问题是ManyToMany创建循环依赖吗?但据我所知,GC还发现了循环依赖关系。
ManyToMany中的EAGER关系带来许多对象。关于LAZY现实,请确保获取它们,因为如果不这样做,则通过完整的对象将其转换为JSON或POJO将会对每个未使用获取初始化的对象抛出查询,这很危险。如果不需要所有这些,可以使用@JsonIgnore批注。