Data Mapper Pattern
This pattern belongs to Data Source Architectural Patterns Catalog and this Catalog belongs to Patterns of Enterprise Application Architecture.
Mapper means an object that sets up a communication between two independent objects.
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
Post a Comment