Java, Scala, .NET, Lisp, Python, IDE's, Hibernate, MATLAB, Mathematica, Physics & Other

Показаны сообщения с ярлыком Hibernate. Показать все сообщения
Показаны сообщения с ярлыком Hibernate. Показать все сообщения

воскресенье, 11 апреля 2010 г.

Как получить rownum в hql

Как получить rownum в hql

Предположим у вас есть серверное java приложение использующее hibernate с Oracle в качестве персистенс провайдера.

rownum это псевдоколонка в базе данных Oracle в которой храниться порядковый номер строки результата какого-либо select запроса. Бывает очень полезно получить например номер записи удовлетворяющей таким-то условиям, или взять из результирующего dataset-а строки в диапазоне от n до m. В общем, в ряде случаев rownum может очень пригодится.

Но вот беда, Hibernate Query Language (hql) не поддерживает rownum! Хуже того, нет эквивалента этой полезной фиче. Есть, конечно, возможность взять записи в диапазоне с помощью методов setFirstResult, setMaxResults в hibernate-овской Query, но это только половина фичи, а если хочется именно получить номер записи?

Вынужден вас огорчить - это невозмжно.
Но.. Как вы знаете, если очень хочется, и очень нужно, то нет ничего невозможного!
Ломать так ломать..

Умные дядьки на форумах советуют использовать native query которую можно сделать в том же EntityManager-е - нам это не подходит т.к. хотелось бы писать на hql-е а не на sql. В случае простых запросов можно, конечно, не поленится использовать sql, но в большом проекте, где hql запрос может создаваться динамически на основе десятков условий и опций, сгенерить подобный sql запрос может оказаться чересчур сложной задачей.

То что не можем сделать мы, отлично делает хибернет! Транслировать hql в sql - это его каждодневная задача. Итак, общая идея такова: берем наш суперсложный hql запрос, средствами хибернета перегоняем его в sql и результат встраиваем в виде подзапроса в довольно тривиальный sql запрос, который в конечном итоге и вытягивает rownum.

А теперь по пунктам:
  1. Преобразование hql в sql. Способ может не совсем легальный, но этот код работает - проверено. Несколько советов:

    • SessionFactory можно вытянуть из EntityManager-а: ((Session)entityManager.getDelegate()).getSessionFactory()
    • После преобразования вы можете с удивлением заметить, что все именованные параметры в запросе стали позиционными (т.е. в теле запроса появились вопросики .. ? .. ?.. вместо ваших .. :myParam1 .. :myParam2 ..). Возможно есть способ как-то допилять приведенный по ссылке код, запретить ему учинять это безобразие - я так и не разобрался. Вместо этого можно написать некий вспомогательный код который перед преобразованием "запомнит" расположение параметров и потом при формировании запроса выставит нужные значения параметров в правильных позициях.

  2. Собственно оберточный sql запрос вытягивающий rownum. Предположим что sql запрос полученный на предыдущем шаге хранится в стринге identifiersCriteriaNativeSql. Предположим также, что результатом предыдущего запроса является одна колонка - уникальные идентификаторы некой вашей сущности. Задача: получить rownum записи с заданным идентификатором.

    Результирующий sql запрос будет формироваться так:

    String firstColumnName = "col_0_0_"
    String subQuery = "select rownum as rn, " + firstColumnName + " from (" + identifiersCriteriaNativeSql + ")";
    String query = "select rn from (" + subQuery + ")" + " where " + firstColumnName + " = :identifierParameter";


    • Вы наверно обратили внимание на магическое "col_0_0_" - так хибернет обозвал первую колонку в select части identifiersCriteriaNativeSql, после преобразования из hql в sql - и это второе разочарование - все ваши алиасы были безвозвратно утеряны. Т.е. col_0_0_ указывает на id вашей сущности.
    • Второй непонятный момент может быть вызван двойной вложенностью запросов. Почему не просто

      String query = "select rownum from (" + identifiersCriteriaNativeSql + ")" + " where " + firstColumnName + " = :identifierParameter";

      А потому что в этом случае rownum будет всегда 1 - т.к. инициализация этой колонки происходит после наложение условий в where. Поэтому нужен еще один подзапрос subQuery без всяких условий.

Ура, работает!!!!......

понедельник, 24 августа 2009 г.

Коварный Hibernate или проблемма @OneToOne(targetEntity = SomeAbstractEntityClass, fetch = FetchType.LAZY)

Как известно хибернет умеет загружать сущности по асоциации OneToOne ленивым образом.
Для этого нужно указать в аннотации @OneToOne что fetch = FetchType.LAZY. Что это дает?
А то что очень часто когда мы достаем некоторую сущность из базы, нам не нужны сущности прилинкованные к ней. Тогда и вытаскивать их из базы нет смысла. Вот как раз FetchType.LAZY дает то, что прилинкованная по OneToOne сущность подгружается только тогда когда мы начинаем обращаться к полям этой сущности. Полезная штука.. НО! Совсем недавно натолкнулся на совершенно невероятную на первый взгляд вещь..

Покажу на примере.
Пусть у нас имеются такие сущности:

AbstractWeapon implements IWeapon - абстрактное оружие.
Sword extends AbstractWeapon implements ISword - меч.
Bow extends AbstractWeapon implements IBow - лук.
Интерфейсы ISward и IBow наследуются от IWeapon.

Warrior - воин. Который владеет неким "абстрактным" оружием:
@OneToOne(targetEntity = AbstractWeapon.class, fetch = FetchType.LAZY)
@JoinColumn(name = "wr_wp_aa")
public IWeapon getWeapon()
.....

И предположим что у воина есть метод, который по типу оружия определяет к какому классу отнести воина.
Если это меч то - мечник, если лук то соответственно - лучник.

   public WarriorType getType()
{
WarriorType type;

if (weapon instanceof ISword)
{
type = WarriorType.SWORDSMAN;
}
else if (weapon instanceof IBow)
{
type = WarriorType.ARCHER;
}
else
{
throw new IllegalStateException("incorrect type of weapon: " + weapon);
}

return type;
}
_Winnie Colorizer


Теперь сохраняем в базу данных одно оружие - меч, и одного воина - который владеет этоим мечем. А теперь внимание вопрос: что будет если сейчас вытащить этого воина средствами хибернета, и попробовать узнать его тип, вызвав метод getType?

Вы было подумали что метод вернет WarriorType.SWORDSMAN? Увы, вынужден вас огорчить, но вы жестоко ошиблись.. Метод кинет исключение IllegalStateException.

Начинаем дебажить и выясняем всю правду:
weapon на самом деле не Sword, а обьект некого таинственного типа AbstractWeapon_$_javassist_1. Смотрим внимательнее, и видим что дерево наследования у AbstractWeapon_$_javassist_1 имеет примерно такой вид:

com.greentea.relaxation.serverplays.shp.business.weapons.AbstractWeapon_$$_javassist_1
extends
com.greentea.relaxation.serverplays.shp.business.weapons.AbstractWeapon
extends
com.greentea.relaxation.serverplays.shp.business.AbstractEntity
implements
com.greentea.relaxation.serverplays.shp.business.IEntity
implements
com.greentea.relaxation.serverplays.shp.business.weapons.IWeapon
extends
com.greentea.relaxation.serverplays.shp.business.IEntity
implements
org.hibernate.proxy.HibernateProxy
extends
java.io.Serializable
javassist.util.proxy.ProxyObject

Нигде! Как вы видите - нигде, нету в этом дереве типа Sword. Итак, выясняется что weapon это прокси
(реализует интерфейсы org.hibernate.proxy.HibernateProxy и javassist.util.proxy.ProxyObject) для
класса AbstractWeapon. И никаким образом в рантайме честно узнать подтип по instanceof или привести к одному из типов производных от AbstractWeapon не получится..

Существуют конечно обходные пути, например:
- Метод Hibernate.getClass - вернет тип Sword.
- Сам пример немножко надуманный, и можно было бы реализовать getType по другому.
- Также можно вообще радикально решить проблемму отказавшись от LAZY.

Мне кажется что Hibernate делает прокси не совсем правильно. По логике, он даже в случае с LAZY мог бы вначале вытащить DiscriminatorValue из таблицы, определить тип, и сделать свое прокси уже от реального производного типа, т.е. от Sword, а не от абстрактного AbstractWeapon. Но вероятно разработчики Hibernate посчитали что если LAZY то LAZY до конца, и один дополнительный запрос - это будет слишком расточительно. Иначе как это еще можно обьяснить.

Постоянные читатели