Spring Boot DB連携

Spring BootのWEB画面上でファイルダウンロード処理とDB更新処理を併用してみた

今回は、以前実装していたSpring BootのWEB画面上でファイルダウンロード処理に、DB更新処理を追加してみたので、そのサンプルプログラムを共有する。

ファイルダウンロードを行う際、最後にInputStreamオブジェクト, OutputStreamオブジェクトをそれぞれcloseする必要があるが、エラー発生時でも確実にcloseできるようにするには、finally句でこれを実行する必要がある。

例えばプログラム上で、DB更新⇒ファイルダウンロードの順に記載している場合、DB更新処理・ファイルダウンロード処理がほぼ同じタイミングで実行されるため、finally句でclose処理を実行しないと、DB更新処理に失敗した場合にclose処理が実行されなくなってしまう。

今回は、下記「前提条件」に記載した記事に、finally句でclose処理を実行する部分と、ファイルダウンロード前のチェック処理の追加を行ったので、その内容について共有する。



前提条件

下記記事の実装が完了していること。

Spring BootのWEB画面上でPDFプレビューを実装してみた前回は、Sping BootのWEB画面上で、ファイルアップロード・ファイルダウンロード機能について記載したが、今回は、PDFの場合のみ...

完成イメージ

ここでは、完成した画面イメージの共有を行う。

Spring Bootアプリケーションを起動し、「http:// (ホスト名):(ポート番号)」とアクセスした場合の初期表示は以下の通りで、「PDFプレビュー」ボタンを押下すると、以下のように、別画面にPDFプレビュー画面が表示されると共に、ダウンロード履歴テーブルにレコードが1件追加される。
ダウンロード履歴作成_実行結果1

ダウンロード履歴作成_実行結果2
select * from download_history

ダウンロード履歴作成_実行結果3
なお、ダウンロード履歴テーブルは、本記事で作成するものとする。

また、「ダウンロード」ボタンを押下すると、以下のように、画面下にファイル操作のダイアログが表示されると共に、ダウンロード履歴テーブルにレコードが1件追加される。
ダウンロード履歴作成_実行結果4_1

ダウンロード履歴作成_実行結果4_2 ダウンロード履歴作成_実行結果5
select * from download_history
ダウンロード履歴作成_実行結果6

さらに、一覧に表示されるものの、ファイルダウンロードデータが無い場合は、以下のように、ダウンロードエラー画面が表示される。
ダウンロード履歴作成_実行結果7

ダウンロード履歴作成_実行結果8

なお、上記の場合は、ダウンロード履歴テーブルへのレコード追加はされない。



やってみたこと

  1. ファイル履歴テーブルとそのシーケンスの作成
  2. サンプルプログラムの作成

 

ファイル履歴テーブルとそのシーケンスの作成

今回は、ファイル履歴テーブルとそのシーケンスを追加した。実行したSQLは以下の通り。

create table download_history (
    history_id number(10) primary key not null,
    file_data_id number(6) not null,
    download_date timestamp not null
);
create sequence download_sequence INCREMENT BY 1 START WITH 1 MAXVALUE 9999999999

実行後の確認結果は以下の通り。テーブルの確認はdescコマンドで、シーケンスの確認はuser_sequencesテーブルで、それぞれ確認している。

desc DOWNLOAD_HISTORY
desc_ダウンロード履歴テーブル
select * from user_sequences where sequence_name = 'DOWNLOAD_SEQUENCE'
select_user_sequences



サンプルプログラムの作成

今回作成したサンプルプログラムの構成は以下の通り。なお、下図の赤枠は、前提条件に記載した記事と変更になったソースコードを示しており、今後記載する。
サンプルプログラムの構成

ダウンロード履歴テーブルにアクセスするためのエンティティクラスは以下の通り。

package com.example.demo;

import lombok.Data;

import java.io.Serializable;
import java.sql.Timestamp;

/**
 * ダウンロード履歴テーブル(download_history)アクセス用エンティティ
 */
@Data
public class DownloadHistory implements Serializable {

    /** シリアルバージョンID */
    private static final long serialVersionUID = 1L;

    /** ダウンロード履歴ID */
    private long historyId;

    /** ファイルデータID */
    private long fileDataId;

    /** ダウンロード日付 */
    private Timestamp downloadDate;

}



また、ダウンロード履歴テーブルにアクセスするためのMapperインタフェースは以下の通り。今回はシーケンスの採番も行っているので、そのselect文も含めている。

package com.example.demo;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DownloadHistoryMapper {

    /**
     * ダウンロード履歴のシーケンス番号を取得する
     * @return ダウンロード履歴のシーケンス番号
     */
    @Select("select DOWNLOAD_SEQUENCE.nextval as nextval from dual")
    long gen_history_sequence();

    /**
     * 指定したダウンロード履歴テーブル(download_history)のデータを追加する
     * @param downloadHistory ダウンロード履歴(download_history)
     */
    @Insert("INSERT INTO download_history ( history_id, file_data_id, download_date ) "
            + " VALUES ( #{historyId}, #{fileDataId}, #{downloadDate} )")
    void insert(DownloadHistory downloadHistory);
}

さらに、コントローラクラスは以下の通り。ここでは、downloadメソッド内で、エラーチェック処理とダウンロード履歴テーブルへの登録処理を追加している。また、InputStreamオブジェクト, OutputStreamオブジェクトのclose処理はfinally句で実装している。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;

@Controller
public class DemoController {

    /**
     * ファイルデータテーブル(file_data)へアクセスするMapper
     */
    @Autowired
    private FileDataMapper fileDataMapper;

    /**
     * ダウンロード履歴テーブル(download_history)へアクセスするMapper
     */
    @Autowired
    private DownloadHistoryMapper downloadHistoryMapper;

    /**
     * ファイルデータ一覧表示処理
     * @param model Modelオブジェクト
     * @return 一覧画面
     */
    @RequestMapping("/")
    public String index(Model model){
        //ファイルデータテーブル(file_data)を全件取得
        List<FileData> list = fileDataMapper.findAll();
        model.addAttribute("fileDataList", list);

        //一覧画面へ移動
        return "list";
    }

    /**
     * ファイルデータ登録画面への遷移処理
     * @return ファイルデータ登録画面
     */
    @PostMapping("/to_add")
    public String to_add(){
        return "add";
    }

    /**
     * ファイルデータ登録処理
     * @param uploadFile アップロードファイル
     * @return ファイルデータ一覧表示処理
     */
    @PostMapping("/add")
    @Transactional(readOnly = false)
    public String add(@RequestParam("upload_file") MultipartFile uploadFile){
        //最大値IDを取得
        long maxId = fileDataMapper.getMaxId();

        //追加するデータを作成
        FileData fileData = new FileData();
        fileData.setId(maxId + 1);
        fileData.setFilePath(uploadFile.getOriginalFilename());
        try{
            fileData.setFileObj(uploadFile.getInputStream());
        }catch(Exception e){
            System.err.println(e);
        }
        //1件追加
        fileDataMapper.insert(fileData);

        //一覧画面へ遷移
        return "redirect:/to_index";
    }

    /**
     * 追加完了後に一覧画面に戻る
     * @param model Modelオブジェクト
     * @return 一覧画面
     */
    @GetMapping("/to_index")
    public String toIndex(Model model){
        return index(model);
    }

    /**
     * ファイルダウンロード処理
     * @param id ID
     * @param response HttpServletResponse
     * @return 画面遷移先
     */
    @Transactional(readOnly = false)
    @RequestMapping("/download")
    public String download(@RequestParam("id") String id
            , HttpServletResponse response){
        //ダウンロード対象のファイルデータを取得
        FileData data = fileDataMapper.findById(Long.parseLong(id));

        //ダウンロード対象のファイルデータがnullの場合はエラー画面に遷移
        if(data == null || data.getFileObj() == null){
            return "download_error";
        }

        //PDFの場合
        if(data.getFilePath().endsWith(".pdf")){
            //PDFプレビューの設定を実施
            response.setContentType("application/pdf");
            response.setHeader("Content-Disposition", "inline;");
        }else{
            //ファイルダウンロードの設定を実施
            response.setContentType("application/octet-stream");  //ファイルの種類は指定しない
            response.setHeader("Content-Disposition"
                ,"attachment;filename=\"" + getFileName(data.getFilePath()) + "\"");
        }
        //その他の設定を実施
        response.setHeader("Cache-Control", "private");
        response.setHeader("Pragma", "");

        OutputStream out = null;
        InputStream in = null;
        try{
            //ダウンロード履歴への書き込み
            DownloadHistory downloadHistory = new DownloadHistory();
            downloadHistory.setHistoryId(downloadHistoryMapper.gen_history_sequence());
            downloadHistory.setFileDataId(data.getId());
            downloadHistory.setDownloadDate(Timestamp.valueOf(LocalDateTime.now()));
            downloadHistoryMapper.insert(downloadHistory);

            //ダウンロードファイルへ出力
            out = response.getOutputStream();
            in = data.getFileObj();
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = in.read(buff, 0, buff.length)) != -1) {
                out.write(buff, 0, len);
            }
            out.flush();
        }catch (Exception e){
            System.err.println(e);
        }finally {
            if(out != null){
                try{
                    out.close();
                }catch (IOException e){
                    System.err.println(e);
                }
            }
            if(in != null){
                try{
                    in.close();
                }catch (IOException e){
                    System.err.println(e);
                }
            }
        }

        //画面遷移先はnullを指定
        return null;
    }

    /**
     * ファイルパスからファイル名を取得する
     * @param filePath ファイルパス
     * @return ファイル名
     */
    private String getFileName(String filePath){
        String fileName = "";
        if(filePath != null && !"".equals(filePath)){
            try{
                //ファイル名をUTF-8でエンコードして指定
                fileName = URLEncoder.encode(new File(filePath).getName(), "UTF-8");
            }catch(Exception e){
                System.err.println(e);
                return "";
            }
        }
        return fileName;
    }

}

さらに、ダウンロードエラー時は、ダウンロードエラー画面に遷移する。ダウンロードエラー画面の実装は以下の通り。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ダウンロードエラー画面</title>
</head>
<body>
    <p><font color="red">ダウンロードエラーが発生しました。</font></p>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-download-updatedb/demo

要点まとめ

  • ファイルダウンロード時の、InputStreamオブジェクト, OutputStreamオブジェクトのclose処理は、例外発生の可能性がある場合は、finally句で実装する必要がある。