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にも便利な機能が実装されています。次はそれらの機能について説明してみます。

2012年11月29日木曜日

テンプレート・エンジン {{ mustache }} を使ってみる

{{ mustache }} というテンプレートエンジンを知っていますか?
http://mustache.github.com/

Google Trendで、Javaで有名なテンプレート・エンジンである Apache Velocity と比較したところ、最近では人気度が上回っているようです。



mustache はHTMLやコンフィグファイル・ソースコードを生成するためのシンプルなテンプレートエンジンで、以下のようないろんな言語をサポートしているようです。
Ruby, JavaScript, Python, Erlang, PHP, Perl, Objective-C, Java, .NET, Android, C++, Go, Lua, ooc, ActionScript, ColdFusion, Scala, Clojure, Fantom, CoffeeScript, D, node.js.

ちなみに、「mustache」とは「口ひげ」という意味で、「{」をタグとして利用したテンプレートということから、「{」を横にしたらひげに見えるから「mustache」と名前が付いたようです。



変数定義
早速どういったもの見てみましょう。以下はマニュアルに載っていたサンプルです。

・テンプレート
Hello {{name}}
You have just won ${{value}}!
{{#in_ca}}
Well, ${{taxed_value}}, after taxes.
{{/in_ca}}

・上記のテンプレートに渡すデータ(hash)をJSONもしくはYAMLで定義します。
{
    "name": "Chris",
    "value": 10000,
    "taxed_value": 10000 - (10000 * 0.4),
    "in_ca": true
}

・結果は以下のように表示されます。
Hello Chris
You have just won $10000!
Well, $6000.0, after taxes.

このテンプレートの「{{」と「}}」で囲まれたタグが、渡したデータ(hash)により変更され、結果として出力されます。上記の例では、{{name}}というタグ部分が、データの"name"の値「Chris」に変更されます。同様に{{value}}タグは、"value"の10000になります。


ちなみに、デフォルトで結果はエスケープされます。(例:「<」や「>」は、「&lt;」や「&gt;」に。)これをエスケープしないためには、ひげを3つにするか(例.{{{tag}}})、「{{&」と「}}」で囲みます。

・テンプレート
{{name}}
{{age}}
{{company}}
{{{company}}}

・データ(hash)
{
  "name": "Chris",
  "company": "<b>GitHub</b>"
}

・結果
Chris
&lt;b&gt;GitHub&lt;/b&gt;
<b>GitHub</b><


条件式やループ
mustacheが、「logic-less templates」というように呼ばれている意味は、どうやらif文やfor/while文などのステートメントが含まれないことからロジックレスという名称が付いているようです。ただし、テンプレート・エンジンでステートメント的なものは必要なので、条件文やループなどがどのように表現されているか説明しますと、「{{#」と「}}」で囲むことで実現できるようです。(ちなみに、ブロックの閉じるタグは「{{/」と「}}」で囲む。)

上記の例ですと、{{#in_ca}}と{{/in_ca}}にあたります。この場合、データ"in_ca"はboolean型なので、if文のように動きます。"in_ca"がtrueなら「Well, ....」が表示され、falseならこの部分が表示されません。(もしくは値が無い場合も表示されません。)

では、ループはというと、これも同様に「{{#」と「}}」で囲みます。ただし、この囲まれたタグの値の型がリストの場合に繰り返されます。以下はサンプルですが、"repo"の値がリストですので、以下のようにリスト分、繰り返されます。

・テンプレート
{{#repo}}
  <b>{{name}}</b>
{{/repo}}

・データ(hash)
{
  "repo": [
    { "name": "resque" },
    { "name": "hub" },
    { "name": "rip" },
  ]
}

・結果
<b>resque</b>
<b>hub</b>
<b>rip</b>

さらに、関数を渡すこともできるようです。これは面白いですね。

・テンプレート
{{#wrapped}}
  {{name}} is awesome.
{{/wrapped}}

・データ(hash)
{
  "name": "Willy",
  "wrapped": function() {
    return function(text) {
      return "<b>" + render(text) + "</b>"
    }
  }
}

・結果
<b>Willy is awesome.</b>

このタグがbooleanでもなく、リストでもない場合に、tagに相当するキー値が存在すれば、以下のようにtrueと同様の動きをするみたいです。

・テンプレート
{{#person?}}
  Hi {{name}}!
{{/person?}}

・データ(hash)
{
  "person?": { "name": "Jon" }
}

・結果
Hi Jon!

また、if文の逆(not if)は以下のように「{{^」と「}}」で囲んで書きます。

・テンプレート
{{#repo}}
  <b>{{name}}</b>
{{/repo}}
{{^repo}}
  No repos
{{/repo}}
Hash:

・データ(hash)
{
  "repo": []
}

・結果
No repos

その他のタグ
あと、コメントは「{{!」と「}}」で囲みます。

別のテンプレートファイルとして定義して読み込みたい場合は、例えばother.mustacheファイルを読み込みたいのであれば、{{>other}}のように「{{>」と「}}」で囲みます。

最後に、「{{」と「}}」以外でテンプレートを作りたい場合、以下のようにテンプレート中で記載することで変更ができます。(この例は、「{{」を「<%」に、「}}」を「%>」に変更)
{{=<% %>=}}

元に戻すには、
<%={{ }}=%>

これをmustache.jsで使うには以下のようなコードで動きます。
var template = 'I'm {{name}}.';
var v = Mustache.to_html(
          template,
          {
            name:"hitocie"
          }
);
console(v);

うーん、結構良くできていますし、Apache Velocityなどと比べると非常にシンプルですね。

今回、ほぼマニュアルの翻訳的な感じになってしまいましたが、ある程度の機能は紹介できたかと思います・・・。

ちなみに、mustacheを拡張したもので、Twitterからオープンソースとして提供されているhogan.jsというものがあります。これは、mustacheの記法で書いたテンプレートをAOT(Ahead Of Time)コンパイルして、JavaScriptに変換するというもので、レンダリングのたびにmustache記法のテンプレートを解析する必要がなく高速に動作するため、WebシステムでJSPなどのように利用できます。


2012年11月24日土曜日

Node.jsが面白い件③ MongoDBで遊ぶ 〜基本編〜

今回はMongoDBの使い方についてです。MongoDBは有名なNoSQLの1つで、Redisと同じようにNode.jsでも簡単に使えます。MongoDB Native NodeJS Driverをダウンロードし、こちらのマニュアルに沿って記述すれば基本的な使い方はO.K.です! ・・・で終わると書いている意味もないので、ここでは基本的な使い方を記述して行きます。 まずはいつも通りnpmでインストール!
 > npm install mongodb

続いて、データベースに接続してコレクションを作成した後ドキュメントを挿入し、コレクションを削除するという簡単なサンプルプログラム。
var mongo = require('mongodb');

// ドキュメントサンプル。
var doc1 = {'key1':'val1'};
var doc2 = {'key2-1':'val2-1', 'key2-2':'val2-2'};
var docs = [doc1, doc2];

// serverの設定。
var server = new mongo.Server('localhost', 27017, {auto_reconnect: true});

// dbの設定。
var db = new mongo.Db('exampleDb', server, {safe:true});
//var db = new mongo.Db('exampleDb', server);

var closeDB = function() {
    db.close(true, function(err, result) {
        console.log("close!");
    });
}

// コネクションオープン。
db.open(function(err, db) {
    if (!err) {
        console.log("open!");
        // コレクションの作成。既に存在する場合はエラー。
        db.createCollection('collection1', {strict:true}, function(cerr, collection) {
            if (!cerr) {
                console.log("create!");

                // ドキュメントの格納。
                collection.insert(docs, function(ierr, res) {
                    if (!ierr) {
                        console.log("insert doc1!");

                        // ドキュメントの検索。
                        collection.find().toArray(function(ferr, items) {
                            if (!ferr) {
                                console.log(items);

                                // コレクションの削除。
                                collection.drop();
                                closeDB();
                            } else {
                                console.log(ferr);

                                collection.drop();
                                closeDB();
                            }
                        });
                    } else {
                        console.log(ierr);

                        collection.drop();
                        closeDB();
                    }
                });
            } else {
                console.log(cerr);

                closeDB();
            }
        });
    } else {
        console.log(err);
    }
});

MongoDBでは、今のところデフォルトで書き込みの結果を待たずにリターンを返します(safe=false)。つまり、書き込みが成功したか失敗したかがすぐにわかりません。これは、データベースを作成する際にsafe=trueを設定することで変更できます。また、書き込みの挙動は以下のように設定することができます。
  1. j:ジャーナル(redoログのようなもの)のコミットを待つ。ジャーナルが設定されていなければそのまま返す。
  2. w:レプリカに指定数反映されるのを待つ。タイムアウト指定も可能。
  3. fsync:ジャーナリングされてない場合は全てのファイルをfsyncするのを待つ。ジャーナリングしている場合は次のグループのコミットを待つ。非推奨なのでjを使うべき。

MongoDBでは動的にcollectionを作成するケースも多いですが、既に作成しようとするcollection存在する場合はエラーを受け取りたい、という場合もあります。その場合はcreateCollectionでstrict:trueを指定します。
  1. createCollectionでstrict:false(デフォルト)の場合、既に存在する場合は何もしない(作成しない)
  2. createCollectionでstrict:trueの場合、既に存在する場合はエラーを返す。
  3. collectionでstrict:false(デフォルト)の場合、コレクションが存在しない場合は最初のinsertで作成する。
  4. collectionでstrict:trueの場合、コレクションが存在しない場合はエラーを返す。
サンプルプログラム内ではinsert/findを実施しています。この辺りはupdateやdeleteも含めて多くの処理がありますので、ドキュメントを見ながら実装していけばよいと思います。
Node.jsというよりMongoDBの話になりましたが、他の言語でMongoDBを使っている開発者ならかなり違和感なく使えそうですね。また、MongoDBはWeb上にもドキュメントが揃ってきていますので、NoSQL初心者でも比較的情報は得やすいですね。

2012年11月22日木曜日

BaaS(Backend as a Service)のプッシュ通知サービス

最近、BaaS(Backend as a Service)っていう言葉をよく聞きます。これは、スマートデバイス(モバイル)アプリケーション向けに、サーバサイド実装の手間を省くため、データベースへの保存・参照、SNSとの連携、プッシュ通知、認証、ファイル保存などの機能を簡単に呼び出すことができるサービスです。(実際はモバイルのみが対象ではないものもあり、本当にモバイル特化の場合は、MBaaSという呼び方をする場合もあるそうです。)これらのサービスにはSDK(開発キット)が提供されておりAPIをコールするだけで呼び出すことができたり、RESTでアクセスする機能が提供されています。
今回は、そのBaaSの中の1つのサービス「プッシュ通知」機能について、各ベンダーごとにどのようなサービスを提供しているのかを見ていきたいと思います。

その前に、プッシュ配信の仕組みについては、ほとんどのベンダーがApple、Google、Microsoftが提供するサービス「APNS(Apple Push Notification Service)」、「GCM(Google Cloud Messaging for Android)」、もしくはMicrosoftの「WNS(Windows Push Notification Service)」をベースに作っています。簡単に言えばラップしているだけのサービスなので、以下のようにBaaSベンダーはプッシュされたメッセージを各ベンダーごとに振り分けるというようなサーバを用意しているというイメージになります。


このことから、以下のページを参考にして、自分で実装することもできます。

ただ、簡単になっていますので、BaaSを使った方が便利ではありますね。

では、各BaaSベンダーのサービスです。

Microsoft Mobile Services
上記でも軽く触れました名前の通りのMicrosoftがAzure上で提供する「Windows Push Notification Service (WNS)」というサービスです。そのため以下のようにWindowsがサポート対象ですが、将来的にはAndroidやiOSにも対応するようです。
・プラットフォーム: Windows Store, Windows Phone
・言語: C#など

ACS(Appcelerator Cloud Services)
モバイル開発ツールで有名な「Titanium Mobile」を作っている会社「Appcelerator」がCocoafishという会社を買収して、ACSという名称で提供しています。
・プラットフォーム: Android or iOS
・言語: JavaScript, REST, AS3, Java for Android

Buddy
ここのサービスは、他とは違いAndroidとiOSに加え、Windows系もサポートしています。内部的には上記のWindows Push Notification Service(WNS)を使っているようです。
・プラットフォーム: Android, iOS, Windows Store, Windows Phone 
・言語: Java, Objective-C, C#

appiaries(国産)
国産のBaaSのようです。ただ、プッシュ配信は、まだ未提供のようです。将来的にはAndroid/iOS対応予定とのこと。

Parse
・プラットフォーム: Android, iOS
・言語: REST, JavaScript, Java for Android, Objective-C for iOS

StackMob

・プラットフォーム: Android, iOS
・言語: Java for Android, Objective-C for iOS, REST
   
Kinvey
http://www.kinvey.com/
・プラットフォーム: Android, iOS
・言語: Java for Android, Objective-C for iOS

その他、ApplicasaQuickbloxSencha.ioなどの有名なBaaSがありますが、あまり変わらなそうなので、とりあえずこれらは未調査。


上記サービスは基本的に、APNS、GCM、WNSをベースに作られていますが、プッシュ配信と言えば、HTML5のWebSocketが有名です。そこで、WebSocketをベースにプッシュ配信サービスを提供しているベンダーを少し挙げてみます。

pusher
PHP, Ruby, JavaScriptなどWebSocket ClientのAPIがあればアクセス可能

Kaazing
http://kaazing.com/
プッシュ通知サービスは、Kaazing WebSocket Gateway (HTML5 Edition)と呼ばれるようです。こちらもWebSocket Clientのライブラリからアクセスできますが、あらかじめ、Java, JavaScript, .Net, SilverLight, AIR, Flex, Flashなどのライブラリが用意されているようです。また、WebSocket以外にも、JMS, AMQP, XMPPもサポートしています。(ただし、WebSocketほどメジャーではないですが。)

以上がプッシュ通知のサービスですが、今後はBuddyなどのようにAPNS、GCM、WNSを連携するのみでなく、さらにWebSocketなどもサポートされていくのではないかと思います。

2012年11月20日火曜日

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

iOS デバイスへのプッシュ通知は、APNs (Apple Push Notification service) を使って実装できます。
加えて、配信するコンテンツを提供するサーバーも必要になります。
今回は、ちょうど使ってみたかった node.js で動かしてみます。

APNs の概要


全容を把握したい場合は、やっぱり公式のガイドがいいと思います。

 Local NotificationおよびPush Notificationプログラミングガイド

現時点では日本語版の方が古いということもなく、英語版の Local and Push Notification Programming Guide と同じ内容みたいですので、安心して日本語版を参照できますね。

APNs を利用したプッシュ通知を単純化すると以下のようになります。
(上記、公式ドキュメントからの引用です)


今回 node.js で作るのは、上図のプロバイダに相当するものです。


まずは iOS Provisioning Portal


APNs は実機でしか使えないので、Provisioning Portal で Provisioning Profile を用意します。手順は基本的に通常の iOS アプリ開発時と同じなので割愛。
ただし、App ID については2点ほど注意が必要です。

1. ワイルドカードを使っていない ID を作る
通常 AppID にはワイルドカードの指定が許されていますが、APNs を利用するアプリではワイルドカードは使用できません。

2. 1 の ID で APNs を有効にする
ID 作成後に、Configure から設定に入り、
Enable Push Notification service にチェックを入れて APNs を有効にした後に
証明書(Push SSL Certificate)を取得する必要があります。
証明書には、Development / Production の2つがありますが、今回は Development にします。


プロバイダを node.js で


簡単に APNs を利用できるモジュールがありますので、それを使います。

node-apn
 https://github.com/argon/node-apn

README がしっかりと用意されているので、そちらを読めば使い方は基本的な使い方は把握出来そうです。
READMEからポイントをいくつか転載。

インストールとコード内での利用
これはいつも通りです。
$ npm install apn
var apns = require('apn');

apns.Connection へ渡すオプションについて
デフォルトは以下のようになっています。
var options = {
    cert: 'cert.pem',                 /* Certificate file path */
    certData: null,                   /* String or Buffer containing certificate data, if supplied uses this instead of cert file path */
    key:  'key.pem',                  /* Key file path */
    keyData: null,                    /* String or Buffer containing key data, as certData */
    passphrase: null,                 /* A passphrase for the Key file */
    ca: null,                         /* String or Buffer of CA data to use for the TLS connection */
    gateway: 'gateway.push.apple.com',/* gateway address */
    port: 2195,                       /* gateway port */
    enhanced: true,                   /* enable enhanced format */
    errorCallback: undefined,         /* Callback when error occurs function(err,notification) */
    cacheLength: 100                  /* Number of notifications to cache for error purposes */
};

var apnsConnection = new apns.Connection(options);

  • cert, key には前節 App ID のところで取得した証明書を使う。
  • Development の Push SSL Certificate を使用する場合は、gateway の値を gateway.sandbox.push.apple.com に!
  • enhanced が true の場合、errorCallback が使用できます。(後述)

APNs の証明書
cert.pem, key.pem は以下のコマンドで作れます。

$ openssl x509 -in cert.cer -inform DER -outform PEM -out cert.pem
$ openssl pkcs12 -in key.p12 -out key.pem -nodes
コマンド中の2つのインプットは次のようにして用意します。
cert.cer ... Provisioning Portal の App ID のところで取得したもの(aps_development.cerとか)
key.p12 ... aps_development.cer をダブルクリック等でインストール後、
  Keychain Access.app を起動 -> My Certificates -> Apple Development iOS Push Services を選択 -> File メニューから Export Items を選択
 で取得できます。



errorCallback について
APNs のエラー応答については、前述の公式ドキュメントの「バイナリインターフェイスの形式と通知形式(p.50)」あたりに詳しいです。
node-apn では、オプションの enhanced が true の状態で
errorCallback に function(err, notification) {} を指定することで APNs からのエラーメッセージが拾えます。




feedback について
フィードバックの概要は、公式ドキュメントの「フィードバックサービス(p.55)」で。
var feedback = new apns.Feedback(options);
で簡単に使用できます。

var options = {
    cert: 'cert.pem',                   /* Certificate file */
    certData: null,                     /* Certificate file contents (String|Buffer) */
    key:  'key.pem',                    /* Key file */
    keyData: null,                      /* Key file contents (String|Buffer) */
    passphrase: null,                   /* A passphrase for the Key file */
    ca: null,                           /* Certificate authority data to pass to the TLS connection */
    address: 'feedback.push.apple.com', /* feedback address */
    port: 2196,                         /* feedback port */
    feedback: false,                    /* enable feedback service, set to callback */
    interval: 3600                      /* interval in seconds to connect to feedback service */
};

options も Connection と同じような感じですね。
address は、開発用を使っている場合は Connection と同じように feedback.sandbox.push.apple.com を使用します。
feedback にコールバックをセットするとインターバルの間隔で呼び出されます。この際、コールバックがとる引数は2つ、サーバーが返す time と デバイストークンを含む Buffer です。

debug モジュール
debug モジュール ( https://github.com/visionmedia/debug ) を導入するとデバッグメッセージを有効にできます。
npm install debug.

$ DEBUG=apn node apns.js
のようにすると APNs 接続に関するデバッグメッセージがコンソールに表示されます。
DEBUG=apnfb では、フィードバックに関するメッセージが出力されます。

token
デバイストークン。
長くなってきたので、これについては iOS 側の実装とともにまた次回の記事で。


2012年11月19日月曜日

Node.jsが面白い件② redisを使う

Node.jsが面白い件①の続きです。今回はredisの使い方について書いていきます。redisはオープンソースのインメモリKVSとして有名で、ディスクへの書き込みやトランザクション機能も提供する優れものです。クラスタに関してはまだ安定版が出ていませんが、単体ではとても簡単に利用できます。以下、Macでのインストール手順です。詳しくはredisのページを参照してください。
  1. redisのダウンロードページからダウンロードする。
  2. 展開したディレクトリに移動しmakeを実行する。
  3. 同ディレクトリでmake testを実行する。
node.js側では以下のようにライブラリredis、hiredisをインストールします。例えば作業ディレクトリをRedisSampleとすると、以下のようにします。
 > mkdir RedisSample
 > cd RedisSample
 > npm install hiredis redis

RedisSampleディレクトリにサンプルプログラムapp.jsを作成し、以下の内容を記述します。

//redisの読み込み。
var redis = require('redis')

try {
    // コネクションの作成。defaultで127.0.0.1:6379を使う。
    console.log("-- connection open.");
    var client = redis.createClient();

    // onメソッドによるerrorイベント時の処理。
    client.on("error", function(err) {
        console.log("Error:" + err);
    });

    // password認証。redis.confでrequirepassにredispassを設定。
    console.log("-- auth");
    client.auth("redispass");

    // 全てのkeyを削除。
    console.log("-- flushdb");
    client.flushdb();

    // 文字列のkey-valueとして値を格納。key:key1、value:val1
    client.set("key1", "val1");

    // 格納した値を取得。 key: key1
    client.get("key1", function(err, obj) {
        console.log("get key1: " + obj);
    });

    // リストの保持。key: lkey、value: lval1, lval2
    client.lpush("lkey1", "lval1");
    client.lpush("lkey1", "lval2");

    // 格納したリスト値を取得。key:lkey1
    client.lrange("lkey1", 0, -1, function(err, obj) {
        console.log("get list lkey1: " + obj);
    });

    // ハッシュの保持。key: hkey1、value: hfld1:hval1, hfld2:hval2
    client.hset("hkey1", "hfld1", "hval1");
    client.hset("hkey1", "hfld2", "hval2");

    // 格納したハッシュ値を取得。 key:hkey1
    client.hgetall("hkey1", function(err, obj) {
        console.log("get hash hkey1");
        console.log(obj);
    });


    // トランザクションその1。成功。
    multi = client.multi();
    multi.set("mkey1", "mval1");
    multi.set("mkey2", "mval2");
    multi.exec(function(err, obj) {
        client.get("mkey1", function(err, rep1){
            console.log("get mkey1: "+rep1);
        });
        client.get("mkey2", function(err, rep2) {
            console.log("get mkey2: "+rep2);
        });

        // トランザクションその2。watchの値が更新されて失敗。
        client.set("wkey1", "wval1");
        client.watch("wkey1");
        client.set("wkey1", "wval-upd");
        multi = client.multi();
        multi.set("mkey1", "mval3");
        multi.set("mkey2", "mval4");
        multi.exec(function(err, obj) {
            client.get("mkey1", function(err, rep1){
                console.log("get mkey1: "+rep1);
            });
            client.get("mkey2", function(err, rep2) {
                console.log("get mkey2: "+rep2);
                console.log("-- connection close.");
                client.quit();
            });
        });
    });
} catch(err) {
    console.err(util.inspect(err));
}

結果は以下のようになります。
 > -- connection open.
 > -- auth
 > -- flushdb
 > get key1: val1
 > get list lkey1: lval2,lval1
 > get hash hkey1
 > { hfld1: 'hval1', hfld2: 'hval2' }
 > get mkey1: mval1
 > get mkey2: mval2
 > get mkey1: mval1
 > get mkey2: mval2
 > -- connection close.

2行目で最初にインストールしたredisを読み込みます。6行目でコネクションを作成して、あとはredisのコマンドにならって処理を実行しています。redisはトランザクションをサポートしていますが、51行目のトランザクション①は成功し、63行目のトランザクション②は失敗します。これはwatchを使って楽観的ロックを実現しており、この例の場合はwkey1をwatchしており、watch実行後からexec実行前までにwkey1が変更されているとトランザクションが失敗するため、結果としてはmkey1、mkey2はmval1、mval2のままとなります。
この例の場合、トランザクション①の結果表示でコールバックを使っているので、コールバック内にトランザクション②の処理を記述しています。Node.jsではコールバック処理をよく使うので処理結果の順序を保持したい場合はこのように意識して記述するか、簡素化のためにstepなどのモジュールを使って処理を記述することができます。
redisはNode.jsでは今のところよく使われる組み合わせなのですが、ここで書いたように基本的な使い方はとても簡単です。軽いプロトタイプなら、Node.jsとredisだけですぐに作れてしまいそうですね。

2012年11月13日火曜日

MyBatis/iBATIS用のコードを自動生成する「MyBatis Generator」を使う

前回は、MyBatisの基本的な使い方を見てきました。今回はそこで手作業にて行ってきた各ファイルの自動生成ツール「MyBatis Generator」の使い方を説明していきたいと思っています。

MyBatis Generator Eclipse pluginのセットアップ
EclipseのメニューからHelp -> New Install Softwareを選択し、以下のURLを入力することでEclipse pluginがセットアップされます。
http://mybatis.googlecode.com/svn/sub-projects/generator/trunk/eclipse/UpdateSite/

インストールできましたら、メニューからFile -> New -> Other...を選択し、MyBatis Generator Configuration Fileを指定して、設定ファイル(デフォルトではgeneratorConfig.xml)を生成し、環境に合わせて編集します。このファイルは、Generateするための設定情報です。


  
    
    
    
    
    

各タグの説明ですが、javaModelGeneratorタグはエンティティクラスファイル、sqlMapGeneratorタグはXMLのMapperファイル、javaClientGeneratorタグはMapperクラスファイルの出力先を指定するためのタグです。tableタグで対象とするテーブル名を記述します。このtableNameにはワイルドカードも指定できるようです。ファイル編集後は、このgeneratorConfig.xmlを選択して、右クリックで「Generate MyBatis/iBATIS Artifacts」を選択しますと、エンティティクラスファイルとMapperファイル、Mapperクラス、Exampleクラスの4つのファイルが生成されます。(ちなみにMyBatis用だけでなく、2.2以上のiBATIS以上のファイルを生成できるようです。)

例えば、blogテーブルの場合は以下の4つのファイルが生成。
  • Blog.java :エンティティクラス。
  • BlogExample.java :Generator独自のクラスっぽい。SQL を呼び出す際、条件を指定するために利用するクラス。
  • BlogMapper.java :クエリを実行するための Java インターフェイス。
  • BlogMapper.xml :Mapper XML ファイルで、ほとんどの SQL はこのファイルに記述。
ちなみに、自動生成された JavaとXML ファイルのコメントには@mbggeneratedというアノテーションがあります。MyBatis Generatorが再度コードを生成するとき、このアノテーションが付加された要素だけを削除してから再度新しい要素を生成されます。そのため、このアノテーションがある場合は、手動で修正すると消えてしまいますのでご注意を。 話はそれましたが、続けますとBlogMapper.xmlを前回のように、mybatis-config.xmlのmapperタグに記述されていればデータベースアクセスできるようになります。例えば以下のようなコードでレコードを登録して、件数をカウントするということが簡単にできます。

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = new Blog();
blog.setId(0);
blog.setContent("Test-Test");
mapper.insert(blog);
int count = mapper.countByExample(new BlogExample());
System.out.println(count);
session.close();

myBatis Generatorをカスタマイズ
このMyBatis GeneratorをPluginを作ることで、カスタマイズできます。具体的にはファイル名やクラス名を変えたり、インターフェースやスーパークラスを追加できたり、メソッド、コンストラクタなどを追加できたりと、結構いろいろ生成するファイルを変更することができます。
まずはここから"Mapping Generator"をダウンロードして、解凍します。そのmybatis-generator-core-x.x.x.jarファイル(私は1.3.2を利用)をEclipseにコピーして、ビルドパスを設定します。
次にorg.mybatis.generator.api.PluginAdapterを継承したクラスを作成します。このクラス内で生成するタイミングに合わせてオーバーライドするメソッドを選択肢、中身を実装します。(例えば、初期化時ならinitializedメソッドをオーバーライド。)以下の例では、エンティティクラスを生成する際に、エンティティクラスにコンストラクタを追加するというサンプルです。

package com.test.generator.plugins;

import java.util.List;

import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.JavaVisibility;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.TopLevelClass;

public class GenPlugin extends PluginAdapter {

 @Override
 public boolean validate(List arg) {
  return true;
 }

 @Override
 public void initialized(IntrospectedTable introspectedTable) {
  super.initialized(introspectedTable);
 }

 @Override
 public boolean modelBaseRecordClassGenerated(
   TopLevelClass topLevelClass,
   IntrospectedTable introspectedTable) {
  Method method = new Method();
  method.setVisibility(JavaVisibility.PUBLIC);
  method.setConstructor(true);
  method.setName(topLevelClass.getType().getShortName());
  method.addBodyLine("System.out.println(\"Called constructor\");");
  topLevelClass.addMethod(method);
  return super.modelBaseRecordClassGenerated(topLevelClass, introspectedTable);
 }
}

この作成したクラスをgeneratorConfig.xmlにcontextタグの中にpluginタグとして、追加します。

  
    
       .....
  

そうすると以下のコンストラクタがBlogクラスに追加されます。

public Blog() {
 System.out.println("Called constructor");
}

このプラグイン機能でいろいろと生成もいじれそうなので、開発の生産性向上に役立ちそうですね。

外部キーなどの対応
ここまで来たら、以下のように参照関係のあるテーブルの場合に自動生成してくれるかを試してみようと思います。

CREATE TABLE blog (
  id INT,
  content VARCHAR(255),
  PRIMARY KEY(id)
);
CREATE TABLE tag (
  id INT,
  blog_id INT,
  name VARCHAR(100),
  PRIMARY KEY(id),
  FOREIGN KEY (blog_id) REFERENCES blog(id)
);

generateConfig.xmlのtableにtagテーブルも追加して、"Generate"!ってやってみたら、Tag関連のファイルは生成されましたらが、参照関係についてのフィールドなどは全然生成されなかったです。本来であれば、Blog.getTags()みたいなメソッドが追加されてList<Blog>を取得できるようなコード生成してくれることを期待したのですが、外部キーの生成は自動的にやってくれないらしいですね。これは手動でやるとMapperなどの修正も必要なので結構めんどくさく、非常に残念。。
こちらのフォーラムにも同様の記事がありました。。。これもまたプラグインを使って作ってみるしかなさそうですね。。

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

オープンソース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について説明したいと思います。



2012年11月6日火曜日

JavaでiPhone/iPadアプリを開発できる「Codename One」

JavaでiPhone/iPad、Androidなどのモバイルアプリケーションを開発できるというツール「Codename One」が、JavaOne 2012で取り上げられていたみたいなので、ちょっと調べてみました。

これはCodename One社のオープンソースプロジェクトで、Javaで書いたコードが、iOSやAndroid、BlackBerryのネイティブコードに変換されて動作する無償のツールみたいです。ツールの中身は、GUI BuilderなどのUIを作るツール、エディタなどのIDE、シミュレータ、Javaフレームワークとそのネイティブコードへの変換ビルダーなどがあるみたいです。(ビルドは使い込んでいくと課金対象のようです。)



有名な類似製品としては、JavaScriptでコードを書くという違いはありますがAppceleratorのTitaniumですね。

また、Macを持っていないユーザ用(?)にクラウドでビルドして、iOS用のインストーラであるipaファイルを生成できるようです。ただ、FAQを見ると、App Storeに上げるときなどに必要と言っているので、結局必要かな。。。と思いきや、ここで始めて知ったのですが、MacinCloudというMacOSのクラウドがあるらしいですね。Mac持っていない人には使えるかも。

話はそれましたが、要はJavaを知っていれば、クロスプラットフォームで稼働するモバイルアプリケーションが作れるっていうツールらしい。昔、TitaniumはAndroid/iOS上で同じコードでちゃんと動かなかったという経験(私の実装が悪かったのかも。。)があるので、こちらには期待したいですね。

アプリ開発の流れ
EclipseかNetBeansで開発することになるようです。

新規プロジェクト作成をすると以下の3つのソースコードが生成されるようです。
  • StateMachineBase: GUI Builderで自動生成された画面のクラスです。このファイルは手動でいじらないようです。
  • StateMachine: StateMachineBaseを継承したクラスで、StateMachineBaseを操作するようなコードをここに書くようですね。例えば、GUI Builderで作ったボタンの押下イベント発生時の処理をメソッドとしてコーディングするとか、あるイベントが発生したときに、GUIのデータセットなどを行うとか、ほとんどの処理はここに書いていくことになりそうです。
  • Main: まぁ、メインですね。AndroidやiOSのフレームワーク同様、ライフサイクルを持っているようです。

ソースコードに加え、以下のようなリソースファイルも。
  • レイアウトデザイン/テーマ/イメージ
  • 国際化
  • データファイル
APIも充実していて、以下のようにセンサーなどのモバイル特有のものにもアクセスが可能です。

  • カメラ
  • GPS
  • ビデオ
  • ストレージ
  • ネットワーク
  • サーバプッシュ
  • メール/SMS/電話
  • JSON/XML
  • 地図 など

稼働確認するには独自のシミュレータで実行することもできますし、クラウド上でビルドして、ビルド完了すると、作成された.ipaファイルのURLをメールなどで受け取り、iPhone/iPadなどの実機でダウンロードして実行できるようです。(Androidのシミュレータは非常に遅いので、これは速いといいのですが。)

まだ、実際にはダウンロードして試していませんが、ネイティブコードに変換されることからPhoneGapなどのWebkitベースのツールより高速に動くと思いますし、Objective-Cはイマイチ慣れないので、作りたいアプリが思いついたら試してみたいと思います。

※若干気になる点はGalleryにApp Storeに公開されているアプリがなさそう。。。まぁ、これからですかね。


2012年11月1日木曜日

Node.jsが面白い件①

別リンクのGoogle Cloud Messaging for Android(GCM)を試してみるで書かれているように、サーバプッシュ技術ということでNode.js+socket.ioあたりのことを書こうと思ってたんですが、ちょっと寄り道してNode.jsについて書いていこうと思います。node.jsすげぇー!(javascriptで全部書けるとか、v8が早いとか、スケールプログラムが書きやすいとか)の解説は他のサイトに譲るとして、ここでは簡単なプログラムを作っていろいろ試して行こうと思います。超簡単な以下のようなものをNode.jsでつくってみることで、アプリケーションに必要な各要素を実現してみます。


この図のそれぞれ黄色の部分、
  1.  データベースアクセスはどうするの?
  2. ログはどうなってるの?
  3. ルーティングってどうやってるの?
  4. セッション関係は?
  5. 認証ってどうやってるの?
という基本的なところをまずは作っていきます。Node.js初心者の方もNode.js熟練者の方も、「普通こうやるだろ」や「こんなときどうするの」などのコメントを自由にもらえると嬉しいです。では、次回に続きます!

2012年10月23日火曜日

Google Cloud Messaging for Android(GCM)を試してみる

モバイルアプリも流行ってきたので、さらにサーバプッシュの必要性が高くなってくると思い、メンバーと一緒にいろいろ試していってみようと思います。(Google Cloud Messaging、Apple Push Notification Service、WebSocketなど)

まずは、Google Cloud Messaging for Android(以下GCM)について試してみました。(ちょっと前までは、C2DMと呼ばれていたものです。)

動作は以下の図のように、Androidデバイスからregister idというものを生成及び登録し、その後サーバなどから登録されたデバイスに対してプッシュすることで、メッセージをリアルタイムに通知するという単純なものです。



実装については、基本的にはこちらのページ通りにやっていけばできると思いますが、ポイントとハマったところをまとめていければと思っています。また、サーバサイドのプッシュをPHPでやっている記事は多いですが、チュートリアルにあるサーブレットでやっている人が少なそうなので、今回はJavaサーブレットを使ってみたいと思います。

環境
Apache Tomcat 7
Android 4.1

API KeyとSENDER ID取得
https://code.google.com/apis/consoleからAPI KeySENDER IDを取得します。簡単なので詳しくは、http://developer.android.com/guide/google/gcm/gs.htmlを参照ください。

サーブレット作成
まずは、サーバサイドの仕組みを作るため、プロジェクト(Eclipse for JavaEEを使った場合は、Dynamic Web Project)を作成します。

AndroidをEclipseで開発するために、Android Development Tools(ADT)pluginをインストールして、Android SDK Managerで「Extras」⇒「Google Cloud Messaging for Android」を選択しGCMをダウンロードします。そのダウンロードしたディレクトリの中のgcm/gcm-server/dist/gcm-server.jarをプロジェクトにコピーします。(WEB-INF/libなどへ)
ただ、このjarファイルだけですと、java.lang.ClassNotFoundExceptionが発生します。
java.lang.ClassNotFoundException: org.json.simple.parser.ParseException
これは、gcm/gcm-server/lib/json_simple-1.1.jarがないため発生してるようなので、同様にコピーします。


では、早速サーバサイドのコードですが、3つのサーブレットを作ることとします。1つはregister IDの登録、2つ目はregister IDの登録解除、最後はメッセージ送信。

①. register ID登録サーブレット
Androidから受け取ったregister IDを適当なデータベースなどに登録しておきます。ここはデータベースではなくてもファイルでもキャッシュでも何でもいいです。

②. register ID登録解除サーブレット
単純に①で登録したものを削除します。

③. メッセージ送信サーブレット(サーバからクライアントへプッシュ)
Message.Builder builder = new Message.Builder().delayWhileIdle(true);
builder.addData("message", "Hello"); // 送信するデータ
Message message = builder.build();

String myAppKey = <API Key>
List<String> devices =<①で登録されたregister IDのList<String>>
Sender sender = new Sender(myAppKey);
MulticastResult result = sender.sendNoRetry(message, devices);


これでdevicesに対して、メッセージを送信できます。しかし、メッセージ送信時に以下のエラーが発生しました・・・。

javax.net.ssl.SSLHandshakeException:
   sun.security.validator.ValidatorException: PKIX path building failed:
   sun.security.provider.certpath.SunCertPathBuilderException:
   unable to find valid certification path to requested target

Caused by: sun.security.validator.ValidatorException:
   PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
   unable to find valid certification path to requested target

Caused by: sun.security.provider.certpath.SunCertPathBuilderException:
   unable to find valid certification path to requested target

これはSSL通信を行うために証明書が必要みたいなので、jssecacertsファイルを作成し、$JAVA_HOME/lib/securityにコピーするで解決できました。(keytoolを使うのもいいですが、私は簡単そうだったのでInstallCert.javaを使いました。)

ちなみに以下のコマンドでjssecacertsファイルを作成。
> java InstallCert android.googleapis.com

Androidプロジェクト作成
Androidプロジェクトを作成し、上記でダウンロードしてきたgcm/gcm-client/dist/gcm.jarをAndroidプロジェクトにコピーします。

次にActivityとServiceを作成します。

AndroidManifest.xml

    <permission android:name="com.scratch.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
    <uses-permission android:name="com.scratch.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <application
        <activity
            android:name=".GCMActivity"
            android:configChanges="orientation|keyboardHidden"
            android:windowSoftInputMode="adjustPan" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver
                android:name="com.google.android.gcm.GCMBroadcastReceiver"
                android:permission="com.google.android.c2dm.permission.SEND" >
                <intent-filter>
                    <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                    <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                    <category android:name="com.scratch" />
                </intent-filter>
        </receiver>
        <service android:name=".GCMIntentService" />
    </application>

Serviceクラス(GCMIntentService)
package com.scratch;

import android.content.Context;
import android.content.Intent;
import android.util.Log;

import com.google.android.gcm.GCMBaseIntentService;

public class GCMIntentService extends GCMBaseIntentService {

        static String SENDER_ID = <SENDER ID>;
        final private String TAG = getClass().getName();

        @Override
        protected void onError(Context context, String regId) {
                Log.v(TAG, "error " + regId);
        }

        @Override
        protected void onMessage(Context context, Intent intent) {
                Log.v(TAG, "Received message");
                Log.v(TAG, intent.getStringExtra("message")); //サーバからプッシュされたデータの受け取り
        }

        @Override
        protected void onRegistered(Context context, String regId) {
                Log.v(TAG, "registered regId=" + regId);
                // ここでサーブレット①にリクエストしてregister idを登録。
        }

        @Override
        protected void onUnregistered(Context context, String regId) {
                Log.v(TAG, "unregistered " + regId);
                // ここでサーブレット②にリクエストしてregister idを削除。
        }

        public GCMIntentService() {
                super(SENDER_ID); // superでSENDER IDを指定するようです。
                Log.v(TAG, "create GCMIntentService()");
        }
}

Activityクラス(GCMActivity)
package com.scratch;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gcm.GCMRegistrar;

public class GCMActivity extends Activity {

        final private String TAG = getClass().getName();

        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);

                // check if this device supports GCM.
                GCMRegistrar.checkDevice(this);

                // check if it's right manifest.
                GCMRegistrar.checkManifest(this);
                final String regId = GCMRegistrar.getRegistrationId(this);
                Log.v(TAG, "regId=" + regId);
                if (regId.equals("")) {
                       // GCMIntentService.onRegisteredがコールされる。
                       GCMRegistrar.register(this, GCMIntentService.SENDER_ID); 
                } else {
                        Log.v(TAG, "Already exists " + regId);
                }
        }
}

ちなみに、unregisterはをするにはGCMRegister.unregisterメソッドを使い、これが実行されるとGCMIntentService.onUnregisteredが呼ばれます。

これでAndroidでサーバプッシュができるはずです。一度お試しを!(詰まったらコメントください。)

次回は、AppleのApple Push Notification Service(APNS)についても試していこうと思います。