はじめに
フロントエンドとバックエンドが分離した時代、私たちの日々は数え切れない機会と課題に満ちています——突然現れて気が狂いそうになるBugもその一つです。今日取り上げるのは、数多くの開発者を泣くにも笑うにも笑えなくさせてきた定番の問題です:バックエンド Long 型IDが長すぎることによるフロントエンドでの精度損失。この問題といえば、まさに「諸悪の根源」ですね、なにしろ JavaScript 安全に処理できるのは Number.MAX_SAFE_INTEGER(つまり 9007199254740991)以内の数値なのだろうか?
この問題で頭を悩ませたことがあっても、心配はいりません、あなただけではありません。今日は、この「世紀の難問」をユーモアを交えて分析するだけでなく、根本から解決し、あなたのコードがもう「精を失う」、つまり精度を失うことのないようにします。
問題の背景:なぜ Long こんなに長いの?
まず、私たちは Long 型から話を始めます。Long、つまり Java における64ビット整数型で、処理するのが好きなビッグデータ、大きな数値を扱う Java にとって、この型はまさに福音だ。しかし、フロントエンドの世界は少々「物知りとは言えず」、53 ビット以内の整数しか扱えない。そう、聞き間違いではない。この点では JavaScript まるで「ワンテンポ遅い」老学者のように、もっと大きな数字を前にすると、小数点や指数をいじり始め、最後には絶望したくなるような数字を吐き出します。こうして、精度が失われる問題が幽霊のように、あなたのプロジェクトをさまよい始めるのです。
最初の出会い:精度損失をめぐるあれこれ
話はある日のバグ報告から始まる:「開発のお兄さん、このID、どうして変わっちゃったの?これは私がデータベースの中のあの ID だよ!」あなたが眉をひそめて見てみると、確かに、9223372036854775807 に変わった 9223372036854776000、いやはや、この余計な数字はまるで手品のようです。バックエンドから渡された値は正しいのに! あなたは頭をかきながら、「これは絶対フロントエンドのせいだ!」と思います。
フロントエンドのせい、それともバックエンドのせい?——根本原因をたどる
実のところ、この問題をフロントエンドのせいにするのも、まったく理由がないわけではありません。では、見てみましょう JavaScript が数値を扱う際の「弱点」。JavaScript の Number 型は IEEE 754 標準に基づく倍精度浮動小数点形式で、安全に表現できるのは 53 桁の二進数まで、つまり Number.MAX_SAFE_INTEGER の値——9007199254740991。
言い換えれば、この範囲を超える整数は、JavaScript 「精度の低下」が始まり、その「脳の容量」が突然足りなくなります。すると、あなたの Long 型の ID は、その目にはあまり「セクシー」ではない数値の集まりに見えるようになります。そのため、フロントエンドでは途方もなく長いこれらの ID を扱う際に、少し「犠牲」を払わざるを得ず、その結果、元の値を忠実に保っていた ID が切り詰められ、「面白い」新しい数値に変わってしまいます。
驚きから冷静さへ:解決策を探す
問題の根源がわかったところで、私たち開発者の腕の見せどころだ。これからいくつかの解決策を詳しく紹介し、それぞれの長所と短所を説明する。何しろ、すべての道はローマに通ずるのだから。
1. 文字列に直接変換:単純で力技ながら効率的
このような問題に直面したとき、真っ先に思いつくのは、きっと最も単純で強引な方法でしょう:直接 Long 型のデータ変換文字列に変換すればいいじゃないですか!せっかく JavaScript が浮動小数点数の熱狂的なファンなら、いっそ問題を投げ返して、こう伝えましょう:「これは文字列だと思ってくれればいい。どれだけ長いかは気にしないで!」
どうすれば実現できるのでしょうか? 実はとても簡単で、使うのは Jackson 提供された ToStringSerializer,私たちは簡単に Long 文字列に変換する。
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
public class UserDto {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
// 其他字段
}こうすることで、バックエンドが返す際に UserDto とき、Long 型の id 文字列としてシリアライズされ、フロントエンドが受け取るのも文字列になるため、精度の問題はすんなり解決します。まるでJavaScriptの頭に付箋を貼って、「これは文字列だから、気にしなくていいよ!」と言うようなものです。
2. フロントエンドでの使用 BigInt:大きな数値でも正確に計算できるようにする
もちろん、単純で強引な方法が、いつもあらゆる場面に適しているわけではありません。フロントエンドでこのIDに何らかの数学的演算を行う必要がある場合を想像してみてください。そのまま文字列に変換するのは、あまりよい方法ではありません。では、フロントエンドはこうした「桁数が非常に多い」IDをどう扱えばよいのでしょうか?
幸いなことに、JavaScript にも取り柄がないわけではありません。導入されたのは BigInt その後、JavaScript ようやく、小数点をいじることしかできないあの間抜けではなくなった。BigInt は、任意精度の整数を扱うために設けられた、新しいプリミティブデータ型です。次のように使用できます:
const id = BigInt("9223372036854775807");
console.log(id + 1n); // 输出:9223372036854775808nこれにより、フロントエンドで正確に処理できます: Long 型のデータを使い、精度が失われる問題を回避する。もちろん、ここで一つ注意しておきたいことがある:BigInt すべてのブラウザーがサポートしているわけではないため、アプリケーションの実行環境で互換性があることを確認する必要があります。
3. カスタムシリアライズ:複雑な問題を単純化する
上記の方法で大半の問題は解決できますが、より細かい制御が必要な場面に遭遇することもあります。そのようなときは、Jackson のカスタムシリアライザーが役に立ちます。
カスタムシリアライザーを実装し、要件に応じて柔軟に制御できます Long 型フィールドのシリアライズ処理。例えば、シリアライズ時に、 Long 文字列に変換するか、特定の条件に応じて異なる処理を行うかを決められます:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class CustomLongSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null) {
gen.writeString(value.toString());
}
}
}次に、必要な箇所でこのカスタムシリアライザーを適用します:
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class UserDto {
@JsonSerialize(using = CustomLongSerializer.class)
private Long id;
// 其他字段
}こうすれば、シリアライズの全過程を制御し、一つひとつの Long すべてを希望どおりに処理できます。もちろん、この方法は柔軟ですが、やや複雑で、コードを数行余分に書く必要があり、保守コストが増える可能性もあります。
4. 全体での処理:時間と手間を省く方法
この問題を全体で解決し、フィールドごとにアノテーションを設定する手間を省きたい場合は、グローバル設定を検討できます ObjectMapper。次の場所で Spring Boot 内でグローバルに設定する ObjectMapper、すべての Long 型のフィールドはすべて自動的に文字列へ変換されます。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
mapper.registerModule(module);
return mapper;
}
}一度設定すれば、特定のフィールドにシリアライザーを設定し忘れる心配もなく、プロジェクト全体で精度が失われない快適さを享受できます。もちろん、グローバル設定を使う際には、変換したくない一部のものにも影響する可能性があるため、注意が必要です。その対象は Long フィールドのため、慎重に検討する必要があります。
実例:紆余曲折のバグ解決への道
これらの解決策の実際の効果をよりよく理解していただくために、実際のプロジェクトでの出来事を紹介しよう。このプロジェクトではユーザーIDの管理を扱っており、ユーザー数が非常に多いため、バックエンドでは Long 型の一意識別子です。プロジェクトのリリースから数日もたたないうちに、ユーザーから「私の ID はどうして変わってしまったのですか?」という声が届きました。
ステップ1:問題の根源を追究する
まずフロントエンドのコードを確認したところ、 JavaScript の Number 型では、この 19 桁にも及ぶ数値を確かに正確に表現できず、そのため精度が失われていた。続いてバックエンドのコードを確認したところ、確かに Long 型のデータはバックエンドでは正しいものの、REST を通じて API フロントエンドに返す際に、数値の精度が失われました。
ステップ2:適切な解決策を選ぶ
問題を迅速に解決するため、まず採用することにしたのは @JsonSerialize(using = ToStringSerializer.class) というシンプルで効果的な方法です。この方法により、フロントエンドが精度の失われた数値を受け取るのを防ぐことができました。
しかし、問題は完全には解決していませんでした。その後の要件では、フロントエンドで ID に対する何らかの演算、例えばユーザー ID の並べ替えを行う必要が生じました。この場合、文字列ではやや力不足です。
ステップ3:使用 BigInt フロントエンドの計算問題を解決する
そこで、フロントエンドに導入することにしたのが BigInt。使用することで BigInt,フロントエンドでは、これらの非常に長い ID を正確に保存できるだけでなく、必要な算術演算も行えます。テストの結果、この方法は主要なブラウザーで良好に動作しました。唯一の欠点は、一部の古いバージョンのブラウザーへの対応が十分ではないことです。
第四ステップ:最終的なグローバル設定
今後、同様の問題が再発するのを防ぐため、私たちは次のものを ToStringSerializer グローバルに有効になるよう設定します。これにより、すべての Long 型のデータがシリアライズ時にすべて自動的に文字列へ変換されるため、フロントエンドのデータの正確性を保証しつつ、コード内での重複した設定も減らせます。
結び:精度損失の終結者
この事例から分かるように、たとえ Long 型の精度が失われる問題は、一見単純ですが、実際のプロジェクトではさまざまな潜在的問題をもたらす可能性があります。複数の解決策を比較して試した結果、最終的に私たちのプロジェクトに最適な方法を見つけました。
ユーモラスで詳しいこの記事が、あなたが Long 型の精度が失われる問題に直面したとき、遠回りをせずに済むでしょう。同じような経験があれば、ぜひコメント欄でお話を聞かせてください。あなたの経験が、ほかの開発者にさらなるヒントを与えるかもしれません。
最後に、コードも人生と同じで、ときどき何かが「失われる」ことは怖くありません。大切なのは、適切な「シリアライザー」を見つけて、正常な状態に戻すことです。皆さんのコードが二度と「精度を失う」ことなく、正確さを保ち、順調に動き続けますように!