【初心者】気分記録アプリ、公開への道④|データベース「日付」の取り扱い|SQLiteとMySQLの違い

気分記録アプリを作っていて、「日付なんて文字列で入れておけばいいでしょ」と思っていたら、画面を見ても絶対に気づけないバグの入り口だと知りました。その記録です。

SQLite とは何か(MySQL を知っている人向け)

私は phpMyAdmin で MySQL を触ったことがある、くらいの経験でした。

今回 Expo でアプリを作るにあたって SQLite というものを使ったのですが、同じ「SQL を書くデータベース」なのに、前提がかなり違って混乱しました。

一番の違いはこれです。

MySQL は「サービス」。SQLite は「ファイル」。

MySQL の場合

MySQL はサーバーとして常駐しているプログラムです。だから接続するのに、

  • ホスト名(localhost とか)
  • ポート番号(3306)
  • ユーザー名とパスワード
  • データベース名

が必要でした。phpMyAdmin は、その MySQL サーバーにブラウザから話しかけるための別のソフトです。

SQLite の場合

SQLite にはサーバーがありません。mood.db という1個のファイルがデータベースそのものです。

接続情報:  なし
ユーザー名: なし
パスワード: なし
ポート:    なし

やること:  ファイルを開く

私が書いたコードも、実際こうなっています。

const db = await openDatabaseAsync('mood.db');

これだけです。「接続」ではなく「ファイルを開いている」。

この1行の意味が分かった瞬間に、だいぶスッキリしました。

比較表

MySQLSQLite
正体常駐するサーバープログラム1個のファイル
接続情報ホスト・ポート・ユーザー・パスワード不要
誰が使う複数のアプリ・複数の人が同時にそのアプリだけ
置き場所サーバーの中アプリの中(今回は iPhone の中)
管理画面phpMyAdmin無い(後述)
向いてる用途Web サービススマホアプリ・組み込み

なぜスマホアプリでは SQLite なのか

今回のアプリは「気分の記録を端末の外に出さない」という方針でした。

通院記録という、かなり繊細な情報も扱うからです。

MySQL だと、どこかにサーバーを置いて、そこにデータを送ることになります。サーバーがある時点で、情報漏洩のリスクと、サーバー代と、ログイン機能が発生します。

SQLite なら、データは iPhone の中のファイル1個。開発者である私も中身を見られません。サーバーが無いことが、そのままセキュリティになるという設計です。

⚠️ 注意:phpMyAdmin がありません

これは正直きつかった点です。

MySQL なら phpMyAdmin を開けば、テーブルの中身が表形式でパッと見られました。SQLite は iPhone の中にあるファイルなので、ブラウザから覗くことができません。

中身を確認する方法は別途あるようですが、少なくとも「とりあえず phpMyAdmin で見る」という手癖は使えません。デバッグの感覚がかなり変わります。

SQLite には「日付型」が無い

ここから本題です。

MySQL には DATE 型や DATETIME 型がありました。phpMyAdmin でテーブルを作るとき、プルダウンから選んでいたあれです。

SQLite には日付型がありません。

SQLite が持っている型は、たったこれだけです。

中身
TEXT文字列
INTEGER整数
REAL小数
BLOBバイナリ
NULL

VARCHAR(255) もありません。DATE もありません。

なので日付は、自分で決めた形式の文字列(TEXT)として入れることになります。私が書いたテーブル定義はこうなりました。

CREATE TABLE moods (
  date       TEXT PRIMARY KEY NOT NULL,   -- '2026-08-02'
  mood       INTEGER NOT NULL CHECK (mood BETWEEN 1 AND 10),
  memo       TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

なぜ 'YYYY-MM-DD' という形式なのか

これは慣習ではなく、明確な理由があると知って驚きました。

この形式は、文字列として並べ替えると、時系列順と完全に一致します。

'2026-01-05'
'2026-01-31'
'2026-02-01'    ← 文字列比較でも、ちゃんとこの順になる

だから日付型が無くても、

SELECT * FROM moods WHERE date BETWEEN '2026-08-01' AND '2026-08-31' ORDER BY date

がそのまま正しく動きます。もしこれが '2026/8/2' みたいな形式だったら、'2026/10/1' より '2026/8/2' のほうが後ろに来てしまって(1 < 8 なので)、並び順が壊れます。

日付型が無いぶん、形式選びが自分の責任になるわけです。

本題の罠 — 深夜に記録すると前日になる

さて、ここからが今回一番学んだところです。

「Date型のデータを 'YYYY-MM-DD' の文字列にすればいいんでしょ」と思って調べると、こういうコードが山ほど出てきます。

const key = date.toISOString().slice(0, 10);   // '2026-08-02'

短くて綺麗です。そして、日本で使うと壊れます。

何が起きるか

スマホやパソコンの中では、時刻は 世界標準時(UTC、だいたいイギリスの時間) を基準に管理されています。日本はそこから 9時間進んでいます

つまり、こうなります。

日本の時刻世界標準時(UTC)
8月2日 00:30(深夜)8月1日 15:30 ← 前日!
8月2日 08:598月1日 23:59 ← まだ前日
8月2日 09:008月2日 00:00 ← ここでやっと追いつく
8月2日 23:598月2日 14:59

toISOString()世界標準時に変換してから文字列にします。

なので 日本時間の 0:00〜8:59 に記録すると、前の日として保存されます。

実際に確かめてみた

同じコードを、タイムゾーンだけ変えて動かしてみました。日本時間の 2026年1月1日 0時30分 のつもりのデータです。

Asia/Tokyo         正しい: 2026-01-01  |  toISOString: 2025-12-31   ← 年まで違う
UTC                正しい: 2026-01-01  |  toISOString: 2026-01-01

元日の深夜に記録したら、去年の大晦日として保存されていました。

何が一番怖いか

バグそのものより、発見できないことが怖いと思いました。

  • エラーは出ません
  • 赤い画面も出ません
  • カレンダーにはちゃんと色が付きます
  • 数字も正しく表示されます

ただ、1日ずれているだけ。

しかも今回のアプリは「診察のときに医師に見せる記録」にもなるものです。

「先週の火曜がしんどかった」という話が全部1日ずれている状態で、それに誰も気づかない。

数か月後に気づいたときには、すでに保存されたデータは直しようがありません(どの記録が深夜入力だったのか、後からは判別できないので)。

対策:変換する場所を1箇所だけにする

やったことは単純です。日付を文字列にする処理を、アプリ全体で1個だけにしました。

// src/utils/date.ts  ← アプリ内で唯一の変換口

export function toDateKey(date: Date): string {
  const pad2 = (n: number) => String(n).padStart(2, '0');
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
}

getFullYear() / getMonth() / getDate() は、端末のタイムゾーンで解釈されるメソッドです。UTC への変換が一度も発生しないので、ズレようがありません。

ポイントは「正しい書き方を知ったこと」ではなく、「他の場所で書けないようにしたこと」だと思っています。

20箇所で日付を扱うなら、19箇所で正しく書けても意味がないので。

第4部:テストを書いたのに、守れていなかった話

この日付の処理には、自動テストを書きました。「深夜0時30分を渡したら、その日の日付が返ること」を確認するテストです。

it('深夜0時台でも、その日のローカル暦日を返す', () => {
  const d = new Date(2026, 0, 1, 0, 30);
  expect(toDateKey(d)).toBe('2026-01-01');
});

これで安心……と思ったのですが、このテストには穴がありました。

先ほどの比較表をもう一度見てください。

Asia/Tokyo    正しい: 2026-01-01  |  誤った実装: 2025-12-31   → テストは落ちる 
UTC           正しい: 2026-01-01  |  誤った実装: 2026-01-01   → テストは通る 

テストを動かすパソコンが世界標準時に設定されていたら、間違ったコードでもテストが「合格」になります。

私の Mac はたまたま日本時間だったので機能していましたが、これは運です。もし将来クラウド上(多くはUTC設定)で自動テストを回すようにしたら、テストは全部緑なのにバグは通り抜けるという、一番たちの悪い状態になっていました。

なので、テストが必ず日本時間で走るように固定しました。

"scripts": {
  "test": "TZ=Asia/Tokyo jest"
}

「テストがあること」と「テストが守れていること」は別問題だというのが、今回一番の学びでした。書いたテストが本当にバグを捕まえられるか、一度わざと壊して確かめる価値があります。

まとめ

SQLite について

  • MySQL は「サーバー」、SQLite は「ファイル1個」
  • 接続情報もパスワードも不要。openDatabaseAsync('mood.db') だけ
  • サーバーが無いこと自体が、そのままセキュリティになる
  • ただし phpMyAdmin に相当する管理画面は無い

日付について

  • SQLite に日付型は無い。TEXT で 'YYYY-MM-DD' を入れる
  • この形式なら、文字列の並べ替え = 時系列の並べ替えになる
  • toISOString() は世界標準時に変換する。日本の深夜0〜9時が前日にずれる
  • 対策は「正しく書く」ではなく「変換場所を1つに絞る
  • テストは、動かす環境のタイムゾーン次第で無力化される

日付は「ただの文字列」だと思っていました。実際には、画面を見ても分からず、後から直せず、しかも一番大事なデータがずれるという、なかなか凶悪な地雷でした。

タイトルとURLをコピーしました