LINUX.ORG.RU

Named entity graph сразу для двух списков портит данные

 , , , ,


0

1

Есть у меня такая БД. В 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, но без фактического выкачивания данных?


А что не так в варианте с Criteria API? Оно же типобезопастно

anonymous ()

Кстати, вместо named query, можно было с помощь distinct criteria API вынуть id сущьностей, а затем вторым запросом и сами сущноcти по id.

anonymous ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.