【Java 实战】如何解决自增 ID 发生重复问题?

264 阅读3分钟

自增 ID , 是一种常见的 ID 生成方式

以 pgsql 为例:

CREATE TABLE COMPANY(
   ID serial PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
);

其中 ID 为自增字段;

但可能,我们会在一些边界情况(例如,数据库迁移)遇到这样的报错:

 could not execute statement [
 	错误: 重复键违反唯一约束"company_pkey"
 	详细:键值"(id)=(1)" 已经存在
 ] [insert into public.company (name) values (?)]

数据在插入时,没有按照 max + 1 的预期添加

问题复现

  1. 初始化数据库,添加默认数据

    CREATE TABLE COMPANY(
       ID serial PRIMARY KEY     NOT NULL,
       NAME           TEXT    NOT NULL,
       AGE            INT     NOT NULL,
       ADDRESS        CHAR(50),
       SALARY         REAL
    );
    INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
    VALUES (1, 'Paul', 32, 'California', 20000.00 );
    
    INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
    VALUES (2, 'Allen', 25, 'Texas', 15000.00 );
    
    INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
    VALUES (3, 'Teddy', 23, 'Norway', 20000.00 );
    
    INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
    VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 );
    
    INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
    VALUES (5, 'David', 27, 'Texas', 85000.00 );
    
    INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)
    VALUES (6, 'Kim', 22, 'South-Hall', 45000.00 );
    
    INSERT INTO COMPANY VALUES (7, 'James', 24, 'Houston', 10000.00 );
    
    
  2. java 实体类

@Entity
@Table(name = "company", schema = "public")
public class Company {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  public Company() {
  }
  // getter/setter
}
  1. RepositoryService
@Repository
public interface CompanyRepository extends JpaRepository<Company, Long> {}
@Service
public class CompanyService {
  private final CompanyRepository companyRepository;

  @Autowired
  public CompanyService(CompanyRepository companyRepository) {
      this.companyRepository = companyRepository;
  }
  public Company addCompany(Company company) {
    return companyRepository.save(company);
  }
}
  1. 运行

    Company company = new Company();
    company.setName("Company 2");
    companyService.addCompany(company);
    
  2. 报错

     could not execute statement [
     	错误: 重复键违反唯一约束"company_pkey"
     	详细:键值"(id)=(1)" 已经存在
     ] [insert into public.company (name) values (?)]
    

问题分析

以 PostgreSQ L数据库为例。

# ...
INSERT INTO COMPANY VALUES (7, 'James', 24, 'Houston', 10000.00 );

数据库初始化的时候,已经预先插入了 7 条数据。

但是在代码运行的时候,生成的 ID 似乎是重新还是计数的。

解决方案

SEQUENCE

一个序列是一个独立于表的对象,它用于生成唯一的整数。

当使用 SERIAL 数据类型为一个表列创建一个自增字段时,PostgreSQL会自动创建一个序列。

但是,在手动插入数据时,这个序列并不会自动更新。

pgAdmin 中查看这个序列的属性,会有更直观的体会

image-20231108170635391.png

注意 currentValue 这个值, 在初始化插入的时候并不会自动更新到 8

解决这个问题,我们可以使用 ALTER SEQUENCE 语句设置序列的字段的值。

ALTER SEQUENCE company_id_seq RESTART WITH 8;

或者在可视化界面中直接修改 .

其他

有的开发人员,可能会考虑,在代码层解决这一问题.

@Entity
@Table(name = "company", schema = "public")
public class Company {
  @Id
  private Long id;
}

@Repository
public interface CompanyRepository extends JpaRepository<Company, Long> {
    @Query("SELECT MAX(c.id) FROM Company c") // JPA Query to find the max ID
    Long findMaxId();
}

@Service
public class CompanyService {
	// ...
    public Company addCompany(Company company) {
        Long maxId = companyRepository.findMaxId(); // New method to find max ID
        maxId = (maxId == null) ? 1 : maxId + 1; // If maxId is null, start from 1
        company.setId(maxId); // Set the new ID
        return companyRepository.save(company); // Save the new company object
    }

}

:warning: 请注意,这种方法绕过了数据库的自增机制,如果有多个应用实例或者多个线程同时插入数据,可能会导致ID冲突。因此,在高并发的生产环境中,这可能不是最佳实践。

在对数据库进行直接的操作时,

除了表以外, 多注意一些独立于表的对象,

可以避免一些意料之外的错误发生