Spring Boot中Flat实体继承Address并结合Builder模式的设计优化及最佳实践问询
Spring Boot中Flat实体继承Address并结合Builder模式的设计优化及最佳实践问询
咱们先从你问的几个核心问题逐个拆解聊起,结合你提供的代码一起分析:
一、标识符与数值类型的选择(short vs int/Long)
先看你用short的几个字段,以及addressId的选型,这里给你具体的建议:
unitNumber(单元号):short的取值范围是-32768 ~ 32767,如果你的公寓单元号都是纯数字且不会突破这个上限,理论上能用,但更建议换成int:- 数据库存储成本差异极小(MySQL中
smallint是2字节,int是4字节,百万级数据的存储差异可以忽略); - 避免后续业务扩容时的溢出风险(比如大型高层公寓的单元号可能超过32767);
- 代码可读性更好,
int是Java通用整数类型,团队协作时无需额外解释选型原因。
- 数据库存储成本差异极小(MySQL中
addressId:
你用Long是非常正确的选择,JPA自增主键用Long可以避免int的溢出问题,是行业通用的稳妥方案。streetNumber(街道号):
和unitNumber同理,虽然大部分街道号不会超过32767,但int的扩展性更好,无额外成本,建议替换。postalCode(邮编):
这里强烈建议换成String类型:- 很多地区的邮编包含前导零(如
0012),用short会丢失前导零导致数据错误; - 部分国家的邮编包含字母(如加拿大的
M5V 2T6),short完全无法存储; - 即使当前只用数字格式,
String的扩展性更强,后续无需修改类型。
- 很多地区的邮编包含前导零(如
二、Builder模式与继承的实现优化
你当前的Builder实现把父类Address的所有字段都重复写在了Flat.Builder里,维护成本极高——如果Address新增/修改字段,Flat.Builder和Flat构造函数都要同步修改。这里给你一套优化方案:
1. 先调整Address的定义(改为映射超类)
Address应该是基类而非独立实体(你不需要单独存储Address数据,只需要把它的字段嵌入到Flat表中),所以用@MappedSuperclass替代@Entity:
package za.ac.shinysneakerz.domain; import jakarta.persistence.MappedSuperclass; @MappedSuperclass // 字段会被子类继承到数据库表中,而非生成独立的Address表 public class Address { protected Long addressId; protected short streetNumber; protected String streetName; protected String suburb; protected String city; protected String province; protected String postalCode; // 建议改为String类型 protected Address() {} // 给子类Builder用的构造函数,复用父类字段赋值逻辑 protected Address(Builder builder) { this.addressId = builder.addressId; this.streetNumber = builder.streetNumber; this.streetName = builder.streetName; this.suburb = builder.suburb; this.city = builder.city; this.province = builder.province; this.postalCode = builder.postalCode; } // Address的通用Builder,子类可继承 public static class Builder { protected Long addressId; protected short streetNumber; protected String streetName; protected String suburb; protected String city; protected String province; protected String postalCode; public Builder setAddressId(Long addressId) { this.addressId = addressId; return this; } public Builder setStreetNumber(short streetNumber) { this.streetNumber = streetNumber; return this; } // 其他字段的setter方法... // 支持复制父类字段 public Builder copy(Address address) { this.addressId = address.addressId; this.streetNumber = address.streetNumber; this.streetName = address.streetName; // 其他父类字段复制... return this; } public Address build() { return new Address(this); } } // 父类字段的Getter方法... }
2. 优化Flat的Builder实现(继承父类Builder)
让Flat.Builder继承Address.Builder,复用父类的字段设置逻辑,无需重复写所有父类字段:
package za.ac.shinysneakerz.domain; import jakarta.persistence.Entity; import jakarta.persistence.Id; @Entity public class Flat extends Address { @Id private short unitNumber; // 建议改为int类型 private String flatName; protected Flat() { super(); } // 调用父类的Builder构造函数,复用父类字段赋值逻辑 private Flat(Builder builder) { super(builder); // 父类处理Address的所有字段 this.unitNumber = builder.unitNumber; this.flatName = builder.flatName; } // 子类字段的Getter方法... @Override public String toString() { return "Flat{" + "flatName='" + flatName + '\'' + ", unitNumber=" + unitNumber + '}'; } // 继承自Address.Builder,仅需定义子类专属字段的逻辑 public static class Builder extends Address.Builder { private short unitNumber; private String flatName; public Builder setUnitNumber(short unitNumber) { this.unitNumber = unitNumber; return this; } public Builder setFlatName(String flatName) { this.flatName = flatName; return this; } // 支持复制Flat的所有字段(包括父类) public Builder copy(Flat flat) { super.copy(flat); // 调用父类的copy方法处理Address字段 this.unitNumber = flat.unitNumber; this.flatName = flat.flatName; return this; } @Override public Flat build() { return new Flat(this); } } }
3. 其他Builder优化点
- 不要在
build()方法里返回null:如果验证失败,抛出自定义异常(如ValidationException)而非返回null,调用方可以明确捕获错误,避免空指针; - 给
Flat生成equals()和hashCode()方法(基于unitNumber主键),方便在集合中使用。
三、Repository与Service层的优化
1. Repository层优化
package za.ac.shinysneakerz.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import za.ac.shinysneakerz.domain.Flat; import java.util.Optional; @Repository public interface FlatRepository extends JpaRepository<Flat, Short> { // 主键改为int的话,这里换成Integer // JPA派生查询可以自动识别父类Address的字段 Optional<Flat> findByFlatName(String flatName); Optional<Flat> findByStreetName(String streetName); // 不需要自定义getAll(),JpaRepository已提供findAll()方法返回List<Flat> // 如果需要Set<Flat>,可在Service层转换:new HashSet<>(flatRepository.findAll()) }
- 移除冗余的
getAll()方法,直接用JpaRepository自带的findAll(); - 如果需要返回
Set,在Service层转换即可,Repository只负责数据访问; - 确保
Address是@MappedSuperclass,否则JPA无法识别父类字段的派生查询。
2. Service层优化
package za.ac.shinysneakerz.service; import za.ac.shinysneakerz.domain.Flat; import java.util.Optional; import java.util.Set; public interface FlatService { // 通用CRUD方法可抽成BaseService接口,让FlatService继承以减少重复代码 Set<Flat> getAllFlats(); Optional<Flat> getFlatByUnitNumber(short unitNumber); // 建议改为int Optional<Flat> getFlatByFlatName(String flatName); Optional<Flat> getFlatByStreetName(String streetName); Flat createFlat(Flat flat); Flat updateFlat(Flat flat); void deleteFlat(short unitNumber); // 建议改为int }
- 不要让Service层完全镜像Repository的方法,应该聚焦业务逻辑:比如在
createFlat中调用Factory的验证逻辑、处理异常,再调用Repository保存; - 通用CRUD方法(如
findById,save)可以抽成BaseService接口,让FlatService继承,减少代码重复; - 实现类中不要直接返回Repository的结果,比如把
List转成Set,或者封装成DTO(如果需要对外暴露的话)。
四、分层Spring Boot应用的常见坑要避免
- 不要在Repository层写业务逻辑:Repository只负责数据访问,复杂的业务判断、数据转换放在Service层;
- 不要在Service层处理HTTP请求/响应:那是Controller层的职责,Service层应该是无状态的,只处理业务逻辑;
- Entity不要写复杂业务方法:Entity是POJO,只负责存储数据,业务逻辑放在Service层;
- 验证逻辑不要返回null:你的
FlatFactory中当前的条件判断是反的(if(Helper.isNullOrEmpty(flatName) || Helper.isValidPostalCode(postalCode)),意思是如果邮编有效就返回null,这是bug!),应该改为验证失败就抛出异常,比如:public static Flat createFlat(...) { if(Helper.isNullOrEmpty(flatName)) { throw new IllegalArgumentException("Flat name cannot be null or empty"); } if(!Helper.isValidPostalCode(postalCode)) { throw new IllegalArgumentException("Invalid postal code: " + postalCode); } // 其他验证逻辑... return new Flat.Builder()...build(); } - JPA继承策略选对:如果基类不需要单独存储,用
@MappedSuperclass;如果需要单独存储基类数据,再用@Entity加继承策略(如@Inheritance(strategy = InheritanceType.JOINED)); - 分层测试:不要只用
TestRestTemplate测接口,结合@DataJpaTest测Repository、@SpringBootTest测Service、@WebMvcTest测Controller,定位问题更高效。
内容来源于stack exchange
相关产品推荐
相关产品推荐

