ラベル ORM の投稿を表示しています。 すべての投稿を表示
ラベル ORM の投稿を表示しています。 すべての投稿を表示

2012年12月26日水曜日

Node.jsが面白い件⑧ MySQLの場合

NoSQLが流行っていると言っても、やはり通常業務ではMySQLを使いたいという場合もまだまだ多いと思います。私個人的には新規で作る場合はNoSQLをできるだけ使いたいと考えていますが、以下のようないろいろな事を考えるとRDBMSの選択肢は捨てられません。

  • NoSQL技術者が少ないため設計や構築、運用が難しい
  • NoSQL自体、種類がいろいろあるため選定が難しい
  • NoSQLの製品自体がまだまだ開発途上(機能追加)中である
  • 複雑なデータ処理をプログラムで記述したくない
  • 他のシステムでRDBを使っている

などなど。いろんなシチュエーションがあると思いますが、新しいものも古いものもメリットを活かして良いシステム環境を作るのがエンジニアの腕の見せ所ですね。ちなみに私はNoSQLを採用する事の最大のメリットは「アプリケーションをシンプルに考えられる」ことだと思います。データ構造をシンプルに考えると、ロジックも必要最低限のもので実装されるので余分なことを考えずに済みます。RDBMSが有効な部分もありますが、きちんと使い分けたいですね。

すこし脱線しましたが、それではMySQLを使ったプログラムを見ていこうと思います。Node.jsのMySQLドライバはいろいろありますが有名なものでdb-mysqlnode-mysqlがあります。今回はnode-mysqlを見てみましょう。

> npm install mysql@2.0.0-alpha5

ここでは上記のようにバージョン指定していますが、公式サイトを見て最適なバージョンをインストールしてください。
以下、サンプルプログラムです。

var mysql = require('mysql');

// コネクション作成
var conn = mysql.createConnection({
  host : 'localhost',
  user : 'kat',
  password : 'password',
  database : 'test'
});

// 接続
conn.connect(function(err) {
  // insert data
  conn.query(
   'insert into histories values (?), (?), (?), (?), (?), (?), (?), (?)',
   [['kat', 'astore', 'notebook', 200],
    ['yas', 'bstore', 'chair', 50000],
    ['kat', 'bstore', 'sugar', 100],
    ['kat', 'astore', 'book', 1200],
    ['ken', 'astore', 'book', 1000],
    ['kat', 'bstore', 'orange', 400],
    ['ken', 'bstore', 'apple', 700],
    ['kat', 'astore', 'pen', 200]],
   function(err, result) {
      if (err) {
        console.log('insert error');
        console.log(err);

        conn.destroy();

      } else {
        console.log(result);

        // select *
        conn.query(
          'select * from histories where user_id = ? and store_id = ?',
          ['kat', 'astore'],
          function(err, results) {
            if (err) {
            } else {
              console.log(results);

              // 切断
              conn.destroy();
            }
          }
        );
     }
   }
  );
});

node-mysqlでは現時点でコネクションプールが使えないようです。SQLを見れば明らかですが、データをinsert/selectしています。*.queryメソッドの第一引数にセットしているSQLで?を使って第二引数の値をセットしています。ドキュメントによると、この処理を行う事でescapeメソッドが呼び出され、SQL Injection攻撃の対策をしているようです(詳しくはドキュメントを参照してください)。
ORMが使いたい場合は有名なところでpersistence.jssequelizeがあり、sequelizeではコネクションプールの機能も提供されているようです。それぞれドキュメントがしっかりしているので、ORMに慣れている方は使ってみても良いですね。

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")

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


2012年12月9日日曜日

Node.jsが面白い件⑤ MongoDBで遊ぶ 〜Mongoose編その②:バリデーション〜

Node.jsが面白い件④ MongoDBで遊ぶ 〜Mongoose編その①〜ではMongooseの基本的な使い方を説明しましたが、今回から数回に分けてMongooseの便利な機能について紹介します。

1. Validator

Mongooseではバリデーション機能として次のようなビルトインバリデータを提供します。
  1. required
  2. 最大/最小(数値)
  3. enum/マッチング(文字列)
前回のスキーマに対して上記のビルトインバリデータを試してみます。まずは前回のスキーマの復習ですが、

   var userSchema  = mongoose.Schema({
        uid : { type: String, required:true, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders }
   });

です。これに、以下のドキュメントを作成します。

   User.create(
      { name: {first: 'KAT', last: 'INTINK'},
        age: 100,
        gender: 'male'
      },(以下省略)

uidを指定していないため、requiredにひっかかり以下のようなエラーとなります。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { uid: 
      { message: 'Validator "required" failed for path uid',
        name: 'ValidatorError',
        path: 'uid',
        type: 'required' } } }

次に、ageを-1にしてみます。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last: 'INTINK'},
        age: -1,
        gender: 'male'
      }, (以下省略)

minにひっかかり以下のようなエラーとなります。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { age: 
      { message: 'Validator "min" failed for path age',
        name: 'ValidatorError',
        path: 'age',
        type: 'min' } } }

genderにenum以外の値、manを指定してみます。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last: 'INTINK'},
        age: 100,
        gender: 'man'
      }, (以下省略)

同様に以下のエラーとなります。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { gender: 
      { message: 'Validator "enum" failed for path gender',
        name: 'ValidatorError',
        path: 'gender',
        type: 'enum' } } }

また、カスタムバリデータとして以下のように定義することも可能です。

    var userSchema  = mongoose.Schema({
        uid : { type: String, required:true, index: {unique:true} },
        name: { first:
                    {type: String,
                     validate: [function(fname) {
                                   return fname.length < 5;
                               }, 'MaxNameLength']
                    },
                last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders }
    });

firstnameが5以上だとMaxNameLengthにひっかかります。試しに以下のドキュメントを作成します。

    User.create(
      { uid: 'kat',
        name: {first: 'KATXX', last: 'INTINK'},
        age: 100,
        gender: 'male'
      }, (以下省略)


以下のようにエラーが発生しました。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { 'name.first': 
      { message: 'Validator "MaxNameLength" failed for path name.first',
        name: 'ValidatorError',
        path: 'name.first',
        type: 'MaxNameLength' } } }

バリデーション機能は便利というよりほぼ必須機能になってますが、上記のように簡単に提供された機能を使って実現することができます。

2012年11月13日火曜日

オープンソースORMのMyBatis(旧iBATIS)を使う

オープンソースのORM(Object-Relational Mapping)で、結構日本では利用されているiBATISの後継であるMyBatisについて触ってみました。

MyBatisはiBATISがApacheファンデーションからスピンアウトして新しくフォークされたiBATISの後継フレームワークであり、バージョンで言うとiBATIS3.0という位置づけらしいです。ライセンスはApache Licenseで自由かつ無償にて利用することができます。現在はGoogle Codeでソースコードが管理されているようです。
http://code.google.com/p/mybatis/

MyBatis/iBATISは、JPA(Java Persistence API)ベースのHibernateOracle Toplinkなどと比べ、SQL文を中心に作成していくということもあり、企業システムなどではSpringなどと組み合わせて多用されているようです。(ちなみにSpringは3.0以上をサポートしているらしいです。こちらを参照。Spring2.xであればiBATISを使う必要がありそうですね。)


ちなみに日本では上記のようなトレンドですが、全世界では下図のように圧倒的にHibernateの方が人気があるようです。

では早速試してみたいと思いますが、ここではEclipse4.2とMySQLデータベースをあらかじめインストールされている前提で進めていきたいと思います。ちなみにデータベースとテーブルはシンプルすぎますが、以下のものを利用します。

CREATE DATABASE mydb;

CREATE TABLE blog (
  id INT NOT NULL,
  content VARCHAR(255)
);

MyBatisセットアップ
ここから"Core Framework"をダウンロードして、解凍します。

mybatis-x.x.x.jar(私は3.1.1を利用)とlibフォルダに入っているjarファイルをすべてEclipse上の自分で作成したプロジェクトにコピーしてビルドパスに設定します。さらにMySQLのJDBCドライバーであるConnector/Jをダウンロードして、同様にコピーしてビルドパスに設定します。
次にMyBatis設定ファイルであるmybatis-config.xmlファイルを作成します。このファイルはデータベースの基本的な接続情報と各Mapperファイル(単純に言えばSQL文の書かれているファイル)への外部リンクURLで構成されています。



  
    
      
      
        
        
        
        
      
      
    
  
  
    
  


コーディング
MyBatis設定ファイルで各Mapperファイルはmapperタグに外部ファイルとして指定します。通常は1テーブル(エンティティ)単位で作成することになると思います。そのblogテーブル(エンティティ)用のMapperファイルであるBlogMapper.xmlは以下のようにSQL文を書くことでBlogクラス(エンティティクラス)と紐付けます。


  
  
  
  
      insert into Blog (id, content) values (#{id}, #{content})
  
  
      delete from Blog;
  


このblogテーブル用のBlogクラスは以下のようになります。このクラスは通常のPOJOで書くことができます。
public class Blog {
 private int id;
 private String content;
 
 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 Blog(int id, String content) {
  this.id = id;
  this.content = content;
 }
 
 public Blog() {
  super();
 }
}

以下はこれらBlogクラスとMapperファイルを利用してデータベースアクセスするサンプルで、mybatis-config.xmlとBlogMapper.xmlを読み込み、Blogクラス経由でblogテーブルにアクセスするコードです。


String resource ="mybatis-config.xml";
InputStream is = Resources.getResourceAsStream(resource);
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sessionFactory.openSession();
// delete records
session.delete("deleteAllBlogs");

// insert records
session.insert("insertBlog", new Blog(0, "Test1"));
session.insert("insertBlog", new Blog(1, "Test2"));

// select records
Blog blog = (Blog) session.selectOne("selectBlog", 1); 
System.out.println(blog);  
for (Object v : session.selectList("selectAllBlogs")) {
 System.out.println((Blog) v);
}
session.close();


詳細は、こちらのチュートリアルを参照ください。

次回は、これらのテーブル情報からエンティティクラスやMapperファイル(クラス)を自動的に生成するMyBatis Generatorについて説明したいと思います。