Пожалуйста, объясните отношения и обязанности репозитория, картографирования и бизнес-уровня

Я много читал об этом материале, и в настоящее время я нахожусь в процессе разработки более крупного веб-приложения и его соответствующей серверной части.

Тем не менее, я начал с разработки, в которой я прошу репозиторий извлечь данные из базы данных и отобразить их в DTO. Почему ДТО? Просто потому, что до сих пор в основном все было просто и больше не было необходимости. Если это стало немного сложнее, то я начал отображать, например, отношения 1-к-n непосредственно на уровне сервиса. Что-то вроде:

// This is Service-Layer
public List<CarDTO> getCarsFromOwner(Long carOwnerId) {

    // Entering Repository-Layer
    List<CarDTO> cars = this.carRepository = this.carRepository.getCars(carOwnerId);
    Map<Long, List<WheelDTO>> wheelMap = this.wheelRepository.getWheels(carId);

    for(CarDTO car : cars) {
        List<WheelDTO> wheels = wheelMap.get(car.getId());
        car.setWheels(wheels);
    }

    return cars;
}

Это работает, конечно, но оказывается, что иногда все усложняется, и я начинаю понимать, что код может выглядеть довольно некрасиво, если я ничего не сделаю с этим.

Конечно, я мог бы загрузить wheelMap в CarRepository, сделайте там отображение и верните только полные объекты, но так как запросы SQL иногда могут выглядеть довольно сложными, я не хочу получать все cars и их wheels плюс заботиться о картировании в getCars(Long ownerId),

Я явно скучаю по бизнес-уровню, верно? Но я просто не могу разобраться с его лучшими практиками.

Давайте предположим, что у меня есть Car и Owner бизнес-объекты. Будет ли мой код выглядеть примерно так:

// This is Service-Layer
public List<CarDTO> getCarsFromOwner(Long carOwnerId) {

    // The new Business-Layer
    CarOwner carOwner = new CarOwner(carOwnerId);
    List<Car> cars = carOwner.getAllCars();

    return cars;
}

который выглядит настолько простым, насколько это возможно, но что произойдет внутри? Вопрос нацелен прежде всего на CarOwner#getAllCars(),

Я предполагаю, что эта функция будет использовать Mappers и Repositories для загрузки данных, и что особенно важна часть реляционного отображения:

List<CarDTO> cars = this.carRepository = this.carRepository.getCars(carOwnerId);
Map<Long, List<WheelDTO>> wheelMap = this.wheelRepository.getWheels(carId);

for(CarDTO car : cars) {
    List<WheelDTO> wheels = wheelMap.get(car.getId());
    car.setWheels(wheels);
}

Но как? Это CarMapper обеспечение функций getAllCarsWithWheels() а также getAllCarsWithoutWheels()? Это также переместит CarRepository и WheelRepository в CarMapper но это правильное место для хранилища?

Я был бы счастлив, если бы кто-нибудь показал мне хороший практический пример для кода выше.


Дополнительная информация

Я не использую ORM - вместо этого я иду с JOOQ. По сути, это просто типобезопасный способ написания SQL-кода (и, кстати, это довольно забавно).

Вот пример того, как это выглядит:

public List<CompanyDTO> getCompanies(Long adminId) {

    LOGGER.debug("Loading companies for user ..");

    Table<?> companyEmployee = this.ctx.select(COMPANY_EMPLOYEE.COMPANY_ID)
        .from(COMPANY_EMPLOYEE)
        .where(COMPANY_EMPLOYEE.ADMIN_ID.eq(adminId))
        .asTable("companyEmployee");

    List<CompanyDTO> fetchInto = this.ctx.select(COMPANY.ID, COMPANY.NAME)
        .from(COMPANY)
        .join(companyEmployee)
            .on(companyEmployee.field(COMPANY_EMPLOYEE.COMPANY_ID).eq(COMPANY.ID))
            .fetchInto(CompanyDTO.class);

    return fetchInto;
}

3 ответа

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

Согласно этому определению, репозиторий не может работать с DTO - это хранилище сущностей домена. Если у вас есть только DTO, вам нужен более общий DAO или, возможно, шаблон CQRS. Обычно существует отдельный интерфейс и реализация репозитория, как это делается, например, в Spring Data (он генерирует реализацию автоматически, поэтому вам нужно только указать интерфейс, возможно, наследуя базовые операции CRUD от общего суперинтерфейса CrudRepository). Пример:

class Car {
   private long ownerId;
   private List<Wheel> wheels;
}

@Repository 
interface CarRepository extends CrudRepository<Car,Long> {
   List<Car> findByOwnerId(long id);
}

Ситуация усложняется, когда ваша доменная модель представляет собой дерево объектов и вы храните их в реляционной базе данных. По определению этой проблемы вам нужен ORM. Каждый фрагмент кода, который загружает реляционный контент в объектную модель, представляет собой ORM, поэтому ваш репозиторий будет иметь ORM в качестве реализации. Обычно JPA ORM выполняют связывание объектов за сценой, более простые решения, такие как настраиваемые сопоставители на основе JOOQ или простой JDBC, должны делать это вручную. Не существует серебряной пули, которая бы эффективно и правильно решала все проблемы ORM: если вы решили написать собственное отображение, все же лучше оставить проводку внутри хранилища, поэтому бизнес-уровень (службы) будет работать с настоящими объектными моделями. В вашем примере CarRepository знает о Cars. Car знает о Wheelс, так CarRepository уже имеет транзитивную зависимость от Wheels. В CarRepository#findByOwnerId() метод вы можете получить Wheelдля автомобиля непосредственно в том же запросе, добавив соединение, или делегировать эту задачу WheelRepository а потом только делаю проводку. Пользователь этого метода получит полностью инициализированное дерево объектов. Пример:

class CarRepositoryImpl implements CarRepository {

  public List<Car> findByOwnerId(long id) {
     // pseudocode for some database query interface
     String sql = select(CARS).join(WHEELS); 
     final Map<Long, Car> carIndex = new HashMap<>();
     execute(sql, record -> { 
          long carId = record.get(CAR_ID);
          Car car = carIndex.putIfAbsent(carId, Car::new);
          ... // map the car if necessary
          Wheel wheel = ...; // map the wheel
          car.addWheel(wheel);
     }); 
     return carIndex.values().stream().collect(toList());
  }
}

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

На мой взгляд, нет правильного ответа. Это действительно зависит от выбранного дизайнерского решения, которое само зависит от вашего стека и комфорта вашей команды.

Случай 1:

Я не согласен с ниже выделенными разделами в вашем заявлении:

"Конечно, я мог бы загрузить wheelMap в CarRepository, сделать там отображение колес и вернуть только завершенные объекты, но поскольку запросы SQL иногда могут выглядеть довольно сложными, я не хочу выбирать все машины и их колеса, а также заботиться о отображение в getCars (Long ownerId)"

SQL присоединиться к выше будет простым. Кроме того, это может быть быстрее, поскольку базы данных оптимизированы для соединений и выборки данных. Теперь я назвал этот подход Case 1, потому что он может быть использован, если вы решите извлекать данные через ваш репозиторий, используя соединения SQL. Вместо того, чтобы просто использовать sql для простого CRUD, а затем манипулировать объектами в Java. (ниже)

Case2: Репозиторий просто используется для извлечения данных для "каждого" объекта домена, который соответствует "одной" таблице.

В этом случае то, что вы делаете, уже правильно. Если вы никогда не будете использовать WheelDTO по отдельности, вам не нужно будет предоставлять отдельный сервис для него. Вы можете подготовить все в автосервисе. Но если вам нужен WheelDTO отдельно, то сделайте разные сервисы для каждого. В этом случае может быть вспомогательный уровень поверх сервисного уровня для создания объекта. Я не предлагаю внедрять orm с нуля, создавая репозитории и загружая все объединения для каждого репо (вместо этого используйте hibernate или mybatis)

Опять же ИМХО, какой бы подход вы ни использовали из вышеперечисленного, сервис или бизнес-уровни просто дополняют его. Таким образом, нет жесткого и быстрого правила, старайтесь быть гибкими в соответствии с вашими требованиями. Если вы решите использовать ORM, некоторые из вышеперечисленных снова изменится.

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

class ShinyCar {
   int key;
   int names;
   int wheeltype;
   List<Wheels> wheels; 
   public ShinyCar(CarDTO car) {
      this.fillFromDTO(car);
   }
   public int getKey() {
      return this.key;
   }
   protected void fillFromDTO(CarDTO car) {
      this.key = car.id
      this.names = car.names;
   }

   protected List<Wheels> getWheels() {
      if(this.wheels == null) {
          List<WheelDTO> wheels = this.ctx.select(...).from(wheels);
          if(wheels != null && wheels.size() > 0) {
              this.wheels = new ArrayList<Wheels>();
              for(WheelDTO wheel : wheels) {
                  wheels.add(new Wheel(wheel));
              }
              return this.wheels;
          }
          else {
              return new ArrayList<Wheel>();
          }
        else {
          return this.wheels;
        }
      }

   }
}
Другие вопросы по тегам