오래간만에 블로그를 쓴다.
9월달 회사에서 사이즈가 큰 과제를 했기에 블로그에 뭔가를 쓸 여력이 없었다. 그럼에도 간간히 책도 5권 정도 읽었고11월 말 부터는 앱을 개발하기위해서 상당히 많은 코드를 새롭게 작성을 했다.
예전에 Database를 쉽게 사용하기 위해서 Orm 모듈을 자체 구현하였다. OrmObejct를 이용하여 Table을 정의하면 OrmHelper, OrmUtils를 이용해서 데이타베이스 관련 코드를 중복으로 작성하지 않아도 쉽게 구현할 수 있었으나, 실제 적용을 하다보니 잘 맞지 않는다는 느낌이다. 가장 큰 이유는 예전에 작성해 놓은 Activity페이지를 동일하게 Orm을 이용하여 구현을 했는데 작성된 소스코드의 라인 수가 거의 줄지를 않았다. 30~40% 이상 코드작성의 번거로움이 줄어들기를 기대했는데 그렇지 않았고, OrmUtils를 통해서 전달되니 실제 구현의 내부는 더 복잡하게만 느껴진다. 그래서 고민끝에 Column, ColumnBuilder, Table, TableBuilder, BaseDto, BaseDao, ModuleManager 라는 여러 클래스를 새로 작성해서 Annotation 처리를 없애고 더 간단하고 명확하게 구현을 했다. 원하는 코딩라인수도 더 줄일수 있었다. OrmUtils를 구현하면서 annotation을 심도있게 공부하게 된 것은 좋은 공부가 되었다.
public static final ColumnString ID = new ColumnBuilder("id").primaryKey().buildString();
public static final ColumnString VALUE = new ColumnBuilder("value").notNull().buildString();
public static final ColumnString EXTRA = new ColumnBuilder("extra").buildString();
public static final Table TABLE = new TableBuilder("common_settings")
.add(ID)
.add(VALUE)
.add(EXTRA)
.setId(ID)
.build();
public class SettingsDao extends BaseDao implements BaseModule {
final CursorToDtoConverter<SettingDto> cursorToDtoConverter = new CursorToDtoConverter<SettingDto>() {
@Override
public SettingDto cursorToDto(Cursor cursor) {
SettingDto dto = new SettingDto();
dto.setKey(ID.getString(cursor));
dto.setValue(VALUE.getString(cursor));
return dto;
}
};
public String select(@NonNull String key, @NonNull String defaultValue) {
SettingDto dto = select(key, cursorToDtoConverter);
if (dto != null) {
return dto.getValue();
} else {
return defaultValue;
}
}
public void updateAsync(@NonNull String key, String value) {
final ContentValues values = new ContentValuesBuilder()
.put(ID, key)
.put(VALUE, value).build();
CommonExecutors.executeBySerial(new Runnable() {
@Override
public void run() {
insertOrUpdate(values);
}
});
}
}
대부분의 구현은 BaseDao의 구현을 따른다.
package com.mdiwebma.base.db;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.Nullable;
import com.mdiwebma.base.AppModules;
import com.mdiwebma.base.db.schema.Column;
import com.mdiwebma.base.db.schema.Table;
import com.mdiwebma.base.debug.DebugUtils;
/**
* @author djkim
*/
public abstract class BaseDao {
public static final String TAG = "BaseDao";
protected BaseSQLiteHelper getDbHelper() {
return AppModules.getDbHelper();
}
abstract protected Table getTable();
protected String table() {
return getTable().name;
}
protected String[] columns() {
return getTable().columns;
}
protected String id() {
return getTable().id;
}
protected long insert(ContentValues values) {
return getDbHelper().getWritableDatabase().insert(table(), null, values);
}
protected long insert(String table, ContentValues values) {
return getDbHelper().getWritableDatabase().insert(table, null, values);
}
protected long insertOrUpdate(ContentValues values) {
return getDbHelper().getWritableDatabase().insertWithOnConflict(table(), null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
protected long insertOrUpdate(String table, ContentValues values) {
return getDbHelper().getWritableDatabase().insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
protected long update(String id, ContentValues values) {
return getDbHelper().getWritableDatabase().update(table(), values, id() + "=?", new String[]{id});
}
protected long update(String table, String id, ContentValues values) {
return getDbHelper().getWritableDatabase().update(table, values, id() + "=?", new String[]{id});
}
protected long update(long id, ContentValues values) {
return getDbHelper().getWritableDatabase().update(table(), values, id() + "=?", new String[]{String.valueOf(id)});
}
protected long update(String table, long id, ContentValues values) {
return getDbHelper().getWritableDatabase().update(table, values, id() + "=?", new String[]{String.valueOf(id)});
}
protected long delete(String id) {
return getDbHelper().getWritableDatabase().delete(table(), id() + "=?", new String[]{id});
}
protected long delete(String table, String id) {
return getDbHelper().getWritableDatabase().delete(table, id() + "=?", new String[]{id});
}
protected long delete(long id) {
return getDbHelper().getWritableDatabase().delete(table(), id() + "=?", new String[]{String.valueOf(id)});
}
protected long delete(String table, long id) {
return getDbHelper().getWritableDatabase().delete(table, id() + "=?", new String[]{String.valueOf(id)});
}
protected interface CursorToDtoConverter<T extends BaseDto> {
T cursorToDto(Cursor cursor);
}
@Nullable
protected <T extends BaseDto> T select(long id, CursorToDtoConverter<T> converter) {
return select(String.valueOf(id), converter);
}
@Nullable
protected <T extends BaseDto> T select(String id, CursorToDtoConverter<T> converter) {
Cursor cursor = null;
try {
cursor = getDbHelper().getWritableDatabase().query(table(), columns(), id() + "=?", new String[]{id}, null, null, null);
DebugUtils.checkTrue(cursor.getCount() <= 1, "id=%s returns %d rows", id, cursor.getCount());
if (cursor.moveToFirst()) {
return converter.cursorToDto(cursor);
} else {
return null;
}
} catch (Exception ex) {
DebugUtils.notReached(ex);
return null;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Nullable
protected <T extends BaseDto> T selectBySql(String rawSql, String[] whereArgs, CursorToDtoConverter<T> converter) {
Cursor cursor = null;
try {
cursor = getDbHelper().getWritableDatabase().rawQuery(rawSql, whereArgs);
DebugUtils.checkTrue(cursor.getCount() <= 1, "%s returns %d rows", rawSql, cursor.getCount());
if (cursor.moveToFirst()) {
return converter.cursorToDto(cursor);
} else {
return null;
}
} catch (Exception ex) {
DebugUtils.notReached(ex);
return null;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
protected <T extends BaseDto> List<T> selectList(String where, String[] whereArgs, String orderBy,
CursorToDtoConverter<T> converter) {
Cursor cursor = null;
List<T> result = Collections.EMPTY_LIST;
try {
cursor = getDbHelper().getWritableDatabase().query(table(), columns(), where, whereArgs, null, null, orderBy);
if (cursor.getCount() > 0) {
result = new ArrayList<T>();
while (cursor.moveToNext()) {
result.add(converter.cursorToDto(cursor));
}
}
return result;
} catch (Exception ex) {
DebugUtils.notReached(ex);
return result;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
protected <T extends BaseDto> List<T> selectListBySql(String rawSql, String[] whereArgs,
CursorToDtoConverter<T> converter) {
Cursor cursor = null;
List<T> result = Collections.EMPTY_LIST;
try {
cursor = getDbHelper().getWritableDatabase().rawQuery(rawSql, whereArgs);
if (cursor.getCount() > 0) {
result = new ArrayList<T>();
while (cursor.moveToNext()) {
result.add(converter.cursorToDto(cursor));
}
}
return result;
} catch (Exception ex) {
DebugUtils.notReached(ex);
return result;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
protected long getFirstLong(String sql, String[] whereArgs, long defaultValue) {
Cursor cursor = null;
try {
cursor = getDbHelper().getWritableDatabase().rawQuery(sql, whereArgs);
if (cursor.moveToFirst()) {
return cursor.getLong(0);
} else {
return defaultValue;
}
} catch (Exception ex) {
DebugUtils.notReached(ex);
return defaultValue;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
protected String getFirstString(String sql, String[] whereArgs, String defaultValue) {
Cursor cursor = null;
try {
cursor = getDbHelper().getWritableDatabase().rawQuery(sql, whereArgs);
if (cursor.moveToFirst()) {
return cursor.getString(0);
} else {
return defaultValue;
}
} catch (Exception ex) {
DebugUtils.notReached(ex);
return defaultValue;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
protected long getMaxValue(Column column) {
return getFirstLong(String.format("select max(%s) from %s", column.name, table()), null, 0);
}
protected int getNextOrder(Column column) {
return (int)getMaxValue(column) + 1;
}
protected String orderByDesc(Column column) {
return column.name + " DESC";
}
protected interface UpdateOrderListener<T extends BaseDto> {
boolean updateOrder(T dto, int newOrder, ContentValues contentValues);
}
public <T extends BaseDto> boolean updateOrderImpl(List<T> items, UpdateOrderListener<T> updateOrderListener) {
boolean changed = false;
if (items.isEmpty()) {
return changed;
}
SQLiteDatabase db = getDbHelper().getWritableDatabase();
db.beginTransaction();
try {
int newOrder = 1;
ContentValues contentValues = new ContentValues();
for (int i = 0; i < items.size(); i++) {
T item = items.get(i);
if (updateOrderListener.updateOrder(item, newOrder, contentValues)) {
changed = true;
}
newOrder++;
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return changed;
}
}
예전에 작성해 놓은 Settings관련 코드도 결국 버리고 새로 작성했다. enum에 세팅관련 정의를 추가하다보니, base 모듈외에서는 새로운 setting새로 정의할 방법이 없는 문제가 있었고, SettingsDao.getInt 처럼 SettingsDao의 사용법을 알고 있어야 하기에 번거롭고, String 세팅인데 getInt 메쏘드를 호출할 수도 있기에 혼란을 줄이고 보다 간단하고 효율적인 방법으로 구현했다,
class abstract class CommonSettings {
public static final SettingString var1 = new SettingString("key", "default");
}
사용할 때는 이렇게
CommonSettings.var1.getValue();
CommonSettings.var1.setValue("value1");
package com.mdiwebma.base.settings;
import java.util.HashMap;
import android.support.annotation.NonNull;
import com.mdiwebma.base.ModuleManager;
import com.mdiwebma.base.debug.AssertUtils;
/**
* @author djkim
*/
public abstract class Setting {
protected static final SettingsDao dao = ModuleManager.getInstance(SettingsDao.class);
private static final Object lock = new Object();
private static final HashMap<String, Setting> keysMap = new HashMap<>();
protected final String key;
public Setting(String key) {
this.key = key;
synchronized (lock) {
if (!keysMap.containsKey(key)) {
keysMap.put(key, this);
} else {
AssertUtils.fail("Setting key is duplicate");
}
}
}
public static Setting valueOf(@NonNull String key) {
synchronized (lock) {
return keysMap.get(key);
}
}
public boolean isBooleanType() {
return this instanceof SettingBoolean;
}
public boolean isDoubleType() {
return this instanceof SettingDouble;
}
public boolean isIntType() {
return this instanceof SettingInt;
}
public boolean isLongType() {
return this instanceof SettingLong;
}
public boolean isStringType() {
return this instanceof SettingString;
}
public abstract String forceString();
}
package com.mdiwebma.base.settings;
import android.support.annotation.NonNull;
/**
* @author djkim
*/
public class SettingString extends Setting {
final String defaultValue;
String value;
public SettingString(String key, String defaultValue) {
super(key);
this.defaultValue = defaultValue;
}
public String getValue() {
if (value == null) {
value = dao.select(key, defaultValue);
}
return value;
}
public void setValue(@NonNull String value) {
if (value == null) {
value = "";
}
if (!value.equals(this.value)) {
this.value = value;
dao.updateAsync(this.key, this.value);
}
}
@Override
public String forceString() {
return getValue();
}
}
보다 간단해 졌다. 아무데서나 정의할 수도 있고, dao 를 몰라도 바로 사용할 수 있다.
Dao클래스는 Singleton이어야 하기 때문에 ModuleManager를 간단히 구현했다.
package com.mdiwebma.base;
import java.util.HashMap;
import com.mdiwebma.base.debug.DebugUtils;
/**
* @author djkim
* @brief 싱글톤 매니저
*/
public abstract class ModuleManager {
private static final HashMap<Class<?>, Object> modulesHolder = new HashMap<>();
public static <T extends BaseModule> T getInstance(Class<T> klass) {
T module = (T)modulesHolder.get(klass);
if (module == null) {
module = newInstance(klass);
}
return (T)module;
}
private synchronized static <T extends BaseModule> T newInstance(Class<T> klass) {
T module = (T)modulesHolder.get(klass);
if (module == null) {
try {
module = klass.newInstance();
modulesHolder.put(klass, module);
} catch (Exception ex) {
DebugUtils.notReached(ex);
}
}
return module;
}
}