SQLite (SQLite3) で JPA

JPA (Java Persistence API) でも SQLite を使いたい!ということで試してみる。いちいちデータベースサーバとか用意したり起動したり面倒くさいし。

環境としてはMacでこんな感じ。要Maven2

$ java -version
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)
$ mvn -v
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
Apache Maven 2.2.0 (r788681; 2009-06-26 22:04:01+0900)
Java version: 1.6.0_20
Java home: /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x" version: "10.6.4" arch: "x86_64" Family: "mac"

まず pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example.jpa</groupId>
  <artifactId>hibernate</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>Hibernate JPA Example</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <inherited>true</inherited>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-eclipse-plugin</artifactId>
        <configuration>
          <downloadJavadocs>true</downloadJavadocs>
          <downloadSources>true</downloadSources>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <!-- SQLite3のJDBCドライバ。Central Repository での最新版 3.6.20 だと不具合あり -->
    <!-- see: http://code.google.com/p/xerial/issues/detail?id=54 -->
    <dependency>
      <groupId>org.xerial</groupId>
      <artifactId>sqlite-jdbc</artifactId>
      <version>3.6.20.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.5.3-Final</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-classic</artifactId>
    	<version>0.9.24</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <!-- Hibernateから自動解決されるバージョンだと上の logback で問題があるので指定 -->
    <dependency>
    	<groupId>org.slf4j</groupId>
    	<artifactId>slf4j-api</artifactId>
    	<version>1.6.1</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
  </dependencies>

  <repositories>
    <repository>
    	<id>Xerial.org</id>
    	<name>Xerial.org Repo</name>
    	<url>http://www.xerial.org/maven/repository/artifact/</url>
    </repository>
    <repository>
    	<id>JBoss.org</id>
    	<name>JBoss.org Repo</name>
    	<url>https://repository.jboss.org/nexus/content/groups/public/</url>
    </repository>
  </repositories>
</project>

HibernateはDBごとの違いをdialect(方言)で吸収しているが、HibernateにはSQLite3用のDialectが無いようなので(EclipseLinkにも無い)、自分で作成する。一番近いのはおそらく MySQL なので、MySQL用の Dialect をもとに差異がある部分のみオーバライド。非常に手抜きで、このエントリのデモが動作する程度しか確認していない。

src/main/java/com/example/jpa/hibernate/SQLite3Dialect.java

package com.example.jpa.hibernate;

import org.hibernate.dialect.MySQL5Dialect;

public class SQLite3Dialect extends MySQL5Dialect {

	public SQLite3Dialect() {
		super();
	}
	
	/**
	 * INSERT後のID取得用SQL。
	 */
	@Override
	public String getIdentitySelectString() {
		return "select last_insert_rowid()";
	}

	/**
	 * 自動採番ID用のカラム定義。
	 * デフォルトだと Long 型は BIGINT になってしまう。
	 */
	@Override
	public String getIdentityColumnString() {
		return "integer";
	}
	
	/**
	 * IDカラムにデフォルトの型を使用するかどうか。
	 * {@link #getIdentityColumnString()} の定義のみを使いたいので false。
	 * true のままだと、"id bigint integer" のような定義になってしまう。
	 */
	@Override
	public boolean hasDataTypeInIdentityColumn() {
		return false;
	}
	
	/**
	 * タイムスタンプ取得用の関数名。
	 * デモでは使わない。これでいいのかも不明。
	 */
	@Override
	public String getCurrentTimestampSQLFunctionName() {
		return "datetime('now')";
	}
	
	/**
	 * タイムスタンプ取得用のSQL。
	 * デモでは使わない。これでいいのかも不明。
	 */
	@Override
	public String getCurrentTimestampSelectString() {
		return "select datetime('now')";
	}
}

JPA管理のエンティティの共通親クラス
src/main/java/com/example/jpa/hibernate/BasicEntity.java

package com.example.jpa.hibernate;

import java.util.Date;

import javax.persistence.*;

@MappedSuperclass
public class BasicEntity {
	
	@Id
	@GeneratedValue
	private Long id;
	@Version
	private Long version;
	private Date createdAt;
	private Date updatedAt;

	public Long getId() {
		return id;
	}
	
	public Long getVersion() {
		return version;
	}
	
	public void setVersion(Long version) {
		this.version = version;
	}

	public Date getCreatedAt() {
		return createdAt;
	}

	public Date getUpdatedAt() {
		return updatedAt;
	}

	@PrePersist
	public void onCreate() {
		createdAt = new Date();
		onUpdate();
	}
	
	@PreUpdate
	public void onUpdate() {
		updatedAt = new Date();
	}
}

テスト用の Customer クラス

src/main/java/com/example/jpa/hibernate/Customer.java

package com.example.jpa.hibernate;

import javax.persistence.*;

@Entity
@NamedQueries({
	@NamedQuery(name = Customer.FIND_ALL, query = "SELECT c FROM Customer c")
})
public class Customer extends BasicEntity {
	
	public static final String FIND_ALL = "findAllCustomers";

	private String name;

	public Customer() {
		super();
	}

	public Customer(String name) {
		this();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	public String toString() {
		return String.format(
				"id=%d, name=%s, version=%d, created_at=%s, updated_at=%s",
				getId(), getName(), getVersion(), getCreatedAt(), getUpdatedAt());
	}
}

JPAの設定。さっき作った Dialect を使うように指定する。

src/main/resources/META-INF/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_0.xsd"
        version="2.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="testPu" transaction-type="RESOURCE_LOCAL">
        <class>com.example.jpa.hibernate.Customer</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.sqlite.JDBC" />
            <property name="javax.persistence.jdbc.url" value="jdbc:sqlite:test.db" />
            
            <property name="hibernate.dialect" value="com.example.jpa.hibernate.SQLite3Dialect" />
            <!-- createdAt (Java) <=> created_at (DB) のような naming strategy  -->
            <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            
            <property name="hibernate.show-sql" value="true" />
            <property name="hibernate.format-sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

テスト用のメインクラス。引数ひとつでその名前の Customer を追加、引数ふたつ(名前、ID)でそのIDの Customer の名前を更新する。

src/main/java/com/example/jpa/hibernate/Customer.java

package com.example.jpa.hibernate;

import javax.persistence.*;

public class App {

	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("testPu");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();

		Customer customer;

		tx.begin();

		if (args.length >= 2) {
			customer = em.find(Customer.class, Long.valueOf(args[1]));
		} else {
			customer = new Customer();        	
		}

		customer.setName(args[0]);
		em.persist(customer);
		tx.commit();

		Customer found = em.find(Customer.class, customer.getId());
		System.out.println("PERSISTED OBJECT: " + found);

		TypedQuery<Customer> query = em.createNamedQuery(Customer.FIND_ALL, Customer.class);
		for (Customer c : query.getResultList()) {
			System.out.println("OBJECTS IN TABLE: " + c);
		}

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

これで、pom.xml のあるフォルダから

mvn exec:java -Dexec.mainClass=com.example.jpa.hibernate.App -Dexec.args="山田太郎"

とやると

OBJECTS IN TABLE: id=1, name=山田太郎, version=0, created_at=Mon Jul 19 18:51:12 JST 2010, updated_at=Mon Jul 19 18:51:12 JST 2010

のようにデータが作成され、

mvn exec:java -Dexec.mainClass=com.example.jpa.hibernate.App -Dexec.args="上田次郎 1"

とやると

OBJECTS IN TABLE: id=1, name=上田次郎, version=1, created_at=2010-07-19 18:51:12.448, updated_at=Mon Jul 19 18:53:11 JST 2010

のように更新されましたとさ*1

EclipseLink でも上と同じようなことを試したけど、

  • 「主キーカラムだけ型のマッピングを変える」ということができないため、id に @Column(columnDefinition = "INTEGER") をつけなきゃいけない
  • @GeneratedValue のデフォルトである strategy = GenerationType.AUTO だと、Hibernateのような「DBごとに適した方法」ではなく、「ID管理用テーブルを作って使う」方式が固定で選択され、しかもデフォルトを変更する方法がない。

等々、だめな部分が目立った。

で、あとで気づきましたが、既に Dialect は存在するようで

*1:created_at のフォーマットが違うのは、最初は自分で new Date() した値がそのまま使われているため Date 型のインスタンスだが、更新後はDBから取得した値なので Timestamp 型になるため