※当サイトの記事には、広告・プロモーションが含まれます。

Spring Data JPAとPostgreSQLで複合一意(ユニーク)制約を試してみる

nazology.net

最近、アメリカのサウスフロリダ大学(USF)に所属する物理学者イヴァン・オレイニク氏ら研究チームは、スーパーコンピュータを用いたシミュレーションにより、ダイヤモンドよりも圧力に対して30%高い抵抗力を示す「BC8」と呼ばれる構造の生成条件が判明したと報告しました。

「ダイヤモンドより硬く割れにくい」炭素構造の生成条件が判明!宇宙には既にある可能性 - ナゾロジー

彼らによると、BC8が生成されるのは、高圧・高温の狭い条件の中だけだという。

具体的には、6000K(約5727℃)の温度と、1050GPaの圧力が必要になると予測されています。

「ダイヤモンドより硬く割れにくい」炭素構造の生成条件が判明!宇宙には既にある可能性 - ナゾロジー

⇧ 実現には至っていないと。 

複合一意(ユニーク)制約とは

Wikipediaによりますと、

In relational database management systems, a unique key is a candidate key. All the candidate keys of a relation can uniquely identify the records of the relation, but only one of them is used as the primary key of the relation. The remaining candidate keys are called unique keys because they can uniquely identify a record in a relation. 

https://en.wikipedia.org/wiki/Unique_key

Unique keys can consist of multiple columns.

Unique keys are also called alternate keys. Unique keys are an alternative to the primary key of the relation. In SQL, the unique keys have a UNIQUE constraint assigned to them in order to prevent duplicates (a duplicate entry is not valid in a unique column). Alternate keys may be used like the primary key when doing a single-table select or when filtering in a where clause, but are not typically used to join multiple tables.

https://en.wikipedia.org/wiki/Unique_key

⇧ とあるように、

  • 一意(ユニーク)制約
    → 単一のカラムによる一意(ユニーク)制約
  • 複合一意(ユニーク)制約
    → 複数のカラムによる一意(ユニーク)制約

ということかと。

ちなみに、

Differences between primary key constraint and unique constraint

Primary key constraint

  1. A primary key cannot allow null (a primary key cannot be defined on columns that allow nulls).
  2. Each table cannot have more than one primary key.
  3. On some RDBMS a primary key generates a clustered index by default.

Unique constraint

  1. A unique constraint can be defined on columns that allow nulls, in which case rows that include nulls may not actually be unique across the set of columns defined by the constraint.
  2. Each table can have multiple unique constraints.
  3. On some RDBMS a unique constraint generates a nonclustered index by default.

https://en.wikipedia.org/wiki/Unique_key

⇧ とあるように、

  • 主キー制約
  • 一意(ユニーク)制約

は役割が異なるので、

  • 複合主キー制約
  • 複合一意(ユニーク)制約

も役割が異なると。

Spring Data JPAPostgreSQLで複合一意(ユニーク)制約は実現できるのか調べてみる

何故か、Red HatJBossのドキュメントが上位表示されて、

access.redhat.com

JPAで「複合一意(ユニーク)制約」を実現できると言っているので、Spring Data JPAでも実現できるんじゃないかと。

ちなみに、

とかの違いについては、

medium.com

⇧ 上記サイト様がまとめてくださっています。

ちなみに、JDBC部分とJPA部分の間の橋渡しをしているHibernateの部分は、

medium.com

⇧ 他のライブラリで代替することも可能と。

Spring Bootを利用していて、「spring-boot-starter-data-jpa」を依存関係に追加している場合は、

spring.pleiades.io

⇧ デフォルトで「Hibernate」が使用されるらしい。

まぁ、Javaの標準APIJPAアノテーションとか使えるようになっているので、「Spring Data JPA」でも「複合一意(ユニーク)制約」を実現できるでしょうと仮説を立てておきますか。

Spring Data JPAPostgreSQLで複合一意(ユニーク)制約を試してみる

それでは、実際に、実現できるか試してみますか。

とりあえず、PostgreSQL 14系を「WSL 2(Windows Subsystem for Linux 2)」にインストールしているRocky Linux 9系にインストールしていくことにします。

まずは、コマンドプロンプトなどで「WSL 2(Windows Subsystem for Linux 2)」にインストールしているRocky Linux 9系を起動。

もう一つ別にコマンドプロンプトを起ち上げて、SSHログインしておきます。

とりあえず、PostgreSQL がインストールされていないか確認。

インストールされていなさそうなので、

www.postgresql.org

⇧ 上記を参考にインストール。

で、PostgreSQL のデフォルトのデータベースのログインでエラーになると...

どうやら、

hackteck.hatenablog.com

⇧ データベースの認証方法が、PostgreSQL のユーザーの認証になっていないようなので、設定。

PostgreSQL の設定ファイルの場所は、環境で異なるっぽい、紛らわしい...

sudo vi /var/lib/pgsql/14/data/pg_hba.conf

で、設定を反映したところ、PostgreSQL のユーザーpostgresでログインできました。

ついでに、

sakutomo.com

⇧ 上記サイト様を参考に、「複合一意(ユニーク)制約」のあるテーブルを作成しておきます。

ちなみに、

zenn.dev

⇧ 上記サイト様によりますと、PostgreSQL 15以降でNULLの重複を弾く便利な機能が導入された模様。

NOT NULL制約が属性(カラム)に付与できれば良いんだけど...

あと、外部のマシンからPostgreSQL のデータベースに接続できるように設定。

PostgreSQL のサービスを再起動して、PostgreSQL の設定を反映。

「WSL 2(Windows Subsystem for Linux 2)」のRocky Linux 9のPostgreSQL に外部から接続できました。

レコードをINSERTしておく。

「複合一意(ユニーク)制約」に違反するデータをINSERTしようとすると、エラーしてINSERTできないようにしてくれるのを確認。

そんでは、アプリケーション側を作成していきます。

Eclipseを起動して、「Spring Boot」なプロジェクトを作成。

ビルドツールとしては、Gradleを選択してます。

依存関係を追加。今回は、最低限、「Spring Data JPA」「PostgreSQL Driver」は必須ですかね。

で、ファイルを用意。

以下のような内容。

■/jpa-repo/build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.4'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}
    

■/jpa-repo/src/main/resources/application.properties

spring.application.name=jpa-repo

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://172.24.91.141:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres    

■/jpa-repo/src/main/java/com/example/demo/entity/Users.java

package com.example.demo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Data;

@Entity
@Table(name="users"
, schema="public"
, uniqueConstraints = {@UniqueConstraint(columnNames={"username", "email"})}
)
@Data
public class Users {

	@Id
	@SequenceGenerator(name = "users_id_seq")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id", columnDefinition = "serial")
	private Integer id;
	
    @Column(name="username")
	private String username;
    
    @Column(name="email")
	private String email;

}

■/jpa-repo/src/main/java/com/example/demo/repository/UsersRepository.java

package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.entity.Users;

public interface UsersRepository extends JpaRepository<Users, Integer> {

	Users findByUsernameAndEmail(String username, String email);
}    

■/jpa-repo/src/test/java/com/example/demo/repository/TestUsersRepository.java

package com.example.demo.repository;

import static org.junit.jupiter.api.Assertions.*;

import java.util.Objects;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import com.example.demo.entity.Users;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class TestUsersRepository {

	@Autowired
	private UsersRepository usersRepository;
	
	@Test
	public void testRead() {
		// インプット
		String username = "鈴木 二郎";
		String email = "suzuki@gmail.com";
		
		testFindByUsernameAndEmail(username, email);
		// テスト対象

	}
	
	@Test
	public void testCreate() {
		// インプット
		String username = "鈴木 二郎";
		String email = "suzuki@gmail.com";
		
		testDuplicationRecords(username, email);
		
	}
	
	public void testFindByUsernameAndEmail(String username, String email) {
		// テスト対象
		Users users = usersRepository.findByUsernameAndEmail(username, email);
		String actualUsername = users.getUsername();
		String actualEmail = users.getEmail();
		
		// NULLがあるケース
		if (Objects.isNull(actualUsername) && Objects.isNull(actualEmail) 
				&& (Objects.isNull(actualUsername) || Objects.isNull(actualEmail) )) {
			// TODO

		}
		
		assertEquals(username, actualUsername);
		assertEquals(email, actualEmail);		
	}

	public void testDuplicationRecords(String username, String email) {
		Users users = new Users();
		users.setUsername(username);
		users.setEmail(email);
		
		try {
			usersRepository.save(users);
		} catch (Exception e) {
			// 
			//testFindByUsernameAndEmail(username, email);
			System.out.println(e.getMessage());
		}	
	}
	
}

⇧ で、JUnitリポジトリのメソッドがテストできると。

... 省略
2024-04-14T20:56:01.095+09:00  INFO 22840 --- [jpa-repo] [           main] c.e.demo.repository.TestUsersRepository  : Started TestUsersRepository in 12.811 seconds (process running for 20.498)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
Hibernate: select u1_0.id,u1_0.email,u1_0.username from public.users u1_0 where u1_0.username=? and u1_0.email=?
Hibernate: insert into public.users (email,username) values (?,?)
2024-04-14T20:56:03.056+09:00  WARN 22840 --- [jpa-repo] [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 23505
2024-04-14T20:56:03.057+09:00 ERROR 22840 --- [jpa-repo] [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: duplicate key value violates unique constraint "users_username_email_key"
  詳細: Key (username, email)=(鈴木 二郎, suzuki@gmail.com) already exists.
could not execute statement [ERROR: duplicate key value violates unique constraint "users_username_email_key"
  詳細: Key (username, email)=(鈴木 二郎, suzuki@gmail.com) already exists.] [insert into public.users (email,username) values (?,?)]; SQL [insert into public.users (email,username) values (?,?)]; constraint [users_username_email_key]
2024-04-14T20:56:04.649+09:00  INFO 22840 --- [jpa-repo] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-04-14T20:56:04.652+09:00  INFO 22840 --- [jpa-repo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-04-14T20:56:04.661+09:00  INFO 22840 --- [jpa-repo] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

⇧ INSERTの処理で、「複合一意(ユニーク)制約」でエラーになっているから上手くいっているということですかね?

とりあえずは、Spring Data JPA で「複合一意(ユニーク)制約」が実現できたということにしておこう。

それにしても、意外にSpring Data JPA で「複合一意(ユニーク)制約」の実装の話がネット上に存在しないという...

Spring Data JPAを使う開発現場が少ないってことなんかね?

毎度モヤモヤ感が半端ない…

今回はこのへんで。