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を少し試してみた内容を書いていきます。

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開発者 (私も含め) が増えてくるのではないかと期待しています。