Как стать автором
Обновить
1115.41
OTUS
Цифровые навыки от ведущих экспертов

Архитектурные шаблоны взаимодействия с базами данных

Уровень сложностиПростой
Время на прочтение11 мин
Количество просмотров11K

В первой статье мы рассмотрели шаблоны проектирования, применимые в программировании приложений. Однако сейчас сложно представить серьезное бизнес-приложение без базы данных. Большие объемы данных требуют хранения и обработки. И то насколько оптимально построена связь между уровнем прикладного кода и уровнем БД во многом зависит быстродействие системы в целом. Поэтому важно правильно построить взаимодействие с СУБД. В этой статье мы рассмотрим шаблоны взаимодействия с базами данных. Правильно выбранный шаблон взаимодействия позволит избежать многих проблем при разработке и получить качественное приложение.

Различные подходы

За долгие годы существования приложений, взаимодействующих с СУБД, утвердились несколько подходов к работе с базами данных. Эти подходы отличаются тем, как осуществляется чтение и обработка данных, представленных в базах, работа со строками и таблицами.

Начнем с рассмотрения Object Relation Mapping – ORM, механизма взаимодействия, который представляет собой вспомогательную прослойку между приложением и базой данных, позволяющую реализовать механизмы представления и взаимодействия между БД и собственно кодом. Суть работы ORM заключается в следующем: мы представляем ряды из таблиц в качестве объектов, свойства которых будут соответствовать именам полей из таблиц, а значения этих свойств – значениям из базы данных. Таким образом мы получаем соответствие одна строка в БД – один объект.

На практике реализация ORM осуществляется с помощью шаблонов DAO, Active Record или DataMapper. Также возможно построение гибридов из нескольких шаблонов.

Познаем DAO

Начнем с рассмотрения шаблона DAO (Data Access Object). Как можно понять из названия, этот шаблон предназначен для доступа к данным. Он предоставляет абстрактный интерфейс для обращений к БД. Основная суть работы данного шаблона заключается в том, чтобы можно было выполнять определенные операции не сильно вдаваясь в детали реализации базы данных.  При использовании шаблона DAO функции для работы c конкретной таблицей хранятся в файле модели. Соответственно, данная модель наследует абстрактный класс, реализующий DAO. Когда будет получен ряд данных в DAO, в результирующем объекте или массиве будут содержаться все поля из БД.

Давайте посмотрим пример кода на Java. Допустим, мы собираемся создать объект Student, действующий как Модель. При этом StudentDao - это интерфейс объекта доступа к данным. А StudentDaoImpl - это конкретный класс, реализующий объектный интерфейс доступа к данным. Dao Pattern Demo, наш демонстрационный класс, который будет использовать StudentDao для демонстрации использования шаблона объекта.

Создаем основной объект.

public class Student {

   private String name;

   private int rollNo;

   Student(String name, int rollNo){

      this.name = name;

      this.rollNo = rollNo;

   }

   public String getName() {

      return name;

   }

   public void setName(String name) {

      this.name = name;

   }

   public int getRollNo() {

      return rollNo;

   }

   public void setRollNo(int rollNo) {

      this.rollNo = rollNo;

   }

}

Создаем интерфейс Java.

import java.util.List;

public interface StudentDao {

   public List<Student> getAllStudents();

   public Student getStudent(int rollNo);

   public void updateStudent(Student student);

   public void deleteStudent(Student student);

}

Создаем класс для работы с этим интерфейсом.

 

import java.util.ArrayList;

import java.util.List;

public class StudentDaoImpl implements StudentDao {

   //list is working as a database

   List<Student> students;


   public StudentDaoImpl(){

      students = new ArrayList<Student>();

      Student student1 = new Student("Robert",0);

      Student student2 = new Student("John",1);

      students.add(student1);

      students.add(student2);               

   }

   @Override

   public void deleteStudent(Student student) {

      students.remove(student.getRollNo());

      System.out.println("Student: Roll No " + student.getRollNo() + ", deleted from database");

   }

   //retrive list of students from the database

   @Override

   public List<Student> getAllStudents() {

      return students;

   }

   @Override

   public Student getStudent(int rollNo) {

      return students.get(rollNo);

   }

   @Override

   public void updateStudent(Student student) {

      students.get(student.getRollNo()).setName(student.getName());

      System.out.println("Student: Roll No " + student.getRollNo() + ", updated in the database");

   }

}

Далее создадим демонстрационный класс для показа работы DAO.

public class DaoPatternDemo {

   public static void main(String[] args) {

      StudentDao studentDao = new StudentDaoImpl();

      //print all students

      for (Student student : studentDao.getAllStudents()) {

         System.out.println("Student: [RollNo : " + student.getRollNo() + ", Name : " + student.getName() + " ]");

      }

      //update student

      Student student =studentDao.getAllStudents().get(0);

      student.setName("Michael");

      studentDao.updateStudent(student);

      //get the student

      studentDao.getStudent(0);

      System.out.println("Student: [RollNo : " + student.getRollNo() + ", Name : " + student.getName() + " ]");                  

   }

}

И вот что мы получим в результате выполнения:

Student: [RollNo : 0, Name : Robert ]

Student: [RollNo : 1, Name : John ]

Student: Roll No 0, updated in the database

Student: [RollNo : 0, Name : Michael ]

Как видно, в результате работы шаблоны мы получаем массив значений. Для получения такого массива нам не требуются дополнительные классы, так как все методы уже реализованы в StudentDaoImpl.

Active Record

Теперь рассмотрим другой шаблон проектирования Active Record. Данный шаблон также отвечает за представление бизнес-логики и данных. Если у нас имеются объекты, требующие постоянного хранения в базе данных, то с помощью Active Record можно достаточно просто создавать и использовать эти объекты.

Основная идея шаблона Active Record заключается в том, чтобы позволить объекту инкапсулировать данные и операции с базой данных, которые вы можете выполнять с ним. То есть, мы передаем классу некоторый набор значений, который затем внутри этого класса будет преобразован в запрос SQL и выполнен. Такой подход позволяет нам не использовать запросы SQL напрямую. И кроме того, позволяет сделать код более безопасным, так как если нельзя выполнить любой запрос напрямую, то и выполнение SQL инъекций становится более затруднительным или даже невозможным.

Посмотрим простой пример:

 

@Entity

public class ChessPlayer extends PanacheEntity {

     public String firstName;

     public String lastName;

     public LocalDate birthDate;

    @Version

    public int version;

    public void setLastName(String lastName) {

        this.lastName = lastName.toUpperCase();

    }

}

В результате будет сформирован SQL запрос следующего вида:

  insert 

    into

        ChessPlayer

        (birthDate, firstName, lastName, version, id) 

    values

        (?, ?, ?, ?, ?)

Останется только добавить значения для соответствующих переменных при обращении к данному классу.

Data Mapper

Шаблон Data Mapper - это слой доступа к данным, который предоставляет двунаправленную работу с данными между БД и хранением данных в памяти (например, на время выполнения кода. В его обязанности входит передача данных между объектами и базой данных и изоляция их друг от друга. Если мы получаем средство отображения данных, объекту в памяти совершенно не обязательно знать, существует база данных или нет.

В шаблоне Data Mapper имеется еще один слой или тип сущности такой как entityManager. Именно этот слой будет отвечать за перенос состояния модели в базу данных и обратно.

Посмотрим пример работы с Data Mapper. Пусть у нас есть класс student для определения атрибутов студентов, включая студенческий билет, имя и оценку. У нас есть интерфейс StudentDataMapper, в котором перечислены возможные варианты запросов к данным студентов. И класс StudentDataMapperImpl для выполнения действий с Students Data.

public final class Student implements Serializable {

    private static final long serialVersionUID = 1L;

    @EqualsAndHashCode.Include

    private int studentId;

    private String name;

    private char grade;

    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;

    }

    public final class StudentDataMapperImpl implements StudentDataMapper {

        @Override

        public Optional<Student> find(int studentId) {

            return this.getStudents().stream().filter(x -> x.getStudentId() == studentId).findFirst();

        }

        @Override

        public void update(Student studentToBeUpdated) throws DataMapperException {

            String name = studentToBeUpdated.getName();

            Integer index = Optional.of(studentToBeUpdated)

                    .map(Student::getStudentId)

                    .flatMap(this::find)

                    .map(students::indexOf)

                    .orElseThrow(() -> new DataMapperException("Student [" + name + "] is not found"));

            students.set(index, studentToBeUpdated);

        }

        @Override

        public void insert(Student studentToBeInserted) throws DataMapperException {

            Optional<Student> student = find(studentToBeInserted.getStudentId());

            if (student.isPresent()) {

                String name = studentToBeInserted.getName();

                throw new DataMapperException("Student already [" + name + "] exists");

            }

            students.add(studentToBeInserted);

        }

        @Override

        public void delete(Student studentToBeDeleted) throws DataMapperException {

            if (!students.remove(studentToBeDeleted)) {

                String name = studentToBeDeleted.getName();

                throw new DataMapperException("Student [" + name + "] is not found");

            }

        }

        public List<Student> getStudents() {

            return this.students;

        }

    }

}

 

Как видно из кода, в нем приведены основные операции для работы с данными: создание, чтение, запись, обновление и удаление.

public final class App {

  private static final String STUDENT_STRING = "App.main(), student : ";

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

    final var mapper = new StudentDataMapperImpl();

    var student = new Student(1, "Adam", 'A');

    mapper.insert(student);

    LOGGER.debug(STUDENT_STRING + student + ", is inserted");

    final var studentToBeFound = mapper.find(student.getStudentId());

    LOGGER.debug(STUDENT_STRING + studentToBeFound + ", is searched");

    student = new Student(student.getStudentId(), "AdamUpdated", 'A');

    mapper.update(student);

    LOGGER.debug(STUDENT_STRING + student + ", is updated");

    LOGGER.debug(STUDENT_STRING + student + ", is going to be deleted");

    mapper.delete(student);

  }
99
  private App() {

  }

}

В данном случае, нам нужно беспокоиться только о бизнес-логике самого приложения. Нам не надо заботиться о том, как и где данные были сохранены.

Чем отличаются

Поговорим немного о том, чем эти шаблоны проектирования отличаются друг от друга. DAO и Active Record кажутся очень похожими, но на самом деле у них есть различия. DAO отвечает за сохранение отдельной сущности на вашем уровне данных. Active Record - это в определенной степени особый метод выполнения DAO, в котором класс, содержащий значения одной строки из таблицы, также отвечает за запросы, обновления, вставки и удаления в эту таблицу. Шаблон проектирования Active Record означает, что ваш объект имеет взаимно однозначное сопоставление с таблицей в вашей базе данных.

Преимуществом использования DAO является то, что легко определить другой формат работы с данными, например, переход от классической БД к облаку, без изменения базовой реализации, в то время как внешний интерфейс остается тем же, что не влияет на другие классы. Преимущество шаблона ActiveRecord это простота реализации.

Недостатком модели Active Record является нарушение принципа единой ответственности, согласно которому, доменный объект должен иметь только одну зону ответственности, то есть только свою бизнес-логику. И если мы используем этот объект для сохранения данных, у нас добавляется дополнительная зона ответственности. Соответственно, в случае внесения изменений в структуру БД нам придется вносить много изменений в код и заново все тестировать.

У Data Mapper для каждого объекта указана своя зона ответственности и здесь особых проблем при внесении изменений быть не должно. Из недостатков здесь стоит отметить сложность кода из-за того, что под управлением находится большее количество объектов, чем в других моделях.

Заключение

В этой статье мы рассмотрели три основные шаблона для работы с базами данных. В следующей статье речь пойдет о работе с микросервисами.

Приглашаю вас на бесплатный вебинар, в процессе которого мы составим дорожную карту для самостоятельного изучения архитектурных принципов и паттернов проектирования.

Теги:
Хабы:
Всего голосов 15: ↑9 и ↓6+3
Комментарии5

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS