You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中Flat实体继承Address并结合Builder模式的设计优化及最佳实践问询

Spring Boot中Flat实体继承Address并结合Builder模式的设计优化及最佳实践问询

咱们先从你问的几个核心问题逐个拆解聊起,结合你提供的代码一起分析:


一、标识符与数值类型的选择(short vs int/Long)

先看你用short的几个字段,以及addressId的选型,这里给你具体的建议:

  1. unitNumber(单元号):
    short的取值范围是-32768 ~ 32767,如果你的公寓单元号都是纯数字且不会突破这个上限,理论上能用,但更建议换成int:
    • 数据库存储成本差异极小(MySQL中smallint是2字节,int是4字节,百万级数据的存储差异可以忽略);
    • 避免后续业务扩容时的溢出风险(比如大型高层公寓的单元号可能超过32767);
    • 代码可读性更好,int是Java通用整数类型,团队协作时无需额外解释选型原因。
  2. addressId:
    你用Long是非常正确的选择,JPA自增主键用Long可以避免int的溢出问题,是行业通用的稳妥方案。
  3. streetNumber(街道号):
    和unitNumber同理,虽然大部分街道号不会超过32767,但int的扩展性更好,无额外成本,建议替换。
  4. 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应用的常见坑要避免

  1. 不要在Repository层写业务逻辑:Repository只负责数据访问,复杂的业务判断、数据转换放在Service层;
  2. 不要在Service层处理HTTP请求/响应:那是Controller层的职责,Service层应该是无状态的,只处理业务逻辑;
  3. Entity不要写复杂业务方法:Entity是POJO,只负责存储数据,业务逻辑放在Service层;
  4. 验证逻辑不要返回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();
    }
    
  5. JPA继承策略选对:如果基类不需要单独存储,用@MappedSuperclass;如果需要单独存储基类数据,再用@Entity加继承策略(如@Inheritance(strategy = InheritanceType.JOINED));
  6. 分层测试:不要只用TestRestTemplate测接口,结合@DataJpaTest测Repository、@SpringBootTest测Service、@WebMvcTest测Controller,定位问题更高效。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 11:18:10