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