• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形
 

WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形

on

  • 592 views

 

Statistics

Views

Total Views
592
Views on SlideShare
592
Embed Views
0

Actions

Likes
0
Downloads
3
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形 WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形 Document Transcript

    • 2012-08/メディアクリエイティブDiv/WEB開発を加 速させる。アジャイル開発に最適なデータ構造とORマ ッパの形(渡辺雄作) 概要 本稿では、JavaオブジェクトをJsonにシリアライズし永続化するフレームワークを実装しました。 本フレームワークでは、任意のJavaフィールドに@IndexedをつけることによりいわゆるPrimaryKeyだけではないフィールドでの検索を可 能にしています。 現在開発中のプロジェクトでは本フレームワークを利用して開発を進めており、グループの基盤データベース構築プロジェクトでの採用を 進めています。 WEBで提供するほとんどのサービスは本フレームワークを利用することにより非常に高い効率で開発できると考えています。 現在はデータストアとしてMySQL, MongoDB, HBaseに対応しています。 序論 BtoCで提供される多くのWEBサービスで求められているのは サービスの開発速度 スケーラビリティ 高パフォーマンス だと思います。 その中でアメーバピグで利用されているIndexPersister FrameworkはKeyValueの形でデータをバイナリでストアするAPIを提供しており、ストアするデータベースの形は問わないアプローチを 採用しています。 実際アメーバピグでは現在の規模でもその形でサービスを提供し続けており、シンプルな(必要最低限な)データ永続化APIがあればサー ビスを開発できることがわかります。 IndexPersisterは完全にkeyValueとしてデータアクセスしていますが、今回開発したJsonPersisterでは任意のJavaフィールドに@Indexed をつけることによりいわゆるPrimaryKeyだけではないフィールドでの検索を可能にしています。 この形はFriendFeedで採用されていたり、最近ではサイボウズが提供しているクラウド型データベースKintoneでも同じアプローチをとっ ているようです。 ※サイボウズKintoneに関してはDevelopers Summitで発表がありましたが、発表資料が都合により削除されてしまっているようです。 現在開発中のプロジェクトではこのフレームワークを利用して開発を進めており、グループの基盤データベース構築プロジェクトでの採用 を進めています。 ※現在のグループではJsonPersisterHBaseの採用を進めています。 WEBで提供するほとんどのサービスはこのフレームワークを利用することにより非常に高い効率で開発できると考えています。 目次 概要 序論 目次 内容 JsonPersisterの特徴 モジュール構成 json_persister_core json_persister_mysql json_persister_mongo json_persister_hbase シリアライズの流れ
    • 主要なインターフェース JsonPersister.save JsonPersister.delete JsonPersister.load JsonPersister.list JsonPersister.createTable JsonPersister.dropTable 高度な使い方 複合インデックス 暗号化 キャッシュ 各モジュールについて json_persister_mysql データ保存形式 QuickStart DIコンテナを利用しない場合 springでの利用 json_persister_mongo json_persister_hbase FAQ このフレームワークを使うメリット・デメリットは? トランザクション使えないんですか? MySQL実装を使うメリットはなんですか? まとめ 参考文献 内容 JsonPersisterの特徴 javaオブジェクトをjsonデータとして永続化するフレームワークです。 javaオブジェクトを直接save、loadするシンプルなAPIを提供し、データストア依存のプログラミングを減らすことにより超高速 にDB連携アプリケーションが開発できます。 特定のフレームワーク(springやseasarなど)に依存せずに利用できます。 データストアに依存せずに共通のインターフェースを通じてjavaオブジェクトをシリアライズすることを目指しています。 現在はMysql, MongoDB, HBaseに対応しています モジュール構成 json_persister_core コアロジックを定義しています json_persister_mysql mysql実装です json_persister_mongo mongo実装です json_persister_hbase hbase実装です シリアライズの流れ 下記POJOを例に挙げる --------------------------------------------------------------------------------------
    • UserData.java @DataBaseId(id = 1) @Persistable(name = "user_data") public class UserData { @PrimaryKey private String userName; @PrimaryKey private String age; @Indexed private Date date; private String address; private Family family; @CompositeIndexed(name={"userName", "age", "address"}) public void composite_index0() {} public static class Family { private List<String> familyNames; public List<String> getFamilyNames() { return familyNames; } public void setFamilyNames(List<String> familyNames) { this.familyNames = familyNames; } } public Family getFamily() { return family; } public void setFamily(Family family) { this.family = family; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; }
    • public Date getDate() { return date; } public void setDate(Date date) { this.date = date;
    • } } 上記のPOJOをjson_persister.save()すると以下のJavaオブジェクトが下記のようなjsonに変換される { "address": "setagaya", "age": "33", "date": 1341914556082, "family": { "familyNames": [ "masami", "akari", "sakutarou" ] }, "userName": "yuhsaku" } 主要なインターフェース JsonPersister.save javaオブジェクトを保存します Sample.java UserData userDataQuery = new UserData(); userDataQuery.setUserName("yuhsaku"); //@PrimaryKey(必須) userDataQuery.setAge(32); //@PrimaryKey(必須) userDataQuery.setDate(new Date()); //@Indexedは(必須) userDataQuery.setAddress("setagaya"); jsonPersister.save(userDataQuery); JsonPersister.delete @PrimaryKey指定で一意なjavaオブジェクトを物理削除します Sample.java UserData userDataQuery = new UserData(); userDataQuery.setUserName("yuhsaku"); //@PrimaryKey(必須) userDataQuery.setAge(32); //@PrimaryKey(必須) jsonPersister.delete(userDataQuery); JsonPersister.load @PrimaryKey指定で一意なjavaオブジェクトを取得します
    • Sample.java UserData userDataQuery = new UserData(); userDataQuery.setUserName("yuhsaku"); userDataQuery.setAge(32); //第三引数は取得するデータの最新性を担保するかどうかです。(falseを指定するとmysqlの場合はslaveからデ ータを取得します。masterから取得したい場合はtrueを渡してください) UserData result_yuhsaku = jsonPersister.load(userDataQuery, UserData.class, false); JsonPersister.list javaオブジェクトを指定した条件でリスト取得します Sample.java //32才のUserDataオブジェクトをリスト取得します(5番目から10件をuserNameで降順で取得) UserData userDataQuery = new UserData(); userDataQuery.setAge(32); Criteria<UserData> criteria = Criteria.createCriteria(UserData.class).andEquals(userDataQuery).offset(5).limit(10).orderBy("userName", Criteria.ORDER.DESC); //第三引数は取得するデータの最新性を担保するかどうかです。(falseを指定するとmysqlの場合はslaveからデ ータを取得します。masterから取得したい場合はtrueを渡してください) //Criteriaでlimitの指定がない場合は100件がデフォルトです(nullを明示的に指定すればlimitは指定されませ ん = 全件取得になります) List<UserData> resultList = jsonPersister.list(criteria, true); Criteriaには他にもLesserThanやGreaterThanなどの比較条件のメソッドも用意されています。 JsonPersister.createTable テーブルを作成します Sample.java jsonPersister.createTable(UserData.class); JsonPersister.dropTable テーブルをdropします Sample.java jsonPersister.dropTable(UserData.class); 高度な使い方
    • 複合インデックス 以下のようなダミーメソッドを作成して@CompositeIndexedを指定するとcreateTable時に複合インデックスが作成されます @CompositeIndexed(name={"userName", "age", "address"}) public void composite_index0() {} nameに指定したプロパティ名の順番で複合インデックスが作成されます 暗号化 以下のようにフィールドに@Encryptoアノテーションをつけることによりそのフィールドが暗号化されて永続化されます。(String型のみ 対応しています) @Encrypto(algorithm="AES", keyLength=256) private String address; 暗号化を有効化するためにはそのクラスはAbstractEncryptoableを継承する必要があります。 以下サンプル SecureInfo.java @DataBaseId(id=0) @Persistable(name="user_secure_info") public class SecureInfo extends AbstractEncryptoable{ @PrimaryKey private String userId; @Encrypto(algorithm="AES", keyLength=256) private String mailAddress; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getMailAddress() { return mailAddress; } public void setMailAddress(String mailAddress) { this.mailAddress = mailAddress; } } 保存、取得する際は暗号化キーをsetする必要があります
    • //保存 SecureInfo secureInfo = new SecureInfo(); secureInfo.setUserId("hogehoge"); secureInfo.setMailAddress("hogehoge@co.jp"); secureInfo.setCryptoSeed("hogehoge_key"); //暗号化、復号化する際のキー文字列 jsonPersister.save(secureInfo); //取得 SecureInfo secureInfoQuery = new SecureInfo(); secureInfoQuery.setUserId("hogehoge"); secureInfoQuery.setCryptoSeed("hogehoge_key"); //暗号化、復号化する際のキー文字列 SecureInfo result = jsonPersister.load(secureInfoQuery, SecureInfo.class, false); @EncryptoアノテーションにはDESやAESなどjavaでサポートされているアルゴリズムを文字列で指定できます。 ※AES-256はJavaランタイムに無制限強度の管轄ポリシーの設定追加が必要です。 javaのインストールディレクトリ配下のjre/lib/securiy/以下に http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html からダウンロードしたzipに入っているlocal_policy.jarとUS_export_policy.jarを上書きコピーしてください キャッシュ classに@Cacheableを指定するヒープに指定した時間キャッシュすることができます @Cacheable(expire=5000) @DataBaseId(id = 1) @Persistable(name = "user_data") public class UserData { 以下略 各モジュールについて json_persister_mysql データ保存形式 jsonに変換された後以下のテーブルに保存される(テーブルは事前にjson_persister.CreateTableExecuteインターフェースを通じて作成し ておく必要がある) -------------------------------------------------------------------------------------CREATE TABLE `user_data` ( `userName_index` text NOT NULL, `age_index` text NOT NULL, `date_index` datetime DEFAULT NULL, `address_index` text, `data` text NOT NULL, PRIMARY KEY (`userName_index`(255),`age_index`(255)), KEY `date_idx` (`date_index`), KEY `composite_index0_idx` (`userName_index`(255),`age_index`(255),`address_index`(255)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | -------------------------------------------------------------------------------------- QuickStart
    • DIコンテナを利用しない場合 ドキュメント整備中 springでの利用 springの設定方法は以下の通り <bean id="perser" class="jp.co.cyberagent.persister.parser.JaksonPerser" /> <context:component-scan base-package="jp.co.cyberagent.persister" /> <bean id="masterConnectionHolder" class="jp.co.cyberagent.persister.mysql.SingleConnectionHolder" > <property name="dataSource" ref="masterDataSourceHolder" /> </bean> <bean id="slaveConnectionHolder" class="jp.co.cyberagent.persister.mysql.SingleConnectionHolder" > <property name="dataSource" ref="slaveDataSourceHolder" /> </bean> <bean id="masterDataSourceHolder" class="jp.co.cyberagent.persister.mysql.SingleConnectionHolder" > <property name="dataSource"> <bean class="org.apache.commons.dbcp.datasources.SharedPoolDataSource"> <property name="connectionPoolDataSource"> <bean class="org.apache.commons.dbcp.cpdsadapter.DriverAdapterCPDS"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/ameba"/> <property name="user" value="root"/> <property name="password" value=""/> </bean> </property> <property name="defaultAutoCommit" value="true"/> <property name="maxActive" value="3"/> <property name="maxIdle" value="1"/> <property name="maxWait" value="500"/> </bean> </property> </bean> <bean id="slaveDataSourceHolder" class="jp.co.cyberagent.persister.mysql.SingleConnectionHolder" > <property name="dataSource"> <bean class="org.apache.commons.dbcp.datasources.SharedPoolDataSource"> <property name="connectionPoolDataSource"> <bean class="org.apache.commons.dbcp.cpdsadapter.DriverAdapterCPDS"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3316/ameba"/> <property name="user" value="root"/> <property name="password" value=""/> </bean> </property> <property name="defaultAutoCommit" value="true"/> <property name="maxActive" value="3"/> <property name="maxIdle" value="1"/> <property name="maxWait" value="500"/> </bean> </property> </bean> json_persister_mongo ドキュメント整備中
    • json_persister_hbase ドキュメント整備中 FAQ このフレームワークを使うメリット・デメリットは? メリット ・シンプルなAPIで学習コストが低い RDMSを操作するようなO/Rマッパーなどは豊富な機能を提供しているが故に慣れるまでそれなりに時間がかかります。 JsonPersisterはアメーバピグでの開発を踏まえ、必要最低限の機能を提供しているためシンプルなAPIでテンポ良く開発 ができます。 JavaObjectをsaveしたりloadしたり、listで複数取得するだけです。 データストアとのやり取りなんてそれぐらいできれば事が済んでしまうことがほとんどだからです。(実際アメーバピグ もこれらのAPIだけでmysqlとやりとりしています) ・データストア依存のコードを隠蔽することにより開発速度が(非常に)高くなる iBatisなどのO/Rマッパーを利用している人がほとんどだと思いますが、ユーザーサービスのアプリケーションにおいては 、複雑なSQLをonlineで発行することはほとんどないと思います。 データストアを意識したコード(SQLやデータバインドなど)を無くし、余計な手間を減らせるため非常に高い開発効率 を実現できます。 ・データストアに依存しない共通のInterFaceでデータを扱う データモデルによってはRDMSに向いているデータとNoSQLに向いているデータがあると思いますが、システム拡張によ るデータストア切り替えや、データモデルによってはストア先を変更するなどの処理をアプリケーションレイヤからは意 識せずデータをストアすることができます つまり、DIコンテナを使っていればデータストアを変更した場合(例えばmysql→mongodbとか)でもアプリケーション 側のコードの変更は一切必要ありません。(DIを使っていない場合はFactory依存のクラスを変更するだけで済みます) デメリット ユーザーサービスでの利用に特化しているためトランザクションやjoinなどのいわゆるRDMS的な豊富な機能はありません 。 それらの機能とトレードオフとしてシンプルなAPIを提供しています。 トランザクション使えないんですか? トランザクションはDBのスケーラビリティを損なうのであえて外してあります。 RDMSで提供しているトランザクションを使うと垂直、水平分散を行う際に妨げになるため切り離して考えています。(スキーマをまたが ったトランザクションを取得できないとかあるので) 代わりにグローバルロック機構をhazelcastを使って実現するというのはいかがでしょうか? http://www.hazelcast.com/index.jsp hazelcastではネットワークを介したクラスタ上でのグローバルロックを実現することも可能です。 http://www.hazelcast.com/docs/2.0/manual/single_html/#Lock
    • Sample.java Lock lock = Hazelcast.getLock(myLockedObject); lock.lock(); try { // データ1をsave // データ2をsave } finally { lock.unlock(); } みたいな。 残念ながらhazelcastを使ってもロールバックはできませんが、mysqlのバックアップやバイナリログからの復旧ができるので障害時などは 運用でカバーできる範囲かと思っています。 トレードオフですが、個人的にはここらへんの機能を削ってでも得られる開発スピードのメリットを推しています。 MySQL実装を使うメリットはなんですか? MySQLはamebaでも非常に実績のあるRDBMSです。 それをデータストアに使うことによって、安定性や運用ノウハウをそのまま利用することができます。 O/Rマッパー経由でMySQLを使うことはできますが、テーブル正規化や仕様変更によるテーブル構成変更はamebaの多くのWEBサービス において運用ボトルネックになりやすいので スキーマレスにデータを保存できるという点でjsonPersisterは優れています。 まとめ 現在進めているプロジェクトでの開発で実際に利用していますが、MyBatisなどを利用する場合に比べて非常に柔軟な開発が可能です。 PrimaryKeyだけは最初に決めておく必要がありますが、それ以外のデータは開発途中での要件変更やデータ構造の変更が容易だからです 。 Scrumなどアジャイルに開発しているプロジェクトではなおさら高い効率を出せるかと思います。 MyBatisなどのORマッパを使うぐらいならこれを利用するほうが断然がシンプルだと感じました。 参考文献 FriendFeed では MySQL を使いどのようにスキーマレスのデータを保存しているのかhttp://www.hyuki.com/yukiwiki/wiki.cgi?HowFri endFeedUsesMySqlToStoreSchemaLessData