2012年12月29日土曜日

Node.jsが面白い件⑨ ログ

Javaの場合にログと言えばlog4jが有名ですが、Node.jsで使えるlog4js-nodeが公開されています。ログローテーションやコンソールでのカラー表示などを提供し、簡単な操作でログの書き込みができます。プログラムを見て行きましょう。まずはインストール。

> npm install log4js

log4jsのexample.jsを少し変えたものが以下です。

var log4js = require('log4js');

// appenderによる出力指定。log4js.jsonファイルを読み込む。
log4js.configure('log4js.json', {});

// appenderのうち、file_logを指定。
var logger = log4js.getLogger('file_log');
logger.setLevel('TRACE');

logger.trace('これはトレース');
logger.debug('これはデバグ');
logger.info('これはインフォメーション');
logger.warn('これは警告');
logger.error('これはエラー');
logger.fatal('これは致命的エラー');

これを実行すると、以下の結果が表示されます。

[2012-12-28 23:31:15.100] [TRACE] file_log - これはトレース
[2012-12-28 23:31:15.107] [DEBUG] file_log - これはデバグ
[2012-12-28 23:31:15.108] [INFO] file_log - これはインフォメーション
[2012-12-28 23:31:15.108] [WARN] file_log - これは警告
[2012-12-28 23:31:15.108] [ERROR] file_log - これはエラー
[2012-12-28 23:31:15.108] [FATAL] file_log - これは致命的エラー

本当はカラー表示されるので、実際に出力してみてください。上記の例では、最初にlog4js.jsonという別ファイルを読み込んでいます。直接このファイルに書き込む事もできますが、例のように別ファイルで記述しているとデフォルトで60秒に1度更新チェックがかかり、更新されているとリロードしてくれるので便利です(更新チェックの間隔は変更できます)。以下が今回使ったlog4js.jsonの内容です。

{
  "appenders" : [
    { "type" : "console" },
    { "type" : "file", "filename" : "file_log.log", "category" : "file_log" },
    { "type" : "dateFile", "filename" : "date_log.log", "pattern" : "-yyyy-MM-dd-hh-mm", "category" : "date_log" }
  ]

例の7行目でfile_logを呼んでいますが、appendersで2つ目に設定しているfileログです。上に示した結果はコンソールの結果ですが、file_log.logにも同様の結果が追記されています。
また、file_logではなくdate_logを指定すると、date_log.logに結果が追記されます。file_logとdate_logの違いは、appenderのタイプの違いですが、date_logは指定したpatternに基づいてログローテーションを実施してくれます。例えば上記のパターンでは分単位でログローテーションを実施してくれます(新たな書き込みがあった時に分が進んでいると新しいログファイルに書き込みます)。
最後に、setLevel()で出力するログレベルを設定できるのでデバグ作業などに便利です。

他にもいくつかappendersが作成されていますが、Multiprocessは試してみても良いかもしれません。

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月21日金曜日

iOS デバイスへのプッシュ通知と node.js (デバイス編)

前回の続きです。

デバイストークン
デバイスの識別や保証に使用されるトークンです。
トークンは、デバイスから iOS への登録要求を通じて取得します。
プロバイダーが APNs に接続する際にこの値を APNs 側に渡す必要があります。したがって、デバイスはトークン取得後はそれをプロバイダーと共有しなければなりません。

具体的な実装の流れは以下のようになります。

1. リモート通知のための登録要求


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{    
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert)];
    ...

APNs を通じて、指定したタイプの通知を受け取るために登録します。登録に成功した場合は後述の  application:didRegisterForRemoteNotificationsWithDeviceToken: が呼ばれます。
(逆に失敗した場合は application:didFailToRegisterForRemoteNotificationsWithError: が呼ばれるので、こちらで原因について調べられます。)


2. application:didRegisterForRemoteNotificationsWithDeviceToken:  の実装


- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
     NSLog(@"token:%@", deviceToken);
}

この段階でデバイストークンが取得されます。(NSData の deviceToken)
この値をプロバイダー側に送ることになります。ただ、今回のように動作を確かめるだけなら、プロバイダーのソースコード(ここでは node アプリの以下の token の部分ですね)にコピペしてしまってももちろん動きます。

    var myDevice = new apns.Device(token);



3. application:didReceiveRemoteNotification: の実装



- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    NSDictionary *apsInfo = [userInfo objectForKey:@"aps"];
    NSLog(@"alert:%@", [apsInfo objectForKey:@"alert"]);
}


リモート通知を受け取った際に呼ばれるデリゲートメソッドを実装します。通知の内容は NSDictionary に格納されていますので、キーで取得します。

以上が、通知を受け取るための最低限の事前実装になります。後は、node 側から通知を送信すれば、デバイス側で受信されるはずです。



 node 側の通知 の実装

通知の実施は以下のような簡単なコードで実行できます。
var myDevice = new apns.Device(token);
var note = new apns.Notification();
note.badge = 1;
note.alert = "From APNs!";
note.device = myDevice;
apnsConnection.sendNotification(note);


トラブルシューティング

プッシュ通知の実装がうまくいかない場合についての情報をまとめた、
トラブルシューティングのためのページが公開されています。
いくつかのケースについてまとめられていますので、うまくつながらない場合は参照してみてください。結構役に立つと思います。

Troubleshooting Push Notifications
http://developer.apple.com/library/ios/#technotes/tn2265/_index.html

なお、iOS 上でプッシュ通知関連のログをオンにするPersistentConnectionLogging.mobileconfig はページ右上の Companion File からダウンロードできます。(ただ残念ながら iOS6 では動かないっぽいです)

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月18日火曜日

Node.jsが面白い件⑦ MongoDBで遊ぶ 〜Mongoose編その④:Middleware〜

今回はMongooseの便利な機能の1つ、middlewareを使ってみます。middlewareにはpreとpostがあり、initやsave、validate、removeの前後の処理をSchemaレベルで定義することができます。

4. pre

まずは前処理のpreから記述してみます。今まで使ってきたuserSchemaを使いますが、わかりやすくするためにcreatedキーを付けます。

    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
        uid : { type: String, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders },
        created : Date
    });

    // preの設定
    userSchema.pre('save', function(next) {
      this.created = new Date;
      next();
    });

この例では、preを使ってsaveされる直前に現在時刻をcreatedに設定する処理を記述しています。それではsaveしてみます。

    // インスタンス(document)作成。
    var user = new User();
    user.uid = 'kat';
    user.name.first = 'KAT';
    user.name.last = 'INTINK';
    user.age = 100;
    user.gender = 'male';

    user.save(function(err) {(以下、省略)

以下、結果です。

[ { created: Tue Dec 18 2012 22:45:22 GMT+0900 (JST),
    gender: 'male',
    age: 100,
    uid: 'kat',
    _id: 50d07372ff75973d29000001,
    __v: 0,
    name: { last: 'INTINK', first: 'KAT' } } ]

createdは事前に設定していませんが、きちんと入っていることがわかります。preで定義した関数の中でnext()が呼ばれていますが、next()が呼ばれることで次の処理が実行されます。複数のpre定義があった場合、next()が呼ばれるたびに次の処理が実行されることになります。

上の例では順次実行でしたが、処理によっては複数の処理を並列で処理したい場合もあります。この場合は以下のようにnext()/done()を使います。

    var checkStr = "a";

    // preの設定
    // 処理①
    userSchema.pre('save', true, function(next, done) {
      console.log(new Date);
      next();    // ->処理②の開始。
      setTimeout(function() {
        checkStr += 'b';
        done();  // ->処理①の終了。
      }, 1000);
    });

    // 処理②
    userSchema.pre('save', true, function(next, done) {
      console.log(new Date);
      next();    // ->処理③の開始。
      setTimeout(function() {
        checkStr += 'c';
        done();  // ->処理②の終了。
      }, 2000);
    });

    // 処理③
    userSchema.pre('save', true, function(next, done) {
      console.log(new Date);
      next();    // ->終了次第、実際の処理(save)
      setTimeout(function() {
        checkStr += 'd';
        done();  // ->処理③の終了。
      }, 3000);
    });

実際の処理ではこのような書き方をしないと思いますが、動作がわかりやすいように各preの定義でsetTimeoutによって処理を実行しています。各関数の中にはnext()とdone()がそれぞれ記述されているのがわかります。next()が呼ばれると前述のように次の処理が実行されますが、それぞれの処理は終了時にdone()を実行しています。この場合、全てのdone()が実行されないとsave処理を実施しませんが、それぞれは並列で実行されるため効率よく処理が終了できます。また、並列で実行する際にはpreの第二引数にtrueの指定を忘れないようにしましょう。念のため、結果は以下のようになります。

Tue Dec 18 2012 22:58:15 GMT+0900 (JST) // -> 処理①開始
Tue Dec 18 2012 22:58:15 GMT+0900 (JST) // -> 処理②開始
Tue Dec 18 2012 22:58:15 GMT+0900 (JST) // -> 処理③開始
Tue Dec 18 2012 22:58:18 GMT+0900 (JST) // -> save開始
checkStr : abcd

上手く使えばいろいろと効率よく処理ができそうですね。ドキュメントには複雑なバリデーションや依存関係のあるドキュメントの削除などが例として記載されています。

4. post

事前処理があれば事後処理も、ということでpostが用意されています。postはよりシンプルで、以下のように定義できます。

    // postの設定
    userSchema.post('save', function(doc) {
      console.log('User: %s was created.', doc.uid);
    });

実行結果は以下のようになります。

User: kat was created.

pre/postを使えばRDBMSで言うトリガーのような処理が実現できます。あまり複雑な処理は重くなるため好ましくないですが、ODMの機能を効果的に使うことでそこそこの実装コスト削減が期待できそうですね。

2012年12月12日水曜日

Node.jsが面白い件⑥ MongoDBで遊ぶ 〜Mongoose編その③:サブドキュメントとリファレンス〜

前回に引き続き、今回はサブドキュメントとリファレンスについてです。MongoDBではEmbedding and Linkingとして解説されており、Joinを持たないMongoDBの設計手法の1つとしてよく使われます。

2. sub-docs

MongooseではSchemaを定義してドキュメントを操作しますが、別のドキュメント配列をSchemaの一部として保持することができます。以下、これまで使用していたuserSchemaにhistSchemaというドキュメントを持たせました。

    // SubDocument Schema
    var operations = 'create,submit,cancel'.split(',');
    var histSchema = mongoose.Schema({
        operation : { type: String, enum: operations },
        date: Date
    });

    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
        uid : { type: String, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders },
        histories : [ histSchema ]
    });

    var User = db.model('User', userSchema);

userドキュメント作成の際に、historiesの値を入れます。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last:'INTINK'},
        age: 100,
        gender: 'male',
        histories: [{ operation : 'create', date: new Date }]
      }, (以下省略)

値を表示すると以下のように_idが自動生成され、ドキュメントとして保持されていることがわかります。
{ uid: 'kat',
  age: 100,
  gender: 'male',
  _id: 50c5fa1329c6e5d70c000002,
  __v: 0,
  histories: 
   [ { operation: 'create',
       date: Tue Dec 11 2012 00:04:51 GMT+0900 (JST),
       _id: 50c5fa1329c6e5d70c000003 } ],
  name: { first: 'KAT', last: 'INTINK' } }

さらにドキュメントを追加します。

user.histories.push({ operation: 'submit', date: new Date });
user.save (function (err) { (以下省略)

{ uid: 'kat',
  age: 100,
  gender: 'male',
  _id: 50c5fae2afa091e00c000002,
  __v: 1,
  histories: 
   [ { operation: 'create',
       date: Tue Dec 11 2012 00:04:51 GMT+0900 (JST),
       _id: 50c5fa1329c6e5d70c000003 },
     { operation: 'submit',
       date: Tue Dec 11 2012 00:08:18 GMT+0900 (JST),
       _id: 50c5fae2afa091e00c000004 } ],
  name: { first: 'KAT', last: 'INTINK' } }

内部に保持されるドキュメントなので、あるオブジェクトが複数の固有のドキュメントを持つような場合に使えます(例だとユーザの操作ログを保持しています)。外部のドキュメントを参照したい場合は、次に説明するリファレンスを使います。

3. population

以下のようなSchemaを考えます。

    // itemSchema
    var itemSchema = new mongoose.Schema({
      name : String,
      color : String
    });

    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
      uid : { type: String, index: {unique:true} },
      name: { first: String, last: String },
      age : { type: Number, min: 1 },
      gender : { type: String, enum: genders },
      histories : [ histSchema ],
      items : [ { type: mongoose.Schema.Types.ObjectId, ref: 'Item' } ]
    });

    var Item = db.model('Item', itemSchema);
    var User = db.model('User', userSchema);

sub-docsで使用したuserSchemaに、itemsを持たせました。ただし、itemsはitemSchemaとして定義されているモデルItemを参照しています。それではドキュメントを作ってみましょう。まずはitemから。

    Item.create(
      { name : 'table', color : 'black' },
      function(err, item1) {
        if (err) {
          console.log('[error] create item');
          console.log(err);
        } else {
          Item.create(
            { name: 'chair', color: 'white' },
            function(err, item2) { (以下省略)

上記のようにitemを2つ作った後、Userにこれら2つのドキュメントをセットします。Userを作ったあとsub-docsと同様にpushします。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last:'INTINK'},
        age: 100,
        gender: 'male',
        histories: [{ operation : 'create', date: new Date }],
      }, function(err, user) { 

(省略)

                  user.items.push(item1);
                  user.items.push(item2);

                  user.save (function(err) {


(以下省略)

結果を見てみます。まずはUserを見ると、以下のように格納されています。

User.findOne().exec(

{ uid: 'kat',
  age: 100,
  gender: 'male',
  _id: 50c886e21f32c23d18000004,
  __v: 2,
  items: [ 50c886e21f32c23d18000002, 50c886e21f32c23d18000003 ],
  histories: 
   [ { operation: 'create',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000005 },
     { operation: 'submit',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000006 } ],
  name: { first: 'KAT', last: 'INTINK' } }

itemsに表示されている結果は、pushにより格納されたitem1、item2の_idです。これだけだと単にitemの_idを保持しているだけですが、mongooseではpopulateを使ってJoinのように各_idに相当する値を一緒に取得することができます。

User.findOne().populate('items').exec(function(err, items) {

この結果は以下のようになります。

{ __v: 2,
  _id: 50c886e21f32c23d18000004,
  age: 100,
  gender: 'male',
  uid: 'kat',
  items: 
   [ { name: 'table',
       color: 'black',
       _id: 50c886e21f32c23d18000002,
       __v: 0 },
     { name: 'chair',
       color: 'white',
       _id: 50c886e21f32c23d18000003,
       __v: 0 } ],
  histories: 
   [ { operation: 'create',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000005 },
     { operation: 'submit',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000006 } ],
  name: { first: 'KAT', last: 'INTINK' } }

itemsの中身が、_idだけではなくname, colorの値も表示されました。このように、Joinに似た操作を使ってより自然なドキュメントの結果を扱うことができます。

以上、ドキュメント内で他のドキュメントを扱うことで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年12月8日土曜日

Node.jsが面白い件④ MongoDBで遊ぶ 〜Mongoose編その①〜

今回はNode.js+MongoDB環境でよく使われるMongooseを使ってみたいと思います。MongooseはODM(Object document mapper)の1つで、validationやmiddlewareなどのより高度な機能を提供します。まずはいつものようにインストールから。

 > npm install mongoose


Mongooseでは最初にSchemaを定義してdocumentを生成します。以下、サンプルプログラムです。

var mongoose = require('mongoose');

// enum
var genders = 'male,female,neuter'.split(',');

// DBクローズ処理。
var closeDB = function() {
  db.close(function(err, result) {
    console.log("close!");
  });
};

// コネクション。dbオプションとしてsafe:true, serverオプションとしてauto_reconnect:trueを設定。
var opts = {server:{auto_reconnect:true}, db:{safe:true}};
//var db = mongoose.createConnection('mongodb://localhost/exampleDB2', opts);
var db = mongoose.createConnection('localhost', 'exampleDB2', 27017, opts);

// 接続エラーの場合
db.on('error', console.error.bind(console, 'connection open error.'));

// 接続オープン
db.once('open', function() {
    // サンプルスキーマ
    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 }
    });

    // collectionの定義。
    userSchema.set('collection', 'users');

    // モデルの作成。
    var User = db.model('User', userSchema);

    // インスタンス(document)作成。
    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last:'INTINK'},
        age: 100,
        gender: 'male'
      }, function(err, user) {
        if (err) {
          console.log('[error] create');
          console.log(err);
          closeDB();
        } else {
          console.log('create done.');

          // ドキュメントの検索。
          User.find().exec(function(err, user) {
            if (err) {
              console.log('[error] query');
              console.log(err);
              closeDB();
            } else {
              console.log('query done.');
              console.log(user);

              // ドキュメントの削除。
              User.remove(function(err) {
                if (err) {
                  console.log('[error] remove');
                  console.log(err);
                  closeDB();
                } else {
                  console.log('remove done.');
                  closeDB();
                }
              });
            }
          });
        }
    });
});


Node.jsが面白い件③ MongoDBで遊ぶ 〜基本編〜で説明したNative Driverを使った方法と違い、Modelを通して処理をしていることがわかります。簡単/単純な処理を記述する場合はNative Driverでも十分ですが、世の中のODMと同じようにMongooseにも便利な機能が実装されています。次はそれらの機能について説明してみます。