Data Mapper Pattern

In this article, we will learn how to use and implement the Data Mapper Pattern in Java with an example.
Check out all Java EE patterns at https://www.sourcecodeexamples.net/p/p-of-eaa.html

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 communication between two independent objects.

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.

Class Diagram


Data Mapper Pattern Implementation in Java Example


Let's refer above a class diagram to create an example to demonstrate this pattern.



Step 1: Create a Student domain class.
public final class Student implements Serializable {
    private static final long serialVersionUID = 1 L;

    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 = 1 L;

    /**
     * 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 behavior 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 the 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);
    }
}

Applicability

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

References

Comments