Ленивая загрузка таблицы данных показывает повторяющиеся результаты с помощью FetchType.EAGER

Я знаю, что есть много вопросов по этому поводу, но ни одно из решений не помогло мне.

Я использую PrimeFaces для создания таблицы данных с ленивой загрузкой. Это означает, что этот список данных является списком LazyDataModel, и мне пришлось разработать реализацию LazyDataModel, в которой я переопределил метод загрузки. Все это можно узнать из демонстрации PrimeFaces, и это прекрасно работает для большинства случаев, когда dataable использует только одну таблицу из базы данных (что не мой случай).

Теперь у меня есть две сущности:

@Entity
@Table(name="UNIVERSITY")
public class University {

    @Id 
    @Column(name="ID_UNIVERSITY")   
    @GeneratedValue(strategy = GenerationType.AUTO ,generator="SQ_UNIVERSITY")  
    @SequenceGenerator(name="SQ_UNIVERSITY", sequenceName="SQ_UNIVERSITY")
    private Long idUniversity;

    @Column(name="NAME")    
    private String name;

    @ManyToOne
    @JoinColumn(name="ID_DIRECTOR")
    private Director director;

    @OneToMany(fetch=FetchType.EAGER, mappedBy = "university")
    private Set<Students> students = new HashSet<Students>(0);

    ...

    public int getStudentsQty(){
        return this.students.size();
    }

    ...

Где я буду использовать метод getStudentsQty() для заполнения одного столбца из моей таблицы данных. А вот сущность Студенты:

@Entity
@Table(name="STUDENTS")
public class Students 
{

    @Id 
    @Column(name="ID_STUDENTS") 
    @GeneratedValue(strategy = GenerationType.AUTO ,generator="SQ_STUDENTS")    
    @SequenceGenerator(name="SQ_STUDENTS", sequenceName="SQ_STUDENTS")
    private Long idStudent;

    @ManyToOne
    @JoinColumn(name="ID_UNIVERSITY")
    private University student;

    @Column(name="NAME")
    private String name;

    ...

Вот метод поиска, который будет использовать моя реализация загрузки:

public List<University> find(int startingAt, int maxPerPage,
        final String sortField, final SortOrder sortOrder, Map<String, String> filters) {

    session = HibernateUtil.getSession();
    Criteria criteria =session.createCriteria(University.class);    

    List<String> aliases = new ArrayList<String>();

    if(maxPerPage > 0){
        criteria.setMaxResults(maxPerPage);
    }

    criteria.setFirstResult(startingAt);

    addFiltersToCriteria(filters, criteria, aliases);

    Order order = Order.desc("name");

    if(sortField != null && !sortField.isEmpty()){

        if(sortField.contains(".")){
            String first = (sortField.split("\\."))[0];
            if(!aliases.contains(first)){
                criteria.createAlias(first, first);
                aliases.add(first);
            }
        }

        if(sortOrder.equals(SortOrder.ASCENDING)){
            order = Order.asc(sortField);
        }
        else if(sortOrder.equals(SortOrder.DESCENDING)){
            order = Order.desc(sortField);
        }
    }
    criteria.addOrder(order);

    return (List<University>) criteria.list();
}

А теперь моя проблема. Если я использую FetchType.LAZY, все работает нормально, но производительность ужасна, поэтому я хочу использовать EAGER. Если я использую EAGER, результаты будут дублироваться, как и ожидалось, и объясняться здесь . Я попытался реализовать методы equals() и hashCode() в объекте University, чтобы использовать LinkedHashSet, как было предложено в последней ссылке, но это не сработало, я не знаю, как это сделать. Я также пытался использовать DISTINCT с Criteria, но это не работает из-за addOrder, который я использую, где он запрашивает соединения.

Итак, я нашел другое предложение, которое работал отлично. В основном решение состоит в том, чтобы выполнить второй запрос Criteria, ища только университеты с идентификатором, включенным в исходный поиск. Как это:

private List<University> removeDuplicates(Order order,
        List<University> universities) {

    Criteria criteria;
    List<University> distinct = null;

    if(universities.size() > 0){

        Set<Long> idlist = new HashSet<Long>();

        for(University univ: universities){
            idlist.add(univ.getIdUniversity());
        }

        criteria = session.createCriteria(University.class);
        criteria.add(Restrictions.in("id", idlist)) ;

        distinct = (List<University>) criteria.list();

        return distinct;
    }
    else{
        return universities;
    }
}   

Таким образом, это принесет, скажем, первые 100 строк для моей ленивой загрузки данных разбиения на страницы. При первом Criteria поиске они будут отсортированы для первой страницы, и те же 100 правильных строк будут присутствовать после моего второго Criteria поиска, но теперь они будут не отсортированы. Это правильные строки для первой страницы, но не отсортированные внутри первой страницы. Я не могу использовать «addOder» во втором Criteria, иначе они будут дублироваться.

И самое странное: если я попытаюсь отсортировать результаты с помощью Collections.sort, результаты будут продублированы!!! Как?? Как я могу заказать свой результат в конце концов?

Спасибо!!

EDIT: количество студентов - это просто пример, мне нужно в других сценариях получить информацию внутри каждого связанного объекта.


person qxlab    schedule 12.11.2013    source источник


Ответы (2)


Если я правильно понимаю, вы выводите таблицу со списком университетов и хотите показать количество студентов для каждого университета. Если да, то загружать x000 студенческих записей в память просто для подсчета — это безумие (независимо от того, делаете вы это охотно или лениво).

Попробуйте одно из следующих действий:

Один

вместо того, чтобы загружать связанных студентов, чтобы получить подсчет, используйте функциональность Hibernates @Formula и добавьте производное свойство к вашему университетскому объекту.

@Formula(value = "select count(*) from students where university_id = ?")
private int studentCount;

Два

Создайте представление базы данных, например, University_summary_data, которое включает этот счет, и создайте объект, сопоставленный с этим представлением (работает так же, как таблица), а затем в Университете:

@OneToOne
private UniversitySummaryData data;

Три

Посмотрите на Hibernate @LazyCollection(LazyCollectionOption.EXTRA), который позволит вам вызывать size() в сопоставленной коллекции, не загружая всех студентов.

Все намного решения проще, чем у вас есть.

person Alan Hay    schedule 12.11.2013
comment
Спасибо, Алан, эти советы мне очень помогают, и я ими воспользуюсь. Но, к сожалению, это был всего лишь пример, у меня есть и другие случаи, когда мне нужно с нетерпением получать связанные объекты. Если никто не придумает решение, я отмечу ваш ответ как правильный. - person qxlab; 13.11.2013
comment
См. слегка обновленный ответ. Загружать x000 студентов в память только для подсчета — очень плохая идея, независимо от механизма выборки. Для рассматриваемого варианта использования полное избегание нагрузки является правильным решением либо с помощью одного из предложенных мной методов, либо с помощью какого-либо другого механизма. - person Alan Hay; 13.11.2013
comment
Спасибо, и я это понимаю. Но, как я уже сказал, подсчет студентов был просто примером. В другом сценарии мне нужно получить список сущностей, у каждой из которых есть список сущностей, у каждой есть список сущностей. И это не для подсчета, мне понадобится вся информация в них. Что я должен сделать? Или в данном случае использование критериев не является хорошей идеей? - person qxlab; 13.11.2013
comment
Пожалуйста, как я должен заполнить ? параметр в @Formula? - person qxlab; 23.11.2013
comment
Hibernate сделает это за вас. - person Alan Hay; 24.11.2013
comment
Алан, я получаю Caused by: java.sql.SQLException: No value specified for parameter 2. А я пробовал поставить 1 вместо ? чтобы посмотреть, что произойдет, и я получил это: Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select count(*) from STUDENTS S where S.ID_UNIVERSITY = 1 as formula0_3_, o' at line 1 - person qxlab; 24.11.2013
comment
Можете ли вы добавить сопоставление формул, которое вы создали? - person Alan Hay; 25.11.2013
comment
Извини. Да, вы не используете заполнитель. Попробуйте @Formula(value = select count(*) from student S, где S.university_id = idUniversity). - person Alan Hay; 26.11.2013
comment
То же самое, Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select count(*) from students S where S.university_id = this_.idUniversity as f' - person qxlab; 26.11.2013

Вы говорите, что хотите переключиться на загрузку EAGER, потому что производительность с загрузкой Lazy ужасна, но с загрузкой EAGER ваша производительность будет такой же. Вы по-прежнему получите объяснение проблемы выбора n + 1, например, здесь и здесь с решением.

Для повышения производительности вам необходимо изменить запрос, который будет генерировать Hibernate. Обычно я делаю левое внешнее соединение в Hibernate, чтобы получить это, например.

sessionFactory.getCurrentSession().createCriteria(University.class)
.createAlias("students", "students_alias", JoinType.LEFT_OUTER_JOIN)
.list();

И лучше всего оставить ленивую загрузку по умолчанию в Hibernate.

person K.C.    schedule 20.11.2013