ORMLite使ってみたメモ
ここ半年位、Android Appを開発しています。関係者一同の頑張りもあって100万ユーザーという非常にうれしい状況なのですが、結構なハードスケジュールで開発が進んだので割と力技で泥臭く実装している箇所があったりします。Database周りもゴリゴリSQL文を書いてたりするのですが、できればOR Mapperを使いたかった... という反省点があり、少し落ち着いた (のか...? 本当に...?) 今のうちにそっち方面を調べておこうかと思います。
Androidではそのスペックの都合上, 軽く動作するOR Mapperが向いていそうです. そういう視点で色々探して見たところORMLite (http://ormlite.com/) が良さそうな気がしました.
ORMLiteの他にはActiveAndroid (https://www.activeandroid.com/) というActiveRecord系のものがありました. Ruby on Railsの経験が長かった私は結構興味を持ったのですがOpen sourceでは無いため選択肢から除外しました (中の実装が見れないと何かあったときに困るので...) .
その他, NeoDatis (http://neodatis.wikidot.com/), ORMAN (https://github.com/ahmetalpbalkan/orman) などの名前も見かけたのですが, 前者はObject Oriented Databaseで方向性が異なる (これ自体は興味ありますが...) ことと開発が停滞しているっぽいこと, 後者はまだ機能的にこなれていない感 (後でちゃんと評価したい...) があるので外しました.
ということで、以降はORMLiteを少し試してみた内容を書いていきます。
Jars
以下のjarをdownloadして適当なところに置いてBuild Pathを通せば使えます.
Sample code
package com.komamitsu.ormtest; # import statementsは省略〜 public class ORMLiteSample2Activity extends Activity { private static final String TAG = ORMLiteSample2Activity.class.getSimpleName(); @DatabaseTable private static class Project { @DatabaseField(generatedId = true) private Integer id; @DatabaseField private String name; @ForeignCollectionField private ForeignCollection<Member> members; Project() {} public Project(String name) { this.name = name; } public String getName() { return name; } public ForeignCollection<Member> getUsers() { return members; } } @DatabaseTable private static class Member { @DatabaseField(generatedId = true) private Integer id; @DatabaseField private String name; @DatabaseField(foreign = true, foreignAutoRefresh = true) private Project project; Member() {} public Member(Project project, String name) { this.project = project; this.name = name; } public String getName() { return name; } } private static class DatabaseHelper extends OrmLiteSqliteOpenHelper { public DatabaseHelper(Context context) { super(context, "hogehoge.db", null, 1); } @Override public void onCreate(SQLiteDatabase arg0, ConnectionSource connectionSource) {} @Override public void onUpgrade(SQLiteDatabase arg0, ConnectionSource arg1, int arg2, int arg3) {} } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); final DatabaseHelper helper = new DatabaseHelper(this); try { TableUtils.dropTable(helper.getConnectionSource(), Project.class, true); TableUtils.dropTable(helper.getConnectionSource(), Member.class, true); TableUtils.createTable(helper.getConnectionSource(), Project.class); TableUtils.createTable(helper.getConnectionSource(), Member.class); final Dao<Project, Integer> projectDao = helper.getDao(Project.class); final Dao<Member, Integer> memberDao = helper.getDao(Member.class); // create projects final Project projectA = new Project("Project A"); final Project projectB = new Project("Project B"); projectDao.create(projectA); projectDao.create(projectB); // create members memberDao.create(new Member(projectA, "Steve Jobs")); memberDao.create(new Member(projectA, "Steve Wozniak")); memberDao.create(new Member(projectB, "Dennis Ritchie")); memberDao.create(new Member(projectB, "John McCarthy")); // display all the projects and members for (Project project : projectDao.queryForAll()) { Log.d(TAG, "project=" + project.getName()); for (Member member : project.getUsers()) { Log.d(TAG, "member=" + member.getName()); } } } catch (SQLException e) { e.printStackTrace(); } } }
などとActivityを書いて実行させると, 以下のようなログが吐かれていることが確認できます。とても簡単にOneToManyの関連ができますね。
I/TableUtils( 370): dropping table 'project' I/TableUtils( 370): executed drop table statement changed 1 rows: DROP TABLE `project` I/TableUtils( 370): dropping table 'member' I/TableUtils( 370): executed drop table statement changed 1 rows: DROP TABLE `member` I/TableUtils( 370): creating table 'project' I/TableUtils( 370): executed create table statement changed 1 rows: CREATE TABLE `project` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `name` VARCHAR ) I/TableUtils( 370): creating table 'member' I/TableUtils( 370): executed create table statement changed 1 rows: CREATE TABLE `member` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `name` VARCHAR , `project_id` INTEGER ) D/ORMLiteSample2Activity( 370): project=Project A D/ORMLiteSample2Activity( 370): member=Steve Jobs D/ORMLiteSample2Activity( 370): member=Steve Wozniak D/ORMLiteSample2Activity( 370): project=Project B D/ORMLiteSample2Activity( 370): member=Dennis Ritchie D/ORMLiteSample2Activity( 370): member=John McCarthy
Performance
あと、気になるのは性能の劣化なのですが、簡単に以下のようなActivityを書いて、素のDatabase Accessと比較してみました.
# import文やmodelは省略〜 public class ORMLiteSampleActivity extends Activity { private static final String TAG = ORMLiteSampleActivity.class.getSimpleName(); private static final int USER_SIZE = 1000; private List<Integer> runTestInsert(Dao<User, Integer> dao) throws SQLException { final User user = new User(); final List<Integer> ids = new ArrayList<Integer>(USER_SIZE); for (int i = 0; i < USER_SIZE; i++) { user.setName(String.valueOf(i)); dao.create(user); ids.add(user.getId()); if (i % 200 == 0) dumpMemInfo(); } return ids; } private void runTestSelect(Dao<User, Integer> dao, List<Integer> ids) throws SQLException { for (int id : ids) { dao.queryForId(id); if (id % 200 == 0) dumpMemInfo(); } } private void runTestUpdate(Dao<User, Integer> dao, User user) throws SQLException { for (int i = 0; i < USER_SIZE; i++) { user.setName(String.valueOf(i)); dao.update(user); if (i % 200 == 0) dumpMemInfo(); } } private void runTest() { final DatabaseHelper helper = new DatabaseHelper(this); Log.i(TAG, "start"); try { TableUtils.dropTable(helper.getConnectionSource(), User.class, true); TableUtils.createTable(helper.getConnectionSource(), User.class); final Dao<User, Integer> dao = helper.getDao(User.class); // test: insert long start = System.currentTimeMillis(); List<Integer> ids = runTestInsert(dao); long end = System.currentTimeMillis(); Log.i(TAG, "insert: " + (end - start)); final Random rand = new Random(); for (int i = 0; i < USER_SIZE; i++) ids.add(rand.nextInt(USER_SIZE)); // test: select start = System.currentTimeMillis(); runTestSelect(dao, ids); end = System.currentTimeMillis(); Log.i(TAG, "select: " + (end - start)); // test: update final User targetUser = dao.queryForId(ids.get(0)); start = System.currentTimeMillis(); runTestUpdate(dao, targetUser); end = System.currentTimeMillis(); Log.i(TAG, "update: " + (end - start)); } catch (SQLException e) { e.printStackTrace(); } finally { Log.i(TAG, "end"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { runTest(); } }); } private String stringOfMemInfo() { final MemoryInfo mem = new MemoryInfo(); Debug.getMemoryInfo(mem); return new StringBuilder().append("dalvikPrivateDirty=").append(mem.dalvikPrivateDirty).append(", dalvikPss=") .append(mem.dalvikPss).append(", dalvikSharedDirty=").append(mem.dalvikSharedDirty) .append(", nativePrivateDirty=").append(mem.nativePrivateDirty).append(", nativePss=").append(mem.nativePss) .append(", nativeSharedDirty=").append(mem.nativeSharedDirty).append(", otherPrivateDirty=") .append(mem.otherPrivateDirty).append(", otherPss=").append(mem.otherPss).append(", otherSharedDirty=") .append(mem.otherSharedDirty).append(", totalPrivateDirty=").append(mem.getTotalPrivateDirty()) .append(", totalPss=").append(mem.getTotalPss()).append(", totalSharedDirty=") .append(mem.getTotalSharedDirty()).toString(); } private void dumpMemInfo() { Log.i(TAG, "meminfo: " + stringOfMemInfo()); } }
このSample Activityと、SQLiteDatabaseを直接使ったもの (こっちは省略...) を三回ずつ実行した結果は以下のようになりました.
Normal (SQLiteDatabase)
insert (ms) | update (ms) | select (ms) | |
---|---|---|---|
1st | 50600 | 19993 | 779 |
2nd | 51690 | 20551 | 691 |
3rd | 51011 | 19627 | 720 |
average | 51100 | 20057 | 730 |
stddev | 550 | 465 | 45 |
ORMLite
insert (ms) | update (ms) | select (ms) | |
---|---|---|---|
1st | 50194 | 19702 | 1125 |
2nd | 49943 | 19714 | 1017 |
3rd | 50009 | 19069 | 1311 |
average | 50049 | 19495 | 1151 |
stddev | 130 | 369 | 149 |
Selectの性能はORMLiteを使うことによって50%以上の性能の低下が見られる一方、InsertとUpdateは逆にORMLiteのほうが若干性能が上がっているような (ざわざわ) ... ま、まぁ後で中の実装を見て何か工夫しているのか確認してみようと思いますが、Selectの性能が致命的なボトルネックになるようなアプリでなければ、通常の用途では性能面で問題無いようですね。
ということで、ひとまず簡単にORMLiteに触ってみたのですが、SQLiteDatabaseを直に触るよりはずっと楽だし、コードの見通しも良くなりそうです。まぁ業務等で正式に採用するためにはもう少ししっかりした評価が必要かとは思いますけれども。
この先、ORMLiteで幸せになれるAndroid App開発者 (私も含め) が増えてくるのではないかと期待しています。