Есть ли способ создать один объект JPA, основанный на множестве таблиц базы данных, и действительно ли я должен это делать или это плохая практика? - PullRequest
2 голосов
/ 21 февраля 2020

Я довольно новичок в технологии Spring Data JPA и в настоящее время сталкиваюсь с одной задачей, с которой не могу справиться. Я ищу наилучшую практику для таких случаев.

В моей базе данных Postgres есть две таблицы, связанные с отношением один ко многим. Таблица «account» имеет поле «type_id», которое является ссылкой внешнего ключа на поле «id» таблицы «account_type»:

schema image

Таким образом, таблица «account_type» воспроизводит только роль словаря. В соответствии с этим я создал для сущностей JPA (Kotlin код):

@Entity
class Account(
  @Id @GeneratedValue var id: Long? = null,
  var amount: Int,
  @ManyToOne var accountType: AccountType
)
@Entity
class AccountType(
  @Id @GeneratedValue var id: Long? = null,
  var type: String
)

В моем приложении Spring Boot я хотел бы иметь RestConroller, который будет отвечать за выдачу всех учетных записей в JSON формат. Для этого я сделал сериализуемые классы сущностей и написал простой restcontroller:

@GetMapping("/getAllAccounts", produces = [APPLICATION_JSON_VALUE])
fun getAccountsData(): String {
    val accountsList = accountRepository.findAll().toMutableList()
    return json.stringify(Account.serializer().list, accountsList)
}

, где accountRepository - это просто интерфейс, который расширяет CrudRepository<Account, Long>.

А теперь, если я go до :8080/getAllAccounts, я получу Json следующего формата (извините за форматирование):

[
  {"id":1,
   "amount":0,
   "accountType":{
      "id":1,
      "type":"DBT"
     }
  },
  {"id":2,
    "amount":0,
    "accountType":{
       "id":2,
       "type":"CRD"
      }
   }
]

Но то, что я действительно хочу от этого контроллера, это просто

[
   {"id":1,
    "amount":0,
    "type":"DBT"
   },
   {"id":2,
    "amount":0,
    "type":"CRD"
   }
]

Of Конечно, я могу создать новый сериализуемый класс для учетных записей, который будет иметь поле String вместо поля AccountType и может сопоставить класс учетной записи JPA с этим классом, извлекая строку типа учетной записи из поля AccountType. Но для меня это выглядит ненужными накладными расходами, и я считаю, что для таких случаев могла бы быть лучшая схема.

Например, у меня в голове есть то, что, возможно, каким-то образом я могу создать один класс сущностей JPA (с полем String, представляющим тип учетной записи), который будет основан на двух таблицах базы данных, и ненужная сложность наличия внутреннего объекта будет сокращается автоматически каждый раз, когда я вызываю методы репозитория :) Более того, я смогу использовать этот класс сущностей в своих бизнес-логи c без каких-либо дополнительных «оболочек».

Ps Я читал аннотацию @SecondaryTable, но она выглядит как, например, он может работать только в тех случаях, когда между двумя таблицами существует взаимно-однозначное отношение, что не в моем случае.

Ответы [ 3 ]

3 голосов
/ 21 февраля 2020

Есть несколько опций, которые позволяют c разрешить чистое разделение без DTO.

Во-первых, вы могли бы взглянуть на использование проекции, которая похожа на DTO, упомянутую в других ответах, но без многих Недостатки:

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections

@Projection(
  name = "accountSummary", 
  types = { Account.class }) 
public Interface AccountSummaryProjection{

    Long getId();

    Integer getAmount();

    @Value("#{target.accountType.type}")
    String getType();
}

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

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projection .dynami c

@GetMapping("/getAllAccounts", produces = [APPLICATION_JSON_VALUE])
@ResponseBody
fun getAccountsData(): List<AccountSummaryProjection>{
    return accountRepository.findAllAsSummary();
}

Альтернативный подход заключается в использовании аннотаций Джексона. Я отмечаю, что в вашем вопросе вы вручную преобразуете результат в строку JSON и возвращаете строку из вашего контроллера. Вам не нужно этого делать, если библиотека Джексона Json находится на пути к классам. См. Мой контроллер выше.

Так что, если вы оставите сериализацию Джексону, вы можете отделить представление от сущности, используя несколько аннотаций. Обратите внимание, что я применил бы их, используя миксин Джексона, вместо того, чтобы загрязнять модель Entity Json инструкциями по обработке, однако вы можете посмотреть это:

@Entity
class Account(

  //in real life I would apply these using a Jacksin mix
  //to prevent polluting the domain model with view concerns.
  @JsonDeserializer(converter = StringToAccountTypeConverter.class)
  @JsonSerializer(converter = AccountTypeToStringConverter.class
  @Id @GeneratedValue var id: Long? = null,
  var amount: Int,
  @ManyToOne var accountType: AccountType
)

Затем вы просто создадите необходимые конвертеры:

public class StringToAccountTypeConverter extends StdConverter<String, CountryType> 
           implements org.springframework.core.convert.converter.Converter<String, AccountType> {

  @Autowired
  private AccountTypeRepository repo;

  @Override
  public AccountType convert(String value) {
      //look up in repo and return
  }
}

и наоборот:

public class AccountTypeToStringConverter extends StdConverter<String, CountryType> 
           implements org.springframework.core.convert.converter.Converter<AccountType, String> {

  @Override
  public String convert(AccountType value) {
      return value.getName();
  }
}
1 голос
/ 21 февраля 2020

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

Получение желаемой структуры классов может оказаться невозможным. Самое близкое, что мне удалось найти, связано с аннотацией @SecondaryTable, но предостережение в том, что это работает только для отношений @OneToOne.

В общем, я бы указал на вашу проблему с вопросом DTO и сущностей . Идея JPA состоит в том, чтобы отобразить схему и содержимое вашей базы данных в код доступным, но точным способом. Он снимает с себя тяжелую работу по управлению SQL запросами, но он предназначен в основном для отражения структуры вашей БД, а не для сопоставления ее с другим набором доменов.

Если организация вашей схемы БД делает не совсем соответствует потребностям связи ввода-вывода вашей системы, это может быть признаком того, что:

  • Ваша БД была спроектирована неправильно;
  • Ваша БД в порядке, но управляемые сущности (таблицы) в нем просто не соответствуют бизнес-сущностям (моделям) в вашей внешней связи.

Во втором случае сущности должны быть сопоставлены с DTO, которые затем могут быть переданы. Один объект может отображаться на несколько разных DTO. Один DTO может занять более одного (связанных!) Объектов для создания. Во-первых, это хорошая практика для систем среднего и большого размера: раздача ссылок на объект, являющийся точкой прямого доступа к вашей базе данных, представляет собой риск.

Помните, что просто потому, что id из accountType не участвует в вашем внешнем общении, не означает, что оно никогда не будет частью вашей бизнес-логики c.

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

0 голосов
/ 21 февраля 2020

Один из способов решить эту проблему - @JsonIgnore accountType и создать метод getType, например

@JsonProperty("type")   
var getType() {
    return accountType.getType();
}
...