본문 바로가기

Dev.../소프트웨어 아키텍처

[SA강좌] Part 4-17 Association Table Mapping 패턴

연관 테이블 매핑(Association Table Mapping) 패턴

연관 테이블 매핑 패턴의 정의

연관 테이블 매핑은 외래키를 갖는 테이블을 연관 관계의 테이블에 저장하는 패턴이다.

그림 -23. 연관 테이블 매핑 패턴의 구조

연관 테이블 매핑 패턴의 설명

개체는 다중값 필드들을 필드 값처럼 집합(collection)을 사용하여 쉽게 조작할 수 있다. 관계형 데이터베이스는 이러한 특성이 없고, 단일 필드 값에 의해서 제약된다. 일-대-일 관계를 매핑할 때는 외래키 매핑(Foreign Key Mapping)을 이용하여 처리 할 수 있다. 근본적으로 연관의 단일 값을 위한 외래키를 사용한다. 그러나, 다-대-다 관계는 이와 같이 할 수 없다. 왜냐하면 외래키를 가지는 단일 값이 없기 때문이다.

이러한 문제의 해결은 분해를 위해 관계의 데이터를 사용하는 전통적 해결 방법을 사용하는 것이다. 즉, 레코드 관계에서 추출 테이블을 생성하는 것이다. 이후 연관 테이블 매핑을 이러한 링크 테이블의 다중값 필드에 매핑에 사용하는 것이다.

연관 테이블 매핑 패턴은 언제 사용하는가?

연관 테이블 매핑을 위한 정규화의 경우는 다-대-다 관계이다. 이러한 이유는 이 경우에 대한 다른 어떤 대안이 없기 때문이다.

연관 테이블 매핑은 연관의 다른 형식을 위해 사용된다. 그러나, 외래키 매핑과 외부 조인을 포함하는 것 보다 더 복잡한하기 때문에 좋은 선택이 아니다.

관계형 데이터베이스에서 관계에 대한 정보를 수행하는 연관 테이블은 자주 설계된다. 예를 들어서 사원의 회사에 대한 고용에 대한 정보를 담고있는 사원/회사 관계 테이블이 있다. 이 경우 사원/회사 테이블은 도메인 개체에 대응된다.

연관 테이블 매핑 패턴의 예제: 직접 SQL 사용

데이터베이스에 직접적으로 접근을 할때, 질의를 최소화하는 것이 중요하다. 아래는 테이블을 위한 DDL 을 보여주고 있다.

 

create table employee(ID int primary key, firstname varchar, lastname varchar)

create table skills(ID int primary key, name varchar)

create table employeeSkills(empoyeeID int, skillID int, primary key(employeeID, skillID))

단일 사원 로딩를 위해 아래와 같은 비슷한 방법을 사용한다. 사원 매퍼는 레이어 수퍼형(Layer Sypertype)에서 추상 메서드를 위해 비슷한 지정을 정의한다.

 

class EmployeeMapper ...

 

public Employee find(long key) {

return find(new Long key);

}

public Employee find(Long key) {

retrun (Employee) abstractFind(key);

}

protected String findStatement() {

return "SELECT " + CLUMN_LIST + " FROM employee WHERE ID = ? ";

}

public static final String CLUMN_LIST = " ID, lastname, firstname ";

 

class AbstractMapper ...

 

protected DomainObject abstractFind(Long id) {

DomainObject result = (DomainObject) loadMap.get(id);

if (result != null) return result;

PreparedStatment stmt = null;

ResultSet rs = null;

 

try {

stmt = DB.prepare(findStatment());

stmt.setLong(1, id.longValue());

rs = stmt.executeQuery();

rs.next();

result = load(rs);

return result;

} catch (SQLException e) {

throw new ApplicationException(e);

} finally {DB.cleanUp(stmt, rs);

}

}

abstract protected String findStatment();

proteceted Map loadedMap = new HashMap();

이후 find 메서든 로드 메서드를 호출한다. 추상 로드 메서드는 employee를 위한 실제 데이터은 employee의 매퍼에서 로드되고 있는 동안ID 로딩을 처리한다.

 

class AbstractMapper ...

 

protected DomainObject load(ResultSet rs) throws SQLException {

Long id = new Long(rs.getLong(1));

return load(id, rs);

}

public DomainObject load(Long id, ResultSet rs) throws SQLException {

if (hashLoaded(id)) return (DomainObject) loadedMap.get(id);

DomainObject result = doLoad(id, rs);

loadedMap.put(id, result);

return result;

}

abstract protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException;

 

class EmployeeMapper ...

 

protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException {

Employee result = rew Employee(id);

result.setFirstName(rs.getString("firstname"));

result.setLastName(rs.getString("lastname"));

result.setSills(loadSills(id));

return result;

}

사원은 스킬 로드를 위해 다른 질의가 필요하다. 그러나, 이 질의는 단일 질의에서 모든 스킬들을 쉽게 로드할 수 있다. 이러한 일을 하는 것은 특정 스킬을 위한 데이터에서 로드를 위한 스킬 매퍼를 호출한다.

 

class EmployeeMapper ...

 

protected List loadSkills(Load employeeID) {

PreparedStatment stmt = null;

ResultSet rs = null;

 

try {

List result = new ArraryList();

stmt = DB.prepare(findSkilsStatment);

stmt.setObject(1, employeeID());

rs = stmt.executeQuery();

while (rs.next()) {

Long skillId = new Long(rs.getLong());

result.add(Skill) MapperRegistry.skill().loadRow(skillId, rs);

}

return rsult;

} catch(SQLException e) {

throw new ApplicationException(e);

} finally {DB.cleanUp(stmt, rs);

}

}

private static final String findSkillsStatment =

"SELECT skill.ID " + skillMapper.CULUM_LIST +

" FROM skills skill, employeeSkills es " +

" WHERE es.employeeID = ? AND skill.ID = es.skillID";

 

class SkillMapper ....

 

public static final String COLUMN_LIST = " skill.name skillname";

 

class AbstractMapper ...

 

protected DomainObjct loadRow(Long id, ResultSet rs) throws SQLException {

return load(id, rs);

}

 

class SkillMapper ...

 

protected DomainObject loadRow(Long id, ResultSet rs) throws SQLException {

return load(id, rs);

}

 

class SkillMapper ...

 

protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException {

Skill result = new Skill(id);

result.setName(rs.getStrig("skillName");

return result;

}

추상 매퍼는 또한 employee 검색을 돕니다.

 

class EmployeeMapper ...

 

public List findAll() {

return findAll(findAllStatment);

}

 

private static find String findAllStatment =

" SELECT " + COLUMN_LIST +

" FROM employees employee " +

" ORDER BY employee.lastname ";

 

class AbstractMapper ...

 

protected List find(String sql) {

PreparedStatment stmt = null;

ResultSet ts = null;

 

try {

List result = new ArraryList();

stmt = DB.prepared(sql);

rs = stmt.executeQuery();

while (rs.next())

result.add(load(rs));

return result;

} catch(SQLException e) {

throw new ApplicationException(e);

}

}

연관 테이블 매핑 패턴의 예제:다중 사원을 위한 단일 질의를 사용한다.

 

class EmployeeMapper ...

 

protected String findStatment() {

return

"SELECT " + COLUMN_LIST +

" FROM employees employee, skills skill, employeeSkills es" +

" WHERE employee.ID = es.employeeID AND skill.ID = es.skillID AND employee.ID = ?";

}

 

public static final String COLUML_LIST =

" employee.ID, employee.lastname, employee.firstname " +

" es.skillID, es.employeeID, skill.ID skillID " +

SkillMapper.COLUMN_LIST;

 

class EmployeeMapper ...

 

protected DomainObject doLoad(Long id, ResultSet rs) throws SQLException {

Employee result = (Employee) loadRow(id, rs);

result.setFirstName(rs.getString("firstname");

result.setLastName(rs.getString("lastname");

return result;

}

 

private boolean rowIsForSameEmployee(Long id, ResultSet rs) throws SQLException {

return id.equals(new Long(rs.getLong(1)));

}

 

private void loadSkillData(Employee person, ResultSet rs) throws SQLException {

Long skillID = new Long(rs.getLong("skillID");

person.addSkill((skill)MapperRegistry.skill().loadRow(skillId, rs);

}

 

 

class EmployeeMapper ...

 

 

public List findAll() {

return findAll(findAllStatment);

}

private static final String findAllStatment =

"SELECT " + COLUMN_LIST +

" FROM employees employee, skills skill, employessSkills es " +

" WHERE employee.ID = es.employee AND skill.ID = es.skillID " +

" ORDER BY employee.lastname ";

proteceted List findAll(String sql) {

AssociationTableLoader loader = new AssociationTableLoader(this, new SkillAdder());

return loader.run(findAllStatment);

}

 

class AssociationTableLoader ...

 

private AbstractMapper sourceMapper;

private Adder targetAdder;

public AssociationTableLoader(AbstractMapper primaryMapper, Adder targetAdder) {

this.sourceMapper = primaryMapper;

this.targetAdder = targetAdder;

}

 

class AssociationTableLoader ...

 

protected List run(String sql) {

loadData(sql);

addAllNewObjectToIdentityMap();

return formResult();

}

 

class AssociationTableLoader ...

 

private ResultSet rs = null;

private void loadData(String sql) {

PreparedStatment stmt = null;

try {

stmt = DB.prepared(sql);

rs = stmt.executeQuery();

while (rs.next()) loadRow();

} catch(SQLException e) {

throw new ApplicationException(e);

} finally(DB.cleanUp(stmt, rs);

}

}

 

class AssociationTableLoader ...

 

private List resultIds = new ArraryList();

private Map inProcess = new Hashmap();

private void loadRow() throws SQLException {

Long ID = new Long(rs.getLong(1));

if (!resultIds.contains(ID)) resultIds.add(ID);

if (!sourceMapper.hasLoaded(ID)) {

if(!inProcess.keySet().contains(ID))

inProcess.put(ID, sourceMapper.loadRow(ID,rs);

}

}

 

class AbstractMapper ...

 

boolean hasLoaded(Long id) {

return loadedMap.containsKey(id);

}

 

class EmployeeMapper ...

 

private static class SkillAdder implements AssociationTableLoader.Adder {

public void add(DomainObject host, ResultSet rs) throws SQLException {

Employee emp = (Employee) host;

Long skillId = new Long(rs.getLong("skillId"));

emp.addSkill((skill)MapperRegistry.skill().loadRow(skillId, rs);

}

 

 

class AssociationTableLoader ...

 

private void addAllNewObjectsToIdentityMap() {

for (Iterator it = inProcess.values().iterator();it.hasNext();)

sourceMapper.putAsLoader((DomainObject)it.next());

}

 

class AbstractMapper ...

 

void putAsLoaded(DomainObject obj) {

loadedMap.put(obj.getID(), obj);

}

 

class AssociationTableLoader ...

 

private List fromResult() {

List result = new ArraryList();

for (Iterator it = resultIds.iterator(); it.hasNext();) {

Long id = (Long)it.next();

result.add(sourceMapper.lookUp(id));

}

return result;

}

 

class AbstractMapper ...

 

protected DomainObject lookUp(Long id) {

return (DomainObject) loadedMap.get(id);

}