# Generic DAO
How to Reduce Repetitive code in Data Access Object
Generic Dao 삽질기
코드는 github 에 있습니다.
sql 쿼리 생성은 mariadb 기준으로 했습니다
Primary key 로 객체 탐색하는 코드
public <T, K> T select(Class<T> tClass, K key) {
String tbName = converter.convertClassNameToDbName(tClass.getSimpleName());
String keyName = converter.convertToDbNameConvention(getKeyName(tClass));
String sql = "SELECT * FROM " + tbName + " WHERE " + keyName+ "=?";
Connection conn = manager.getConn();
try {
assert conn != null;
try(PreparedStatement preparedStatement = conn.prepareStatement(sql)){
preparedStatement.setObject(1, key);
ResultSet rs = preparedStatement.executeQuery();
if (rs.next()) {
return setPOJO(tClass, rs);
}
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
manager.close(conn);
}
return null;
}
ResultSet 에서 Object 를 만들때 Column index 또는 Column name 만 다른 같은 코드들이 대량 발생하고
순서가 조금 바뀌거나 name 오타시 디버깅이 매우 힘들다.
Result set 에서 Class 에 따라 맵핑
public <T> T setPOJO(Class<T> target, ResultSet rs)
throws InstantiationException, IllegalAccessException {
T out = target.newInstance();
cachedFields.computeIfAbsent(target, k -> target.getDeclaredFields());
Field[] fields = cachedFields.get(target);
for (Field f: fields) {
f.setAccessible(true);
String name = converter.convertToDbNameConvention(f.getName());
try{
f.set(out, rs.getObject(name));
}catch (Exception e){
e.printStackTrace();
}
}
return out;
}
public <T> T insertMapper (T t) {
String tbName = converter.convertClassNameToDbName(t.getClass().getSimpleName());
ArrayList<Object> list = new ArrayList<>();
StringJoiner columns = new StringJoiner(",","(",")");
StringJoiner values = new StringJoiner(",","(",")");
Field[] fields = t.getClass().getDeclaredFields();
for(Field f: fields) {
f.setAccessible(true);
Object o = null;
try {
o = f.get(t);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (o != null) {
columns.add(converter.convertToDbNameConvention(f.getName()));
values.add("?");
list.add(o);
}
}
String sqlPre = "INSERT INTO {table} {columns} VALUES {values}";
String sql = sqlPre.replace("{table}", tbName)
.replace("{columns}", columns.toString())
.replace("{values}", values.toString());
Connection conn = manager.getConn();
try{
try(PreparedStatement preparedStatement = conn.prepareStatement(sql)){
for (int i=0;i<list.size();i++){
Object o = list.get(i);
preparedStatement.setObject(i+1, o);
}
if (preparedStatement.executeUpdate() > 0) {
conn.commit();
return t;
}
}
}catch (Exception e) {
e.printStackTrace();
}
finally {
manager.close(conn);
}
return null;
}
// primary key 값으로 삭제
public <T, K> boolean deleteByKey(Class<T> tClass, K key) {
String tbName = converter.convertClassNameToDbName(tClass.getSimpleName());
String keyName = converter.convertToDbNameConvention(getKeyName(tClass));
StringBuilder sb = new StringBuilder();
sb.append("DELETE FROM ").append(tbName).append(" WHERE ").append(keyName).append("=?");
Connection conn = manager.getConn();
try {
assert conn != null;
try (PreparedStatement preparedStatement = conn.prepareStatement(sb.toString())) {
preparedStatement.setObject(1, key);
if (preparedStatement.executeUpdate() > 0) {
conn.commit();
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
manager.close(conn);
}
return false;
}
// update
public <T> T updateMapper (T t) throws SQLException ,IllegalAnnotationException{
String tbName = converter.convertClassNameToDbName(t.getClass().getSimpleName());
String keyName = getKeyName(t.getClass());
ArrayList<Object> list = new ArrayList<>();
StringJoiner values = new StringJoiner(",");
Field[] fields = t.getClass().getDeclaredFields();
Object keyObj = null;
for (Field f:fields) {
f.setAccessible(true);
try{
Object o = f.get(t);
String name = converter.convertToDbNameConvention(f.getName());
if (o != null) {
if (f.getName().equals(keyName)){
keyObj = f.get(t);
}
else {
values.add(name + "=?");
list.add(o);
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
if (keyObj == null) {
throw new IllegalArgumentException("key object should not be null");
}
String preSql = "UPDATE {table} SET {values} WHERE {key}=?";
String sql = preSql.replace("{table}", tbName)
.replace("{values}", values.toString())
.replace("{key}", keyName);
Connection conn = manager.getConn();
try {
assert conn != null;
try (PreparedStatement prst = conn.prepareStatement(sql)){
for (int i=0;i<list.size();i++) {
prst.setObject(i + 1, list.get(i));
}
prst.setObject(list.size()+1, keyObj);
if (prst.executeUpdate() > 0) {
conn.commit();
return t;
}
}
} catch (SQLException e) {
throw new SQLException(e);
}
finally {
manager.close(conn);
}
return null;
}
문제 : 같은 table 에서 select 할때도 서로 다른 키가 다르면 여러개의 메소드를 만들어야 한다
쿼리들을 java 로 처리할수 있게 만들자
Filter 객체를 사용해서 Where 절 뒤에 오는 조건들은 구현
public <T> List<T> filteredSelect(Class<T> tClass, ObjectFilter<T> option) {
String tbName = converter.convertClassNameToDbName(tClass.getSimpleName());
String sqlp = "SELECT * FROM " + tbName + " WHERE " ;
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM ").append(tbName).append(" WHERE ");
List<Object> list = new ArrayList<>();
List<FilterOption> options = option.getOptions();
List<FilterOrder> orders = option.getOrders();
int limit = option.getLimit();
int offset = option.getOffset();
int numberOfOptions = options.size();
for (int i = 0; i< numberOfOptions; i++) {
FilterOption f = options.get(i);
sql.append(converter.convertToDbNameConvention(f.getKey()));
sql.append(f.getOpcode().getOp());
sql.append("?").append(" ");
if (i != numberOfOptions - 1) {
sql.append(f.getLogical()).append(" ");
}
list.add(f.getValue());
}
if (!orders.isEmpty()){
sql.append("ORDER BY ");
StringJoiner joiner = new StringJoiner(",");
orders.forEach(f -> {
joiner.add(converter.convertToDbNameConvention(f.getKey()) + " " + f.getOrder());
});
sql.append(joiner);
}
if (limit != 0) {
sql.append("LIMIT ?");
list.add(limit);
if (offset != 0){
sql.append(" OFFSET ?");
list.add(offset);
}
}
Connection conn = manager.getConn();
try {
assert conn != null;
try(PreparedStatement prst = conn.prepareStatement(sql.toString())){
for (int i=0;i<list.size();i++) {
prst.setObject(i+1, list.get(i));
}
System.out.println(sql);
System.out.println(list.get(0));
ResultSet rs = prst.executeQuery();
List<T> out = new ArrayList<>();
while (rs.next()){
out.add(setPOJO(tClass, rs));
}
return out;
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
manager.close(conn);
}
return null;
}
Interface
public interface GenericAccess<T> {
List<T> selectAll();
List<T> selectWithFilter(ObjectFilter<T> keyOptions) throws IllegalAnnotationException;
<K> T select(K key) throws IllegalAnnotationException;
T insert(T t) throws SQLException, IllegalAnnotationException;
T update(T t) throws SQLException, IllegalAnnotationException;
boolean delete(T t) throws SQLException, IllegalAnnotationException;
<K> boolean deleteById(K key) throws SQLException, IllegalAnnotationException;
ObjectFilter<T> getFilter();
}
Interface 구현체
public abstract class GenericAccessImpl<T> implements GenericAccess<T> {
private Class<T> entity;
protected SqlMapper mapper = new SqlMapper();
private String keyName;
public void set(Class<T> tClass) throws NullPointerException, IllegalAnnotationException{
if (tClass == null) {
throw new NullPointerException("class should not be null");
}
// key annotation 이 없으면 exception 발생
keyName = mapper.getKeyName(tClass);
entity = tClass;
}
@Override
public List<T> selectAll() {
return mapper.selectAll(entity);
}
@Override
public <K> T select(K key) throws IllegalAnnotationException {
return mapper.select(entity, key);
}
@Override
public T insert(T t) throws SQLException, IllegalAnnotationException {
return mapper.insertMapper(t);
}
@Override
public T update(T t) throws SQLException, IllegalAnnotationException {
return mapper.updateMapper(t);
}
@Override
public boolean delete(T t) throws SQLException, IllegalAnnotationException {
return mapper.deleteMapper(t);
}
@Override
public <K> boolean deleteById(K key) throws SQLException, IllegalAnnotationException {
return mapper.deleteByKey(entity, key);
}
@Override
public List<T> selectWithFilter(ObjectFilter<T> keyOptions) throws IllegalAnnotationException {
return mapper.filteredSelect(entity, keyOptions);
}
@Override
public ObjectFilter<T> getFilter() {
return new ObjectFilter<>();
}
}
데이터에 따른 메서드를 통합하기 위해 제네릭 클래스와 메서드들을 사용해서 시도 해보았습니다.
간단한 CRUD 작업시 에는 사용 할수 있겠지만 그이상 작업시에는 프레임워크를 제작하는 수준이 되어 배보다 배꼽이 더 커지는 상황이되어 여기 까지만 만들었습니다.
구현한 부분
구현 하지 못한부분
그동안 직접 사용해보지 못해서 정확한 개념이 없던 Generic Class 와 Reflection 을 사용 하려는 시도로 삽질을 하면서 개념이 조금 잡힌 느낌입니다.
작업을 하던 도중에 Hibernate 라는 ORM 이 있다는 것을 알았고 JPA 와 Hibernate를 공부 해야겠습니다.
JPA 를 사용하자