Data Mapper Pattern

This pattern belongs to Data Source Architectural Patterns Catalog and this Catalog belongs to Patterns of Enterprise Application Architecture.

Intent

A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.
Mapper means an object that sets up a communication between two independent objects.

Applicability

Use the Data Mapper in any of the following situations
  • when you want to decouple data objects from DB access layer
  • when you want to write multiple data retrieval/persistence implementations

Explanation

  • The Data Mapper is a layer of software that separates the in-memory objects from the database.
  • Its responsibility is to transfer data between the two and also to isolate them from each other. 
  • With Data Mapper the in-memory objects needn't know even that there's a database present; they need no SQL interface code, and certainly no knowledge of the database schema. 

How It Works


The separation between domain and data source is the main function of a Data Mapper, but there are plenty of details that have to be addressed to make this happen.

A simple case would have a Person and Person Mapper class. To load a person from the database, a client would call a find method on the mapper.The mapper uses an Identity Map pattern to see if the person is already loaded; if not, it loads it.

Sample Code


Let's create a Class Diagram for sample code of StudentMapper to demonstrate this pattern.

Step 1: Create Student domain class.
public final class Student implements Serializable {

private static final long serialVersionUID = 1L;

private int studentId;
private String name;
private char grade;


/**
* Use this constructor to create a Student with all details
*
* @param studentId as unique student id
* @param name as student name
* @param grade as respective grade of student
*/

public Student(final int studentId, final String name, final char grade) {
this.studentId = studentId;
this.name = name;
this.grade = grade;
}

/**
*
* @return the student id
*/

public int getStudentId() {
return studentId;
}

/**
*
* @param studentId as unique student id
*/

public void setStudentId(final int studentId) {
this.studentId = studentId;
}

/**
*
* @return name of student
*/

public String getName() {
return name;
}

/**
*
* @param name as 'name' of student
*/

public void setName(final String name) {
this.name = name;
}

/**
*
* @return grade of student
*/

public char getGrade() {
return grade;
}

/**
*
* @param grade as 'grade of student'
*/

public void setGrade(final char grade) {
this.grade = grade;
}

/**
*
*/

@Override
public boolean equals(final Object inputObject) {

boolean isEqual = false;

/* Check if both objects are same */
if (this == inputObject) {

isEqual = true;
} else if (inputObject != null && getClass() == inputObject.getClass()) {

final Student inputStudent = (Student) inputObject;

/* If student id matched */
if (this.getStudentId() == inputStudent.getStudentId()) {

isEqual = true;
}
}

return isEqual;
}

/**
*
*/

@Override
public int hashCode() {

/* Student id is assumed to be unique */
return this.getStudentId();
}

/**
*
*/

@Override
public String toString() {
return "Student [studentId=" + studentId + ", name=" + name + ", grade=" + grade + "]";
}
}
Step 2: Let's create using Runtime Exception for avoiding dependency on implementation exceptions. This helps in decoupling.
public final class DataMapperException extends RuntimeException {

private static final long serialVersionUID = 1L;

/**
* Constructs a new runtime exception with the specified detail message. The cause is not
* initialized, and may subsequently be initialized by a call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for later retrieval by the
* {@link #getMessage()} method.
*/

public DataMapperException(final String message) {
super(message);
}
}
Step 3: Create StudentDataMapper interface - Interface lists out the possible behaviour for all possible student mappers.
public interface StudentDataMapper {

Optional<Student> find(int studentId);

void insert(Student student) throws DataMapperException;

void update(Student student) throws DataMapperException;

void delete(Student student) throws DataMapperException;
}
Step 4: Let's implement above interface - Implementation of Actions on Students Data.This implementation is in-memory, you can use database connection here.
public final class StudentDataMapperImpl implements StudentDataMapper {

/* Note: Normally this would be in the form of an actual database */
private List<Student> students = new ArrayList<>();

@Override
public Optional<Student> find(int studentId) {

/* Compare with existing students */
for (final Student student : this.getStudents()) {

/* Check if student is found */
if (student.getStudentId() == studentId) {

return Optional.of(student);
}
}

/* Return empty value */
return Optional.empty();
}

@Override
public void update(Student studentToBeUpdated) throws DataMapperException {


/* Check with existing students */
if (this.getStudents().contains(studentToBeUpdated)) {

/* Get the index of student in list */
final int index = this.getStudents().indexOf(studentToBeUpdated);

/* Update the student in list */
this.getStudents().set(index, studentToBeUpdated);

} else {

/* Throw user error after wrapping in a runtime exception */
throw new DataMapperException("Student [" + studentToBeUpdated.getName() + "] is not found");
}
}

@Override
public void insert(Student studentToBeInserted) throws DataMapperException {

/* Check with existing students */
if (!this.getStudents().contains(studentToBeInserted)) {

/* Add student in list */
this.getStudents().add(studentToBeInserted);

} else {

/* Throw user error after wrapping in a runtime exception */
throw new DataMapperException("Student already [" + studentToBeInserted.getName() + "] exists");
}
}

@Override
public void delete(Student studentToBeDeleted) throws DataMapperException {

/* Check with existing students */
if (this.getStudents().contains(studentToBeDeleted)) {

/* Delete the student from list */
this.getStudents().remove(studentToBeDeleted);

} else {

/* Throw user error after wrapping in a runtime exception */
throw new DataMapperException("Student [" + studentToBeDeleted.getName() + "] is not found");
}
}

public List<Student> getStudents() {
return this.students;
}
}
Step 5: Let's test this pattern. The below Client class demonstrates basic CRUD operations: Create, Read, Update, and Delete.
public final class Client {

private static Logger log = Logger.getLogger(App.class);

/**
* Program entry point.
*
* @param args command line args.
*/

public static void main(final String... args) {

/* Create new data mapper for type 'first' */
final StudentDataMapper mapper = new StudentDataMapperImpl();

/* Create new student */
Student student = new Student(1, "Adam", 'A');

/* Add student in respectibe store */
mapper.insert(student);

log.debug("App.main(), student : " + student + ", is inserted");

/* Find this student */
final Optional<Student> studentToBeFound = mapper.find(student.getStudentId());

log.debug("App.main(), student : " + studentToBeFound + ", is searched");

/* Update existing student object */
student = new Student(student.getStudentId(), "AdamUpdated", 'A');

/* Update student in respectibe db */
mapper.update(student);

log.debug("App.main(), student : " + student + ", is updated");
log.debug("App.main(), student : " + student + ", is going to be deleted");

/* Delete student in db */
mapper.delete(student);
}

}

Comments

Popular posts from this blog

Design Patterns used in Hibernate Framework

Information Expert GRASP Pattern