【问题标题】:Is there a way to create one JPA entity based on many database tables and do I really have to do this or is it a bad practice?有没有一种方法可以基于许多数据库表创建一个 JPA 实体,我真的必须这样做还是一种不好的做法?
【发布时间】:2020-06-05 07:09:47
【问题描述】:

我对 Spring Data JPA 技术还很陌生,目前面临一项我无法处理的任务。我正在寻求此类案例的最佳实践。

在我的 Postgres 数据库中,我有两个以一对多关系连接的表。表“account”有一个字段“type_id”,它是对表“account_type”的字段“id”的外键引用:

所以'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> 的接口。

现在如果我去: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"
   }
]

当然,我可以为具有 String 字段而不是 AccountType 字段的帐户创建新的可序列化类,并且可以将 JPA Account 类映射到从 AccountType 字段中提取帐户类型字符串的类。但对我来说,这看起来像是不必要的开销,我相信对于这种情况可能会有更好的模式。

例如,我脑子里想的是,我可能会以某种方式创建一个 JPA 实体类(字符串字段表示帐户类型),它将基于两个数据库表,并且每个拥有内部对象的不必要的复杂性将自动减少当我调用存储库方法时:)此外,我将能够在我的业务逻辑中使用这个实体类,而无需任何额外的“包装器”。

附:我读到了 @SecondaryTable 注释,但它看起来只能在两个表之间存在一对一关系的情况下才有效,这不是我的情况。

【问题讨论】:

  • 我会为 AccountType 使用 Enum(为方便起见保存为字符串,但如果您想保留架构,您可以依赖序数,或添加 AttributeConverter)请参阅:vladmihalcea.com/…

标签: spring hibernate jpa kotlin spring-data-jpa


【解决方案1】:

有几个选项可以在没有 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 返回类型的查询方法或编写一个将 proection 类作为 arg 的方法。

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

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

另一种方法是使用 Jackson 注释。我在您的问题中注意到您正在手动将结果转换为 JSON 字符串并从控制器返回一个字符串。如果 Jackson Json 库位于类路径中,则不需要这样做。请参阅上面的控制器。

因此,如果您将序列化留给 Jackson,您可以使用几个注释将视图与实体分开。请注意,我将使用 Jackson mixin 应用这些,而不必使用 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();
  }
}

【讨论】:

  • 对 OP 的好建议!但是,我很好奇您想到的 DTO 的缺点是什么?毕竟,基于界面的投影只是同一来源提到的基于对象(基于 dto)的投影的简化等效项。
  • 使用 DTO 的人通常会将他们的整个域模型复制为 DTO 层,而无需采用全有或全无的方法。除了违反 DRY 和维护繁琐的映射代码之外,Spring Data JPA Web 扩展的好处也丢失了,这使得在零代码的域模型之上构建通用的 rest api(页面、排序、过滤器)变得很容易。 docs.spring.io/spring-data/jpa/docs/current/reference/html/… 使用 DTO 模型,那是您需要自己编写的又一大乏味样板文件。
  • 虽然您的推理完全正确,但我认为您所指的预测实际上是 Spring 对 DTO 的干净、集成良好的实现。它们继承了所有 DTO 所做的相同想法,即将实体的可见性限制在将有关它的信息传递给另一方所需的最低限度。这些预测高度优于基于data class 的 DTO,但它们仍然是系统角色意义上的 DTO。不要将其最直接的实现的性能不佳和 spring-integration 归咎于这个想法。
  • 我并不是说他们不是。我反对全有或全无 DTO 方法。公开你的领域模型,并在必要时使用投影、Jackson 注释或其他任何东西对其进行调整。将整个实体模型复制为 DTO 模型(通常会发生)加上映射样板似乎有点 10 年前。
  • 非常感谢你们的讨论。我想现在我对问题的本质有了更广泛的了解,并且更清楚地看到了我的应用程序层之间的区别。我将尝试对@Projection 使用方法,因为它现在确实适合我的需求。感谢您的帮助
【解决方案2】:

实现您的目标的最简单的方法之一 - 至少从外部客户的角度来看 - 与自定义序列化、您似乎知道的内容以及 @YoManTaMero 扩展的内容有关.

可能无法获得所需的类结构。我设法找到的最接近的与 @SecondaryTable 注释有关,但需要注意的是这仅适用于 @OneToOne 关系。

一般来说,我会将您的问题归结为DTOs and Entities 的问题。 JPA 背后的想法是以一种可访问但准确的方式将数据库的架构和内容映射到代码。它消除了管理 SQL 查询的繁重工作,但它的设计主要是为了反映数据库的结构,而不是将其映射到不同的域集。

如果您的数据库架构的组织与系统 I/O 通信的需求不完全匹配,这可能表明:

  • 您的数据库设计不正确;
  • 您的数据库很好,但其中的可管理实体(表)与外部通信中的业务实体(模型)不直接匹配。

如果是第二种情况,实体应该映射到 DTO,然后可以传递。单个实体可以映射到几个不同的 DTO。单个 DTO 可能需要创建多个(相关!)实体。首先,这对于大中型系统来说是一个很好的实践——分发对作为数据库直接访问点的对象的引用是有风险的。

请注意,仅仅因为 accountTypeid 不参与您的外部通信并不意味着它永远不会成为您业务逻辑的一部分。

总结一下:JPA 在设计时考虑到了易于数据库访问,而不是为了平滑外部通信。为此,其他工具 - 例如杰克逊序列化器 - 被使用,或某些设计模式 - 如 DTO - 正在被使用。

【讨论】:

  • 感谢您提供有用的链接和对我的应用程序架构的思考的详细回复。欣赏它
【解决方案3】:

解决此问题的一种方法是 @JsonIgnore accountType 并创建 getType 方法,如

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-24
    • 2022-01-19
    • 1970-01-01
    相关资源
    最近更新 更多