JPA 没有主键的数据表,创建实体类,并查询某个字段值最大的那条数据,结果中有大量重复数据

本文探讨了一个数据表中存在大量重复数据的问题,导致无法设置单一主键。面对这种情况,提出了两种解决方案:一是选择不会重复的字段作为主键,二是针对查询需求,将查询结果中唯一字段设为主键。通过调整实体类中@Id注解的位置,成功解决了数据重复的问题,并实现了查询特定时间段内设备最新记录的业务需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据表简介:

该数据表用于记录某个设备发送的数据。在该表中,有大量重复的数据,因此,无论给哪个字段添加主键,或者设置联合主键都是不可行的。

codecreateTimeequipNumreqTimewight
83011629788252000SNU2348972021-08-24 15:14:2930
83011629788261000SNU2480932021-08-24 15:22:0322
83011629787680000SNU2348972021-08-24 15:18:1832
83051629787680000GBY2984562021-08-24 15:20:0230
83051629795000000GBY2984562021-08-24 17:18:0130
83021629794700000GBY2984562021-08-24 17:20:0230
83011629794700000SNU2348972021-08-24 17:16:0232
83011629794700000GBY2984562021-08-24 15:18:0230

而Jpa强制实体类中需要有主键。所以就很随便的选了第一个字段,在它上边加了注解@Id

实体类
@Entity
@Table(name = "equip")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Equip{

    @Id
    @Column(name = "code")
	private String code;
    
    @Column(name = "createTime")
	private String createTime;
    
    @Column(name = "equipNum")
	private String equipNum;
    
    @Column(name = "reqTime")
	private String reqTime;
    
    @Column(name = "wight")
	private String wight;

}

业务场景需求:

查询特定时间段内,某个设备(equipNum)最新(createTime)的那条记录。

实现思路:

按createTime范围进行筛选,之后按设备号(equipNum)进行分组,并获得某个设备号所对应的createTime值最大的那条记录

mysql
SELECT DISTINCT
	code,
	createTime,
	equipNum,
	reqTime,
	wight 
FROM
	equip eq
	JOIN (
	SELECT
		max( createTime ) mtime,
		equipNum meq,
		max( reqTime ) mreqt 
	FROM
		equip eq 
	WHERE
		createTime BETWEEN '1629424800000' 
		AND '1629425399999' 
	GROUP BY
		eq.equipNum 
	) AS maxtime ON eq.equipNum = maxtime.meq 
	AND eq.createTime = maxtime.mtime 
	AND eq.reqt = maxtime.mreqt 
ORDER BY
	equipNum;
持久层
@Repository
public interface EquipDao extends JpaRepository<Equip,String>{
    
    @Query(nativeQuery = true, value = "SELECT DISTINCT code,createTime,equipNum,reqTime,wight FROM equip eq JOIN ( SELECT max( createTime ) mtime, equipNum meq, max( reqTime ) mreqt FROM equip eq  WHERE createTime BETWEEN ?1  AND ?2 GROUP BY eq.equipNum ) AS maxtime ON eq.equipNum = maxtime.meq AND eq.createTime = maxtime.mtime AND eq.reqt = maxtime.mreqt ORDER BY equipNum;")
    public List<Equip> findAllByCreateTimeBetween(String startTime, String endTime);
}

问题:

运行后发现,输出的数据好多重复的,但是list.size()的大小和数据库中查的记录条数一致。

问题分析

其实问题是出在实体类中。@Id 这个注解虽然加了,但是我们去查看表结构的时候会发现,code 字段并没有成为主键,当然这也是我当前场景下所希望的结果,不要出现任何主键。然而,加了 @Id 注解的属性,却要临时的担起主键的部分职责,那就是唯一。但很明显,code 字段重复的记录很多,所以在最终的查询结果中,凡是 code 字段的值一样的记录,其他字段的值也都和第一条一样了。

解决
  • 方案1:找个不会重复的字段就可以了。在不会重复的属性上边加上 @Id,如果需要用联合主键的,那么就给多个属性上边加 @Id

    public class Equip{
    
        @Id
        @Column(name = "code")
    	private String code;
        
        @Id
        @Column(name = "createTime")
    	private String createTime;
        
        @Id
        @Column(name = "equipNum")
    	private String equipNum;
        
        @Column(name = "reqTime")
    	private String reqTime;
        
        @Column(name = "wight")
    	private String wight;
    }
    
  • 方案2:然而我这个表实在特殊,无论哪个字段都会重复,甚至会出现两条一模一样的记录,单个主键和联合主键都行不通。
    但是我这儿最终查询的结果,是基于 equipNum 分组的,group by equipNum,也就意味着最终的查询结果里的equipNum是唯一的,那就只需要给 equipNum 属性加上 @Id 注解即可

    public class Equip{
    
        @Column(name = "code")
    	private String code;
        
        @Column(name = "createTime")
    	private String createTime;
        
        @Id //查询结果中equipNum不会重复,所以加在这里
        @Column(name = "equipNum")
    	private String equipNum;
        
        @Column(name = "reqTime")
    	private String reqTime;
        
        @Column(name = "wight")
    	private String wight;
    }
    
测试通过,大功告成
<think>我们正在讨论的是在不创建数据库表(即没有对应的数据库表结构)的情况下,是否可以创建实体类(Entity Class)。实体类Java Persistence API (JPA) 中通常用于映射数据库表,因此,如果没有对应的数据库表,创建实体类通常是不符合常规做法的。但是,我们可以从不同的角度来考虑这个问题。 ### 1. **实体类的定义与作用** - 在JPA中,实体类(Entity Class)是一个映射到数据库表的Java类。它的每个实例代表表中的一行数据实体类通常使用`@Entity`注解标记,且必须有一个无参构造函数和主键(使用`@Id`标注)[^2]。 - 实体类的作用是进行对象关系映射(ORM),将对象的状态持久化到数据库中。因此,如果没有对应的数据库表,那么创建实体类就失去了其核心意义。 ### 2. **没有数据库表的情况下创建实体类的可行性** - **技术上可行**:Java语言允许你创建一个普通的Java类(POJO),即使它没有对应的数据库表。你可以在这个类中定义字段和方法,就像其他Java类一样。但是,这样的类不能称为实体类(Entity),因为它没有数据库表建立映射关系。 - **使用场景**:这样的类可以作为数据传输对象(DTO)或对象(VO)来使用,用于在不同层之间传递数据。例如,在前后端交互中,可以使用DTO来封装数据,而不需要与数据库表结构一致。 ### 3. **你的具体场景** 根据你之前的问题,你有一个体检接口,需要处理两种设备(T8和中医四诊)的数据。你不想为每个设备单独设计CRUD接口,而是希望通过一个统一的DTO(`ExaminationDataDTO`)来接收数据。这个DTO中包含了两种设备的数据字段,根据设备类型(`deviceType`)来决定使用哪个字段。 在这种情况下,你不需要为体检单独创建一个实体类,因为: - 你已经有两个设备的数据表(`T8ExamnationData`和`ZyszyData`),它们分别有自己的实体类。 - 体检数据实际上就是这两种设备数据的一种表现形式,没有独立的体检表。 但是,如果你希望使用JPA的基础功能(如保存、查询等),那么每个实体类必须对应一个数据库表。所以,如果你没有体检表,那么就不能创建对应的体检实体类。 ### 4. **替代方案:使用DTO作为数据传输载体** 你可以继续使用`ExaminationDataDTO`作为前端传入的数据结构,然后在服务层进行如下处理: - 根据`deviceType`的,将DTO中的相应数据(`t8Data`或`zyszyData`)提取出来,分别转换为对应的实体类(`T8ExamnationData`或`ZyszyData`)。 - 然后,使用各自的Repository(DAO)来保存这些实体。 这样,你不需要一个统一的体检实体类,而是利用已有的两个实体类分别处理。 ### 5. **总结:没有数据库表映射的实体类不可行** - **严格意义上的实体类(Entity Class)**:必须映射到数据库表,否则无法使用JPA进行持久化操作。因此,没有对应的表,就不能创建这样的实体类。 - **普通Java类(POJO)**:可以创建,但它不是实体类,不能用于数据库操作。它可以作为DTO或VO使用。 ### 6. **建议** 根据你的描述,你已经有设备数据表(T8和中医四诊),所以你应该为这两个设备分别创建实体类(如果还没有的话),然后通过一个DTO来统一接收数据,再根据设备类型将数据拆分到各自的实体类中。这样,你不需要一个统一的体检实体类,也不需要创建体检表。 示例代码: ```java // 服务层方法 @Transactional public void saveExaminationData(ExaminationDataDTO dto) { if (dto.getDeviceType() == DeviceType.T8) { T8ExamnationData t8Data = dto.getT8Data(); // 设置t8Data的其他公共字段(如用户ID等) t8Data.setUserId(dto.getUserId()); t8Repository.save(t8Data); } else if (dto.getDeviceType() == DeviceType.TCM) { ZyszyData zyszyData = dto.getZyszyData(); zyszyData.setUserId(dto.getUserId()); zyszyRepository.save(zyszyData); } } ``` 这样,你既没有创建新的体检表,也实现了统一接口接收数据,同时利用已有的两个实体类进行持久化。 ### 7. **注意事项** - **数据验证**:在保存数据前,需要验证DTO中的公共字段(如用户ID)以及设备特定数据的有效性[^2]。 - **事务管理**:确保保存操作在事务中执行,以保证数据的一致性。 ### 相关问题 1. 如何设计DTO与多个实体类之间的转换逻辑? 2. 在Spring Boot中如何实现动态选择Repository进行保存? 3. 如何为不同的设备类型设计统一的数据校验机制? 4. 如果未来增加新的设备类型,如何扩展当前的统一接口? 5. 使用DTO接收动态设备数据时,如何保证数据结构的可扩展性?</think>### 不创建体检表且无字段映射时创建实体类的可行性分析 #### 1. **实体类的本质与作用** 实体类JPA框架中必须满足两个核心条件: - **数据库表映射**:通过`@Entity`和`@Table`注解与数据库表建立映射关系[^2] - **字段映射**:通过`@Column`等注解实现对象属性与表字段的对应[^1] 若**不创建体检表且无字段映射**,创建实体类将面临以下问题: ```java // 无效的实体类示例(缺少必要注解和映射) public class Examination { // 无@Id注解 → 违反JPA规范 private Long id; // 无@Column注解 → 无法映射到表字段 private String deviceType; } ``` #### 2. **技术可行性评估** | 方案 | 可行性 | 原因说明 | |---------------------|--------|--------------------------------------------------------------------------| | **传统JPA实体类** | ❌ 不可行 | 违反ORM核心原则:无表映射的实体类无法持久化,启动时会抛出`MappingException`[^2] | | **DTO作为伪实体** | ⚠️ 部分可行 | 可作为内存对象使用,但无法执行数据库操作(save/find等)[^5] | | **JSONB存储方案** | ✅ 可行 | 通过单字段存储JSON数据,规避字段映射需求(需PostgreSQL等支持JSONB的数据库)| #### 3. **推荐替代方案:JSONB存储** 在**不新建体检表**的前提下,可通过JSONB字段实现灵活存储: ```java // 实体类定义 @Entity @Table(name = "existing_table") // 复用已有表 public class ExistingEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // JSONB字段存储体检数据 @Column(columnDefinition = "jsonb") private String examinationData; } // DTO结构 public class ExaminationDataDTO { private Integer deviceType; // 1=T8, 2=中医四诊 private Map<String, Object> t8Data; // 动态字段 private Map<String, Object> tcmData; // 动态字段 } ``` #### 4. **操作流程** 1. **数据接收**:前端传入`ExaminationDataDTO` 2. **JSON转换**:将DTO序列化为JSON字符串 ```java ObjectMapper mapper = new ObjectMapper(); String jsonData = mapper.writeValueAsString(dto); ``` 3. **数据存储**:存入JSONB字段 ```java ExistingEntity entity = new ExistingEntity(); entity.setExaminationData(jsonData); repository.save(entity); ``` 4. **数据读取**:反序列化使用 ```java ExistingEntity entity = repository.findById(id).get(); ExaminationDataDTO dto = mapper.readValue( entity.getExaminationData(), ExaminationDataDTO.class ); ``` #### 5. **优势与风险** **✅ 优势:** - 避免新建体检表,复用现有表结构[^3] - 完美支持动态字段(T8/中医四诊的差异化数据结构) - 简化CRUD操作(仅需处理单个JSON字段) **⚠️ 风险:** 1. **查询性能**:JSON字段查询效率低于结构化字段[^4] ```sql -- JSON字段查询示例(效率较低) SELECT * FROM existing_table WHERE examination_data->>'deviceType' = '1' ``` 2. **数据验证**:需在业务层实现JSON结构校验[^2] 3. **索引限制**:只能对完整JSONB字段创建GIN索引,无法为内部字段单独索引 #### 6. **验证与优化建议** 1. **JSON Schema验证**(确保数据完整性) ```java JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V7); JsonSchema schema = factory.getSchema(validationJson); schema.validate(mapper.valueToTree(dto)); // 抛出ValidationException ``` 2. **高频查询优化**:将`deviceType`等高频字段提取为独立列 ```java @Column(name = "exam_device_type") private Integer deviceType; // 独立字段可加索引 @Column(columnDefinition = "jsonb") private String deviceSpecificData; // 仅存设备专用数据 ``` ### 结论 **可以创建实体类**,但需满足: 1. 实体类必须映射到**现有数据库表**(非新建体检表) 2. 使用**JSONB字段**代替直接字段映射 3. 通过DTO实现内存中的结构化表示 4. 在业务层实现数据验证和转换逻辑[^2] > 此方案符合引用[2]强调的**数据合法性校验**原则[^2],同时通过JSONB实现元数据管理(业务元数据+技术元数据)[^5],是平衡灵活性与存储效率的最佳选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值