今更ながら、ORMLiteを使ってアドレス帳アプリを作ってみた

Androidで使えるORMライブラリについて調べて、ORMLiteを使ってアドレス帳アプリをサンプルとして作ってみました。ORMLiteを使うのは今更感はありますが。

ORMライブラリについては、@pside氏の 天下一「AndroidのORM」武道会 の記事が詳しいです。主要なライブラリは、次の通りで、私が調べたFull text search 3(FTS3)のサポート状況を付け加えます。

  • ORMLite: 直接なサポートはないが、raw queyインターフェエースを使うといいという話。 FTS3 searches in ORMLite?
  • ActiveAndroid: full text searchはサポートされているようだ。 Add FTS support #272
  • greenDAO: サポートされていない。
  • Ollie: 調査せず
  • SugarORM: サポートの有無不明。
  • DBFlow: サポートの有無不明。
  • Realm: サポートせず。中の人によると、Coreでタスクリストにあるとのこと。
  • couchbase: サポートせず。

上で引用した @pside 氏の記事によると、NoSQLのRealmは極端に処理が速いとのことですが、FTS3をサポートしていません。SQLiteを使っているGreenDaoは、SQLiteを使っているORMの中で一番処理が速いとのことですが、これもFTS3をサポートしていません。私が調べた限りでは、FTS3が使えるのは、ORMLiteとActiveRecordですが、@pside氏の記事では、ORMLiteの方が若干性能がいいようなので、今回は、ORMLiteを使いました。

今回は、ORMLiteを使ってアドレス帳のアプリをサンプルとして作りました。上で、FTS3について言及していますが、今回は、FTS3の機能は使わず、データベースのテーブルに関連がある場合のアプリの実装の練習をしました。題材は、アドレス帳で、テーブルは、2つあります。1つめのテーブルは、personというテーブルで、カラムにid、name、addressを持ちます。もう1つのテーブルは、addressというテーブルで、カラムに、id、zipcode、prefecture、city、otherを持ちます。2つのテーブルは、1対1の関係にあります。

まず、Intellijで、Gradle: Android Moduleで新規プロジェクトを作り、app/build.gradleのdependenciesに

compile 'com.j256.ormlite:ormlite-android:4.48'

を追加します。

エンティティクラスは、

Person.java

@DatabaseTable(tableName = "person")
public class Person {

    @DatabaseField(generatedId = true)
    private Integer id;
    @DatabaseField
    private String name;
    @DatabaseField(foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true)
    private Address address;

    public Person() {
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

Address.java

@DatabaseTable(tableName = "address")
public class Address {

    @DatabaseField(generatedId = true)
    private Integer id;
    @DatabaseField
    private String zipcode;
    @DatabaseField
    private String prefecture;
    @DatabaseField
    private String city;
    @DatabaseField
    private String other;


    public Address() {
    }

    public Integer getId() {
        return id;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    public String getPrefecture() {
        return prefecture;
    }

    public void setPrefecture(String prefecture) {
        this.prefecture = prefecture;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getOther() {
        return other;
    }

    public void setOther(String other) {
        this.other = other;
    }
}

です。フィールドには、@DatabaseFieldアノーテーションを付け、自動生成する主キーには、generatedId = trueを付けます。また、Personは、Addressを持っているので、Personクラスで、Address型のフィールドが、関連する永続化フィールドであることをしめすために、foregin = trueを付けます。あと、foreignAutoCreate = trueは、関連に設定されたエンティティを自動で生成するために、foreignAutoRefresh = trueは、クエリが発行された際に自動で更新するために付けます。

次に、com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper を継承したクラスを作ります。

DatabaseHelper.java

public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
    private final static String TAG = DatabaseHelper.class.getSimpleName();

    private final static String DATABASE_NAME = "addressbook.db";
    private final static int DATABASE_VERSION = 1;

    private Dao<Person, Integer> mPersonDao;
    private Dao<Address, Integer> mAddressDao;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
        try {
            // エンティティを指定してcreate tableする
            TableUtils.createTable(connectionSource, Person.class);
            TableUtils.createTable(connectionSource, Address.class);
        } catch (SQLException e) {
            Log.e(TAG, "データベースを作成できませんでした", e);
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {
        // 省略
    }

    public Dao<Person, Integer> getPersonDao() throws SQLException {
        if (mPersonDao == null) {
            mPersonDao = getDao(Person.class);
        }
        return mPersonDao;
    }

    public Dao<Address, Integer> getAddressDao() throws SQLException {
        if (mAddressDao == null) {
            mAddressDao = getDao(Address.class);
        }
        return mAddressDao;
    }
}

onCreateメソッドとonUpgradeメソッドをオーバーライドするのは、SQLiteの場合と同じですが、onCreateメソッドでは、TableUtilsクラスを使ってテーブルを生成します。また、ORMLiteでは、Daoクラスの生成はコストがかかるということで、それぞれ、PersonのDaoとAddressのDaoオブジェクトがないときに限り、getPersonDaoとgetAddressDaoメソッドは、getDaoメソッドでDaoオブジェクトを作り値を返し、Daoオブジェクトがある場合は、それを返します。

そして、各アクティビティで、Daoメソッドを使うため、Applicationクラスを継承したクラスで、DatabaseHelperオブジェクトを返すメソッドを定義します。

AddressApplication.java

public class AddressApplication extends Application {
    private static AddressApplication instance;
    private DatabaseHelper mDatabaseHelper;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }

    public static AddressApplication getInstance() {
        return instance;
    }

    public DatabaseHelper getDatabaseHelper() {
        if (mDatabaseHelper == null) {
            mDatabaseHelper = new DatabaseHelper(this);
        }
        return mDatabaseHelper;
    }
}

最後に、アプリのメイン画面は、名前リストで、アクティビティは、

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private final static String TAG = MainActivity.class.getSimpleName();

    private List<String > mList = new ArrayList<String>();
    private ListView mListView;
    private DatabaseHelper mHelper;

    public final static String TYPE = "type";
    public final static String CREATE_DATA_TYPE = "create";
    public final static String UPDATE_DATA_TYPE = "update";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHelper = AddressApplication.getInstance().getDatabaseHelper();

        mListView = (ListView)findViewById(android.R.id.list);

        mList = getAllList();

        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, R.layout.rowdata, mList);
        mListView.setAdapter(arrayAdapter);

        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                ListView listView = (ListView)parent;
                String name = (String)listView.getItemAtPosition(position);
                Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
                intent.putExtra(TYPE, UPDATE_DATA_TYPE);
                intent.putExtra("name", name);
                startActivity(intent);
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);

        MenuItem item = menu.add("NEW");
        item.setIcon(android.R.drawable.ic_input_add);
        item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem menuItem) {
                Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
                intent.putExtra(TYPE, CREATE_DATA_TYPE);
                startActivity(intent);

                return false;
            }
        });
        MenuItemCompat.setShowAsAction(item,
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public List<String> getAllList() {
        List<String> list  = new ArrayList<String>();
        try {
            List<Person> persons =  mHelper.getPersonDao().queryForAll();
            for (Person person: persons) {
                list.add(person.getName());
            }
            return list;
        } catch (SQLException e) {
            Log.e(TAG, "例外が発生しました", e);
            return null;
        }
    }

    @Override
    protected void onRestart() {
        mListView = (ListView)findViewById(android.R.id.list);

        mList = getAllList();

        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(this, R.layout.rowdata, mList);
        mListView.setAdapter(arrayAdapter);

        super.onRestart();
    }
}

リストは、名前のリストなので、

public List<String> getAllList() {
        List<String> list  = new ArrayList<String>();
        try {
            List<Person> persons =  mHelper.getPersonDao().queryForAll();
            for (Person person: persons) {
                list.add(person.getName());
            }
            return list;
        } catch (SQLException e) {
            Log.e(TAG, "例外が発生しました", e);
            return null;
        }
    }

のように、mHelperからPersonのDaoを得て、全件取得 queryForAllを実行しています。

メイン画面のActionBarのメニューのプラスアイコンをタップすると、名前、アドレス入力画面に遷移し、リストに名前がある時、リストの行をタップすると、アドレス更新画面に遷移します。遷移先のアクティビティのクラスは、次のように定義されています。

RegisterActivity.java

public class RegisterActivity extends AppCompatActivity
        implements View.OnClickListener {
    private final static String TAG = RegisterActivity.class.getSimpleName();

    private DatabaseHelper mHelper;
    private Button mBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);

        mHelper = AddressApplication.getInstance().getDatabaseHelper();

        mBtn = (Button)findViewById(R.id.btn);
        mBtn.setOnClickListener(this);

        String type = getIntent().getStringExtra(MainActivity.TYPE);

        Resources resources = getResources();
        if (type.equals(MainActivity.CREATE_DATA_TYPE)) {
            mBtn.setText(resources.getString(R.string.register));
        } else if (type.equals(MainActivity.UPDATE_DATA_TYPE)) {
            mBtn.setText(resources.getString(R.string.update));
            showAddress();
        }
    }

    @Override
    public void onClick(View view) {
        int id = view.getId();
        if (id == R.id.btn) {
            String type = getIntent().getStringExtra(MainActivity.TYPE);
            if (type.equals(MainActivity.CREATE_DATA_TYPE)) {
                registerAddressBook();
            } else if (type.equals(MainActivity.UPDATE_DATA_TYPE)) {
                updateAddressBook();
            }
        }
    }

    public void registerAddressBook() {
        String name = ((EditText)findViewById(R.id.nameEt)).getText().toString();
        String zipcode = ((EditText)findViewById(R.id.zipcodeEt)).getText().toString();
        String prefecture = ((EditText)findViewById(R.id.prefectureEt)).getText().toString();
        String city = ((EditText)findViewById(R.id.cityEt)).getText().toString();
        String other = ((EditText)findViewById(R.id.otherEt)).getText().toString();

        if (!checkDatum(name, zipcode, prefecture, city, other)) {
            return;
        }

        if (isExists(name)) {
            Toast.makeText(this, name + " has been registered already or some error occurs.", Toast.LENGTH_LONG).show();
            return;
        }

        Address address = new Address();
        address.setZipcode(zipcode);
        address.setPrefecture(prefecture);
        address.setCity(city);
        address.setOther(other);
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        try {
            mHelper.getPersonDao().create(person);
            finish();
        } catch (SQLException e) {
            Toast.makeText(this, "cannot register address.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
        }
    }

    public void updateAddressBook() {
        String name = ((EditText)findViewById(R.id.nameEt)).getText().toString();
        String zipcode = ((EditText)findViewById(R.id.zipcodeEt)).getText().toString();
        String prefecture = ((EditText)findViewById(R.id.prefectureEt)).getText().toString();
        String city = ((EditText)findViewById(R.id.cityEt)).getText().toString();
        String other = ((EditText)findViewById(R.id.otherEt)).getText().toString();

        if (!checkDatum(name, zipcode, prefecture, city, other)) {
            return;
        }

        try {
            Person person = mHelper.getPersonDao().queryForEq("name", name).get(0);
            Address address = person.getAddress();
            address.setZipcode(zipcode);
            address.setPrefecture(prefecture);
            address.setCity(city);
            address.setOther(other);
            mHelper.getAddressDao().update(address);
            mHelper.getPersonDao().update(person);
            finish();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }
    }

    public void showAddress() {
        String type = getIntent().getStringExtra(MainActivity.TYPE);
        if (!type.equals(MainActivity.UPDATE_DATA_TYPE)) return;

        String name = getIntent().getStringExtra("name");

        List<Person> list = null;
        Person person;
        Address address;

        try {
            list = mHelper.getPersonDao().queryForEq("name", name);
            person = list.get(0);
        } catch (SQLException e) {
            // ありえない
            Log.e(TAG, e.getMessage());
            return;
        }

        address = person.getAddress();

        String zipcode = address.getZipcode();
        String prefecture = address.getPrefecture();
        String city = address.getCity();
        String other = address.getOther();

        ((EditText)findViewById(R.id.nameEt)).setText(name);
        ((EditText)findViewById(R.id.nameEt)).setEnabled(false);
        ((EditText)findViewById(R.id.zipcodeEt)).setText(zipcode);
        ((EditText)findViewById(R.id.prefectureEt)).setText(prefecture);
        ((EditText)findViewById(R.id.cityEt)).setText(city);
        ((EditText)findViewById(R.id.otherEt)).setText(other);
    }

    public boolean isExists(String name) {
        List<Person> list = null;
        try {
            list = mHelper.getPersonDao().queryForEq("name", name);
            if (list.isEmpty()) {
                return false;
            } else {
                return true;
            }
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
            return false;
        }

    }

    public boolean checkDatum(String name, String zipcode, String prefecture,
                           String city, String other) {
        if (TextUtils.isEmpty(name)) {
            Toast.makeText(this, "name is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(zipcode)) {
            Toast.makeText(this, "zipcode is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(prefecture)) {
            Toast.makeText(this, "prefecture is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(city)) {
            Toast.makeText(this, "city is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(other)) {
            Toast.makeText(this, "other is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }
}

新規にアドレスを入力するケースでは、

        Address address = new Address();
        address.setZipcode(zipcode);
        address.setPrefecture(prefecture);
        address.setCity(city);
        address.setOther(other);
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        try {
            mHelper.getPersonDao().create(person);
            finish();
        } catch (SQLException e) {
            Toast.makeText(this, "cannot register address.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
        }

のように、まず、Addressオブジェクトを作り、住所を入れ、次に、Personオブジェクトを作り、名前と住所を入れます。そして、mHeperのPersonに関するDaoオブジェクトを得て、createメソッドの引数にpersonを渡して、データ登録をします。

データ更新のケースでは、簡単のため、名前の変更はないものとして、次のようにデータ更新を行います。

        try {
            Person person = mHelper.getPersonDao().queryForEq("name", name).get(0);
            Address address = person.getAddress();
            address.setZipcode(zipcode);
            address.setPrefecture(prefecture);
            address.setCity(city);
            address.setOther(other);
            person.setAddress(address);
            mHelper.getAddressDao().update(address);
            mHelper.getPersonDao().update(person);
            finish();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }

名前の変更はないし、同じ名前を入力しようとすると、警告を表示してデータ登録をできない実装にしているため、メイン画面で名前をタップすると、当然データがあるので、mHelper.getPersonDao().queryForEq(“name”, name)は1個しか要素を含まないListを返すので、get(0)でPersonオブジェクトを得ます。次に、Personオブジェクトから、getAddressメソッドで住所を得ます。そして、更新のため入力された住所情報でAddressオブジェクトを更新シ、さらに、更新された住所でPersonオブジェクトを更新します。mHelper.getAddressDao().update(address)とmHelper.getPersonDao().update(person)でデータベースのデータを更新します。

作ったサンプルを

andropenguin/AddressBookORMlite

に公開します。

参考サイト
天下一「AndroidのORM」武道会
Ormlite for Android Using Ormlite for clean and efficient database managment
OrmLite調査メモ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です