Hibernate для самых маленьких и не только

    Доброго всем времени суток! При написании программы, которая так или иначе будет взаимодействовать с базой данных, пользуются разными средствами. Это и старый добрый jdbc, также применяют: EclipseLink,TopLink, iBatis (уже MyBatis), Spring Framework и конечно же герой нашей статьи — Hibernate. Конечно я здесь перечислил не все средства работы с базой данных, но постарался указать самые распространенные. В данной статье будет показано, как при помощи Hibernate вызывать хранимые процедуры, маппить как таблицы, так и запросы к классам. В качестве подопытной базы данных возьмем Oracle.
    Подготовим все необходимое для экспериментов. Начнем с базы данных. Для начала создадим 3 таблички, над которыми мы и будем практиковаться.
    CREATE TABLE book
    (
        id             NUMBER NOT NULL,
        name           VARCHAR2 (100 BYTE) NOT NULL,
        description    VARCHAR2 (1000 BYTE) NOT NULL,
        CONSTRAINT pk$book_id PRIMARY KEY (id)
    )
    CREATE TABLE student
    (
        id         NUMBER NOT NULL,
        name       VARCHAR2 (100 BYTE) NOT NULL,
        CONSTRAINT pk$student_id PRIMARY KEY (id)
    )
    CREATE TABLE catalog
    (
        id_student   NUMBER NOT NULL,
        id_book      NUMBER NOT NULL
    )
    

    Теперь создадим функцию. которая будет нам возвращать название книги по ее id, пример глупый, но зато будет показывать принцип вызова функции с входным числовым параметром и выходным — строковым.
    CREATE OR REPLACE
    FUNCTION get_book_name_by_id (p_id IN NUMBER)
        RETURN VARCHAR2
    IS
        v_name   VARCHAR2 (100);
    BEGIN
        SELECT name
          INTO v_name
          FROM book
         WHERE id = p_id;
        RETURN v_name;
    EXCEPTION
        WHEN NO_DATA_FOUND
        THEN
            RETURN 'ну нет такой книги!!';
    END;
    

    И также в качестве эксперимента создадим простую процедуру сохранения в БД, у которой будут как входные параметры, так и выходной.
    CREATE OR REPLACE 
    PROCEDURE save_book (p_id      IN OUT NUMBER,
                             p_name    IN     VARCHAR2,
                             p_descr   IN     VARCHAR2)
    IS
    BEGIN
        IF p_id > 0
        THEN
            UPDATE book
               SET name = p_name, description = p_descr
             WHERE id = p_id;
        ELSE
            SELECT catalog_seq.NEXTVAL INTO p_id FROM DUAL;
            INSERT INTO book
            VALUES (p_id, p_name, p_descr);
        END IF;
    END;
    

    Теперь перейдем к классам на Java.
    Отобразим наши 3 таблицы базы данных в классах следующим образом:
    @Entity
    @Table
    public class Student implements Serializable {
    	private static final long serialVersionUID = -5170875020617735653L;
    	@Id  
    	@GeneratedValue(strategy = GenerationType.AUTO, generator = "my_entity_seq_gen")
    	@SequenceGenerator(name = "my_entity_seq_gen", sequenceName = "catalog_seq")
    	private long id;
    	@Column
    	private String name;
    	@OneToMany(mappedBy = "student", fetch = FetchType.LAZY)
    	private Set<Book> bookList;
    		// здесь идет реализация методов  getter, setter, hashCode(), equals(), toString()
    }
    

    Немного пояснений по коду:
    • если у вас название класса не совпадает с названием таблицы, то например пишем так: Table(name = «STUDENT»);
    • одно из требований фреймворка — у каждой таблицы должен быть id, также если название поля id таблицы и название нашей переменной не совпадают, тогда аннотация будет выглядить так: Id @Column(name=«id таблицы в базе»);
    • аннотациями @GeneratedValue и @SequenceGenerator мы определяем стратегию генерации уникального идентификатора, в данном случае мы говорим, что при сохранении информации в базу данных, мы берем число из sequence с названием «catalog_seq»;
    • так как у нас один студент может иметь несколько книг, то отобразим это при помощи аннотации таким образом @OneToMany(mappedBy = «student», fetch = FetchType.LAZY), где mappedBy = «student» — это имя поля в классе Book(смотри ниже), а FetchType.LAZY — говорит нам о том, что коллекцию
      Set<Book> bookList
      мы будем загружать данными только по требованию.

    Маппинг следующих 2 таблиц БД будет выглядеть так:
    @Entity
    @Table
    public class Book implements Serializable {
    	private static final long serialVersionUID = 1L;
    	@Id
    	@Column(name="ID")
    	@GeneratedValue(strategy = GenerationType.AUTO, generator = "my_entity_seq_gen")
    	@SequenceGenerator(name = "my_entity_seq_gen", sequenceName = "catalog_seq")
    	private long id;
    	@Column(name="NAME",unique = true, nullable = false, length = 100)
    	private String name;
    	@Column(name="DESCRIPTION",unique = true, nullable = false, length = 100)
    	private String description;
    	@ManyToOne(fetch = FetchType.LAZY,optional=true)
    	@JoinTable(name = "CATALOG", joinColumns = @JoinColumn(name = "ID_BOOK"), inverseJoinColumns = @JoinColumn(name = "ID_STUDENT"))
    	private Student student;
    	// здесь идет реализация методов  getter, setter, hashCode(), equals(), toString() 
    }
    

    Ограничения для полей можно задавать прямо в аннотации, это делается следующей строкой @Column(name=«NAME»,unique = true, nullable = false, length = 100). Аннотациями @ManyToOne и @JoinTable(name = «CATALOG») мы говорим, что таблица Book и таблица Student имеют связь «многие-ко-многим» через таблицу Catalog, следовательно и все изменения в Book и в Student, автоматом будут применяться и в таблице Catalog.
    Ну вот, мы закончили с маппингом таблиц, и теперь перейдем к непосредственной работе с базой данных.
    Извлечение данных с одновременным заполнением коллекции можно производить разными способами:
    1. При помощи HQL (Hibernate Query Language) запросов
      	List<Book> book = (List<Book>)session.createQuery("from Book order by name").list();
      	
    2. при помощи SQL-запросов
      	List<Book> book = (List<Book>)session.createSQLQuery("select ID, DESCRIPTION, NAME from book order by NAME")
      	.addScalar("id",Hibernate.LONG).addScalar("name").addScalar("description")
      	.setResultTransformer(Transformers.aliasToBean(Book.class)).list();
      	
    3. при помощи Criteria
      	List<Book> book=(List<Book>)session.createCriteria(Book.class).createAlias("student", "st").add(Restrictions.eq("st.name", "Maxim")).list();
      	

    Перейдем к работе с хранимыми процедурами. Для того, чтобы вызвать функцию, которая по id вернет нам название книги, выполним следующие действия:
    String bookName = (String)session.createSQLQuery("{? = call get_book_name_by_id (:id)}").setLong("id",1).uniqueResult();
    
    Допустим у нас на сервере нет таблицы с названием Student, а есть только функция, которая возвращает курсор:
    FUNCTION get_all_students
        RETURN SYS_REFCURSOR
    IS
        l_cur   SYS_REFCURSOR;
    BEGIN
        OPEN l_cur FOR
            SELECT *
              FROM student
           ORDER BY 1;
        RETURN l_cur;
    END;
    
    тогда маппинг будем проходить следующим образом, вместо аннотации @Table(name="STUDENT") напишем
    @NamedNativeQuery(name="getAllStudent",query="{? = call get_all_students}", callable=true, resultClass=Student.class)
    А вызов этой функции будет следующим:
     List<Student> student = (List<Student>) session.getNamedQuery("entity").list();

    Ну, а чтобы вызвать нашу процедуру save_book проделаем следующие манипуляции:
    CallableStatement st = session.connection().prepareCall("{call save_book(?,?,?)}");
    			st.setLong(1,0);
    			st.setString(2, "Золотой ключик, или Приключения Буратино");
    			st.setString(3,"повесть-сказка Алексея Николаевича Толстого");
    			st.registerOutParameter(1, java.sql.Types.NUMERIC);
    			st.execute();
    System.out.println(st.getLong(1));
    
    Как Вы наверно успели заметить, при написании команд для обращения к базе данных и заполнением коллекции, использовалось слово session. В нашем случае это слово указывает на главный интерфейс между нашим Java-приложением и фреймворком Hibernate, то есть org.hibernate.Session session. Но для начала нам необходимо воспользоваться еще одним основополагающим и важным интерфейсом — SessionFactory. SessionFactory — это глобальная фабрика, ответственная за конкретную базу данных. Чтобы получить эту фабрику нам необходимо получить экземпляр класса org.hibernate.cfg.Configuration. Делается это так:
     SessionFactory sessions = new Configuration().configure().buildSessionFactory();
    
    Где Configuration().configure().buildSessionFactory() парсит файлик с названием hibernate.cfg.xml, который находится рядом с вызываемой программой, конечно если не указан путь. Приведем файл конфигурации, в котором показана настройка соединения к базе данных и отображение наших таблиц:
     <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
    <hibernate-configuration>
        <session-factory>
             <property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
            <property name="hibernate.connection.url">jdbc:oracle:thin:@localhost:port:baseName</property>
            <property name="hibernate.connection.username">username</property>
            <property name="hibernate.connection.password">password</property>
            <property name="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</property>
            <property name="show_sql">true</property>
            
            <mapping class="com.sample.javaHibernate.data.Book" />
            <mapping class="com.sample.javaHibernate.data.Student" />        
        </session-factory>
    </hibernate-configuration>
    И теперь, когда мы имеем фабрику, в которой есть вся необходимая конфигурация (коннект к базе через пул, маппинг/отображение таблиц и др.) мы можем работать с Session
    Session session = sessions.openSession();
    Работать с транзакциями можно так: session.beginTransaction(); и соответственно session.getTransaction().commit();
    Вроде бы все. Конечно я не все смог осветить, но думаю для быстрого старта этого хватит.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 26

      +7
      Как вступительная статья ок.
      Пара замечаний:

      1. Хорошим тоном является прописывание имени таблицы всегда (@Table(name =«TABLE_NAME»)) даже в том случае, если имя класса совпадает с именем таблицы.

      2. То, что вы пишете внутри @Column за исключением имени колонки имеет значение, только если вы генерите базу по коду. В рантайме эти аттрибуты не используются.

      3. Не всегда с хибернейтом работают на прямую. Он реализует спецификацию JPA, таким образом программист никаких Session никогда не увидит.
        0
        благодарю Вас за ценные замечания, уверен это также поможет новичкам при быстром старте
          +4
          >> 2. То, что вы пишете внутри @Column за исключением имени колонки имеет значение, только если вы генерите базу по коду. В рантайме эти аттрибуты не используются.

          А как же insertable & updatable?
            +2
            Эти используются, сорри. Пруф
            0
            По пункту 2. Работает not-null в рантайме.
            +2
            Статья неплохая, но я бы добавил несколько абзацев про маппинги, без них hibernate не hibernate.
              0
              вы имеете ввиду маппинг при помощи xml а не аннотаций?
                0
                Именно. Как-то, более эстетично это, что ли.
                  +3
                  Последний раз видел мапиннги в живом проекте где-то 4 года назад и уже тогда это казалось слишком громоздким и неудобным. Хотя, в теории, да, маппинг стоит отделять от самих объектов. Но на практике — в коде с JPA-аннотациями проще ориентироваться, так как всё собрано вместе.
                    0
                    Я это написал исключительно потому, что работаю над вполне живым проектом, в котором очень активно используются маппинги)
                      +2
                      Некоторые люди поддерживают проекты на Delphi, но это не повод его изучать.

                      P.s. У нас тоже еще используется xml маппинг — будь он проклят…
                      0
                      xml — маппинг является единственным способом связать JavaBean с hibernato'm если нету возможности редактировать этот самый бин. Например если бин генерируется по xsd описанию через jaxb
                        0
                        Хороший пример. Согласен, что в этом случае XML-маппинг — наименьшее из зол.
                      +3
                      У вас странные представления об эстетике :-)
                        0
                        Связи конечно страшно прописываются Аннотациями
                  0
                  А есть для Hibernate какой-то аналог FluentNHibernate? Мне всегда больше нравилось прописывать маппинг не в xml, и не в аннотациях (чтобы сохранить классы доменной модели POJO), а с помощью fluent интерфейса.
                    +1
                    Насколько мне известно — нет. Возможно c приходом Java 8 с замыканиями появится такая же удобная реализация.
                    +1
                    Эх, была бы такая статья года два назад, возможно не стали бы писать свою библиотеку ( code.google.com/p/jdbc-proc/ ) для маппинга хранимых процедур на классы.
                      0
                      Ну право не стоит, свои велосипеды тоже очень помогают разобраться в теме, я уверен Вы теперь отлично разбираетесь и в маппинге и в JPA и в создании своих аннотаций. А вообще очень не дурно, даже со спрингом интеграция есть. Молодцы одним словом!
                      0
                      А Maven-прототипа нет для hibernate?
                        0
                        Это точно не «для самых маленьких». Я, как начинающий, ничего не понял из статьи. Какие-то обрывочные наброски. Ни структуры проекта, ни как коннектор настроить, ни куда файлы класть. Из статьи только понятно, что вам что-то понятно. Вот пример хорошего тутора: www.mkyong.com/hibernate/maven-3-hibernate-3-6-oracle-11g-example-xml-mapping/
                          0
                          Вы наверно путаете, если бы я назвал статью «Java для самых маленьких», то конечно бы рассказал про структуру проекта, как создавать и т.п. А так, если кто-то даже структуру создать не может, то наверно про Hibernate ему еще рано читать. Насчет настройки коннекшена. Да, туториалы этого хорошего человека mkyong многим в свое время помогли, за что ему огромное спасибо.
                            0
                            Кстати, теперь появился Hibernate для языка D (HibernateD) dlang.ru/hibernated---orm-dlya-d
                              0
                              Спасибо. Очень полезная статья. Особенно про связб многие-ко-многим.
                                0
                                Спустя 8 лет стыдно читать это все, в ближайшем будущем данная «статья» будет переработана.
                                  0
                                  Нормально! ;) Ну за 8 лет я думаю экспиренса поднакопилось достаточное количество. :)

                              Only users with full accounts can post comments. Log in, please.