【Java・SpringBoot】RowCallbackHandlerでコールバック / CSV出力機能（SpringBootアプリケーション実践編18）

ホーム画面からユーザー一覧画面に遷移し、ユーザーの詳細を表示するアプリケーションを作成して、Spring JDBCの使い方について学びます⭐️
今回は**RowCallbackHandler**を使って、**ユーザー一覧画面からCSV出力する機能**を作ります^^
構成は前回/これまでの記事を参考にしてください

⭐️前回の記事
[【Java・SpringBoot】Spring JDBC / ResultSetExtractor（SpringBootアプリケーション実践編17）](https://qiita.com/suema0331/items/b55919b2076207790164)

#RowCallbackHandler
* RowCallbackHandlerでコールバックして、**ユーザー一覧画面からCSV出力する機能**を作ります

###コールバックとは
* 時間のかかるような処理を**別のコールバック関数に実行依頼**する
* その処理が終わるまで**他の処理を行うことができる**
* コールバック関数の処理が終わったら結果を受け取る

###RowCallbackHandlerの使い方
* RowCallbackHandlerをimplements
    * 以下の例では、`processRow()`メソッド内で、`ResultSet`から取得した値を`sample.csv`に書き込む処理をしている
    * **RowCallbackHandler**では、既に１回`next()`メソッドが実行された状態になっている
        * cf: **ResultSetExtractor**では、ResultSetの`next()`メソッドを使わないと、レコードの値を取得できない
    *  →`while`文ではなく、`do～while`文でループ処理う
    *  プロジェクトフォルダの直下に`sample.csv`が作られる

```UserRowCallbackHandler.java
package com.example.demo.login.domain.repository.jdbc;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowCallbackHandler;

public class UserRowCallbackHandler implements RowCallbackHandler {

    @Override
    public void processRow(ResultSet rs) throws SQLException {

        try {

            //ファイル書き込みの準備
            File file = new File("sample.csv");
            FileWriter fw = new FileWriter(file.getAbsoluteFile());
            BufferedWriter bw = new BufferedWriter(fw);

            //取得件数分loop
            do {

                //ResultSetから値を取得してStringにセット
                String str = rs.getString("user_id") + ","
                        + rs.getString("password") + ","
                        + rs.getString("user_name") + ","
                        + rs.getDate("birthday") + ","
                        + rs.getInt("age") + ","
                        + rs.getBoolean("marriage") + ","
                        + rs.getString("role");

                //ファイルに書き込み＆改行
                bw.write(str);
                bw.newLine();

            } while(rs.next());

            //強制的に書き込み＆ファイルクローズ
            bw.flush();
            bw.close();

        } catch (IOException e) {
            e.printStackTrace();
            throw new SQLException(e);
        }
    }
}

```

#UserDaoJdbcImpl
* RowCallbackHandlerを使ったCSV出力処理を追加

```UserDaoJdbcImpl.java
//中略(全文は下記参考)
//SQL取得結果をサーバーにCSVで保存する
@Override
public void userCsvOut() throws DataAccessException {

    // M_USERテーブルのデータを全件取得するSQL
    String sql = "SELECT * FROM m_user";

    // ResultSetExtractorの生成
    UserRowCallbackHandler handler = new UserRowCallbackHandler();

    //SQL実行＆CSV出力
    jdbc.query(sql, handler);
}

```

#サービスクラス
* ユーザー一覧をsample.csvファイルに出力する
    * リポジトリークラス（UserDaoJdbcImpl）のCSV出力メソッドを呼び出す
        * `dao.userCsvOut();`
* メソッドファイルを取得するメソッド追加
    * 引数で指定されたファイル名をサーバーから取得
        * `FileSystem fs = FileSystems.getDefault();`
        * `Path p = fs.getPath(fileName);`
    * ファイルの中身をbyte型の配列にしてリターン
 
```UserService.java
//中略(全文は下記参考)

// ユーザー一覧をCSV出力する.
public void userCsvOut() throws DataAccessException {
    // CSV出力
    dao.userCsvOut();
}

//サーバーに保存されているファイルを取得して、byte配列に変換する.
public byte[] getFile(String fileName) throws IOException {

    // ファイルシステム（デフォルト）の取得
    FileSystem fs = FileSystems.getDefault();

    // ファイル取得
    Path p = fs.getPath(fileName);

    // ファイルをbyte配列に変換
    byte[] bytes = Files.readAllBytes(p);

    return bytes;
}

```

#コントローラクラス
###CSV出力メソッド追加
* サービスクラスのCSV出力とファイル取得メソッドを呼び出し、**HTTPヘッダーの値をセット**して返す
    * `header.add("Content-Type", "text/csv; charset=UTF-8");`
    * `header.setContentDispositionFormData("filename", "sample.csv");`
* メソッドの戻り値を`ResponseEntity`型にすると、**Thymeleafのテンプレート（html）ではなく、ファイル（byte型の配列）を呼び出し元に返却**できる

```HomeController.java
//中略(全文は下記参考)
/**
 * ユーザー一覧のCSV出力用処理.
 */
@GetMapping("/userList/csv")

public ResponseEntity<byte[]> getUserListCsv(Model model) {

    //ユーザーを全件取得して、CSVをサーバーに保存する
    userService.userCsvOut();

    byte[] bytes = null;

    try {

        //サーバーに保存されているsample.csvファイルをbyteで取得する
        bytes = userService.getFile("sample.csv");

    } catch (IOException e) {
        e.printStackTrace();
    }

    //HTTPヘッダーの設定
    HttpHeaders header = new HttpHeaders();
    header.add("Content-Type", "text/csv; charset=UTF-8");
    header.setContentDispositionFormData("filename", "sample.csv");

    //sample.csvを戻す
    return new ResponseEntity<>(bytes, header, HttpStatus.OK);

}
```

#SpringBootを起動してホーム画面確認！
* http://localhost:8080/home
* ユーザー一覧に移りCSV出力ボタンを押すと、プロジェクトフォルダー直下にsample.csvがダウンロードされる
* 中身を見ると、**ユーザーマスタテーブルの内容が出力されている**事が確認できました〜〜^^

![csv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/71cb7bc9-8cf7-e586-7c54-683f2ab7ba14.png)

![usercsv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/0f8da5c0-45fa-949e-0697-9d6446ab78e5.png)



#(参考)コード全文

```UserDaoJdbcImpl.java
package com.example.demo.login.domain.repository.jdbc;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Repository("UserDaoJdbcImpl")
public class UserDaoJdbcImpl implements UserDao {

    @Autowired
    JdbcTemplate jdbc;

    // Userテーブルの件数を取得.
    @Override
    public int count() throws DataAccessException {

        //全件取得してカウント
        int count = jdbc.queryForObject("SELECT COUNT(*) FROM m_user", Integer.class);

        return count;
    }

    // Userテーブルにデータを1件insert.
    @Override
    public int insertOne(User user) throws DataAccessException {

        //１件登録
        int rowNumber = jdbc.update("INSERT INTO m_user(user_id,"
                + " password,"
                + " user_name,"
                + " birthday,"
                + " age,"
                + " marriage,"
                + " role)"
                + " VALUES(?, ?, ?, ?, ?, ?, ?)",
                user.getUserId(),
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getRole());

        return rowNumber;

    }

    // Userテーブルのデータを１件取得
    @Override
    public User selectOne(String userId) throws DataAccessException {

        // １件取得
        Map<String, Object> map = jdbc.queryForMap("SELECT * FROM m_user"
                + " WHERE user_id = ?", userId);

        // 結果返却用の変数
        User user = new User();

        // 取得したデータを結果返却用の変数にセットしていく
        user.setUserId((String) map.get("user_id")); //ユーザーID
        user.setPassword((String) map.get("password")); //パスワード
        user.setUserName((String) map.get("user_name")); //ユーザー名
        user.setBirthday((Date) map.get("birthday")); //誕生日
        user.setAge((Integer) map.get("age")); //年齢
        user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
        user.setRole((String) map.get("role")); //ロール

        return user;

    }

    // Userテーブルの全データを取得.
    @Override
    public List<User> selectMany() throws DataAccessException {

        // M_USERテーブルのデータを全件取得
        List<Map<String, Object>> getList = jdbc.queryForList("SELECT * FROM m_user");

        // 結果返却用の変数
        List<User> userList = new ArrayList<>();

        // 取得したデータを結果返却用のListに格納していく
        for (Map<String, Object> map : getList) {

            //Userインスタンスの生成
            User user = new User();

            // Userインスタンスに取得したデータをセットする
            user.setUserId((String) map.get("user_id")); //ユーザーID
            user.setPassword((String) map.get("password")); //パスワード
            user.setUserName((String) map.get("user_name")); //ユーザー名
            user.setBirthday((Date) map.get("birthday")); //誕生日
            user.setAge((Integer) map.get("age")); //年齢
            user.setMarriage((Boolean) map.get("marriage")); //結婚ステータス
            user.setRole((String) map.get("role")); //ロール

            //結果返却用のListに追加
            userList.add(user);
        }

        return userList;

    }

    // Userテーブルを１件更新.
    @Override
    public int updateOne(User user) throws DataAccessException {

        //１件更新
        int rowNumber = jdbc.update("UPDATE M_USER"
                + " SET"
                + " password = ?,"
                + " user_name = ?,"
                + " birthday = ?,"
                + " age = ?,"
                + " marriage = ?"
                + " WHERE user_id = ?",
                user.getPassword(),
                user.getUserName(),
                user.getBirthday(),
                user.getAge(),
                user.isMarriage(),
                user.getUserId());

        return rowNumber;

    }

    // Userテーブルを１件削除.
    @Override
    public int deleteOne(String userId) throws DataAccessException {

        //１件削除
        int rowNumber = jdbc.update("DELETE FROM m_user WHERE user_id = ?", userId);

        return rowNumber;
    }

    //SQL取得結果をサーバーにCSVで保存する
    @Override
    public void userCsvOut() throws DataAccessException {

        // M_USERテーブルのデータを全件取得するSQL
        String sql = "SELECT * FROM m_user";

        // ResultSetExtractorの生成
        UserRowCallbackHandler handler = new UserRowCallbackHandler();

        //SQL実行＆CSV出力
        jdbc.query(sql, handler);
    }
}
```

```UserService.java
package com.example.demo.login.domain.service;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.repository.UserDao;

@Service
public class UserService {
    @Autowired
    @Qualifier("UserDaoJdbcImpl4")
    UserDao dao;

    /**
     * insert用メソッド.
     */
    public boolean insert(User user) {
        // insert実行
        int rowNumber = dao.insertOne(user);
        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // insert成功
            result = true;
        }
        return result;
    }

    /**
     * カウント用メソッド.
     */
    public int count() {
        return dao.count();
    }

    /**
     * 全件取得用メソッド.
     */
    public List<User> selectMany() {
        // 全件取得
        return dao.selectMany();
    }

    /**
     * １件取得用メソッド.
     */
    public User selectOne(String userId) {
        // selectOne実行
        return dao.selectOne(userId);
    }

    /**
     * １件更新用メソッド.
     */
    public boolean updateOne(User user) {

        // 判定用変数
        boolean result = false;

        // １件更新
        int rowNumber = dao.updateOne(user);

        if (rowNumber > 0) {
            // update成功
            result = true;
        }

        return result;
    }

    /**
     * １件削除用メソッド.
     */
    public boolean deleteOne(String userId) {

        // １件削除
        int rowNumber = dao.deleteOne(userId);

        // 判定用変数
        boolean result = false;

        if (rowNumber > 0) {
            // delete成功
            result = true;
        }
        return result;
    }

    // ユーザー一覧をCSV出力する.
    public void userCsvOut() throws DataAccessException {
        // CSV出力
        dao.userCsvOut();
    }

    /**
     * サーバーに保存されているファイルを取得して、byte配列に変換する.
     */
    public byte[] getFile(String fileName) throws IOException {

        // ファイルシステム（デフォルト）の取得
        FileSystem fs = FileSystems.getDefault();

        // ファイル取得
        Path p = fs.getPath(fileName);

        // ファイルをbyte配列に変換
        byte[] bytes = Files.readAllBytes(p);

        return bytes;
    }
}
```

```HomeController.java
package com.example.demo.login.controller;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class HomeController {

    @Autowired
    UserService userService;

    //結婚ステータスのラジオボタン用変数
    private Map<String, String> radioMarriage;

    /**
     * ラジオボタンの初期化メソッド（ユーザー登録画面と同じ）.
     */
    private Map<String, String> initRadioMarrige() {

        Map<String, String> radio = new LinkedHashMap<>();

        // 既婚、未婚をMapに格納
        radio.put("既婚", "true");
        radio.put("未婚", "false");

        return radio;
    }

    /**
     * ホーム画面のGET用メソッド
     */
    @GetMapping("/home")
    public String getHome(Model model) {

        //コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/home :: home_contents");

        return "login/homeLayout";
    }

    /**
     * ユーザー一覧画面のGETメソッド用処理.
     */
    @GetMapping("/userList")
    public String getUserList(Model model) {

        //コンテンツ部分にユーザー一覧を表示するための文字列を登録
        model.addAttribute("contents", "login/userList :: userList_contents");

        //ユーザー一覧の生成
        List<User> userList = userService.selectMany();

        //Modelにユーザーリストを登録
        model.addAttribute("userList", userList);

        //データ件数を取得
        int count = userService.count();
        model.addAttribute("userListCount", count);

        return "login/homeLayout";
    }

    /**
     * ユーザー詳細画面のGETメソッド用処理.
     */
    @GetMapping("/userDetail/{id:.+}")
    public String getUserDetail(@ModelAttribute SignupForm form,
            Model model,
            @PathVariable("id") String userId) {

        // ユーザーID確認（デバッグ）
        System.out.println("userId = " + userId);

        // コンテンツ部分にユーザー詳細を表示するための文字列を登録
        model.addAttribute("contents", "login/userDetail :: userDetail_contents");

        // 結婚ステータス用ラジオボタンの初期化
        radioMarriage = initRadioMarrige();

        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // ユーザーIDのチェック
        if (userId != null && userId.length() > 0) {

            // ユーザー情報を取得
            User user = userService.selectOne(userId);

            // Userクラスをフォームクラスに変換
            form.setUserId(user.getUserId()); //ユーザーID
            form.setUserName(user.getUserName()); //ユーザー名
            form.setBirthday(user.getBirthday()); //誕生日
            form.setAge(user.getAge()); //年齢
            form.setMarriage(user.isMarriage()); //結婚ステータス

            // Modelに登録
            model.addAttribute("signupForm", form);
        }

        return "login/homeLayout";
    }

    /**
     * ユーザー更新用処理.
     */
    @PostMapping(value = "/userDetail", params = "update")
    public String postUserDetailUpdate(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("更新ボタンの処理");

        //Userインスタンスの生成
        User user = new User();

        //フォームクラスをUserクラスに変換
        user.setUserId(form.getUserId());
        user.setPassword(form.getPassword());
        user.setUserName(form.getUserName());
        user.setBirthday(form.getBirthday());
        user.setAge(form.getAge());
        user.setMarriage(form.isMarriage());

            //更新実行
            boolean result = userService.updateOne(user);

            if (result == true) {
                model.addAttribute("result", "更新成功");
            } else {
                model.addAttribute("result", "更新失敗");
            }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ユーザー削除用処理.
     */
    @PostMapping(value = "/userDetail", params = "delete")
    public String postUserDetailDelete(@ModelAttribute SignupForm form,
            Model model) {

        System.out.println("削除ボタンの処理");

        //削除実行
        boolean result = userService.deleteOne(form.getUserId());

        if (result == true) {

            model.addAttribute("result", "削除成功");

        } else {

            model.addAttribute("result", "削除失敗");

        }

        //ユーザー一覧画面を表示
        return getUserList(model);
    }

    /**
     * ログアウト用処理.
     */
    @PostMapping("/logout")
    public String postLogout() {

        //ログイン画面にリダイレクト
        return "redirect:/login";
    }

    /**
     * ユーザー一覧のCSV出力用処理.
     */
    @GetMapping("/userList/csv")
    public ResponseEntity<byte[]> getUserListCsv(Model model) {

        //ユーザーを全件取得して、CSVをサーバーに保存する
        userService.userCsvOut();

        byte[] bytes = null;

        try {

            //サーバーに保存されているsample.csvファイルをbyteで取得する
            bytes = userService.getFile("sample.csv");

        } catch (IOException e) {
            e.printStackTrace();
        }

        //HTTPヘッダーの設定
        HttpHeaders header = new HttpHeaders();
        header.add("Content-Type", "text/csv; charset=UTF-8");
        header.setContentDispositionFormData("filename", "sample.csv");

        //sample.csvを戻す
        return new ResponseEntity<>(bytes, header, HttpStatus.OK);
    }
}

```