前準備 †
Entity の自動生成 †
- プロジェクトの対象サーバを設定
- データベースからエンティティを作成
- 新しいデータソース を選択して、先ほど作った MySQL への接続を選ぶ (← 本来は Glassfish の Connection Pool を指定すれば良いはずだが、Netbeans 7.3 では Glassfish の Connection Pool を指定するとデータベース情報が読み取られない)
- テーブルが読み取られた
- Order テーブルは、Order1 クラスになるのね ("Order by" とかぶるからだろう)
- 終了
- Entity クラス と persistence.xml が生成された
なんか、pom.xml も勝手にいじってくれて eclipse-link が provide 属性で登録されている
- DataSource? を Glassfish に定義されているものに変更する
- Glassfish の Connection Pool を指定して、うまく Entity クラスが自動生成されれば必要ない作業。workround で作った jdbc/warehouse でテーブル情報を読み取ったので、jdbc/warehouse を使うような設定で自動生成されている。
- persistence.xml のデータソースを Glassfish の Datasource に変更する
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="warehousePU" transaction-type="JTA">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/MySQLDataSource</jta-data-source>
<class>com.mycompany.entity.Customer</class>
<class>com.mycompany.entity.Item</class>
<class>com.mycompany.entity.Order1</class>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>
- glassfish-resources.xml に、jdbc://warehouse の内容が定義されているので削除する
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN"
"http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
</resources>
自動生成されたコードの内容 †
テーブル構造 †
CREATE TABLE `warehouse`.`customer` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(255) NOT NULL ,
`address` VARCHAR(1024) ,
PRIMARY KEY (`id`) );
CREATE TABLE `warehouse`.`item` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(255) NOT NULL ,
`price` BIGINT ,
`stock` BIGINT ,
PRIMARY KEY (`id`) );
CREATE TABLE `warehouse`.`order` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`customer_id` BIGINT NOT NULL ,
`item_id` BIGINT NOT NULL ,
`amount` BIGINT NOT NULL,
`shipdate` DATETIME NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`customer_id`) references customer(`id`),
FOREIGN KEY (`item_id`) references item(`id`) );
Customer.java †
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.mycompany.entity;
import java.io.Serializable;
import java.util.Collection;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
* Customer Table
* @author atsushi
*/
@Entity
@Table(name = "customer")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Customer.findAll", query = "SELECT c FROM Customer c"),
@NamedQuery(name = "Customer.findById", query = "SELECT c FROM Customer c WHERE c.id = :id"),
@NamedQuery(name = "Customer.findByName", query = "SELECT c FROM Customer c WHERE c.name = :name"),
@NamedQuery(name = "Customer.findByAddress", query = "SELECT c FROM Customer c WHERE c.address = :address")})
public class Customer implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 255)
@Column(name = "name")
private String name;
@Size(max = 1024)
@Column(name = "address")
private String address;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "customerId")
private Collection<Order1> order1Collection;
public Customer() {
}
public Customer(Long id) {
this.id = id;
}
public Customer(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@XmlTransient
public Collection<Order1> getOrder1Collection() {
return order1Collection;
}
public void setOrder1Collection(Collection<Order1> order1Collection) {
this.order1Collection = order1Collection;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Customer)) {
return false;
}
Customer other = (Customer) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.mycompany.entity.Customer[ id=" + id + " ]";
}
}
レコード ⇔ Entity Class †
- @Entity : Entity Class 宣言
- @Table(name = "customer") : 対応するテーブル
- @NamedQueries? : → JPQL
カラム ⇔ Field変数 †
- @Column(name = "id") : カラムに対応する変数宣言
- @Basic
- (optional = true/false) : Null を許可するか
- (fetcy = FetcyType?.EAGER/FetchType?.LAZY) : 省略時 EAGER。LAZY のとき、getXXX() されるまで RDB から値をとってこない。BLOB 項目などで使う。
- @NotNull?
- @Size(min = 1, max = 255)
PK †
- @Id : Primary Key
- @GeneratedValue? : オートインクリメント
- (strategy = GenerationType?.SEQUENCE) : RDBの機能でキーを生成
- (strategy = GenerationType?.IDENTITY) : RDBの機能でキーを生成
- (strategy = GenerationType?.TABLE) : SEQUENCE テーブル。[キー名]と[値] の 2 列からなるテーブル
- (strategy = GenerationType?.AUTO) : 自動 (デフォルト値)
- MySQL や Postgresql 、Derby (Java DB) などには、AUTO_INCREMENT 型が有る
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
- MySQL
CREATE TABLE xxx_tbl
(id long AUTO_INCREMENT, col_name2 data_type2, ...,
INDEX(id));
- Postgresql
CREATE TABLE xxx_tbl
(id bigserial PRIMARY KEY, col-name2 data_type2, ...)
- Oracle の場合
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="シーケンステーブル名")
@SequenceGenerator(name="シーケンステーブル名", sequenceName="シーケンステーブル名", allocationSize=1)
@Column(name="id", nullable = false, insertable = false, updatable = false)
private Long id;
http://otn.oracle.co.jp/forum/thread.jspa?threadID=35001797
リレーション (1:n) †
Item.java †
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.mycompany.entity;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
*
* @author atsushi
*/
@Entity
@Table(name = "item")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Item.findAll", query = "SELECT i FROM Item i"),
@NamedQuery(name = "Item.findById", query = "SELECT i FROM Item i WHERE i.id = :id"),
@NamedQuery(name = "Item.findByName", query = "SELECT i FROM Item i WHERE i.name = :name"),
@NamedQuery(name = "Item.findByPrice", query = "SELECT i FROM Item i WHERE i.price = :price"),
@NamedQuery(name = "Item.findByStock", query = "SELECT i FROM Item i WHERE i.stock = :stock")})
public class Item implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 255)
@Column(name = "name")
private String name;
@Column(name = "price")
private BigInteger price;
@Column(name = "stock")
private BigInteger stock;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "itemId")
private Collection<Order1> order1Collection;
public Item() {
}
public Item(Long id) {
this.id = id;
}
public Item(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BigInteger getPrice() {
return price;
}
public void setPrice(BigInteger price) {
this.price = price;
}
public BigInteger getStock() {
return stock;
}
public void setStock(BigInteger stock) {
this.stock = stock;
}
@XmlTransient
public Collection<Order1> getOrder1Collection() {
return order1Collection;
}
public void setOrder1Collection(Collection<Order1> order1Collection) {
this.order1Collection = order1Collection;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Item)) {
return false;
}
Item other = (Item) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.mycompany.entity.Item[ id=" + id + " ]";
}
}
Order1.java †
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.mycompany.entity;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author atsushi
*/
@Entity
@Table(name = "order")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Order1.findAll", query = "SELECT o FROM Order1 o"),
@NamedQuery(name = "Order1.findById", query = "SELECT o FROM Order1 o WHERE o.id = :id"),
@NamedQuery(name = "Order1.findByAmount", query = "SELECT o FROM Order1 o WHERE o.amount = :amount"),
@NamedQuery(name = "Order1.findByShipdate", query = "SELECT o FROM Order1 o WHERE o.shipdate = :shipdate")})
public class Order1 implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
@Basic(optional = false)
@NotNull
@Column(name = "amount")
private long amount;
@Basic(optional = false)
@NotNull
@Column(name = "shipdate")
@Temporal(TemporalType.TIMESTAMP)
private Date shipdate;
@JoinColumn(name = "item_id", referencedColumnName = "id")
@ManyToOne(optional = false)
private Item itemId;
@JoinColumn(name = "customer_id", referencedColumnName = "id")
@ManyToOne(optional = false)
private Customer customerId;
public Order1() {
}
public Order1(Long id) {
this.id = id;
}
public Order1(Long id, long amount, Date shipdate) {
this.id = id;
this.amount = amount;
this.shipdate = shipdate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public long getAmount() {
return amount;
}
public void setAmount(long amount) {
this.amount = amount;
}
public Date getShipdate() {
return shipdate;
}
public void setShipdate(Date shipdate) {
this.shipdate = shipdate;
}
public Item getItemId() {
return itemId;
}
public void setItemId(Item itemId) {
this.itemId = itemId;
}
public Customer getCustomerId() {
return customerId;
}
public void setCustomerId(Customer customerId) {
this.customerId = customerId;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Order1)) {
return false;
}
Order1 other = (Order1) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.mycompany.entity.Order1[ id=" + id + " ]";
}
}
リレーション (n:1) †
- @ManyToOne?
- OneToMany? のデフォルトのフェッチ方式は EAGER
- フェッチ方式を変えたい場合は (fetch = FetchType?.LAZY)
時刻型へのマッピング †
- @Temporal(TemporalType?.TIMESTAMP)
- Entity のフィールド変数は Date
- RDB のカラムの型は何かを指定する。
- TemporalType?.TIMESTAMP
- TemporalType?.DATE
- TemporalType?.TIME
その他の機能 †
複合PK †
- 複合PKとかお願いだからやめて !! 年金みたいな事になるよ (3億レコードのうち5000万レコードが壊れてるってどういうことやねん)
- PK は、人工キーでお願いします。(BIGINT 型で Auto Increment の "id" 一項目)
- @EmbeddedId? : 複合キーの Bean を Entity の EmbeddedId? にする
public class ComplrexId {
private String name;
private String age;
//… getter, setter, queals, hashcode …
}
@Entity
public class Account {
@EmbeddedId
private ComprexId id;
//… getter, setter, queals, hashcode …
}
- @IdClass? :
public class ComplrexId {
private String name;
private String age;
//… getter, setter, queals, hashcode …
}
@Entity
@IdClass(ComprexId.class)
public class Account {
@Id
private String name;
@Id
private String age;
//… getter, setter, queals, hashcode …
}
RDB と関係しない項目 †
JAXB †
- @XmlRootElement? : JAXB で XML とマッピングできることを示すアノテーション
- NetBeans? の自動生成でついてくるからそのままにしておく
- テストなんかで、XML から Entity を作ったりするのかなぁ
コード値 †
public enum STATE {
ON,
OFF,
ACTIVE,
PASSIVE
}
@Entity
public class Sensor {
@Id
private long id;
@Enumerated
priavte STATE state;
}
- データベースの state カラムは int とか char(1) とか
- ON のとき 0、OFF のとき 1、ACTIVE のとき 2、PASSIVE のとき 3 が入る
- STATE の項目を増やすときには、後ろに追加すること
- データベース上も文字列にしたい場合には @Enumerated(EnumType?.STRING)
リレーションまとめ †
- @OneToOne? (デフォルトのフェッチ方式は EAGER)
- @ManyToOne? (デフォルトのフェッチ方式は EAGER)
- @OneToMany? (デフォルトのフェッチ方式は LAZY)
- @ManyToMany? (デフォルトのフェッチ方式は LAZY)
キャッシュ (☆必ず無効化する) †
@Entity
@Cacheable(true)
public class Item {
...
}
で、Entity が EntityManager? 内にキャッシュされるようになる
Unit Test で動かす †
- テスト用のディレクトリを作る
- プロジェクト View からはこう見える (一度 [実行]-[プロジェクトをビルド] をする必要があるかも知れない)
- pom.xml に junit と derby への依存関係を追記する
<dependencies>
...
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.9.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
- テスト用の persistence.xml を準備する
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="warehouseTestPU" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>com.mycompany.entity.Customer</class>
<class>com.mycompany.entity.Item</class>
<class>com.mycompany.entity.Order1</class>
<properties>
<property name="eclipselink.target-database" value="Derby"/>
<property name="javax.persistence.jdbc.driber" value="org.apache.derby.jdbc.ClientDriver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:derby:/tmp/testdb;create=true"/>
<property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
<!-- OFF,SEVERE,WARNING,INFO,CONFIG,FINE,FINER,FINEST,ALL -->
<property name="eclipselink.logging.level" value="FINE" />
<property name="eclipselink.logging.timestamp" value="false" />
<property name="eclipselink.logging.session" value="false" />
<property name="eclipselink.logging.thread" value="false" />
</properties>
</persistence-unit>
</persistence>
- Derby の組み込み DB
- /tmp/testdb にデータベースファイルを造り、実行時にはテーブルを削除
- Entity Class からテーブルを作る
- eclipselink のログ設定は、persistence.xml に定義する。level を FINE にすると、発行する SQL 文まで見える
- JAXB のマッピング用に jaxb.index ファイルを作る (JAXB で XML⇔Object 変換を行うためには、パッケージ配下に ObjectFactory? か jaxb.index ファイルが必要)
Customer
Item
Order1
- テストデータ Index1.xml
<?xml version="1.0" encoding="UTF-8"?>
<item>
<name>こんぼう</name>
<stock>255</stock>
<price>100</price>
</item>
- Unit Test (ItemTest?.java)
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.mycompany.test.entity;
import com.mycompany.entity.Item;
import java.io.File;
import java.math.BigInteger;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.equalTo;
/**
*
* @author atsushi
*/
public class ItemTest {
private static JAXBContext jaxbContext;
@BeforeClass
public static void setUpClass() throws JAXBException {
jaxbContext = JAXBContext.newInstance("com.mycompany.entity");
}
@Test
public void testCreateItem() throws JAXBException {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("warehouseTestPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// JAXBでXMLを読み込んでItemを作成し、RDBに登録
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
Item item1 = (Item) unmarshaller.unmarshal(new File("src/test/resources/item1.xml"));
tx.begin();
em.persist(item1);
tx.commit();
// 全件検索
Query query = em.createNamedQuery("Item.findAll");
List<Item> items = query.getResultList();
// 検索結果の検証
assertThat(items.size(), is(equalTo(1)));
assertThat(items.get(0).getId(), is(equalTo(1L))); // Auto Incremet
assertThat(items.get(0).getName(), is(equalTo("こんぼう")));
assertThat(items.get(0).getPrice(), is(equalTo(new BigInteger("100"))));
assertThat(items.get(0).getStock(), is(equalTo(new BigInteger("255"))));
em.close();
emf.close();
}
}
- 実行結果
- リファクタリング
- price と stock が BigInteger? だと扱いにくいので int に変更する → 再テストをして合格なので OK
- ... とは厳密には言えないんだよなぁ ... Unit Test は Derby で、本番環境は MySQL。MySQL で動くことは証明できない
- 手軽に手元でテストできるというメリットと、本番環境と違うというデメリットを勘案する必要あり
- 松案 Unit Test 用に開発者分のインスタンスを準備する (そうしないと同時実行できない)
- 竹案 開発者端末にデータベースを準備する
- 梅案 開発者端末での Unit Test は、Derby でやる。ただし Jenkins で定時テストをする際には MySQL で行う
- MySQL ならまだ良いけど、Oracle とか DB2 とかだと費用的な問題でもっと話はやっかいになる。XE(Express Edition) を使うという手もあるけど、有象無象のサポートをやることを考えると梅案も捨てがたい
EntityManager? †
- 要は、EntityManager? 管理下の Entity は、flush() で RDB と同期される
- EntityManager? 管理外の Entity は、RDB と同期されない
- Entity の状態遷移
- EntityManager?
void persist(Object entity) | インスタンスを管理対象にする。flush() で RDB に挿入される |
<T> find(Class<T> entityClass, Object primaryKey) | 主キーを指定して、RDB からデータを取得する |
<T> getReference(Class<T> entityClass, Object primaryKey) | 主キーを指定して、RDB からデータを取得する。中身は遅延ロード |
void remove(Object entity) | エンティティを RDB から削除する |
<T> T merge(T entity) | 分離状態のエンティティを EM 管理状態にする。flush() で RDB が更新される |
void refresh(Object entity) | エンティティの状態を RDB の内容で上書きする |
void flush() | EM 管理下のエンティティの内容と RDB の状態を同期する。DBで採番された値(Postgresql の BIGSERIAL や Oracle の SEQUENCE)がトランザクション中で必要な場合に flush() すると、採番値が Entity Bean に反映される (cf.補足) |
void clear() | EM 管理下の全てのエンティティを、管理外(分離状態)にする。flush() で RDB が更新されなくなる |
void detach(Object entity) | EM 管理下の全ての entity を、管理外(分離状態)にする。flush() で RDB が更新されなくなる |
boolean contains(Object entity) | entity が、EM 管理下であるかどうかを検証する |
- 分離状態の使い方
- Web 層で RDB から読み込んだ Entity を使うときに、分離状態にする
- 分離状態にしないまま Web 層で Entity を書き換えると RDB に書き込まれちゃう
- 意図しないデータの上書きを防ぐために clear() / merge(entity) はやった方が良いだろう
- flush() についての補足
基本的に、Entity クラスに名前付き Query として定義して、Entity クラスのテストクラスで単体テストする。
JPQL の使い方 †
Query query = EntityManager#create***Query( … );
query.setParameter("city", req.getName());
query.setParameter("deliveryFrom", req.getFrom());
query.setParameter("deliveryTo", req.getTo());
query.setLockMode( … );
Item item = query.getSingleResult();
Query の作成 †
オススメ | Entity Manager のメソッド | 概要 |
× | Query | em.createNamedQuery?(String name) | 名前付き Query を指定 |
○ | TypedQuery?<T> | em.createNamedQuery?(String name, Class<T> resultClass) | 名前付き Query を指定。返値の型指定版。JPQLはEntity の持ち物にして、業務ロジック内に書かない方が後々不幸を招かないと思う |
× | Query | em.createNativeQuery?(String sql) | sql文指定 |
△ | Query | em.createNativeQuery?(String sql, Class<T> resultClass) | sql文指定。返値の型指定版。PL/SQL 呼び出しなど |
× | Query | em.createQuery(String jpql) | jpql直接指定 |
△ | TypedQuery?<T> | em.createQuery(String jpql, Class<T> resultClass) | jpql直接指定。返値の型指定版。どうしても名前付き Query を使えないときに使う。業務ロジック内で WHERE 句に指定する項目を動的に変更しなければいけないときなど |
× | TypedQuery?<T> | em.createQuery(CriterialQuery?<T> criterialQuery) | Criterial API を利用したクエリ。可読性悪し |
名前付き Query †
@Entity
@Table(name = "order")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Order1.findAll", query = "SELECT o FROM Order1 o"),
@NamedQuery(name = "Order1.findById", query = "SELECT o FROM Order1 o WHERE o.id = :id"),
@NamedQuery(name = "Order1.findByAmount", query = "SELECT o FROM Order1 o WHERE o.amount = :amount"),
@NamedQuery(name = "Order1.findByShipdate", query = "SELECT o FROM Order1 o WHERE o.shipdate = :shipdate")})
public class Order1 implements Serializable {
public static final String FIND_ALL = "Order1.findAll";
public static final String FIND_BY_ID = "Order1.findById";
public static final String FIND_BY_AMOUNT = "Order1.findByAmount";
public static final String FIND_BY_SHIPDATE = "Order1.findByShipdate";
...
- Entity のアノテーションで名前付き Query を定義する
- Net Beans は自動生成してくれないけど、public static final String で、名前付き Query の名前を定義しておくことが重要
SELECT文 †
- 注文主 で Order を検索
SELECT o.
FROM Order1 o
WHERE o.customer.id = :id
ORDER BY o.country DESC
- 指定された国の注文主別の受注数
SELECT o.customer.name, count(o)
FROM Order1 o
GROUP BY o.customer.id
HAVING o.customer.country = :country
- WHERE と GROUP BY/HAVING は共存できない
- パラメータは :country または ?1 で定義できる。query.setParameter("id", req.getId()) で埋め込む
- ORDER BY o.x DESC (降順 Descendant) / ASC (昇順 Ascendant)。ORDER BY 句には、カンマ区切りで複数の項目を指定可能
- 検索結果の受け取り方
SELECT NEW †
CASE句 / 集計関数 / スカラ式 †
副問い合わせ †
SELECT c
FROM Customer c
WHERE c.creditLimit < (SELECT SUM(o.price) FROM Order1 o WHERE o.customer = c)
受注額が与信枠を超えている顧客の一覧。
JPQL副問い合わせの WHERE 句が Object 同士の比較なことに注意。発行される SQL 文は、order.customerId = customer.id とか FK,PK の比較に展開される。
WHERE句 †
- =、>、>=、<、<=、<>
- WHERE o.price BETWEEN a AND b / WHERE o.price NOT BETWEEN a AND b
- WHERE o.name LIKE %ABC
- WHERE o.name LIKE _ABC
- WHERE o.name IS NULL / WHERE o.name IS NOT NULL
- WHERE o.name IN (a, b, c) / WHERE o.name NOT IN (a, b, c)
- WHERE :item MEMBER OF o.item.name
DELETE文 (一括削除) †
DELETE FROM Order1 o
WHERE o.shipdate < :date
ある日付以前の受注を削除
UPDATE文 (一括更新) †
UPDATE Customer c
SET c.creditLimit = 1000000
WHERE c.group = :group
指定グループの顧客の与信枠を 100万円にする
埋め込みパラメータの直接比較はできない †
SELECT *
FROM User u
WHERE
(:name = "" OR u.name = :name)
AND (:sex = "" OR u.sex = :sex)
AND (:age = "" OR u.age = :age)
- たとえば、ユーザ検索をする画面に「名前」「性別」「年齢」入力欄があるとする
- 入力欄が未記入ならワイルドカード (絞り込み条件に使わない) として扱いたい
- そこで、上記のような JPQL が書けるかと思ったけどダメみたい
- 対処案
| PROS | CONS |
対処案1 | 実行時に JPQL を動的に組み上げる | コードがわかりやすい | 実行速度が遅い(Named Query より 10倍遅い) |
対処案2 | Criteria を使う | 実行速度が早い | コードがわかりにくい |
対処案3 | 空文字か一致する時 True になる RDBのストアードプロシージャ を作る | コードわかりやすく、実行速度早い | FUNCTION句はJPA2.1(JavaEE7)から |
Lock †
楽観ロック †
- 楽観ロックは、各テーブルにバージョン列を作っておき、UPDATE でカウントアップする。もしも UPDATE しようとしたときにカウントアップされていたら、ほかの誰かが UPDATE してしまったことになる
// 1. データ取得
SELECT flag, version FROM ORDER WHERE ID=...
var v0 = version;
:
: 2.業務処理
:
// 3. 他の人が更新していないか確認
SELECT version FROM ORDER WHERE ID=...
if (v0 < version) {
throw new OptimisticException();
}
// 4. 更新処理
UPDATE ORDER
SET flag = shipped, version = (v0+1)
WHERE ID = ...
- JPA では、バージョン列に対応する Entity のフィールドに @Version アノテーションをつけておけば、勝手に楽観ロックの処理をしてくれる → すべてのテーブルにバージョン列を作るべき
- @Version アノテーションをつけられるフィールドの型は、int, Integer, short, Short, long, Long, Timestamp
- @Version 項目は、ユーザプログラムで変更しない (Commit 時に OptimisticException? が発生する)
- ということで、テーブルを作り直してみる
- あと、Order が SQL の予約語とかぶっているために、回避するのが大変。テーブル名には _table をつけるようにする
CREATE TABLE `warehouse`.`customer_table` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(255) NOT NULL ,
`address` VARCHAR(1024) ,
`version` BIGINT ,
PRIMARY KEY (`id`) );
CREATE TABLE `warehouse`.`item_table` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(255) NOT NULL ,
`price` BIGINT ,
`stock` BIGINT ,
`version` BIGINT ,
PRIMARY KEY (`id`) );
CREATE TABLE `warehouse`.`order_table` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`customer_id` BIGINT NOT NULL ,
`item_id` BIGINT NOT NULL,
`amount` BIGINT NOT NULL,
`shipdate` DATETIME NOT NULL,
`version` BIGINT,
PRIMARY KEY (`id`),
INDEX INDEX_ORDER (`shipdate`),
FOREIGN KEY (`customer_id`) references customer_table(`id`),
FOREIGN KEY (`item_id`) references item_table(`id`) );
- CustomerTable?.java の version フィールドに @Version を追加する
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.mycompany.entity;
import java.io.Serializable;
import java.util.Collection;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Version;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
*
* @author atsushi
*/
@Entity
@Table(name = "customer_table")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "CustomerTable.findAll", query = "SELECT c FROM CustomerTable c"),
@NamedQuery(name = "CustomerTable.findById", query = "SELECT c FROM CustomerTable c WHERE c.id = :id"),
@NamedQuery(name = "CustomerTable.findByName", query = "SELECT c FROM CustomerTable c WHERE c.name = :name"),
@NamedQuery(name = "CustomerTable.findByAddress", query = "SELECT c FROM CustomerTable c WHERE c.address = :address"),
@NamedQuery(name = "CustomerTable.findByVersion", query = "SELECT c FROM CustomerTable c WHERE c.version = :version")})
public class CustomerTable implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 255)
@Column(name = "name")
private String name;
@Size(max = 1024)
@Column(name = "address")
private String address;
@Version
@Column(name = "version")
private long version;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "customerId")
private Collection<OrderTable> orderTableCollection;
public CustomerTable() {
}
public CustomerTable(Long id) {
this.id = id;
}
public CustomerTable(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
@XmlTransient
public Collection<OrderTable> getOrderTableCollection() {
return orderTableCollection;
}
public void setOrderTableCollection(Collection<OrderTable> orderTableCollection) {
this.orderTableCollection = orderTableCollection;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof CustomerTable)) {
return false;
}
CustomerTable other = (CustomerTable) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "com.mycompany.entity.CustomerTable[ id=" + id + " ]";
}
}
- Version 項目は、 BigInteger? で生成されるけど long に変更する
- @NamedQuery? の findByVersion? は消す
- カウントアップされることを確認
package com.mycompany.test.entity;
import com.mycompany.entity.CustomerTable;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.LockModeType;
import javax.persistence.Persistence;
import javax.xml.bind.JAXBException;
import org.junit.Test;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.equalTo;
public class CustomerTest {
@Test
public void testCreateItem() throws JAXBException {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("warehouseTestPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// 1. 顧客を作成して DB に登録
CustomerTable cust = new CustomerTable();
cust.setName("文左衛門");
cust.setAddress("江戸日本橋");
// ID と Version は指定しない
tx.begin();
em.persist(cust);
tx.commit();
// 2. 1.で登録した顧客を読み出して更新
tx.begin();
CustomerTable cust2 = em.find(CustomerTable.class, 1L);
assertThat(cust2.getId(), is(equalTo(1L))); // 顧客番号は自動採番
assertThat(cust2.getName(), is(equalTo("文左衛門")));
assertThat(cust2.getAddress(), is(equalTo("江戸日本橋")));
assertThat(cust2.getVersion(), is(equalTo(1L))); // Versionの初期値
cust2.setAddress("東京日本橋");
tx.commit(); // ← ここで楽観ロック処理。i.バージョン確認 ii.カウントアップ
// 3. 2.で更新した顧客を読み出して Version がインクリメントされているか確認
tx.begin();
CustomerTable cust3 = em.find(CustomerTable.class, 1L);
assertThat(cust3.getId(), is(equalTo(1L))); // IDは変わらない
assertThat(cust3.getName(), is(equalTo("文左衛門")));
assertThat(cust3.getAddress(), is(equalTo("東京日本橋")));
assertThat(cust3.getVersion(), is(equalTo(2L))); // Versionはカウントアップ
tx.commit();
em.close();
emf.close();
}
}
- 実行されたSQL文
[EL Fine]: Connection(510066109)--INSERT INTO customer_table (address, name, version) VALUES (?, ?, ?)
bind => [江戸日本橋, 文左衛門, 1]
[EL Fine]: Connection(510066109)--values IDENTITY_VAL_LOCAL()
[EL Fine]: Connection(510066109)--UPDATE customer_table SET address = ?, version = ? WHERE ((id = ?) AND (version = ?))
bind => [東京日本橋, 2, 1, 1]
- JPA の楽観ロックは、同時に実行されているトランザクション間でのデータの整合性を確保するためのもの
- 通常 DBMS の分離レベルは READ COMMITTED にせざるを得ない
ISOLATION LEVEL | 概要 | PHANTOM READ | NON-REPEATABLE READ | DIRTY READ | SPEED |
SERIALIZABLE | トランザクションを並列実行しない | なし | なし | なし | SLOWEST |
REPEATABLE READ | 左記を保証 | あり | なし | なし | SLOWER |
READ COMMITTED | 左記を保証 | あり | あり | なし | FASTER |
READ UNCOMMITTED | トランザクション間の排他制御をしない | あり | あり | あり | FASTEST |
- PHANTOM READ : 別トランザクションでのコミット済みの挿入・削除が見えてしまう (さっきの検索で見えなかった行が今回の検索で見つかるかも)
- NON-REPEATABLE READ : 別トランザクションでのコミット済みの変更が見えてしまう (さっき読み込んだ行をもう一度読み込むと内容が変わっているかも)
- DIRTY READ : 別トランザクションでコミットされていない挿入・削除・変更が見えてしまう
- NON-REPEATABLE READ が起きうる設定のデータベースで、データの整合性を確保するために楽観ロックを行う → たいていの場合は楽観ロックで OK
- タイミングによっては、楽観ロックをすり抜けてデータの整合性が崩れる可能性がある → 多少速度が犠牲になっても高信頼性が要求されるシステムでは悲観ロックを使う
- 人間系のロック (Aさんが画面で編集している間に、BさんがDBを上書きしちゃったら) は、別途業務ロジックとして実装する必要あり。
- @Version 項目を利用して、ほかのユーザが変更していないか調べても良いが、何も考えずに画面で編集した結果を EntityManager?にマージ→コミットして、OptimisticException? が起きるのを期待するのではなしに、業務ロジックとして値を比較してエラー画面を出すようにすべき。
- DBのトランザクションが不整合なのと、人間系のトランザクションの不整合はきちんと分けるべき
悲観ロック †
- SELECT FOR UPDATE で、行ロックをかける
- ロックできないときは PessimisticLockException? が発生する
- EntityManager?から
- em.find(Class<T> entityClass, Object primaryKey, LockModeType? lockMode);
- em.lock(Object entity, LockModeType? lockMode);
- em.refresh(Object entity, LockModeType? lockMode);
- em.getLockMode?(Object entity)
- Queryから
- query.setLockMode?(LockModeType? lockMode);
- query.getLockMode?()
- LockModeType?
OPTIMISTIC | 楽観ロック。(@Versionつきのエンティティは自動的に楽観ロックになるので指定不要) |
OPTIMISTIC_FORCE_INCREMENT | 更新しなくても @Version をカウントアップする。OneToMany? で、One を変更したときに配下の Many 側も内容は変わっていないけど @Version をカウントアップしたいときなど |
PESSIMISTIC_READ | 読み込み時に SELECT id FROM tbl WHERE id = :id FOR UPDATE を発行 |
PESSIMISTIC_WRITE | 書き込み時に SELECT id FROM tbl WHERE id = :id FOR UPDATE を発行 |
PESSIMISTIC_FORCE_INCREMENT | 悲観ロック + @Version をカウントアップ |
NONE | ロックしない |
- 実行例1 (PESSIMISTIC_READ)
// 1. 顧客を作成して DB に登録
CustomerTable cust = new CustomerTable();
cust.setName("文左衛門");
cust.setAddress("江戸日本橋");
// ID と Version は指定しない
tx.begin();
em.persist(cust);
tx.commit();
// 2. 1.で登録した顧客を読み出して更新
tx.begin();
CustomerTable cust2 = em.find(CustomerTable.class, 1L, LockModeType.PESSIMISTIC_READ);
//em.lock(cust2, LockModeType.PESSIMISTIC_WRITE);
cust2.setAddress("東京日本橋");
tx.commit(); // ← ここで楽観ロック処理。i.バージョン確認 ii.カウントアップ
[EL Fine]: Connection(1764839676)--INSERT INTO customer_table (address, name, version) VALUES (?, ?, ?)
bind => [江戸日本橋, 文左衛門, 1]
[EL Fine]: Connection(1764839676)--values IDENTITY_VAL_LOCAL()
[EL Fine]: Connection(1764839676)--SELECT id, address, name, version FROM customer_table WHERE (id = ?) FOR UPDATE WITH RS
bind => [1]
[EL Fine]: Connection(1764839676)--SELECT id, amount, shipdate, version, customer_id, item_id FROM order_table
WHERE (customer_id = ?)
bind => [1]
[EL Fine]: Connection(1764839676)--UPDATE customer_table SET address = ?, version = ? WHERE ((id = ?) AND (version = ?))
bind => [東京日本橋, 2, 1, 1]
- 実行例2 (PESSIMISTIC_WRITE)
// 1. 顧客を作成して DB に登録
CustomerTable cust = new CustomerTable();
cust.setName("文左衛門");
cust.setAddress("江戸日本橋");
// ID と Version は指定しない
tx.begin();
em.persist(cust);
tx.commit();
// 2. 1.で登録した顧客を読み出して更新
tx.begin();
CustomerTable cust2 = em.find(CustomerTable.class, 1L, LockModeType.PESSIMISTIC_WRITE);
//em.lock(cust2, LockModeType.PESSIMISTIC_WRITE);
cust2.setAddress("東京日本橋");
tx.commit(); // ← ここで楽観ロック処理。i.バージョン確認 ii.カウントアップ
[EL Fine]: Connection(1879115578)--INSERT INTO customer_table (address, name, version) VALUES (?, ?, ?)
bind => [江戸日本橋, 文左衛門, 1]
[EL Fine]: Connection(1879115578)--values IDENTITY_VAL_LOCAL()
[EL Fine]: Connection(1879115578)--SELECT id, address, name, version FROM customer_table WHERE (id = ?) FOR UPDATE WITH RS
bind => [1]
[EL Fine]: Connection(1879115578)--SELECT id, amount, shipdate, version, customer_id, item_id FROM order_table
WHERE (customer_id = ?)
bind => [1]
[EL Fine]: Connection(1879115578)--UPDATE customer_table SET address = ?, version = ? WHERE ((id = ?) AND (version = ?))
bind => [東京日本橋, 2, 1, 1]
Eclipselink の実装は READ も WRITE も同じ SQL 操作がされるっぽい
- 実行例3 (PESSIMISTIC_FORCE_INCREMENT)
// 1. 顧客を作成して DB に登録
CustomerTable cust = new CustomerTable();
cust.setName("文左衛門");
cust.setAddress("江戸日本橋");
// ID と Version は指定しない
tx.begin();
em.persist(cust);
tx.commit();
// 2. 1.で登録した顧客を読み出して更新
tx.begin();
CustomerTable cust2 = em.find(CustomerTable.class, 1L, LockModeType.PESSIMISTIC_WRITE);
//em.lock(cust2, LockModeType.PESSIMISTIC_WRITE);
cust2.setAddress("東京日本橋");
tx.commit(); // ← ここで楽観ロック処理。i.バージョン確認 ii.カウントアップ
[EL Fine]: Connection(1356591055)--INSERT INTO customer_table (address, name, version) VALUES (?, ?, ?)
bind => [江戸日本橋, 文左衛門, 1]
[EL Fine]: Connection(1356591055)--values IDENTITY_VAL_LOCAL()
[EL Fine]: Connection(1356591055)--SELECT id, address, name, version FROM customer_table WHERE (id = ?) FOR UPDATE WITH RS
bind => [1]
[EL Fine]: Connection(1356591055)--SELECT id, amount, shipdate, version, customer_id, item_id FROM order_table
WHERE (customer_id = ?)
bind => [1]
[EL Fine]: Connection(1356591055)--UPDATE customer_table SET address = ?, version = ? WHERE ((id = ?) AND (version = ?))
bind => [東京日本橋, 2, 1, 1]
Eclipselink の実装では Version がインクリメントされないっぽい。READ や WRITE と同じ SQL 操作がされるっぽい
- ようは、まともに規格通りに実装されていない。PESSIMISTIC_READ を使えば良いだろう
Event †
- Entity Bean には、イベントハンドラを設定することができる
- イベントリスナを作って複数のクラスの状態遷移を監視することもできる。
- アノテーションは Entity Bean とおなじ
- persistence.xml に登録する
- あんまり使うことはないかな
JPAのためのテーブル設計 †
CREATE TABLE `warehouse`.`customer_table` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(255) NOT NULL ,
`address` VARCHAR(1024) ,
`version` BIGINT ,
PRIMARY KEY (`id`) );
CREATE TABLE `warehouse`.`item_table` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`jancode` INT NOT NULL ,
`name` VARCHAR(255) NOT NULL ,
`price` BIGINT ,
`stock` BIGINT ,
`version` BIGINT ,
INDEX INDEX_ORDER (`jancode`),
PRIMARY KEY (`id`) );
CREATE TABLE `warehouse`.`order_table` (
`id` BIGINT NOT NULL AUTO_INCREMENT ,
`customer_id` BIGINT NOT NULL ,
`item_id` BIGINT NOT NULL,
`amount` BIGINT NOT NULL,
`shipdate` DATETIME NOT NULL,
`version` BIGINT,
PRIMARY KEY (`id`),
FOREIGN KEY (`customer_id`) references customer_table(`id`),
FOREIGN KEY (`item_id`) references item_table(`id`) );
- テーブル名には "_TABLE" をつける。SQLの予約語との衝突を避けるため
- Primary Key は、BIGINT 型で AUTO_INCREMENT の id 一つのみ
- PK には、自然キーではなく人工キーを使う
- 中には自然キーを PK として使えることもあるかも知れないけれども、何も考えずに人工キーを PK にした方が良い
- 自然キーには INDEX を設定する
- 例 : 上記の ITEM_TABLE
- 概念設計と物理設計
- FOREIGN KEY の設定はちゃんとする
- 全テーブルに version 列をつける
- NetBeans? で Entity Class を自動生成すると BIGINT は、java.math.BigInteger? 型になる。必要があれば long 型にする
Java#Glassfish