概要
ORM またはオブジェクト リレーショナル マッピングは、オブジェクト指向モデルを使用してデータベースのデータをクエリおよび操作できるようにする手法です。 これにより、SQL ステートメントではなく、オブジェクトを介してデータベース上のデータを直接操作できます。
ORM を使用しない場合の例を次に示します。
String sql = "INSERT INTO t_person VALUES (username=\"buithanh\", password=\"password\")";
database.exeSQL(sql);
ORMを使用する場合
Person p = new Person("buithanh", "password");
p.save();
メリット
デメリット
ORM ライブラリ
ORM ライブラリの構築
この記事では、Java 言語でAndroid プラットフォームのみで ORM ライブラリをビルドする方法のみを紹介します。
ご存知のように、リレーショナル データベースはテーブルで構成されており、テーブルにはエンティティ (エンティティ) に関する情報を格納するレコードが含まれています。 OOP に関して言えば、すべてがオブジェクトであり、オブジェクトはリレーショナル データベースのレコードのような情報ストレージの単位でもあります。
したがって、クラス名はテーブル名に相当し、オブジェクトはレコードに相当し、その属性はフィールドに相当するという対応があることがわかります。 ライブラリをビルドする手順は次のとおりです。
新しいアノテーションタイプを作成する
アノテーションは、プログラムに関する情報を提供するメタデータの形式であり、アノテーションを付けるコードには直接影響しません。 アノテーションを使用できるオブジェクトは、クラス、メソッド、フィールド、パラメーター、およびパッケージです。
Annotationを通じて、どのクラスがどのテーブルに対応するかを知ることができますか? その属性はどのフィールドに対応していますか? また、フィールドにはどのようなプロパティがありますか? 「PRIMARY KEY」、「NOT NULL」、「UNSIGNED」など...
クラスのアノテーションを作成する
@Documented
@Target(ElementType.TYPE)
@Inherited
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Table {
public static final String ID_NAME = "id";
public String id() default ID_NAME;
public String name();
}
注: 実行時にこの情報を取得するには、実行時に @Retention アノテーションと値 RetentionPolicy.RUNTIME を使用する必要があります。
フィルドのアノテーションを作成する
@Documented
@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public static String BLANK = "";
public String name() default BLANK;
}
上記で、属性がどのフィールド名に対応するかという簡単なアノテーションを定義しました。 もちろん、使用するライブラリのサポート レベルに応じて、属性を説明する他の多くの属性を作成できます。たとえば、キー、デフォルト、エクストラなどです。
モデルを作成する
ビジネス モデルを構築し、アノテーションを介してそのモデルをデータベース テーブルにアタッチします。 ここでは、対応するテーブル名 t_person
とその属性を持つ Person
という名前のモデルを作成します。
@Table(id="t_id", name="t_person")
public class Person extends Model {
@Column(name="t_username")
public String username;
@Column(name="t_password")
public String password;
...
// Similar to defining other attributes
}
モデル情報を取得する
リフレクションは、Java プログラムが自分自身を検査できるようにする Java プログラミング言語の機能です。実行時に自分自身を検査し、クラス、メソッド、または属性を自動的に呼び出すことができます。
リフレクションを使用して、上記で説明したアノテーションを通じて、モデルのテーブル名、主キー、フィールド名などの情報を取得します。
// The class stores information about the model such as ids or fields
public class ModelManager {
protected Class<? extends Model> type;
protected String id;
protected String name;
// List stores the list of fields
private List<Field> columns = new ArrayList<>();
// Contructor
public ModelManager(Class<? extends Model> type) {
this.type = type;
Table an = type.getAnnotation(Table.class);
if (an != null) {
id = an.id();
name = an.name();
} else {
id = Table.ID_NAME;
name = type.getSimpleName();
}
columns.add(this.getIdField(type));
columns.addAll(this.getColumnFieldsWithoutId(type));
}
// Get primary key
protected Field getIdField(Class type) {
if (Model.class.equals(type)) {
try {
return type.getDeclaredField(SQLiteUtils.ID);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
} else if (type.getSuperclass() != null) {
return getIdField(type.getSuperclass());
}
return null;
}
// Get a list of fields that do not include the primary key with annotation
public List<Field> getColumnFieldsWithoutId(Class<?> type) {
Field[] fields = type.getDeclaredFields();
List<Field> columns = new ArrayList<>();
for (Field field : fields) {
if (field.isAnnotationPresent(Column.class)) {
columns.add(field);
}
}
Class<?> parentType = type.getSuperclass();
if(parentType != null) {
columns.addAll(getColumnFieldsWithoutId(parentType));
}
return columns;
}
}
モデルの操作メソッドを作成する
リフレクションを使用すると、ここで頻繁に使用する SET メソッドを使用せずに、プロパティの値を自動的に自動的に設定することもできます。
オブジェクトをデータベースに保存する
private final long ID_NOT_SET = -1;
public void save() {
// Get the list of fields of the table
List<Field> columns = table.getColumnFields();
ContentValues values = new ContentValues(table.getSize());
String colName;
for (Field column : columns) {
column.setAccessible(true);
if(SQLiteUtils.ID.equals(column.getName())) {
// In case of insert, the id will be not set
if(id == ID_NOT_SET) continue;
colName = table.id;
} else {
colName = column.getAnnotation(Column.class).name();
}
try {
Object obj = column.get(this);
values.put(colName, (obj == null) ? "" : String.valueOf(obj));
} catch (Exception ex) {
AppLog.log(ex.getLocalizedMessage());
}
}
if (id == ID_NOT_SET) {
id = Database.insert(table.getName(), values);
}
else {
int cnt = Database.update(table.getName(), values, table.id + "= ?", new String[] {String.valueOf(id)});
}
}
レコードを取得し、対応するモデルに変換します
データベースがサポートするデータ型は、使用しているプログラミング言語のデータ型にマップされない場合がありあますので、ここではデータ型ごとに SET を手動で処理する必要があります。
public static <T extends Model> T findById(Class<T> type, long id) {
T entity = null;
Cursor c = null;
try {
entity = type.newInstance();
c = Database.query(entity.table.getName(), null, entity.table.id + " = ?", new String[] { String.valueOf(id) });
while (c.moveToNext()) {
entity.getRecord(c);
break;
}
} catch (Exception ex) {
AppLog.log(ex.getLocalizedMessage());
} finally {
if (c != null) {
c.close();
}
}
return entity;
}
protected void getRecord(Cursor c) {
String typeString = null, colName;
for (Field field : table.getColumnFields()) {
field.setAccessible(true);
try {
typeString = field.getType().getName();
colName = (SQLiteUtils.ID.equals(field.getName())) ? table.id : field.getAnnotation(Column.class).name();
if (typeString.equals("java.lang.String")) {
String val = c.getString(c.getColumnIndex(colName));
field.set(this, val.equals("null") ? null : val);
} else if (typeString.equals("short")
|| typeString.equals("java.lang.Short")) {
field.set(this, c.getShort(c.getColumnIndex(colName)));
}
// We do the same with the remaining data types.
...
}
レコードを削除
public int delete() {
int toRet = Database.delete(table.getName(), table.id + " = ?", new String[] { String.valueOf(id) });
return toRet;
}
同様に、次のようなモデルの操作メソッドを作成します。
その他の機能
挿入、更新、削除などの基本的な操作を実行することに加えて、事前に作成したスクリプトに従って DB を自動的にアップグレードする機能を作成しました。 バージョンを管理し、何らかの理由でユーザーのデータの全部または一部を削除できるなどの重大な結果を回避するのに役立つため、これは非常に必要であると私は考えています。
public void upgradeFromSQLScript(SQLiteDatabase db, int oldVersion, int newVersion) {
int index = 1;
for (List<String> builder : QueryBuilder.getBuilder()) {
if(index > oldVersion && index <= newVersion) {
for (String sql : builder) {
try {
db.execSQL(sql);
} catch (Exception e) {
AppLog.log(e.getLocalizedMessage());
}
AppLog.log("Upgrade from sql script: " + sql);
}
}
index ++;
}
}
// QueryBuilder class
public static List<List<String>> getBuilder() {
List<List<String>> builder = new ArrayList<>();
// Version 1
List<String> ver1 = new ArrayList<>();
String TABLE_PERSON = "CREATE TABLE t_person (t_id INTEGER PRIMARY KEY AUTOINCREMENT, t_username text, t_password text)";
ver1.add(TABLE_PERSON);
builder.add(ver1);
// Version 2
List<String> ver2 = new ArrayList<>();
String ALTER_PERSON = "ALTER TABLE t_person ADD COLUMN dt_modified TIMESTAMP";
ver2.add(ALTER_PERSON);
builder.add(ver2);
// Same for later upgrade versions
...
return builder;
}
デモ
// Insert new record
Person p = new Person("buithanh", "password");
p.save();
Result:
Person: buithanh | password
// Update record
p.password = "12345678";
p.save();
Result:
Person: buithanh | 12345678
// Delete record
p.delete();
List<Person> persons = Model.findByColumn(Person.class, "t_username", "buithanh");
if (persons == null
|| persons.size() <= 0) {
AppLog.log("{buithanh} is not found!");
} else {
AppLog.log("{buithanh} count: " + persons.size());
}
Result:
{buithanh} is not found!
結論
私が作成したライブラリは、非常に基本的な操作しか実行できず、複雑なコマンドをサポートしておらず、多くの付属ユーティリティがありません。 私のソースコードはこちらで公開されています。Active Record-Android
参考資料
https://docs.oracle.com/javase/tutorial/java/annotations/index.html
https://docs.oracle.com/javase/tutorial/reflect/index.html