S2jdbcでエンティティ共通化とDao作成

前置き

DBを使ったプログラムをやってると、全部のテーブルに共通するカラムがあったりします。
例えば作成日時とか最新更新日時とか。
そういう場合にすべてのエンティティクラスでそれらを定義するんではなくて、親クラスで共通なものは定義すると各エンティティの定義が楽になります。
また、その親クラスを継承させておくことで処理も共通化でき、コードをシンプルにすることもできます。

AbstractEntityクラス(エンティティの基底クラス)とSimpleDaoクラス(DB共通処理)の作成例を示します。

エンティティ共通化

共通でこのようなカラムを持たせるものとします(下の例はPostgresql

CREATE TABLE hoge_hoge (
  id BIGSERIAL NOT NULL PRIMARY KEY,
  --ここに各テーブル独自カラム
  version BIGINT NOT NULL,
  create_ts TIMESTAMP,
  create_user INTEGER,
  update_ts TIMESTAMP,
  update_user INTEGER
);
  • idは識別子です。各テーブルには必ずユニークとなるようなIDが振られるものとします。この考え方に好みもあると思いますが、便宜上このようにしてます。
  • versionはバージョンチェック用のカラムです参考。更新時にバージョンチェックを行うため、必ず指定する必要があります。
  • create_ts、update_tsはそれぞれ作成時、最終更新時のタイムスタンプです
  • create_user、update_userはそれぞれ作成者、最終更新者をあらわすユーザの識別子です。単なるログ情報なので外部キーは張らないです。また、BIGINTでもいいんですが大抵の場合INTEGERの最大値を超えることはないと思うのでINTEGERにしてます。

S2Jdbcでは、@MappedSuperclassアノテーションを親クラスに設定することでその子クラスで共通のカラムを使用することができます。

kamegu.common.jdbc.AbstractEntity
package kamegu.common.jdbc;

import java.sql.Timestamp;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;

@MappedSuperclass
public abstract class AbstractEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	@Version
	public Long version;

	@Temporal(TemporalType.TIMESTAMP)
	public Timestamp createTs;

	public Integer createUser;

	@Temporal(TemporalType.TIMESTAMP)
	public Timestamp updateTs;

	public Integer updateUser;
}

これを使えばエンティティはこのようにかけます。

package kamegu.entity;

import javax.persistence.Entity;

import kamegu.common.jdbc.AbstractEntity;
import kamegu.enumerable.Sex;

@Entity
public class Customer extends AbstractEntity {
	public String name;
	public String email;
	public Sex sex;
}

Sexはenumです。

DBアクセス処理共通化

シンプルなDBアクセスを共通化します。

  • PKを元に1件取得
  • insert
  • update

これらは通常、それぞれ下記のように書く必要があります。

jdbcManager.from(Customer.class).id(customer.id).getSingleResult();
jdbcManager.insert(customer).execute();
jdbcManager.update(customer).execute();
jdbcManager.delete(customer).execute();

また、上記の処理に加えてinsert時にはupdate_ts,update_user、update時にはinsert_ts,insert_userを更新対象から外す必要があります。

これらをふまえて次のようなクラスを作ります。
ここでは、まだinsert_userとupdate_userは登録するようになってません。

package kamegu.common.jdbc;

import java.sql.Timestamp;

import javax.annotation.Resource;

import org.seasar.extension.jdbc.JdbcManager;

public class SimpleDao {

	@Resource
	private JdbcManager jdbcManager;

	public <T extends AbstractEntity> int insert(T entity) {
		entity.createTs = new Timestamp(System.currentTimeMillis());
		return jdbcManager.insert(entity).excludes("updateTs", "updateUser").execute();
	}

	public <T extends AbstractEntity> int update(T entity) {
		entity.updateTs = new Timestamp(System.currentTimeMillis());
		return jdbcManager.update(entity).excludes("createTs", "createUser").execute();
	}

	public <T extends AbstractEntity> int delete(T entity) {
		return jdbcManager.delete(entity).execute();
	}

	public <T> T find(Class<T> clazz, Object... id) {
		return jdbcManager.from(clazz).id(id).getSingleResult();
	}

	@SuppressWarnings("unchecked")
	public <T extends AbstractEntity> T find(T entity) {
		return jdbcManager.from((Class<T>)entity.getClass()).id(entity.id).getSingleResult();
	}
}

最後にこれをコンポーネント登録します。

app.dicon に追加
  <component name="simpleDao" class="kamegu.common.jdbc.SimpleDao" instance="singleton" />

動作確認

struts2とseasar2でCRUD処理CRUD処理(validation編)で作った画面に手を加えます。
ファイルの中身は全て再掲します。

JspCrudAction
package kamegu.action.fwtest;

import java.util.List;

import javax.annotation.Resource;

import kamegu.common.MyActionSupport;
import kamegu.entity.Customer;
import kamegu.enumerable.Sex;

import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.Results;
import org.seasar.extension.jdbc.JdbcManager;

import com.opensymphony.xwork2.validator.annotations.RequiredStringValidator;
import com.opensymphony.xwork2.validator.annotations.Validations;

@Results({
	@Result(name="list", type="redirect", location="./jsp-crud")
})
public class JspCrudAction extends MyActionSupport {

	public List<Customer> customers;
	public Customer customer = new Customer();

	public Sex[] sexs = Sex.values();

	@Resource
	private JdbcManager jdbcManager;

	public String execute() {
		customers = jdbcManager.from(Customer.class).orderBy("name").getResultList();
		return SUCCESS;
	}

	public String input() {
		if (customer.id != null) {
			customer = jdbcManager.from(Customer.class).id(customer.id).getSingleResult();
		}
		return INPUT;
	}

	@Validations(
			requiredStrings = {
					@RequiredStringValidator(fieldName="customer.name", message="必須項目です"),
					@RequiredStringValidator(fieldName="customer.email", message="必須項目です")
			}
	)
	public String save() {
		if (customer.id != null) {
			jdbcManager.update(customer).execute();
		} else {
			jdbcManager.insert(customer).execute();
		}
		return "list";
	}

	public String delete() {
		jdbcManager.delete(customer).execute();
		return "list";
	}
}
jsp-crud.jsp
<%@page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ taglib prefix="sj" uri="/struts-jquery-tags"%>
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <sj:head locale="ja" loadAtOnce="true"/>
  <link href="<s:url value="/bootstrap/css/bootstrap.min.css"/>" rel="stylesheet" media="screen">
  <script src="<s:url value="/bootstrap/js/bootstrap.min.js"/>"></script>
  <script>
    var bootstrapBtn = $.fn.button.noConflict();
    $.fn.bootstrapBtn = bootstrapBtn;
  </script>
</head>
<body>
  <table class="table table-condensed table-bordered">
    <thead>
      <tr>
        <th>ID</th><th>名前</th><th>E-Mail</th><th>性別</th><th></th>
      </tr>
    </thead>
    <tbody>
    <s:iterator value="customers">
      <tr>
        <td>${id}</td>
        <td><s:property value="name"/></td>
        <td>${email}</td>
        <td><s:property value="sex"/>(${sex})</td>
        <td>
          <a type="button" class="btn btn-small iframe cbox" href="jsp-crud!input?customer.id=${id}">編集</a>
        </td>
      </tr>
    </s:iterator>
    </tbody>
  </table>
  <a type="button" class="btn btn-small iframe cbox" href="<s:url method="input"/>">新規</a>
</body>

</html>
jsp-crud-input.jsp
<%@page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<%@ taglib prefix="sj" uri="/struts-jquery-tags"%>
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <sj:head locale="ja" loadAtOnce="true"/>
  <link href="<s:url value="/bootstrap/css/bootstrap.min.css"/>" rel="stylesheet" media="screen">
  <script src="<s:url value="/bootstrap/js/bootstrap.min.js"/>"></script>
  <script>
    var bootstrapBtn = $.fn.button.noConflict();
    $.fn.bootstrapBtn = bootstrapBtn;
  </script>
</head>
<body>
  <s:form method="POST">
    <s:if test="customer.id != null">
      <s:hidden name="customer.id"/>
      <s:hidden name="customer.version"/>
    </s:if>
    <table>
      <tr>
        <td>名前</td>
        <td><s:textfield name="customer.name" placeholder="名前"/><s:fielderror fieldName="customer.name"/></td>
      </tr>
      <tr>
        <td>E-Mail</td>
        <td><s:textfield name="customer.email" type="email" placeholder="E-Mail"/><s:fielderror fieldName="customer.email"/></td>
      </tr>
      <tr>
        <td>性別</td>
        <td><s:select name="customer.sex" listKey="name()" list="sexs" value="customer.sex.name()"/></td>
      </tr>
    </table>
    <s:submit cssClass="btn btn-primary" value="登録" method="save"/>
    <s:if test="customer.id != null">
      <s:submit cssClass="btn btn-primary" value="削除" method="delete"/>
    </s:if>
  </s:form>
  <s:debug/>
</body>
</html>

ポイントとしては、更新時にidと一緒にversionもパラメータとして渡しています。

テーブル作成 (Postgresqlの場合)
CREATE TABLE customer (
  id BIGSERIAL NOT NULL PRIMARY KEY,
  name TEXT,
  email TEXT,
  sex INTEGER,
  version BIGINT NOT NULL,
  create_ts TIMESTAMP,
  create_user INTEGER,
  update_ts TIMESTAMP,
  update_user INTEGER
);

これで
/myapp/fwtest/jsp-crud
にアクセスすればCRUD処理ができて、DB上のversion, create_ts, update_tsが正しく登録されていることがわかるはずです。

以上