LoginSignup
5
2

More than 1 year has passed since last update.

【Java】JPA備忘録(Entity・ID自動採番・Repository・Specification・@Transational)

Last updated at Posted at 2022-04-10

まえがき

SpringBootでWEBアプリケーションを作るとき、よくSpring Data JPAを利用する。
Entityクラス関連のアノテーションや、トランザクションの動きについて時間が経つと忘れるので、備忘として残す。

Entityクラス

@Entity
@Table(name = "sample_user")
// @CreatedDate @LastModifiedDate を活性化するために必要
@EntityListeners(AuditingEntityListener.class)
public class SampleUserEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId;
    private String name;
    private Integer age;
    private String gender;
    private LocalDate birthDate;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private long version;

    @Column(name = "user_id")
    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    // その他ゲッター、セッターは割愛 

    @CreatedDate
    @Column(name = "created_at", updatable = true)
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    @LastModifiedDate
    @Column(name = "updated_at")
    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    @Version
    public long getVersion() {
        return version;
    }
}

主キーをAUTO INCREMENTする

下記の記事に分かりやすくまとまっていたので、リンクを共有させていただく。

MySQL

@GeneratedValue(strategy = GenerationType.IDENTITY)をフィールドに付与。
・IDカラムにAUTO_INCREMENTをつける
のパターンが一番手軽にAUTO INCREMENTできそう。
MySQLはシーケンスをサポートしていない

PostgreSQL

@GeneratedValue(strategy = GenerationType.IDENTITY)をフィールドに付与。
・IDカラムの型をserialにする
のパターンが一番手軽にAUTO INCREMENTできそう。
image.png

@Column

・特定のカラムに@Column(nullable = true, insertable = true, updatable = false)のように指定することで、EntityManagerで登録/更新を行うときに「このカラムはinsert/updateのSQLに含まない / このカラムがnullだったらERROR出す。」の制御ができる。

@Columnをつける場所 フィールド or ゲッター

@Columnはメソッドかフィールドに付与できる。
で、どっちにつけるべきなんだって話だが、公式ドキュメントにはゲッターにつけているのでゲッターにつける。
※おそらく、EntityManagerでSQLを発行する際に@Columnがついているゲッター経由で値を取得してSQLを組み立てている。

@CreatedDate @LastModifiedDate

・JPA Auditingの仕組みで、登録日時・更新日時が自動で入力される。
・Mainクラスに@EnableJpaAuditingを付与する。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@Version

・楽観ロックのために利用するバージョンカラムにつける。

@JoinColumn

1対1

・1対1(Company -- Prefecture)
CompanyとPrefectureの間にFK制約はない
・都道府県名はPrefectureテーブルで保持。CompanyテーブルはこのPrefectureCodeを保持。

Companyテーブル

companyId companyName prefectureCode
データ1 01 会社A 01
データ2 02 会社B 02
データ3 03 会社C 03

Prefectureテーブル

prefectureCode prefectureName
データ1 01 北海道
データ2 02 東京都
データ3 03 沖縄

データとして取得したい形式

companyId companyName prefectureName
01 会社A 北海道
02 会社B 東京都
03 会社C 沖縄
CompanyEntity.java
@Data
@Entity
@Table(name = "company")
public class CompanyEntity implements Serializable {
   @Id
   @Column(name = "company_id", length = 2)
   private String companyId;
   @Column(name = "company_name", length = 50)
   private String companyName;
   @Column(name = "prefecture_code", length = 2)
   private String prefectureCode;
   @OneToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "prefecture_code", referencedColumnName = "prefecture_code")
   private PrefectureEntity prefecture;
}
PrefectureEntity.java
@Data
@Entity
@Table(name = "prefecture")
public class PrefectureEntity implements Serializable {
   @Id
   @Column(name = "prefecture_code", length = 2)
   private String prefectureCode;
   @Column(name = "prefecture_name", length = 5)
   private String prefectureName;
   @OneToOne(mappedBy = "prefecture")
   private CompanyEntity company;
}

多対1 -> 参考

・Companyを軸に取得したい。
・多対1(Company -- Prefecture)

CompanyEntity.java
@Data
@Entity
@Table(name = "company")
public class CompanyEntity implements Serializable {
   @Id
   @Column(name = "company_id", length = 2)
   private String companyId;
   @Column(name = "company_name", length = 50)
   private String companyName;

  @ManyToOne(fetch = FetchType.LAZY)
  // 紐づくものがなくても無視する。
  @NotFound(action = NotFoundAction.IGNORE)
  // name : doctorInfoEntityのJOINしたいカラム名   referencedColumnName : prefectureテーブルにあるJOINしたいカラム名
  // insertable : DoctorInfoEntityでsave()実行する際に、insert対象のカラムとしない
  // updatable  : DoctorInfoEntityでsave()実行する際に、update対象のカラムとしない
  // foreignKey : the JPA provider to not generate the foreign key constraint.
  @JoinColumn(name = "prefecture_code", referencedColumnName = "prefecture_code",
      insertable = false, updatable = false,
      foreignKey = @javax.persistence
          .ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
  private PrefectureMasterEntity prefecture;

}
PrefectureEntity.java
@Data
@Entity
@Table(name = "prefecture_master")
public class PrefectureMasterEntity {

  @Id
  @Column(name = "prefecture_code", nullable = false, length = 2)
  private String prefectureCode;

  @Column(name = "prefecture_name", length = 5)
  private String prefectureName;

  // DoctorInfoEntityクラス内で@JoinColumnを付与しているプロパティ名
  @OneToMany(mappedBy = "prefecture")
  private Set<CompanyEntity> companies;

}

多対多

・多対多(Employee -- Department)

Repository

・CrudRepository か JpaRepository を継承したInterfaceを定義する。

@Repository
public interface SampleUserRepository extends CrudRepository<SampleUserEntity, String> {}

CrudRepositoryとJpaRepositoryのちがい

CrudRepository mainly provides CRUD functions.
JpaRepository provides some JPA-related methods such as flushing the persistence context and deleting records in a batch.
Because of the inheritance mentioned above, JpaRepository will have all the functions of CrudRepository and PagingAndSortingRepository. So if you don't need the repository to have the functions provided by JpaRepository and PagingAndSortingRepository , use CrudRepository.

Specificationで動的検索

Repository

JpaSpecificationExecutor<T>を継承する。

@Repository
public interface SampleUserRepository extends JpaSpecificationExecutor<SampleUserEntity> {}

Specification

@Component
public class SampleUserSpecification {

    public Specification<SampleUserEntity> containsName(String name) {
        return StringUtils.isBlank(name) ? null :
                (root, query, builder) -> builder.like(root.get("name"), "%" + name + "%");
    }

    public Specification<SampleUserEntity> equalsAge(Integer age) {
        return age == 0 || age == null ? null :
                (root, query, builder) -> builder.equal(root.get("age"), age);
    }

    public Specification<SampleUserEntity> equalsGender(String gender) {
        return StringUtils.isBlank(gender) ? null :
                (root, query, builder) -> builder.equal(root.get("gender"), gender);
    }

    public Specification<SampleUserEntity> equalsBirthDate(LocalDate birthDate) {
        return birthDate == null ? null :
                (root, query, builder) -> builder.equal(root.get("birth_date"), birthDate);
    }
    
}

Service

public List<SampleUserEntity> search(SampleUserSearchRequest request) {
    Optional<Specification<SampleUserEntity>> specs =
        Optional.ofNullable(Specification.where(sampleUserSpecification.containsName(request.getName())));
    specs = specs.map(spec -> spec.or(sampleUserSpecification.equalsAge(request.getAge())));

    return sampleUserRepository.findAll(specs.isPresent() ? specs.get() : Specification.where(null));
}

@Transationalをつけなかったらどうなる?

・Springコンテナが自動で「各Repositoryメソッド」に対して@Transationalを割り当ててくれる。

@Service
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    public void insert(List<CustomerEntity> customers) {
         // トランザクション1が走る。
         customerRespository.insert(customers.get(0);
         // トランザクション2が走る。
         customerRespository.insert(customers.get(1);
         // トランザクション3が走る。
         customerRespository.insert(customers.get(2);
    }
}

・↑の例だと、トランザクション3でこけても1と2はロールバックされない。
・1つのトランザクションとしてまとめたい場合は、メソッドに@Transatinalを付与する。

@Service
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    // このメソッドが終了するときにコミットされる。
    @Transational
    public void insert(List<CustomerEntity> customers) {
         customerRespository.insert(customers.get(0);
         customerRespository.insert(customers.get(1);
         customerRespository.insert(customers.get(2);
    }
}
5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2