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調査メモ