web開発を加速させる。アジャイル開発に最適なデータ構造とorマッパの形
TRANSCRIPT
![Page 1: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/1.jpg)
2012-08/メディアクリエイティブDiv/WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形(渡辺雄作)
概要本稿では、JavaオブジェクトをJsonにシリアライズし永続化するフレームワークを実装しました。本フレームワークでは、任意のJavaフィールドに@IndexedをつけることによりいわゆるPrimaryKeyだけではないフィールドでの検索を可能にしています。現在開発中のプロジェクトでは本フレームワークを利用して開発を進めており、グループの基盤データベース構築プロジェクトでの採用を進めています。WEBで提供するほとんどのサービスは本フレームワークを利用することにより非常に高い効率で開発できると考えています。現在はデータストアとしてMySQL, MongoDB, HBaseに対応しています。
序論BtoCで提供される多くのWEBサービスで求められているのは
サービスの開発速度スケーラビリティ高パフォーマンス
だと思います。
その中でアメーバピグで利用されているIndexPersisterFrameworkはKeyValueの形でデータをバイナリでストアするAPIを提供しており、ストアするデータベースの形は問わないアプローチを採用しています。
実際アメーバピグでは現在の規模でもその形でサービスを提供し続けており、シンプルな(必要最低限な)データ永続化APIがあればサービスを開発できることがわかります。
IndexPersisterは完全にkeyValueとしてデータアクセスしていますが、今回開発したJsonPersisterでは任意のJavaフィールドに@IndexedをつけることによりいわゆるPrimaryKeyだけではないフィールドでの検索を可能にしています。
この形はFriendFeedで採用されていたり、最近ではサイボウズが提供しているクラウド型データベースKintoneでも同じアプローチをとっているようです。
※サイボウズKintoneに関してはDevelopers Summitで発表がありましたが、発表資料が都合により削除されてしまっているようです。
現在開発中のプロジェクトではこのフレームワークを利用して開発を進めており、グループの基盤データベース構築プロジェクトでの採用を進めています。
※現在のグループではJsonPersisterHBaseの採用を進めています。
WEBで提供するほとんどのサービスはこのフレームワークを利用することにより非常に高い効率で開発できると考えています。
目次概要序論目次内容
JsonPersisterの特徴モジュール構成
json_persister_corejson_persister_mysqljson_persister_mongojson_persister_hbase
シリアライズの流れ
![Page 2: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/2.jpg)
主要なインターフェースJsonPersister.saveJsonPersister.deleteJsonPersister.loadJsonPersister.listJsonPersister.createTableJsonPersister.dropTable
高度な使い方複合インデックス暗号化キャッシュ
各モジュールについてjson_persister_mysql
データ保存形式QuickStart
DIコンテナを利用しない場合springでの利用
json_persister_mongojson_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を例に挙げる
--------------------------------------------------------------------------------------
![Page 3: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/3.jpg)
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; }
![Page 4: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/4.jpg)
public Date getDate() { return date; } public void setDate(Date date) { this.date = date;
![Page 5: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/5.jpg)
}}
上記の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オブジェクトを取得します
![Page 6: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/6.jpg)
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);
高度な使い方
![Page 7: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/7.jpg)
複合インデックス
以下のようなダミーメソッドを作成して@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する必要があります
![Page 8: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/8.jpg)
//保存SecureInfo secureInfo = new SecureInfo();secureInfo.setUserId("hogehoge");secureInfo.setMailAddress("[email protected]");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
![Page 9: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/9.jpg)
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
ドキュメント整備中
![Page 10: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/10.jpg)
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.jsphazelcastではネットワークを介したクラスタ上でのグローバルロックを実現することも可能です。http://www.hazelcast.com/docs/2.0/manual/single_html/#Lock
![Page 11: WEB開発を加速させる。アジャイル開発に最適なデータ構造とORマッパの形](https://reader037.vdocuments.net/reader037/viewer/2022100420/554a133ab4c9055c598b4cd4/html5/thumbnails/11.jpg)
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?HowFriendFeedUsesMySqlToStoreSchemaLessData