1.19.2015

6. Работа с объектами сущности. Hibernate



Объект  сущности может находиться в четырех состояниях:  transient, persistent, detached, removed.  Transient объект создаётся командой new,  может быть заполнен данными но, он  ни когда не сохранялся в сессии hibernate  т.е. не ассоциирован со строкой в таблице БД.  Persistent объект, это объект, в данный момент связанный с некоторой сессией hibernate, сессия "работает" с экземпляром объекта в данный промежуток времени, т.е. подразумевается что объект сущности ассоциирован со строкой в таблице БД.  Получить persistent объект можно двумя способами,  1-й способ запросить объект следующими методами:  get(), load(),  list(),  uniqueResult(), iterate(), scroll(), find(), getReference().   2-й способ перевести наш  transient объект сущности в persistent объект путем вызова таких методов как save(), saveOrUpdate(), persist() или merge().   Detached объект - это persistent объект,  отсоединенный от сессии, это состояние объекта возникает после закрытия сессии close(), которая работала с объектом до этого или при вызовах методов evict(), clear() сессии.  Переход из состояния detached  обратно в состояние persistent объекта сущности возможно при вызове методов сессии  update(), saveOrUpdate() или merge() .  Removed  объект - это persistent объект сущности удаленный в сессии методом  delete() или remove() в jpa, после применения транзакции объект будет удален из таблицы базы данных.  Detached объекты можно использовать дальше, при работе с новой сессией, если к ним применить такие команды как update(),  saveOrUpdate() или merge(),  то они переходят в состояние persistent как упоминалось выше. В Hibernate такие операции называются reattached mode или merging mode в jpa.  Давайте все рассмотрим на примере, взглянем на листинг:

            Person person = new Person();
            person.setName("Vitaly");
            System.out.printf("Person объект находиться в состоянии transient: %s\n", person);
            session = sessionFactory.openSession();
            session.getTransaction().begin();
            session.save(person);
            System.out.printf("Person объект перевели в состоянии persistent: %s\n", person);
            person.setName("Victorovich");
            session.getTransaction().commit();
            session.close();
            System.out.printf("Person объект после закрытия сессии перешел  в состояние detached: %s\n", person);
            person.setName("Lopanov");
            session = sessionFactory.openSession();
            session.getTransaction().begin();
            session.update(person);
            session.flush();
            System.out.printf("Person объект перевели в состоянии persistent , используя операцию reattached mode: %s\n", person);
            session.delete(person);
            session.flush();
            System.out.printf("Person объект перевели в состоянии removed: %s\n", person);
            session.getTransaction().commit();

при этом  hibernate генерирует следующий  sql код:

Hibernate: create table Person (id bigint generated by default as identity, name varchar(255), primary key (id))
Person объект находиться в состоянии transient: Person{id=null, name='Vitaly'}
Hibernate: insert into Person (id, name) values (null, ?)
Person объект перевели в состоянии persistent: Person{id=1, name='Vitaly'}
Hibernate: update Person set name=? where id=?
Person объект после закрытия сессии перешел  в состояние detached: Person{id=1, name='Victorovich'}
Hibernate: update Person set name=? where id=?
Person объект перевели в состоянии persistent , используя операцию reattached mode: Person{id=1, name='Lopanov'}
Hibernate: delete from Person where id=?
Person объект перевели в состоянии removed: Person{id=1, name='Lopanov'}

Разобравшись с состояниями, в котором может находиться наш объект, теперь настало время поговорить,  о командах load() и get() объекта session, которые по ключу возвращают сущность. Различие между  load() и get() состоит в том,  что оба метода предназначены для получения объекта из базы данных, отличие в том что если метод  get() не находит запись в таблице БД,  то возвращает  null,  метод  load() может вернуть прокси объект (если разрешена реализация lazy объектов - по умолчанию с версии Hibernate 3 работает ленивая загрузка), вместо реального объекта.  Заполнение реальными данными proxy-объекта происходит, только после вызова любого метода прокси объекта исключение составляет запрос первичного ключа (getId()) ибо по первичному ключу, обычно идентифицируется любой объект  и он присутствует всегда будь то реальный объект или proxy . Hibernate выполняет sql запрос только тогда, когда нужны реальные данные, если существует такая запись в таблице БД, то она  подгружается и заполняет  поля proxy-объекта значениями. Если запись не найдена в таблице БД, то происходит Exception,   org.hibernate.ObjectNotFoundException.class.  Смотрите комментарии в  листинге:

@Test()
    public void loadProxyPersons(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //в бд нет записи с первичным ключом 10L load()  возвращает proxy  объект
        Person person2 = (Person) session.load(Person.class, 10L);
        //проверяем существование proxy  объекта
        assertNotNull(person2);
        session.getTransaction().commit();
        session.close();
    }

    @Test(expected = org.hibernate.ObjectNotFoundException.class)
    public void loadProxyPersonsException(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //в бд нет записи 10L load()  возвращает proxy  объект
        Person person2 = (Person) session.load(Person.class, 10L);
        /*пытаемся заполнить проси объект реальными  данными из таблицы БД, вызываем исключение  org.hibernate.ObjectNotFoundException.class */
        person2.getFirstName();
        session.getTransaction().commit();
        session.close();
    }

    @Test()
    public void getNullPerson(){
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        //запращиваем объект
        Person person2 = (Person) session.get(Person.class, 10L);
        //проверяем что get возвращает null
        assertNull(person2);
        session.getTransaction().commit();
        session.close();
    }

Если рассматривать jpa, то в нем есть похожие методы, find() очень похож по своему поведению на метод get(), а метод getReference() подобен методу load() hibernate. Внимательный  читатель заметил наверно, что в предыдущем  коде встречалась  такая строчка session.flush();  зачем она нужна и почему, давайте это выясним.  По умолчанию Hibernate, всегда использует кэш первого уровня в транзакции, его не возможно отключить.  Т.е когда мы производим операции над persistent объектом изменения не сразу попадают в БД, это нужно  для уменьшения количества выполняемых sql запросов к базе данных. Допустим, объект модифицируется в одной сессии несколько раз, все изменения происходят в памяти (aka кеше первого уровня), в итоге при завершении транзакции, генерируется  один sql запрос update,  который скидывает только последние изменения по последнему состоянию, все предыдущие состояния объекта не учитываются и не сохраняются в БД. Если мы вызываем команду  .flush(), то hibernate принудительно скидывает все изменения из кэша  в базу данных. Рассмотрим пример, обратите внимание на комментарии в листинге:

public pers2() {
        try {
            sessionFactory = new Configuration().configure().buildSessionFactory();
            Statistics statistics = sessionFactory.getStatistics();
            Session session = null;
            try {
                //создаем объект сущности
                Person person = new Person();
                person.setName("Vitaly");
                System.out.printf("transient object Person: %s\n", person);
                session = sessionFactory.openSession();
                session.getTransaction().begin();
                session.save(person);
                Long id = person.getId();
                session.getTransaction().commit();
                session.close();
                //включаем статистику для сбора информации.
                statistics.setStatisticsEnabled(true);
                session = sessionFactory.openSession();
                session.getTransaction().begin();
                //загружаем объект из БД
                person = (Person) session.get(Person.class, id);
                //проверяем, сохранен ли наш объект в кеше  -  true
                System.out.println("contains in cache person: " + session.contains(person));
                //меняем наш объект в кеше
                person.setName("Tom");
                //если применить команду session.flush(); то изменения из кеша пойдут в базу данных!
                //.... выполняем еще какие-то операции
                //запрашиваем заново наш объект, sql не выполняется, объект подсовывается из кеша
                Person person1 = (Person) session.get(Person.class, id);
                //проверяем находиться ли наш объект в кеше -  true
                System.out.println("contains in cache person1: " + session.contains(person1));
                //выгружаем объект из кеша  т.е. имя Tom затирается в кеше
                //и он (имя Tom) ни когда уже не попадет в таблицу базы данных!
                if (session.contains(person1)) {session.evict(person1); }
                //проверяем находиться ли наш объект в кеше -  false
                System.out.println("contains in cache person1: " + session.contains(person1));
                //объект не найден в кеше, выполняется sql запрос для загрузки данных
                Person person2 = (Person) session.get(Person.class, id);
                // проверяем находиться ли наш объект в кеше -  true
                System.out.println("contains in cache person2: " + session.contains(person2));
                System.out.printf("object Person: %s\n", person);
                System.out.printf("object Person2: %s\n", person2);
                //перед session.getTransaction().commit(); неявно выполняется команда session.flush();
                session.getTransaction().commit();
            } finally {
                if (session != null) session.close();
            }
            System.out.println("InsertCount: " + statistics.getEntityInsertCount() + " " +
                    "UpdateCount: " + statistics.getEntityUpdateCount() + " " +
                    "FlushCount: " + statistics.getFlushCount() + " " +
                    "TransactionCount: " + statistics.getTransactionCount() + " " +
                    "SuccessfulTransactionCount: " + statistics.getSuccessfulTransactionCount());
        } finally {
            if (sessionFactory != null) sessionFactory.close();
        }
    }

этот пример даёт  интересный лог,  обратите внимание,  что мы меняли состояние объекта только один раз ( person.setName("Tom");  ),  а sql - update  не выполнился ни разу, это связано с тем что мы удалили из кэша первого уровня все изменения по нашей сущности методом  .evict(person1)  и кэш откатился к первоначальному своему состоянию в котором прибывал до изменений,  смотрим на лог вывода:

transient object Person: Person{id=null, name='Vitaly'}
Hibernate: insert into Person (id, name) values (null, ?)
Hibernate: select person0_.id as id0_0_, person0_.name as name0_0_ from Person person0_ where person0_.id=?
contains in cache person: true
contains in cache person1: true
contains in cache person1: false
Hibernate: select person0_.id as id0_0_, person0_.name as name0_0_ from Person person0_ where person0_.id=?
contains in cache person2: true
object Person: Person{id=1, name='Tom'}
object Person2: Person{id=1, name='Vitaly'}
InsertCount: 0 UpdateCount: 0 FlushCount: 1 TransactionCount: 1 SuccessfulTransactionCount: 1

Помните всегда что, при использовании методов save(), update(), saveOrUpdate(), load(), get(), list(), iterate(), scroll() всегда будет задействован кэш первого уровня,  для того что бы немедленно сохранить объект в БД, нужно выполнить команду session.flush().  А для непосредственной загрузки объекта из таблицы БД sql-м, методами load(), get(), list(), iterate(), scroll(), а не из кэша первого уровня, нужно вызвать команду session.evict(person)  удаления объекта из кэша. Так же существует команда, которая очищает полностью кэш первого уровня session.clear() .  И так подведем итог по команде session.flush(),  все изменения,  которые происходят в persistent объектах фиксируются в кэше первого уровня, которые потом нужно скинуть в таблицу БД посредством команды flush(), что бы изменения появились и в базе данных.  Генерация sql кода и скидывание данных из кэша по объекту,  происходит всегда, когда у транзакции вызывается метод commit ()  и когда явно вызывается команда  flush(),  иногда происходит перед выполнением запроса данных,  когда данные могут повлиять на результаты запроса данных непосредственно, других сущностей.  Изменить режим  работы кэша сессии hibernate, чтобы данные по чаще или по реже скидывались в таблицу БД  можно,  для этого нужно поменять flush  стратегию сессии командой session.setFlushMode().    Эта команда принимает  параметр типа перечисления FlushMode:

FlushMode.NEVER

Ни когда, не происходит автоматического  скидывания измененных данных из кэша в таблицу БД, нужно вызывать непосредственно команду flush для синхронизации кэша. Этот параметр объявлен как Deprecated, рекомендуют использовать  FlushMode.MANUAL

FlushMode.MANUAL

Синхронизация кэша и таблицы БД происходит при выполнении команды session.flush(), рекомендуют использовать при read only транзакциях

FlushMode.COMMIT

Изменения из кэша в таблицу попадают при коммите транзакции, session.getTransaction().commit()

FlushMode.AUTO

Этот режим установлен по умолчанию.  Повторюсь, синхронизация кэша с БД происходит всегда в двух случаях, когда у транзакции вызывается метод commit ()  и когда явно вызывается команда  flush() и иногда происходит перед выполнением запроса данных,  когда данные могут повлиять на результаты  другого  запроса непосредственно
FlushMode.ALWAYS
Скидывание кэша в БД  происходит при каждом считывании/запросе данных.

Разработчики hibernate  рекомендуют не менять  flush стратегию, без острой необходимости и призывают использовать стратегию по умолчанию.  Еще осталось упомянуть одну мелочь. При удалении объекта иногда, не очень часто бывает необходимо вернуть автоматически прежний идентификатор-ключ объекту (null), для того что бы пользоваться им, объектом  дальше в коде. Для этого есть параметр:

<property name="hibernate.use_identifier_rollback">true</property>

если он установлен в true то, после удаления и операции flush, появляется "чистый" transient объект, с которым можно работать, то есть  полю помеченному аннотацией @Id будет присвоено значение null.

            Session session = sessionFactory.openSession();
            session.beginTransaction();
            //запращиваем объект
            Person person1 = (Person) session.get(Person.class, id);
            //удаляем объект
            session.delete(person 1);
            //заносим наши изменения в БД
            session.getTransaction().commit();
            session.close();
            System.out.printf("person1.getId() = %d\n", person 1.getId());

...
Hibernate: delete from Person where Id=?
person1.getId() = null
...

если  hibernate.use_identifier_rollback  равен false, а  по умолчанию параметр установлен в false у hibernate, то первичный ключ не очиститься, и у него останется прежнее его значение:

...
Hibernate: delete from Person where Id=?
phone1.getId() = 1
...

Комментариев нет:

Отправить комментарий