今回は、Spring Boot上でマテリアライズドビューを利用するサンプルプログラムを作成してみたので、共有する。
マテリアライズドビューを利用すると、データベースでSELECTした結果をテーブルとして保持できるため、複雑な集計処理の高速化やデータ整合性の確保を簡単に実現しつつ、SELECT処理を効率的に行うことができる。
前提条件
下記記事の「マテリアライズドビューの作成」が完了していること。
また、下記記事の実装が完了していること。
サンプルプログラムの作成
作成したサンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから変更したプログラムである。
UserDataMapperクラスの内容は以下の通りで、マテリアライズドビュー(mv_user_data)をリフレッシュするメソッド「refreshMvUserData」を追加している。
package com.example.demo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.data.domain.Pageable;
import java.util.Collection;
@Mapper
public interface UserDataMapper {
/**
* ユーザーデータテーブル(user_data)から検索条件に合うデータを取得する
* @param searchForm 検索用Formオブジェクト
* @param pageable ページネーションオブジェクト
* @return ユーザーデータテーブル(user_data)の検索条件に合うデータ
*/
Collection<UserData> findBySearchForm(
@Param("searchForm") SearchForm searchForm
, @Param("pageable") Pageable pageable);
/**
* 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する
* @param id ID
* @return ユーザーデータテーブル(user_data)の指定したIDのデータ
*/
UserData findById(Long id);
/**
* 指定したIDをもつユーザーデータテーブル(user_data)のデータを削除する
* @param id ID
*/
void deleteById(Long id);
/**
* 指定したユーザーデータテーブル(user_data)のデータを追加する
* @param userData ユーザーデータテーブル(user_data)の追加データ
*/
void create(UserData userData);
/**
* 指定したユーザーデータテーブル(user_data)のデータを更新する
* @param userData ユーザーデータテーブル(user_data)の更新データ
*/
void update(UserData userData);
/**
* ユーザーデータテーブル(user_data)の最大値IDを取得する
* @return ユーザーデータテーブル(user_data)の最大値ID
*/
long findMaxId();
/**
* マテリアライズドビュー(mv_user_data)をリフレッシュする
*/
void refreshMvUserData();
}
また、UserDataMapperのXMLファイルの内容は以下の通り。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.UserDataMapper">
<resultMap id="userDataResultMap" type="com.example.demo.UserData" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="birthY" property="birthY" jdbcType="VARCHAR" />
<result column="birthM" property="birthM" jdbcType="VARCHAR" />
<result column="birthD" property="birthD" jdbcType="VARCHAR" />
<result column="sex" property="sex" jdbcType="VARCHAR" />
<result column="memo" property="memo" jdbcType="VARCHAR" />
<result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
</resultMap>
<select id="findBySearchForm" resultMap="userDataResultMap">
SELECT u.id, u.name, u.birth_year as birthY, u.birth_month as birthM
, u.birth_day as birthD, u.sex, u.memo, u.sex_value
FROM
( SELECT
u1.id, u1.name, u1.birth_year, u1.birth_month, u1.birth_day
, u1.sex, u1.memo, m.sex_value
, ROW_NUMBER() OVER (ORDER BY u1.id) AS rn
FROM mv_user_data u1, M_SEX m
WHERE u1.sex = m.sex_cd
<if test="searchForm.searchName != null and searchForm.searchName != ''">
AND u1.name like '%' || #{searchForm.searchName} || '%'
</if>
<if test="searchForm.fromBirthYear != null
and searchForm.fromBirthYear != ''">
AND #{searchForm.fromBirthYear}
|| lpad(#{searchForm.fromBirthMonth}, 2, '0')
|| lpad(#{searchForm.fromBirthDay}, 2, '0')
<= u1.birth_year || lpad(u1.birth_month, 2, '0')
|| lpad(u1.birth_day, 2, '0')
</if>
<if test="searchForm.toBirthYear != null and searchForm.toBirthYear != ''">
AND u1.birth_year || lpad(u1.birth_month, 2, '0')
|| lpad(u1.birth_day, 2, '0')
<= #{searchForm.toBirthYear}
|| lpad(#{searchForm.toBirthMonth}, 2, '0')
|| lpad(#{searchForm.toBirthDay}, 2, '0')
</if>
<if test="searchForm.searchSex != null and searchForm.searchSex != ''">
AND u1.sex = #{searchForm.searchSex}
</if>
ORDER BY u1.id
) u
<if test="pageable != null and pageable.pageSize > 0">
<where>
u.rn between #{pageable.offset}
and (#{pageable.offset} + #{pageable.pageSize} - 1)
</where>
</if>
</select>
<select id="findById" resultMap="userDataResultMap">
SELECT id, name, birth_year as birthY, birth_month as birthM
, birth_day as birthD, sex, memo
FROM mv_user_data
WHERE id = #{id}
</select>
<delete id="deleteById" parameterType="java.lang.Long">
DELETE FROM USER_DATA WHERE id = #{id}
</delete>
<insert id="create" parameterType="com.example.demo.UserData">
INSERT INTO USER_DATA ( id, name, birth_year, birth_month
, birth_day, sex, memo )
VALUES (#{id}, #{name}, #{birthY}, #{birthM}
, #{birthD}, #{sex}, #{memo,jdbcType=VARCHAR})
</insert>
<update id="update" parameterType="com.example.demo.UserData">
UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}
, birth_month = #{birthM}, birth_day = #{birthD}
, sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
WHERE id = #{id}
</update>
<select id="findMaxId" resultType="long">
SELECT NVL(max(id), 0) FROM mv_user_data
</select>
<select id="refreshMvUserData" statementType="CALLABLE">
{call dbms_mview.refresh('mv_user_data')}
</select>
</mapper>idが「findBySearchForm」「findById」「findMaxId」であるSQL文は、「user_data」テーブルから参照していた部分を、マテリアライズドビュー「mv_user_data」から参照するように修正している。また、idが「refreshMvUserData」であるSQL文では、マテリアライズドビューのリフレッシュを行う処理を呼び出している。
さらに、サービスクラスの内容は以下の通りで、「deleteById」「createOrUpdate」の各メソッド内で、user_dataテーブルの更新後に、マテリアライズドビューのリフレッシュを行う処理を追加している。
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.Pageable;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class DemoServiceImpl implements DemoService{
/**
* ユーザーデータテーブル(user_data)へアクセスするマッパー
*/
@Autowired
private UserDataMapper mapper;
/**
* 1ページに表示する行数(application.propertiesから取得)
*/
@Value("${demo.list.pageSize}")
private String listPageSize;
/**
* {@inheritDoc}
*/
@Override
public List<DemoForm> demoFormList(SearchForm searchForm, Pageable pageable) {
List<DemoForm> demoFormList = new ArrayList<>();
//ユーザーデータテーブル(user_data)から検索条件に合うデータを取得する
Collection<UserData> userDataList
= mapper.findBySearchForm(searchForm, pageable);
for (UserData userData : userDataList) {
demoFormList.add(getDemoForm(userData));
}
return demoFormList;
}
/**
* {@inheritDoc}
*/
@Override
public DemoForm findById(String id) {
Long longId = stringToLong(id);
UserData userData = mapper.findById(longId);
return getDemoForm(userData);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = false)
public void deleteById(String id){
Long longId = stringToLong(id);
//指定したIDのデータを削除
mapper.deleteById(longId);
//マテリアライズドビューのリフレッシュ
mapper.refreshMvUserData();
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(readOnly = false)
public void createOrUpdate(DemoForm demoForm){
//更新・追加処理を行うエンティティを生成
UserData userData = getUserData(demoForm);
//追加・更新処理
if(demoForm.getId() == null){
userData.setId(mapper.findMaxId() + 1);
mapper.create(userData);
}else{
mapper.update(userData);
}
//マテリアライズドビューのリフレッシュ
mapper.refreshMvUserData();
}
/**
* {@inheritDoc}
*/
@Override
public Pageable getPageable(int pageNumber){
Pageable pageable = new Pageable() {
@Override
public int getPageNumber() {
//現在ページ数を返却
return pageNumber;
}
@Override
public int getPageSize() {
//1ページに表示する行数を返却
//listPageSizeは、本プログラムの先頭に定義している
return Integer.parseInt(listPageSize);
}
@Override
public int getOffset() {
//表示開始位置を返却
//例えば、1ページに2行表示する場合の、2ページ目の表示開始位置は
//(2-1)*2+1=3 で計算される
return ((pageNumber - 1) * Integer.parseInt(listPageSize) + 1);
}
@Override
public Sort getSort() {
//ソートは使わないのでnullを返却
return null;
}
};
return pageable;
}
/**
* {@inheritDoc}
*/
@Override
public int getAllPageNum(SearchForm searchForm) {
//1ページに表示する行数を取得
int listPageSizeNum = Integer.parseInt(listPageSize);
if(listPageSizeNum == 0){
return 1;
}
//一覧画面に表示する全データを取得
//第二引数のpageableにnullを設定することで、一覧画面に表示する
//全データが取得できる
Collection<UserData> userDataList = mapper.findBySearchForm(searchForm, null);
//全ページ数を計算
//例えば、1ページに2行表示する場合で、全データ件数が5の場合、
//(5+2-1)/2=3 と計算される
int allPageNum = (userDataList.size() + listPageSizeNum - 1) / listPageSizeNum;
return allPageNum == 0 ? 1 : allPageNum;
}
/**
* DemoFormオブジェクトに引数のユーザーデータの各値を設定する
* @param userData ユーザーデータ
* @return DemoFormオブジェクト
*/
private DemoForm getDemoForm(UserData userData){
if(userData == null){
return null;
}
DemoForm demoForm = new DemoForm();
demoForm.setId(String.valueOf(userData.getId()));
demoForm.setName(userData.getName());
demoForm.setBirthYear(String.valueOf(userData.getBirthY()));
demoForm.setBirthMonth(String.valueOf(userData.getBirthM()));
demoForm.setBirthDay(String.valueOf(userData.getBirthD()));
demoForm.setSex(userData.getSex());
demoForm.setMemo(userData.getMemo());
demoForm.setSex_value(userData.getSex_value());
return demoForm;
}
/**
* UserDataオブジェクトに引数のフォームの各値を設定する
* @param demoForm DemoFormオブジェクト
* @return ユーザーデータ
*/
private UserData getUserData(DemoForm demoForm){
UserData userData = new UserData();
if(!StringUtils.isEmpty(demoForm.getId())){
userData.setId(Long.valueOf(demoForm.getId()));
}
userData.setName(demoForm.getName());
userData.setBirthY(Integer.valueOf(demoForm.getBirthYear()));
userData.setBirthM(Integer.valueOf(demoForm.getBirthMonth()));
userData.setBirthD(Integer.valueOf(demoForm.getBirthDay()));
userData.setSex(demoForm.getSex());
userData.setMemo(demoForm.getMemo());
userData.setSex_value(demoForm.getSex_value());
return userData;
}
/**
* 引数の文字列をLong型に変換する
* @param id ID
* @return Long型のID
*/
private Long stringToLong(String id){
try{
return Long.parseLong(id);
}catch(NumberFormatException ex){
return null;
}
}
}
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-materialized-view/demo
サンプルプログラムの実行結果
画面上でのサンプルプログラム実行結果は以下の通り。
また、データ追加を行った場合のログ出力内容は以下の通りで、追加後にマテリアライズドビューの更新処理が実施されている。

要点まとめ
- マテリアライズドビューのデータを取得するには、select文のfrom句で直接、マテリアライズドビューを指定すればよい。
- マテリアライズドビューのリフレッシュするには、PL/SQLのdbms_mview.refresh句を利用すればよい。





