まえがき
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できそう。
@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 | 沖縄 |
@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;
}
@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)
@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;
}
@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);
}
}