2012年12月19日水曜日

Hibernate4とSpring Framework3を組み合わせる

流行のネタではないですが、せっかく前回MyBatis/iBATISを取り上げたので、ダントツNo.1のORMであるHibernateも試してみたいと思います。ただ、Hibernate単体の記事は既に多く取り上げられているようなので、今回は意外と情報の少なかった(?)HibernateとSpring Frameworkとの連携に注目してみようと思います。



Hibernate 4.1.8
Spring Framework 3.2 RC2

まずは、上記URLからjarファイル群をダウンロードして、クラスパスに設定します。今回利用したjarファイルは、以下のようです。(不要なものも含まれているかもしれませんが、とりあえず入れたものをすべてリストアップしておきます。また、Webアプリケーションとして利用する場合は、もう少し足す必要があります。)

・spring-core-3.2.0.RC2.jar
・spring-aop-3.2.0.RC2.jar
・spring-aspects-3.2.0.RC2.jar
・spring-beans-3.2.0.RC2.jar
・spring-context-3.2.0.RC2.jar
・spring-expression-3.2.0.RC2.jar
・spring-instrument-3.2.0.RC2.jar
・spring-jdbc-3.2.0.RC2.jar
・spring-tx-3.2.0.RC2.jar
・spring-orm-3.2.0.RC2.jar
・antlr-2.7.7.jar
・dom4j-1.6.1.jar
・hibernate-core-4.1.8.Final.jar
・hibernate-commons-annotations-4.0.1.Final.jar
・hibernate-jpa-2.0-api-1.0.1.Final.jar
・javassist-3.15.0-GA.jar
・jboss-logging-3.1.0.GA.jar
・jboss-transaction-api_1.1_spec-1.0.0.Final.jar
・mysql-connector-java-5.1.22-bin.jar
・commons-logging-1.1.1.jar
・commons-dbcp-1.4.jar
・commons-pool-1.6.jar
・aopalliance.jar
・aspectjweaver.jar

次にSpring Frameworkを使うために、コンポーネントを管理するための設定ファイルであるapplicationContext.xmlを作ります。通常Hibernateではhibernate.cfg.xmlに設定情報を記述しますが、Spring Frameworkと連携して利用する場合は、applicationContext.xmlで必要な情報も加えることもできます。



        
        
        

        
                com.mysql.jdbc.Driver
                URL
                ユーザ名
                パスワード
        

        
                
                        
                                com.test.entities.Blog
                                com.test.entities.Tag
                        
                
                
                        
                            org.hibernate.dialect.MySQLInnoDBDialect
                            true
                        
                
                
        

        
                
        

テーブル
サンプルとして、以下のようなblogテーブルとその参照関係を持ったtagテーブルを用意します。(今回は、MySQLを使っています。)

CREATE TABLE blog (
  id INT AUTO_INCREMENT,
  content VARCHAR(255),
  PRIMARY KEY(id)
) ENGINE=INNODB;

CREATE TABLE tag (
  id INT,
  blog_id INT,
  name VARCHAR(100),
  PRIMARY KEY(id, blog_id),
  FOREIGN KEY (blog_id) REFERENCES blog(id)
) ENGINE=INNODB;


Hibernateのエンティティクラス
上記のテーブルとリンクするBlogとTagクラスを作成します。

@Entity
@Table(name="blog")
public class Blog implements Serializable {

 private static final long serialVersionUID = 1L;

 @Id
 @GenericGenerator(name="bloggen", strategy="increment")
 @GeneratedValue(generator="bloggen")
 @Column(name="id", unique=true, nullable=false)
 private int id;
 
 @Column(name="content")
 private String content;
 
 @OneToMany(fetch=FetchType.EAGER, mappedBy="blog")
 private Set<Tag> tags;
 
 public int getId() {
  return id;
 }
 public void setId(int id) {
  this.id = id;
 }
 public String getContent() {
  return content;
 }
 public void setContent(String content) {
  this.content = content;
 }
 public Set<Tag> getTags() {
  return tags;
 }
 public void setTags(Set<Tag> tags) {
  this.tags = tags;
 }
}
@Entity
@Table(name="tag")
@IdClass(Tag.TagPk.class) // 複合キー
public class Tag {

 @Id
 @Column(name="id")
 private int id;
 
 @Id
 @Column(name="name")
 private String name;

 @ManyToOne(fetch=FetchType.EAGER)
 @JoinColumn(name="blog_id", nullable=false, unique=true)
 private Blog blog;
 
 public int getId() {
  return id;
 }
 public void setId(int id) {
  this.id = id;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public Blog getBlog() {
  return blog;
 }
 public void setBlog(Blog blog) {
  this.blog = blog;
 }
 
 // 複合キーの主キーはこんな感じで書くようです。
 public static class TagPk implements Serializable {

  private static final long serialVersionUID = 1L;
  
  private int id;
  private String name;
  
  public int getId() {
   return this.id;
  }
  public void setId(int id) {
   this.id = id;
  }
  public String getName() {
   return this.name;
  }
  public void setName(String name) {
   this.name = name;
  }
  
  public int hashCode() {
   int hashCode = 0;
   hashCode ^= Integer.valueOf(id).hashCode();
   hashCode ^= name.hashCode();
   return hashCode;
  }
  
       public boolean equals(Object obj) {
           if (!(obj instanceof TagPk))
               return false;
           TagPk target = (TagPk) obj;
           return (target.id == this.id && target.name.equals(this.name));
       }
 }
}

HibernateのエンティティにアクセスするDaoクラス
上記エンティティクラスにCRUDアクセス(Create/Read/Update/Delete)するためのDaoクラスを定義します。(これは独自に作ったので以下のものと違うものを作ってもOKです!)
public abstract class BaseDao<T, PK extends Serializable> {

 @Resource
 protected SessionFactory sessionFactory;
 
 private Class<T> type;
 
 protected Session getSession() {
  Configuration config = new Configuration().configure();
  ServiceRegistry registry = 
    new ServiceRegistryBuilder().applySettings(config.getProperties()).buildServiceRegistry();
  SessionFactory factory = config.buildSessionFactory(registry);
  return factory.openSession();
 }
 
 // basic CRUD APIs
 @SuppressWarnings("unchecked")
 public PK create(T t) {
  return (PK) getSession().save(t);
 }
 
 @SuppressWarnings("unchecked")
 public T read(PK id) {
  return (T) getSession().get(type, id);
 }
 
 public void update(T t) {
  getSession().update(t);
 }
 
 public void delete(T t) {
  getSession().delete(t);
 }

 // additional CRUD APIs
 @SuppressWarnings("unchecked")
 public List<T> find(Criteria c) {
  return (List<T>) c.list();
 }
 
 @SuppressWarnings("unchecked")
 public List<T> findAll() {
  return (List<T>) getSession().createCriteria(type).list();
 }

 // options 
 public Criteria createCriteria() {
  return getSession().createCriteria(type);
 }
 
 // constructor 
 protected BaseDao(Class<T> type) {
  this.type = type;
 }
}

ロジッククラス
上記Daoを組み合わせてロジックを記述するため、ここではサービスクラス(ロジッククラス)を作成します。このサービスクラスの各メソッドでトランザクションを制御するためにSpring Frameworkにて管理します。(ここでやっとSpringの出番です!)例えば、commit/rollbackを記述することなく、メソッド内の処理がすべて正常に実行できれば自動的にコミットされ、もし例外が発生すればロールバックされます。これは、開発者にとって、冗長なコードを書く必要もなく、また書き忘れもなくて便利な機能です。

public interface BlogService {
 public void save(Blog blog);
 public List<Blog> findAll();
}
@Service("blogService")
public class BlogServiceImpl implements BlogService {

 @Resource
 private BlogDao blogDao;
 
 @Resource
 private TagDao tagDao;
 
 public void save(Blog blog) {
  blogDao.create(blog);
  for (Tag tag : blog.getTags()) {
   tag.setBlog(blog);
   tagDao.create(tag);
  }
 }
 
 public List<Blog> findAll() {
  return blogDao.findAll();
 }
}

最後、これらを呼び出すコードです。
  Tag t1 = new Tag();
  t1.setId(1);
  t1.setName("NAME1");

  Tag t2 = new Tag();
  t2.setId(2);
  t2.setName("NAME2");
  
  Blog blog = new Blog();
  blog.setContent("Test102");
  Set<Tag> tags = new HashSet<Tag>();
  tags.add(t1);
  tags.add(t2);
  blog.setTags(tags);
  
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  BlogService service = (BlogServiceImpl) context.getBean("blogService");
  service.save(blog);
  for (Blog b : service.findAll()) {
     System.out.println(b);
  }


はまった点
ここまでたどり着くのにいくつかはまりましたので、メモっておきます。

・TransactionManagerを使ってもcommitされない
どうやってもコミットがされないので、いろいろ調べてみるとHibernateのSessionオブジェクトを生成するのに、SessionFactory.openSession()ではなく、SessionFactory.getCurrentSession()を使うようです。
※ちなみに、HibernateTemplateやHibernateDaoSupportを利用する記事もいくつか見かけましたが、Hibernate4からサポートされなくなったらしいです。

・ただし、getCurrentSessionを使うと以下のエラー
org.springframework.orm.hibernate4.HibernateSystemException : No Session found for current thread
これは以下のようにapplicationContext.xmlを変更して、TransactionManagerをアノテーションベースとし、サービスクラスのメソッド(save/findAll)に@Transactionalを付与することで解決できました。



  



・次なるエラー。。。
java.lang.ClassCastException: $Proxy19 cannot be cast to <クラス名>

これは、以下のようにapplicationContext.xmlのannotation-driven行にproxy-target-class="true"を設定することで解決。

もっと簡単な方法は、以下のように単純にインターフェースで受ければOKでした。
NG
BlogService service = (BlogServiceImpl) context.getBean("blogService");

OK
BlogService service = (BlogService) context.getBean("blogService");


・もう1つ動かない要素がありました。。。
org.hibernate.LazyInitializationException: could not initialize proxy - no Session

これはHibernateのLazy機能を使わないようにすれば解決した。いろいろな記事を見ましたが、Lazyを使うなと・・・。これはいいのか分からないけどとりあえず解決(?)本当はLazyの機能使いたいのですが、今回はとりあえずこの方法で。。。。

NG
@OneToMany(fetch=FetchType.LAZY, mappedBy="blog")

OK
@OneToMany(fetch=FetchType.EAGER, mappedBy="blog")

これだけ、みごとにはまっておけば、これから触る人の役に立つかもしれないですね。。


0 件のコメント:

コメントを投稿