[Java] 59. Spring bootのJPAでEntityManagerを使い方


Study / Java    作成日付 : 2022/02/25 18:27:48   修正日付 : 2022/02/25 19:20:25

こんにちは。明月です。


この投稿はSpring bootのJPAでEntityManagerを使い方に関する説明です。


以前の投稿でSpring boot framework環境でJPAを設定して使う方法に関して説明したことがあります。

リンク - [Java] 58. EclipseでSpring bootのJPAを設定する方法


しかし、以前の問題はJpaRepositoryインターフェースを継承して使うことです。JpaRepositoryインターフェースのことが問題があることではありません。データベースコネクションを簡単にアクセス可能にするし、トランザクション処理を自動に処理することで凄く楽な部分です。でも、この自動に処理することが問題があることです。

自動というのは初期設定は凄く簡単にすることで良い部分ですが、結局、トランザクションをコントロールすることで限界があることです。例えば、複数のテーブルを同時に入力して処理する途中でエラーが発生します。その場合はすべてロールバックしなければならないですが、トランザクションを一つのテーブルではなく、同時に複数のテーブルを制御することが簡単ではないし、ソースが複雑になる可能性があります。

つまり、JpaRepositoryもトランザクションを別に取得して制御が可能ですが、統合的(?)な管理がならないので逆にソースが複雑になるし、リソース管理などの明確な流れが制御になれません。


まず、AbstractDaoとFactoryDaoを作成しましょう。

リンク - [Java] 50. JPAプロジェクトでDAOクラスを作成する方法

リンク - [Java] 52. SpringフレームワークでDAOをFactory method Patternを利用して依存性注入する方法


まず、AbstractDaoの抽象クラスを作成しましょう。

package com.example.demo.dao;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
 
// Dao抽象クラス
public abstract class AbstractDao<T> {
  // Spring bootにはpersistence.xmlファイルがないので、下記のコードでエラーが発生する。
  private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
  private Class<T> clazz;
  // ラムダ式のためのinterface
  protected interface EntityManagerRunable {
    void run(EntityManager em);
  }
  // ラムダ式のためのinterface
  protected interface EntityManagerCallable<V> {
    V run(EntityManager em);
  }
  // コンストラクタをprotectedに設定
  protected AbstractDao(Class<T> clazz) {
    this.clazz = clazz;
  }
  // クラスタイプを取得する関数
  protected final Class<T> getClazz() {
    return clazz;
  }
  // テーブルからkeyの条件でデータを取得する。
  public T findOne(Object id) {
    return transaction((em) -> {
      return em.find(clazz, id);
    });
  }
  // EntityをデータベースにInsertする。
  public T create(T entity) {
    return transaction((em) -> {
      em.persist(entity);
      return entity;
    });
  }
  // EntityをデータベースにUpdateする。
  public T update(T entity) {
    return transaction((em) -> {
      // クラスをデータベースのデータとマッピングする。
      em.detach(entity);
      // update
      return em.merge(entity);
    });
  }
  // EntityをデータベースにDeleteする。
  public void delete(T entity) {
    transaction((em) -> {
      // クラスをデータベースのデータとマッピングする。
      em.detach(entity);
      // データをupdateして削除する。
      em.remove(em.merge(entity));
    });
  }
  // リターン値があるトランザクション(一般トランザクションでデータを更新可能)
  public <V> V transaction(EntityManagerCallable<V> callable) {
    return transaction(callable, false);
  }
  // リターン値があるトランザクション(readonlyをtrueに設定すれば関数を呼び出す間にcommitを実行しません。)
  public <V> V transaction(EntityManagerCallable<V> callable, boolean readonly) {
    // Managerを生成する。 EntityManagerFactoryをpersistence.xmlから取得できないので、EntityManagerを取得できない。
    EntityManager em = emf.createEntityManager();
    // transactionを取得
    EntityTransaction transaction = em.getTransaction();
    // トランザクション開始
    transaction.begin();
    try {
      // ラムダ式を実行する。
      V ret = callable.run(em);
      // readonlyがtrueならrollbackする。
      if (readonly) {
        transaction.rollback();
      } else {
        // トランザクションをデータベースに格納
        transaction.commit();
      }
      // 結果をリターンする。
      return ret;
      // エラーが発生する場合。
    } catch (Throwable e) {
      // transactionが活性中なら
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeExceptionに変換
      throw new RuntimeException(e);
    } finally {
      // Managerを閉める。
      em.close();
    }
  }
  // リターン値がないtransaction(一般トランザクションでデータを更新可能)
  public void transaction(EntityManagerRunable runnable) {
    transaction(runnable, false);
  }
  // リターン値がないtransaction(readonlyをtrueに設定すれば関数を呼び出す間にcommitを実行しません。)
  public void transaction(EntityManagerRunable runnable, boolean readonly) {
    // Managerを生成する。 EntityManagerFactoryをpersistence.xmlから取得できないので、EntityManagerを取得できない。
    EntityManager em = emf.createEntityManager();
    // transactionを取得
    EntityTransaction transaction = em.getTransaction();
    // トランザクション開始
    transaction.begin();
    try {
      // ラムダ式を実行する。
      runnable.run(em);
      // readonlyがtrueならrollbackする。
      if (readonly) {
        transaction.rollback();
      } else {
        // トランザクションをデータベースに格納
        transaction.commit();
      }
      // エラーが発生する場合。
    } catch (Throwable e) {
      // transactionが活性中なら
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeExceptionに変換
      throw new RuntimeException(e);
    } finally {
      // Managerを閉める。
      em.close();
    }
  }
}


上のソースをそのままに継承してDaoを作って実行するとエラーが発生します。

つまり、AbstractDaoで11番目のラインの内容を見るとSpring bootにはpersistence.xmlファイルがないので、EntityManagerFactoryを取得できません。

76番目のラインと116番目のラインを見るとEntityManagerFactoryからEntityManagerを取得してtransactionを受け取って実行するので、やはりEntityManagerFactoryを取得出来なかったからすべてエラーが発生することです。


そうするとEntityManagerFactoryを取得して解決したらいいけど。。EntityManagerFactoryを何処で取得するかというとApplicationConfigクラス、つまり@Configurationアノテーションが設定されているクラスで依存性注入で取得することができます。

でも、クラスの割り当てる順番はFactoryDaoクラスがApplicationConfigより先に生成される部分なので、Singletonパターンで解決しなければならないです。

package com.example.demo.Controller;

import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.demo.dao.FactoryDao;
import com.example.demo.dao.UserDao;

// 設定アトリビュート
@Configuration
public class ApplicationConfig {
  // EntityManagerFactoryインスタンスを依存性注入で受け取る。
  @Autowired
  private EntityManagerFactory emf;
  // シングルトン設定
  private static ApplicationConfig instance;
  // コンストラクタ
  public ApplicationConfig() {
    // シングルトンのインスタンスを設定
    ApplicationConfig.instance = this;
  }
  // シングルトンのインスタンスを取得関数
  public static ApplicationConfig getInstance() {
    // シングルトンのインスタンスをリターン
    return ApplicationConfig.instance;
  }
  // EntityManagerFactoryインスタンスを取得関数
  public EntityManagerFactory getEntityManagerFactory() {
    // EntityManagerFactoryインスタンスをリターン
    return this.emf;
  }
  // Bean設定、名はUserDao
  @Bean(name = "UserDao")
  public UserDao getUserDao() {
    // FactoryDaoからUserDaoのインスタンスを取得
    return FactoryDao.getDao(UserDao.class);
  }
}

また、AbstractDaoを修正します。後でAbstractDaoを継承したUserDaoクラスとDaoクラスを管理するFactoryDaoクラスを作成しましょう。

package com.example.demo.dao;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
 
// Dao抽象クラス
public abstract class AbstractDao<T> {
  private Class<T> clazz;
  // ラムダ式のためのinterface
  protected interface EntityManagerRunable {
    void run(EntityManager em);
  }
  // ラムダ式のためのinterface
  protected interface EntityManagerCallable<V> {
    V run(EntityManager em);
  }
  // コンストラクタをprotectedに設定
  protected AbstractDao(Class<T> clazz) {
    this.clazz = clazz;
  }
  // クラスタイプを取得する関数
  protected final Class<T> getClazz() {
    return clazz;
  }
  // テーブルからkeyの条件でデータを取得する。
  public T findOne(Object id) {
    return transaction((em) -> {
      return em.find(clazz, id);
    });
  }
  // EntityをデータベースにInsertする。
  public T create(T entity) {
    return transaction((em) -> {
      em.persist(entity);
      return entity;
    });
  }
  // EntityをデータベースにUpdateする。
  public T update(T entity) {
    return transaction((em) -> {
      // クラスをデータベースのデータとマッピングする。
      em.detach(entity);
      // update
      return em.merge(entity);
    });
  }
  // EntityをデータベースにDeleteする。
  public void delete(T entity) {
    transaction((em) -> {
      // クラスをデータベースのデータとマッピングする。
      em.detach(entity);
      // データをupdateして削除する。
      em.remove(em.merge(entity));
    });
  }
  // リターン値があるトランザクション(一般トランザクションでデータを更新可能)
  public <V> V transaction(EntityManagerCallable<V> callable) {
    return transaction(callable, false);
  }
  // リターン値があるトランザクション(readonlyをtrueに設定すれば関数を呼び出す間にcommitを実行しません。)
  public <V> V transaction(EntityManagerCallable<V> callable, boolean readonly) {
    // ApplicationConfigインスタンスでEntityManagerFactoryを取得してManagerを生成する。
    EntityManager em = ApplicationConfig.getInstance().getEntityManagerFactory().createEntityManager();
    // transactionを取得
    EntityTransaction transaction = em.getTransaction();
    // トランザクション開始
    transaction.begin();
    try {
      // ラムダ式を実行する。
      V ret = callable.run(em);
      // readonlyがtrueならrollbackする。
      if (readonly) {
        transaction.rollback();
      } else {
        // トランザクションをデータベースに格納
        transaction.commit();
      }
      // 結果をリターンする。
      return ret;
      // エラーが発生する場合。
    } catch (Throwable e) {
      // transactionが活性中なら
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeExceptionに変換
      throw new RuntimeException(e);
    } finally {
      // Managerを閉める。
      em.close();
    }
  }
  // リターン値がないtransaction(一般トランザクションでデータを更新可能)
  public void transaction(EntityManagerRunable runnable) {
    transaction(runnable, false);
  }
  // リターン値がないtransaction(readonlyをtrueに設定すれば関数を呼び出す間にcommitを実行しません。)
  public void transaction(EntityManagerRunable runnable, boolean readonly) {
    // ApplicationConfigインスタンスでEntityManagerFactoryを取得してManagerを生成する。
    EntityManager em = ApplicationConfig.getInstance().getEntityManagerFactory().createEntityManager();
    // transactionを取得
    EntityTransaction transaction = em.getTransaction();
    // トランザクション開始
    transaction.begin();
    try {
      // ラムダ式を実行する。
      runnable.run(em);
      // readonlyがtrueならrollbackする。
      if (readonly) {
        transaction.rollback();
      } else {
        // トランザクションをデータベースに格納
        transaction.commit();
      }
      // エラーが発生する場合。
    } catch (Throwable e) {
      // transactionが活性中なら
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeExceptionに変換
      throw new RuntimeException(e);
    } finally {
      // Managerを閉める。
      em.close();
    }
  }
}

transaction関数でApplicationConfigのシングルトンインスタンスを取得してEntityManagerFactoryを取得しましょう。そしてEntityManagerインスタンスを取得します。

その以外のtransactionの内容は同じです。ラムダ式のインターフェースをパラメータで受け取って関数を実行します。

package com.example.demo.dao;

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
 
public class FactoryDao {
  // FactoryDaoクラスのsingletonパターンのインスタンス変数
  private static FactoryDao instance = null;
  // Daoクラスのインスタンスを格納するflyweightパターンのマップ
  private final Map<Class<?>, AbstractDao<?>> flyweight;
  // Singletonパターンを守るためにコンストラクタをprivateタイプに設定
  private FactoryDao() {
    // flyweightパターンのマップのインスタンス生成
    flyweight = new HashMap<Class<?>, AbstractDao<?>>();
  }
  @SuppressWarnings("unchecked")
  // DAOインスタンスを取得するためのSingletonパターンの関数
  public static <T> T getDao(Class<T> clz) {
    try {
      // FactoryDaoのインスタンスがなければ生成する。
      if (instance == null) {
        // インスタンス生成
        instance = new FactoryDao();
      }
      // FactoryDaoのflyweightマップでパラメータのクラスタイプのDAOが存在しない場合
      if (!instance.flyweight.containsKey(clz)) {
        // Reflection機能でコンストラクタを探す
        Constructor<T> constructor = clz.getDeclaredConstructor();
        // アクセス修飾子に関係せず、アクセス可能にする設定
        constructor.setAccessible(true);
        // flyweightのマップにクラスタイプをキーに設定してインスタンスを格納する。
        instance.flyweight.put(clz, (AbstractDao<?>) constructor.newInstance());
      }
      // flyweightのマップに格納されたDAOインスタンスをリターンする。
      return (T) instance.flyweight.get(clz);
    } catch (Throwable e) {
      // エラーが発生
      throw new RuntimeException(e);
    }
  }
}

FactoryDaoはReflection機能を使ってクラスのタイプを受け取ってflyweightパターンでインスタンスを格納して受け取る形のクラスです。

UserDaoはデータベースに接続してデータを取得するクラスです。

package com.example.demo.dao;

import java.util.List;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.example.demo.model.User;

// UserデータDaoクラス、AbstractDaoを継承してジェネリックタイプはUserクラスを設定する。
public class UserDao extends AbstractDao<User> {
  // コンストラクタの再定義、protectedからprivateに変更してパラメータを再設定する。
  private UserDao() {
    // protectedコンストラクタを呼び出す。
    super(User.class);
  }
  // テーブルの全体レコードを取得関数
  @SuppressWarnings("unchecked")
  public List<User> findAll() {
    // AbstractDaoの抽象クラスのtransaction関数を使う。
    return transaction((em) -> {
      try {
        // Userクラスの@NamedQueryのクエリで取得
        Query query = em.createNamedQuery("User.findAll", User.class);
        // 結果リターン
        return (List<User>) query.getResultList();
      } catch (NoResultException e) {
        return null;
      }
    });
  }
  // Idによりデータを取得する。
  public User selectById(String id) {
    // AbstractDaoの抽象クラスのtransaction関数を使う。
    return super.transaction((em) -> {
      // クエリを作成する。(実践ではcreateQueryではなく、createNamedQueryを使ってEntityでクエリを一括管理する。)
      Query query = em.createQuery("select u from User u where u.id = :id");
      // パラメータ設定
      query.setParameter("id", id);
      try {
        // 結果リターン
        return (User) query.getSingleResult();
      } catch (NoResultException e) {
        // データがないならエラーが発生してnullをリターン
        return null;
      }
    });
  }
}

これからHomeControllerでUserDaoを依存性注入でインスタンスを取得してデータベースからデータを取得して画面に出力します。

package com.example.demo.Controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.dao.UserDao;

// コントローラーアトリビュート
@Controller
// Controllerクラス
public class HomeController {
  // 依存性注入
  @Autowired
  // ApplicationConfigクラスで設定したbean-id
  @Qualifier("UserDao")
  private UserDao userdao;
  // マッピングアドレス
  @RequestMapping(value = { "/", "/index.html" })
  public String index(Model model) {
    // UserDaoを利用してnowonbunでデータを取得する。
    // データをテンプレートに渡す。
    model.addAttribute("data", userdao.selectById("nowonbun"));
    // テンプレートのファイル名
    return "Home/index";
  }
}


Repositoryインターフェースではなく、Daoクラスでデータを取得します。


全体的のプロジェクト構造は下記通りになります。


以前のSpring Web Frameworkで構成した方法で構成されました。もちろん、以前のRepository方式が間違っていることではありません。各者の開発のスタイルがあるので、楽な方法で使ったら良いでしょう。

私の場合はtransactionを管理して各クエリで直接にデータベースのデータを管理する方法がもっと直感的だし、管理がしやすいので、私はこの方法が良いです。


ここまでSpring bootのJPAでEntityManagerを使い方に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿