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