ぺーぺーSEのブログ

備忘録・メモ用サイト。

JPA2.1入門

JavaEE 7のJPA2.1をさわってみる。
事始めなので2.1新機能とかには全然ふれない。

用語とか

  • Persistence Unit
    • EntityManagerのインスタンスを生成するファクトリクラスを定義
    • EntityクラスのRDBにマッピングを設定する
    • データベースへの接続用データソース設定
    • など
  • persistence.xml
    • 1つ以上のPersistence Unitを定義可能
    • @Entityアノテーションが付加されたクラスを検索
  • EntityManager
    • Entityのインスタンスのライフサイクルを管理するAPIを提供
    • 永続化エンジンとプログラミングによる対話が可能
    • 提供機能例
      • ライフサイクル操作:persist(),remove(),refresh(),merge()
      • 検索処理:find(),getReference()
      • クエリの発行:createQuery(),createNamedQuery(),createNativeQuery()
      • Persistence Contextの管理系:flush(),clear(),close(),getTransaction()
  • Persistence Context
    • 管理するEntityの集合
    • 1つのPersistence Unitに属する

EntityManagerには2種類ある。

  1. コンテナ管理のEntityManager
    • Java EE環境用
    • @PersistenceContext(unitname="unit")アノテーション
  2. アプリケーション管理のEntityManager
    • Java SE/EE両方使える
    • 次の例のようにFactoryから生成

今回は後者のEntityManagerでサンプルを作ってみる。

  • JPQL(Java Persistence Query Language)
    • SQLに似たJPA用のクエリ
    • クエリは4種類
      • 動的クエリ:アプリ実行時にクエリ生成
      • 名前付きクエリ:静的にクエリ定義
      • ネイティブクエリ:DB固有機能を使用するためのクエリ
      • Criteria API:JPQLに相当するクエリをAPIベースで記述できる(JPA2.1〜)

■JPQL例

SELECT c FROM Customer c WHERE c.lastName = 'Yamada' ORDER BY c.id

JPA2.1新機能

  • ストアドプロシージャ対応
  • バルク更新・削除対応
  • JPQLのON句サポート
  • エンティティからインデックスを自動生成
  • コンバータ
    • 「DB上は文字列で保持し、Entity上はIntegerで保持する」といったもの
    • AttributeConverter等を継承して実装し、@Convertや@Converterで適用する



サンプルアプリ 〜アプリケーション管理のEntityManager版〜

■build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.apache.derby:derby:10.11.1.1'
    compile 'org.eclipse.persistence:org.eclipse.persistence.jpa:2.6.0-M3'
}

JPA実装はEclipseLinkを使う。


■persistence.xml(クラスパスの通った「META-INF」配下)

<?xml version="1.0" encoding="UTF-8" ?>
<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_1.xsd"
  version="2.1" xmlns="http://java.sun.com/xml/ns/persistence">

  <!-- PersistenceUnitの定義 -->
  <persistence-unit name="sampleUnit" transaction-type="RESOURCE_LOCAL">

    <!-- Entityの定義 -->
    <class>jp.sample.model.Person</class>

    <!-- DB接続設定 -->
    <properties>
      <!-- JDBCドライバ -->
      <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
      <!-- DB接続先 -->
      <property name="javax.persistence.jdbc.url" value="jdbc:derby:H:/Derby/JpaSample;create=true" />
      <!-- <property name="javax.persistence.jdbc.user" value="test" /> -->
      <!-- <property name="javax.persistence.jdbc.password" value="test" /> -->

      <!-- EclipseLinkのテーブル自動作成設定 -->
      <property name="eclipselink.ddl-generation" value="create-tables" />
      <property name="eclipselink.ddl-generation.output-mode" value="database" />
    </properties>

  </persistence-unit>

</persistence>

persistent.xmlはクラスパス直下の「META-INF」内に作る。
今回は「src/main/resources/META-INF」内に作った。


■jp.sample.model.Person(エンティティ)

package jp.sample.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Person {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private Integer age;

	public Person() {}

	public Person(String name, Integer age) {
		this.name = name;
		this.age = age;
	}

	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 Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}
}



■jp.sample.Main

package jp.sample;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import jp.sample.model.Person;

public class Main {

	public static void main(String[] args) {
		// PersistenceUnitからEntityManagerFactoryを生成
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("sampleUnit");

		// EntityManagerFactoryからEntityManagerを作成
		EntityManager em = emf.createEntityManager();

		// EntityManagerからEntityTransactionを取得
		EntityTransaction tx = em.getTransaction();

		// Entityの作成
		Person person = new Person();
		person.setName("Hoge");
		person.setAge(20);

		tx.begin(); // トランザクション開始
		em.persist(person); // 管理状態のEntity
		tx.commit(); // トランザクション終了

		em.close();
		emf.close();
	}
}

DBを覗くとテーブルも自動でできてるしいい感じ。

> java org.apache.derby.tools.ij
ij version 10.11
ij> connect 'jdbc:derby:H:/Derby/JpaSample';
ij> select * from PERSON;
ID                  |AGE        |NAME
------------------------------------------
1                   |20         |Hoge

1 rows selected

なにこれかんたんw

Derbyの接続方法下記参照。
http://d.hatena.ne.jp/tanakakns/20140926/1411722693


サンプルアプリ 〜コンテナ管理のEntityManager版〜

SpringMVCのRestでやってみる。

くっそハマった。
ハマったときの参考:
http://revelfire.com/spring-webmvc-unit-test-fails-caused-by-java-lang-illegalargumentexception-a-servletcontext-is-required-to-configure-default-servlet-handling/
http://stackoverflow.com/questions/10769051/eclipselinkjpavendoradapter-instead-of-hibernatejpavendoradapter-issue
http://www.baeldung.com/2011/12/13/the-persistence-layer-with-spring-3-1-and-jpa/

■build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'tomcat'

[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.2.4'
    }
}
repositories {
    mavenCentral()
}

dependencies {
    compile 'org.apache.derby:derby:10.11.1.1'
    compile 'org.eclipse.persistence:org.eclipse.persistence.jpa:2.6.0-M3'
    compile 'org.springframework:spring-webmvc:4.1.1.RELEASE'
    compile 'org.springframework:spring-orm:4.1.1.RELEASE'
    compile 'javax.servlet:javax.servlet-api:3.1.0'
    runtime 'com.fasterxml.jackson.core:jackson-databind:2.4.2'

    def tomcatVersion = '8.0.12'
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}",
           "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
        exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
}



■persistence.xml

<?xml version="1.0" encoding="UTF-8" ?>
<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_1.xsd"
  version="2.1" xmlns="http://java.sun.com/xml/ns/persistence">

  <!-- PersistenceUnitの定義 -->
  <persistence-unit name="sampleUnit" transaction-type="RESOURCE_LOCAL">

    <!-- Entityの定義 -->
    <class>jp.sample.model.Person</class>

    <!-- PersistenceProviderをEclipseLinkに指定 -->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>

    <!-- DB接続設定 -->
    <properties>
      <!-- JDBCドライバ -->
      <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
      <!-- DB接続先 -->
      <property name="javax.persistence.jdbc.url" value="jdbc:derby:H:/Derby/JpaSample;create=true" />
      <!-- <property name="javax.persistence.jdbc.user" value="test" /> -->
      <!-- <property name="javax.persistence.jdbc.password" value="test" /> -->

      <!-- EclipseLinkのテーブル自動作成設定 -->
      <property name="eclipselink.ddl-generation" value="create-tables" />
      <property name="eclipselink.ddl-generation.output-mode" value="database" />

      <!-- Weavingオフ -->
      <property name="eclipselink.weaving" value="false" />

      <!-- ログ出力設定 -->
      <property name="eclipselink.logging.level.sql" value="FINE" />
      <property name="eclipselink.logging.parameters" value="true" />
    </properties>

  </persistence-unit>

</persistence>

WeavingはEntityをバイナリにして高速化する仕組みだとかなんとか。
よく知らん。
※「@EnableLoadTimeWeaving」とかいうアノテーションを見かけた。。。
persistence.xmlの書きっぷりは以下あたりを参考。
http://itdoc.hitachi.co.jp/manuals/link/cosmi_v0870/APR2/EU150094.HTM



■jp.sample.config.AppConfig

package jp.sample.conf;

import javax.persistence.EntityManagerFactory;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan(basePackages = { "jp.sample" })
@EnableTransactionManagement
public class AppConfig {

	@Bean
	public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
		LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
		em.setPersistenceUnitName("sampleUnit");

		return em;
	}

	@Bean
	public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
		JpaTransactionManager tm = new JpaTransactionManager();
		tm.setEntityManagerFactory(emf);
		return tm;
	}
}

SpringでJPA使うときは「LocalContainerEntityManagerFactoryBean」ってのを使うくさい。
また、トランザクションもSpringのJpaTransactionManagerを使う。



■jp.sample.config.WebMvcConfig

package jp.sample.conf;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableWebMvc
@Profile("container")
public class WebMvcConfig extends WebMvcConfigurerAdapter {

	@Override
	public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
		configurer.ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON);
	}
}



■jp.sample.config.WebAppInitializer

package jp.sample.conf;

import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.springframework.context.annotation.Profile;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

@Profile("container")
public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {
        // ルートコンテキストの作成
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(AppConfig.class);

        // ルートコンテキストをリスナーに追加
        servletContext.addListener(new ContextLoaderListener(rootContext));

        // MVC用のコンテキストを作成
        AnnotationConfigWebApplicationContext mvcContext = new AnnotationConfigWebApplicationContext();
        mvcContext.register(WebMvcConfig.class);

        // スキャンパッケージを指定
        mvcContext.scan("jp.sample");

        // Spring用のディスパッチャーサーブレットを作成
        ServletRegistration.Dynamic dispatcherServlet = servletContext.addServlet("dispatcherServlet", new DispatcherServlet(mvcContext));
        dispatcherServlet.setLoadOnStartup(1);
        Set<String> mappingConflicts = dispatcherServlet.addMapping("/");

        // 「container」プロファイルをアクティブに
        dispatcherServlet.setInitParameter("spring.profiles.active", "container");

        // マッピングのコンフリクトチェック
        if (!mappingConflicts.isEmpty()) {
          for (String s : mappingConflicts) {
            System.out.println("[ERROR] Mapping conflict: " + s);
          }
          throw new IllegalStateException(
              "'webservice' cannot be mapped to '/'");
        }

    }
}



■jp.sample.controller.PersonController

package jp.sample.controller;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import jp.sample.model.Person;

import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PersonController {

	@PersistenceContext
	private EntityManager em;

	@Transactional
	@RequestMapping(value="/persons", method=RequestMethod.POST)
	public void create(@RequestBody Person person) {
		em.persist(person);
	}
}

gradle tomcatRunWarでトム猫起動。
http://localhost:8080/[Context]/persons.json」へ「Content-Type: application/json」をつけてPOSTでJSONを送信。

■BodyのJSON例

{
  "name":"Yamada",
  "age":30
  }

初回アクセスのレスポンス超遅す。。。

出力されたSQLログ。

[EL Info]: 2014-10-14 11:40:44.039--ServerSession(2870262)--EclipseLink, version: Eclipse Persistence Services - 2.6.0.v20140809-296a69f
[EL Info]: connection: 2014-10-14 11:41:26.06--ServerSession(2870262)--file:/H:/tools/Luna/pleiades/workspace/JPA/build/libs/JPA.war_sampleUnit login successful
[EL Fine]: sql: 2014-10-14 11:41:30.103--ServerSession(2870262)--Connection(30001505)--SELECT ID FROM PERSON WHERE ID <> ID
[EL Fine]: sql: 2014-10-14 11:41:56.28--ClientSession(9818516)--Connection(30001505)--INSERT INTO PERSON (AGE, NAME) VALUES (?, ?)
        bind => [11, KUMAKICHI]
[EL Fine]: sql: 2014-10-14 11:42:00.341--ClientSession(9818516)--Connection(30001505)--values IDENTITY_VAL_LOCAL()