LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


Androidで使えるOR Mapper: ORMLite

こんにちは。開発チームの駒津です。

ここ半年ほど、弊社アプリLINEのAndroid版を開発しています。関係者一同の頑張りもあってAndroidユーザー 100万人達成という非常にうれしい状況なのですが、かなりのハイスピードで開発が進みましたのであまり冒険せずに、力技で少し泥臭く実装している箇所もあります。

データベース周りも普通にSQLiteDatabase経由でSQL文を書いているのですが、できればOR Mapperを使いたかった... という反省点があり、現在開発状況が少し落ち着いた (のか...? 本当に...?) 今のうちにそっち方面を調べておこうかと思います。

Androidではそのスペックの都合上, 軽く動作するOR Mapperが向いていそうです。そういう視点で色々探して見たところORMLiteが良さそうな気がしました。正式にAndroidに対応していると謳っているのも嬉しいところ。

ORMLiteの他にはActiveAndroidというActiveRecord系のものがありました。Ruby on Railsの経験が長かった私は結構興味を持ったのですがオープンソースでは無いため選択肢から除外しました (中の実装が見れないと何かあったときに困る...)。

それ以外にも、NeoDatis, ORMANなどの名前も見かけたのですが、前者はObject Oriented Databaseで方向性が異なる (これ自体は興味ありますが...) ことと開発が停滞しているっぽいこと、後者はまだ機能的にこなれていない感 (後でちゃんと評価したい...) があるので外しました。

ということで、以降はORMLiteの使い方と、試してみた内容について簡単に書いていきます。

Jars

以下のjarをdownloadして適当なところに置いてBuild Pathを通しましょう。

Sample Code

テーブルの生成や、Daoの簡単な使い方を含んだサンプルコードを以下に記します。ページの都合上、強引にActivityに詰め込んでいますがご容赦ください。

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 members;

        Project() {}
        public Project(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public ForeignCollection 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 projectDao = helper.getDao(Project.class);
            final Dao 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(); }

        }
    }
}

主要なAnnotationとしては以下のものがあります。それぞれ"generatedId = true"のように属性を指定することにより細かい設定が可能です。

  • データベースのテーブルに対応する@DatabaseTable
  • テーブルのカラムに対応する@DatabaseField
  • 一対多の関連をあらわす@ForeignCollectionField

そして、TableUtilsのdropTable(), createTable()でそれぞれDROP TABLE, CREATE TABLE文が発行されます。Daoのcreate(), queryXXX()メソッドでは、レコードのINSERT, SELECT文が発行できます。

このSample 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

あと、気になるのはORMLiteを使うことによる性能の劣化です。ザザッと以下のようなActivityを書いて、素のDatabase Accessと比較してみました。単純に1000件のレコードをinsert, update, selectしています。

import文やmodelは省略~

public class ORMLiteSampleActivity extends Activity {
    private static final String TAG = ORMLiteSampleActivity.class.getSimpleName();
    private static final int USER_SIZE = 1000;

    private List runTestInsert(Dao dao) throws SQLException {
        final User user = new User();
        final List ids = new ArrayList(USER_SIZE);
        for (int i = 0; i < USER_SIZE; i++) {
            user.setName(String.valueOf(i));
            dao.create(user);
            ids.add(user.getId());
        }
        return ids;
    }

    private void runTestSelect(Dao dao, List ids) throws SQLException {
        for (int id: ids) {
            dao.queryForId(id);
        }
    }

    private void runTestUpdate(Dao dao, User user) throws SQLException {
        for (int i = 0; i < USER_SIZE; i++) {
            user.setName(String.valueOf(i));
            dao.update(user);
        }
    }

    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 dao = helper.getDao(User.class);

            // test: insert long start = System.currentTimeMillis(); List 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();
            }
        });

    }
}

このORMLite使用版と、SQLiteDatabaseを直接使ったもの (こっちのソースコードは省略... selectは毎回cursorをclose.) を三回ずつ実行した結果は以下のようになりました.

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