Аннотация Hibernate для одного объекта выбора (где) вместо коллекции для многих

На данный момент у нас есть класс, который выглядит примерно так (удалены обезличенные и ненужные части):

@Entity
@Table(name = "MAIN_TABLE")
public class MainTable extends AbstractTable {

  @OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable")
  @OrderBy("CREATED_ON DESC")
  private Set<MainTableState> states;

  ...

  public MainTableState getActiveState(){
    if(this.states == null || this.states.isEmpty()){
      return null;
    }
    MainTableState latest = states.iterator().next();
    // The reason we use this for-loop, even though we have the @OrderBy annotation,
    // Is because we can later add states to this list, which aren't automatically ordered
    for(MainTableState state : states){
      if(state.getCreatedOn() != null && latest.getCreatedOn() != null &&
           state.getCreatedOn().after(latest.getCreatedOn()){
        latest = state;
      }
    }
    return latest;
  }

  ...
}

Таким образом, в настоящее время он будет извлекать все MainTableState из БД по умолчанию, и если нам нужно активное состояние, мы используем метод for-loop. Очевидно, что это очень плохо для производительности. В настоящее время мы вообще не используем этот список (цель состояла в том, чтобы иметь историю состояний, но это было отложено на будущее), но мы довольно часто используем метод getActiveState(), в основном для отображения строки внутри MainTableState-класс в пользовательском интерфейсе.

Кроме того, даже если бы мы всегда использовали TreeSet и сохраняли бы его отсортированным, поэтому нам не понадобится цикл, а вместо этого нужен только states.iterator().next(), он все равно будет инициализировать список состояний. После тщательного тестирования производительности у нас было более 1 миллиона MainTableState-экземпляров, когда он рухнул с ошибкой java.lang.OutOfMemoryError: GC overhead limit exceeded.

Итак, мы хотим изменить его на следующее:

@Entity
@Table(name = "MAIN_TABLE")
public class MainTable extends AbstractEntity {

  @???
  private MainTableState activeState;

  ...

  public MainTableStates getActiveState(){
    return activeState;
  }

  ...
}

Итак, мой вопрос, что я должен поставить на @???, чтобы добиться этого? Я предполагаю, что мне нужен @Formula или что-то подобное, но как я могу сказать, что для перехода в спящий режим он должен возвращать объект MainTableState? Я видел, как @Formula используется с MAX для даты, но это должно было получить это свойство даты, а не получить весь объект на основе этой максимальной даты.


По предложению @user2447161 я использовал аннотацию @Where, которая действительно помогает уменьшить размер коллекции до 1 (иногда), но у меня есть еще два связанных вопроса:

  1. Как использовать @OnToMany и @Where, но получить один объект вместо списка объектов единичного размера? Это вообще возможно? Здесь в ответе от декабря 2010 г. указано, что это не так. . Это было исправлено где-то за последние шесть лет?

  2. Как быть со случайным псевдонимом в предложении where? Я мог бы сделать что-то вроде этого:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "mainTable") @Where(clause = "CREATED_ON = (SELECT MAX(mts.CREATED_ON) FROM MAIN_TABLE_STATES mts WHERE mts.FK_MAIN_ID = ???.MAIN_ID)") private Установить состояния ; // TODO Получить один объект вместо коллекции размером 1

Проблема в том, что ??? — это случайный псевдоним, сгенерированный hibernate (иногда это this_, иногда что-то вроде mainTable_1_ и т. д.). Как установить этот псевдоним для всего запроса к БД, чтобы использовать его здесь? Я также попробовал вместо этого MAIN_TABLE.MAIN_ID, который не работает, и без псевдонима он также не работает, потому что он использует псевдоним MainTableState вместо псевдонима MainTable (как показано ниже).

from
    MAIN_TABLE this_ 
left outer join
    MAIN_TABLE_STATUSES mainstat2_ 
        on this_.main_id=mainstat2_.fk_main_id 
        and (
            mainstat2_.created_on = (
                SELECT
                    MAX(mts.created_on) 
            FROM
                MAIN_TABLE_STATUSES mts 
            WHERE
-- mainstat2_.main_id should be this_.main_id instead here:
                mts.fk_main_id = mainstat2_.main_id
        )
    )

person Kevin Cruijssen    schedule 03.02.2017    source источник
comment
Если у вас нет безумного количества строк, нет индексов и/или очень строгих требований к производительности, я не вижу, что это будет проблемой производительности. На современных компьютерах цикл for обычно занимает наносекунды, и загрузка набора, а не одной строки, должна быть незначительной. -загрузка (вероятно, на несколько величин больше, чем фактическое зацикливание..)   -  person Tobb    schedule 03.02.2017
comment
@Tobb Я добавил еще одну строку к вопросу. Цикл for не является основной проблемой, это java.lang.OutOfMemoryError: GC overhead limit exceeded с более чем 1 миллионом MainTableState-экземпляров. Это основная причина, по которой мы хотим сохранить только одно активное состояние, а не целые коллекции (которые в настоящее время не используются).   -  person Kevin Cruijssen    schedule 03.02.2017
comment
Возможно, проверьте @where и фильтры... ">stackoverflow.com/questions/12365285/. Или создайте свой собственный DTO из пользовательского запроса...   -  person user2447161    schedule 03.02.2017


Ответы (1)


Ну, что касается вашего вопроса № 2, похоже, вам нужно быстрое решение с минимальным влиянием на ваш существующий код, это может быть приемлемо: вы можете использовать Interceptor для работы с псевдонимом и создания правильного оператора SQL. Сделай это:

  1. используйте уникальную строку в качестве заполнителя псевдонима в предложении @Where, например: ...WHERE mts.FK_MAIN_ID = ${MAIN_TABLE_ALIAS}.MAIN_ID...

  2. если в вашем приложении его еще нет, создайте класс Interceptor, расширяющий EmptyInterceptor, и настройте его как перехватчик SessionFactory.

  3. переопределите метод onPrepareStatement, чтобы заменить заполнитель псевдонимом, найденным после «из MAIN_TABLE», на что-то вроде этого:

    public String onPrepareStatement(String sql) { String modifiedSql = sql; if (sql.contains("${MAIN_TABLE_ALIAS}")) { String mainTableAlias = findMainTableAlias(sql); modifiedSql = sql.replace("${MAIN_TABLE_ALIAS}", mainTableAlias); } return modifiedSql; }

Имейте в виду, что этот метод будет вызываться для каждого оператора sql, который hibernate генерирует в вашем приложении.

Кроме того, ваше предложение @Where работает должным образом только при использовании объединения, поэтому вы должны установить режим выборки, явно добавляя @Fetch(FetchMode.JOIN) к свойству states, чтобы избежать того, что спящий режим может использовать режим выбора.

person jorgegm    schedule 06.02.2017