🔰はじめに
既存システムに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部)を書こうと思います。
初心者の実装メモなので、間違ってることやもっとこうしたらいいなどがあったら、教えてもらえると嬉しいです!
最後まで見ていただきありがとうございました😊