5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSV 出力機能を Java で実装してみた(第1部)

5
Last updated at Posted at 2026-01-27

🔰はじめに

既存システムにCSV形式でデータをダウンロードする機能を実装したとき、
「なんだこれ?」ってなったことをまとめたメモです。

まずこの 第一部 では、出力ファイルがまだ 1種類だけ だった時に
特につまづいたポイント(例:MyBatisって何?/DTOって?/CSVの仕様って?など)を中心にまとめてみました!


第1部:出力ファイルが1種類だけだった時


1. ざっくり全体フロー

ユーザーがボタンを押す
  ↓
Controller がリクエストを受ける
  ↓
Mapper で DB からデータ取得  (❓ MyBatis とは)
  ↓
Service で DTO を CSV の行データに変換  (❓ DTO とは)
  ↓
Utils で CSV の仕様に沿って整形  (❓ RFC 4180、BOM とは)
  ↓
CSV ファイルとしてブラウザに返す

2. MyBatis と出会う

既存システムのデータ取得方法を調べたところ、
***Mapper.java」「***Mapper.xml」というファイルが出てきました。なんだこれ?
ここで初めて MyBatis という仕組みを知りました。

特徴:

  • SQL を XML に書く
  • Java 側はメソッド定義だけ
  • 取得結果を DTO に自動でマッピングしてくれる
  • <if> を使って WHERE 句を動的に切り替えられる

💡 MyBatis は Java と DB の橋渡しをしてくれる便利なフレームワーク!


3. DTO ってなに(Bean との違い)

MyBatisの特徴に 取得結果を DTO に自動でマッピングしてくれるとあったけど、
DTOってなんですか。データの箱はBeanじゃないの?
調べて分かったのはこんな感じでした。


📝Data Transfer Objectは、“JavaBean の形をした層間データ転送オブジェクト”
  Beanは”形(仕様)”の呼び名で、DTOは”役割”の呼び名

項目 Bean (形) DTO(役割)
目的 データ保持の器 層間のデータ転送
構造 Javaの規則に従った固定形 用途ごとに柔軟に構成
ロジック 持つこともある 原則持たない
主な利用 フレームワークのデータバインド 画面・API・CSVなどの受け渡し

CSV 出力では、「必要な項目だけ持ちたい」「層をまたいでデータを渡したい」
という理由から DTO を Bean形式 で作ることに決定。

💡 データの流れはこう整理できました:

DB → Mapper → DTO → Service → Utils → CSV

4. DB のデータを CSV の行に変換する(Service)

Mapper から DTO のリストが返ってくるので、それを 1 行ずつ配列に変換します。

⚠️ 問題1:null が混ざると "null" が CSV に出てくる

❌ 問題のあったコード:

private String[] convertToRow(PerformanceDataDTO performanceData) {
    return new String[]{
        // ... 省略 ...
        performanceData.getMonthManHours().toString()  // ← null だと "null" になる!
    };
}

DB の値が null の場合、toString() が文字列の "null" を返してしまい、
CSV に "null" という文字が出力されてしまいました。

✅ 対策1:Service 層で null チェック

private String[] convertToRow(PerformanceDataDTO performanceData) {
    return new String[]{
        // ... 省略 ...
        (performanceData.getMonthManHours() != null) 
            ? performanceData.getMonthManHours().toString() 
            : ""  // ← null の場合は空文字に変換
    };
}

✅ 対策2:Utils 層でも null チェック(二重防御)

public static String escapeAndQuote(String value) {
    if (value == null) {  // ← ここでも null をガード
        return "\"\"";
    }
    return "\"" + value.replace("\"", "\"\"") + "\"";
}

📝なぜ二重防御が必要?

  • Service 層で対処していても、他の開発者が別の場所から Utils を呼ぶ可能性がある
  • 将来的にコードが変更されて、Service での null チェックが漏れる可能性がある
  • Utils は汎用的な関数なので、どこから呼ばれても安全に動作すべき

💡 これは「防御的プログラミング」という考え方らしい!
 各層に安全策を仕込んでおくことで、思わぬバグを事前に防ぐことが大事。


5. CSV の仕様に引っかかる(RFC 4180)

最初は「CSV はカンマ区切りで join すればいいかな」と思い、こうしていました。

String.join(",", fields);

⚠️ 問題2:列がズレる行がある(" を含むとアウト)

" を含むデータを出すと、Excel で列がズレました。

❌ 原因:
CSV には RFC 4180 という仕様があり、" を含むフィールドには以下のルールがあります。

  • " を含むフィールドは全体を " で囲む
  • 中の """ に変換

✅ 対策:

/**
 * CSV フィールドを RFC 4180 に準拠するようにエスケープしてクォートします。
 * ダブルクォート含むフィールドはクォート内のダブルクォートを二重化して処理します。
 * 
 * @param value エスケープ対象の値
 * @return クォートされた文字列
 */
public static String escapeAndQuote(String value) {
    if (value == null) {
        return "\"\"";
    }
    return "\"" + value.replace("\"", "\"\"") + "\"";
}

6. Excel でだけ日本語が文字化けする

UTF-8 で出力しているのに、Excel で開くと日本語が文字化けしました。

❌ 原因:
Excel が CSV の文字コードを自動判定するとき、日本語が含まれると Shift-JIS と勘違いしやすいという Excel 側の仕様(クセ)があるそう。

✅ 対策:BOM をつける

BOM(Byte Order Mark) は、「このファイルは UTF-8 ですよ」という目印のようなものです。UTF-8 の BOM は 3 バイトで、EF BB BF という値になっています。

CSV の先頭にこれをつけると、Excel が確実に UTF-8 と判断してくれるので文字化けしませんでした!

private static final byte[] UTF_8_BOM = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};

outputStream.write(UTF_8_BOM);

⚠️ 注意点:RFC 4180 では BOM は非推奨

RFC 4180 での規定:

  • UTF-8 の BOM は「前置き」として扱われ、正式な仕様では推奨されていない
  • BOM があると、他のシステム(特に Unix/Linux ベース)での互換性が落ちる可能性がある
  • 実務での標準は「BOM なし UTF-8」が推奨されている

今回 BOM をつけたのは以下の理由からです。

  • ユーザーは Excel でしか開かない
  • Excel は BOM があると日本語が確実に表示される
  • システムの外部連携がない(他システムとのやり取りがない)

💡 もし今後 Excel 以外で開いたり、外部連携などするなら改修が必要。


7. 今後やるとしたら…

今回のCSV出力は、

  • 内部向け(社内)
  • Excelでしか開かない

という前提があり、要求もシンプルだったためライブラリは使わず自作してみました。
ただ、もし今後こんなことが出てきたら、
もう少しちゃんとした対応にした方がいいかも…というメモ。

こうなったらちゃんと対応が必要:

  • BOM を外したい
  • 他システムに CSV を渡す(外部連携)
  • CSV 仕様(RFC 4180)にもちゃんと合わせたい(カンマ・改行・空白など)
  • フィールドが増えたり、複雑になりそう

その時の対応方針:

こういう場面が来たら、

  • 自前のエスケープをちゃんとした RFC 4180 準拠版に書き直す
  • もしくは OpenCSV みたいなライブラリに切り替える

…という方向に進めるといいのかなと思いました。
(いまは Excel 前提で問題なく動いているのでこのままで、、、。)


📝まとめ

  • MyBatis で DB からデータを取る流れがだんだん掴めてきた
  • Bean と DTO の役割の違いを理解して、CSV 用 DTO も用意できた
  • CSV には RFC 4180 という仕様がある
  • Excel は UTF-8 を誤判定するので、BOM つけると文字化けしない
    (でも RFC 的には BOM は微妙…という現実も知った)
  • null はService と Utils の 2 箇所でチェックすると安心
    (これが “防御的プログラミング” らしい)

➡️次は...

CSV の種類が増えたときの設計(第2部)を書こうと思います。
初心者の実装メモなので、間違ってることやもっとこうしたらいいなどがあったら、教えてもらえると嬉しいです!
最後まで見ていただきありがとうございました😊

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?