Есть у меня такая БД.
В JPA сущностях описана так.
Playlist:
@Entity
@NamedEntityGraph(
name = "Playlist.full",
attributeNodes = {@NamedAttributeNode(value = "playlistItems"), @NamedAttributeNode(value = "organizations")})
public class Playlist implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
@Column(nullable = false)
private String title;
@OneToMany(mappedBy = "playlist")
@OrderBy("number ASC")
@JsonIgnore
private List<PlaylistItem> playlistItems = new ArrayList<>();
@OneToMany(mappedBy = "playlist")
@JsonIgnore
private Set<Organization> organizations = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<PlaylistItem> getPlaylistItems() {
return playlistItems;
}
public void setPlaylistItems(List<PlaylistItem> playlistItems) {
this.playlistItems = playlistItems;
}
public Set<Organization> getOrganizations() {
return organizations;
}
public void setOrganizations(Set<Organization> organizations) {
this.organizations = organizations;
}
}
PlaylistItem:
@Entity(name = "playlist_item")
public class PlaylistItem extends AbstractIdentifiable implements Serializable {
@Column(nullable = false)
private Integer number;
@JoinColumn(nullable = false)
@ManyToOne
@JsonIgnore
private Video video;
@JoinColumn(nullable = false)
@ManyToOne
@JsonIgnore
private Playlist playlist;
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public Video getVideo() {
return video;
}
public void setVideo(Video video) {
this.video = video;
}
public Playlist getPlaylist() {
return playlist;
}
public void setPlaylist(Playlist playlist) {
this.playlist = playlist;
}
}
Organization:
@Entity
public class Organization extends AbstractIdentifiable implements Serializable {
@Column(nullable = false)
@NotBlank
private String title;
@OneToMany(mappedBy = "organization")
@JsonIgnore
private Set<Location> locations = new HashSet<>();
@JsonIgnore
@ManyToMany(mappedBy = "organizations")
private Set<Member> members = new HashSet<>();
@ManyToOne
@JoinColumn
@JsonIgnore
private Playlist playlist;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Set<Location> getLocations() {
return locations;
}
public void setLocations(Set<Location> locations) {
this.locations = locations;
}
public Set<Member> getMembers() {
return members;
}
public void setMembers(Set<Member> members) {
this.members = members;
}
public Playlist getPlaylist() {
return playlist;
}
public void setPlaylist(Playlist playlist) {
this.playlist = playlist;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Organization that = (Organization) o;
return Objects.equals(getId(), that.getId()) &&
Objects.equals(title, that.title);
}
@Override
public int hashCode() {
return Objects.hash(getId(), title);
}
}
На одной странице мне нужно перечислить плейлисты, по каждому отображая его название, число организаций и число элементов.
На другой странице я хочу перечислить список организаций и элементов по выбранному плейлисту.
Разрабатывая на C++ я привыкла реализовывать выборку данных минимизируя число запросов, избегая запросы в циклах, чтобы по возможности их число не зависело от данных. Думаю все согласятся, что это правильный подход.
Ленивая загрузка данных сразу отпадает в этом случае, потому я использую named entity graphs. Мне казалось, что это панацея, но при загрузке сущности Playlist по id я получаю невалидные данные.
Сервисный класс получения данных:
@Transactional
@Service(value = "dataService")
public class DataServiceImpl implements DataService {
private static final Logger logger = Logger.getLogger(DataServiceImpl.class);
private static final String hint = "javax.persistence.fetchgraph";
@PersistenceContext
private EntityManager entityManager;
@Override
public <T> T get(Class<T> entityClass, Long id) {
return entityManager.find(entityClass, id);
}
@Override
public <T> T get(Class<T> entityClass, Long id, String entityGraph) {
Map<String,Object> hints = new HashMap<>();
hints.put(hint, entityManager.getEntityGraph(entityGraph));
return entityManager.find(entityClass, id, hints);
}
@Override
public <T> List<T> getAll(Class<T> entityClass) {
return getAll(entityClass, null);
}
@Override
public <T> List<T> getAll(Class<T> entityClass, String entityGraph) {
CriteriaQuery<T> cq = entityManager.getCriteriaBuilder().createQuery(entityClass);
Root<T> rootEntry = cq.from(entityClass);
CriteriaQuery<T> all = cq.select(rootEntry);
TypedQuery<T> allQuery = entityManager.createQuery(all);
if (entityGraph != null) {
allQuery.setHint(hint, entityManager.getEntityGraph(entityGraph));
}
return allQuery.getResultList();
}
@Override
public <T> T getByColumn(Class<T> entityClass, String field, Object value) {
try {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> criteria = criteriaBuilder.createQuery(entityClass);
Root<T> root = criteria.from(entityClass);
criteria.select(root);
criteria.where(criteriaBuilder.equal(root.get(field), value));
return entityManager.createQuery(criteria).getSingleResult();
} catch (NoResultException e) {
return null;
}
}
@Override
public <T> List<T> getListByColumn(Class<T> entityClass, String field, Object value) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> cq = criteriaBuilder.createQuery(entityClass);
Root<T> rootEntry = cq.from(entityClass);
CriteriaQuery<T> all = cq.select(rootEntry).where(criteriaBuilder.equal(rootEntry.get(field), value));
TypedQuery<T> allQuery = entityManager.createQuery(all);
return allQuery.getResultList();
}
@Override
public void persist(Object entity) {
entityManager.persist(entity);
}
@Override
public Object merge(Object entity) {
return entityManager.merge(entity);
}
@Override
public void remove(Object entity) {
entityManager.remove(entityManager.merge(entity));
}
}
Давайте рассмотрим второй случай, получение списка организаций и элементов по выбранному плейлисту:
@RequestMapping(value = "/{id}/items/edit", method = RequestMethod.GET)
public String itemsEdit(@PathVariable(name = "id") Long id, Model model) {
if (!model.containsAttribute("playlist")) {
Playlist playlist = dataService.get(Playlist.class, id, "Playlist.full");
if (playlist == null) {
//TODO: отобразить сообщение
return "redirect:/admin/playlists";
} else {
model.addAttribute("playlist", playlist);
}
}
model.addAttribute("videos", dataService.getAll(Video.class));
return "admin/playlists/items";
}
В итоге формируется hibernate генерирует такой запрос:
select playlist0_.id as id1_5_0_, playlist0_.title as title2_5_0_, organizati1_.playlist_id as playlist3_3_1_, organizati1_.id as id1_3_1_, organizati1_.id as id1_3_2_, organizati1_.playlist_id as playlist3_3_2_, organizati1_.title as title2_3_2_, playlistit2_.playlist_id as playlist3_6_3_, playlistit2_.id as id1_6_3_, playlistit2_.id as id1_6_4_, playlistit2_.number as number2_6_4_, playlistit2_.playlist_id as playlist3_6_4_, playlistit2_.video_id as video_id4_6_4_
from Playlist playlist0_
left outer join Organization organizati1_ on playlist0_.id=organizati1_.playlist_id
left outer join playlist_item playlistit2_ on playlist0_.id=playlistit2_.playlist_id
where playlist0_.id=? order by playlistit2_.number asc
Для плейлиста, у которого есть две организации и два элемента плейлиста я получаю сущность Playlist, у которой есть четыре организации и четыре плейлиста. Причина понятна, сначала мы выбираем плейлист - это одна запись. Потом делаем left join организаций, выходит две записи. Затем делаем left join для полученных для полученных двух записей еще двух записей.
Как правильнее в данной ситуации с помощью JPA получить корректные данные минимумом запросов и телодвижений?
Я попробовала так:
@Entity
@NamedEntityGraphs({
@NamedEntityGraph(
name = "Playlist.organizations",
attributeNodes = {@NamedAttributeNode(value = "organizations")}),
@NamedEntityGraph(
name = "Playlist.playlistItems",
attributeNodes = {@NamedAttributeNode(value = "playlistItems", subgraph = "video")},
subgraphs = {@NamedSubgraph(name = "video", attributeNodes = @NamedAttributeNode("video"))})
})
public class Playlist implements Serializable {
...
И вместо:
Playlist playlist = dataService.get(Playlist.class, id, "Playlist.full");
Пишем:
Playlist playlist = dataService.get(Playlist.class, id, "Playlist.organizations");
playlist = dataService.get(Playlist.class, id, "Playlist.playlistItems");
Тогда выходит то, что нужно. Но это как-то костыльно выглядит.
Теперь первый случай, отображение списка плейлистов с числом организаций и элементов. Проблема та же из-за двух left join'ов. Но вопрос такой: как правильно запросить только число этих связанных сущностей? На каждый случай писать свои запросы с помощью NamedQuery или CriteriaBuilder? Или же есть способ используя сущности JPA, но без фактического выкачивания данных?