บทนำ
ในยุคที่ส่วนหน้าและส่วนหลังแยกจากกัน ชีวิตของเราเต็มไปด้วยโอกาสและความท้าทายนับไม่ถ้วน——รวมถึง Bug ที่โผล่มาอย่างกะทันหันจนทำให้แทบคลั่ง วันนี้เราจะพูดถึงปัญหาคลาสสิกที่ทำให้นักพัฒนานับไม่ถ้วนทั้งขำทั้งปวดหัว:แบ็กเอนด์ Long ID ประเภทยาวเกินไปทำให้ฟรอนต์เอนด์สูญเสียความแม่นยำของตัวเลข。พูดถึงปัญหานี้แล้ว ก็เรียกได้ว่าเป็น “ต้นตอแห่งความชั่วร้ายทั้งปวง” จริง ๆ ก็ใครใช้ให้ JavaScript สามารถจัดการได้อย่างปลอดภัยเฉพาะ Number.MAX_SAFE_INTEGER(ก็คือ 9007199254740991)ล่ะ?
หากคุณเคยขบคิดอย่างหนักกับปัญหานี้ ก็อย่ากังวลไป คุณไม่ได้เผชิญอยู่คนเดียว วันนี้เราจะไม่เพียงวิเคราะห์ “ปัญหาระดับศตวรรษ” นี้อย่างมีอารมณ์ขัน แต่ยังพาคุณแก้ไขถึงต้นตอ เพื่อให้โค้ดของคุณไม่ “สูญเสียความแม่นยำ” อีกต่อไป。
ที่มาของปัญหา:เพราะเหตุใด Long ยาวขนาดนี้เลยหรือ?
ก่อนอื่น เราต้องเริ่มจาก Long เริ่มจากชนิดข้อมูล。Long,ก็คือ Java ชนิดจำนวนเต็ม 64 บิตในนั้น สำหรับผู้ที่ชอบจัดการกับข้อมูลขนาดใหญ่、สำหรับ Java ที่จัดการตัวเลขขนาดใหญ่ ชนิดข้อมูลนี้แทบจะเป็นพรจากสวรรค์เลยทีเดียว ทว่าโลกฝั่งฟรอนต์เอนด์กลับค่อนข้าง “รู้น้อยเห็นน้อย” เพราะมันจัดการได้เพียงจำนวนเต็มที่มีขนาดไม่เกิน 53 บิต ใช่แล้ว คุณฟังไม่ผิด ในเรื่องนี้ JavaScript มันเหมือนปราชญ์เฒ่าที่ “ช้ากว่าครึ่งจังหวะ” เมื่อเจอตัวเลขที่ใหญ่ขึ้น ก็เริ่มขยับจุดทศนิยมและเลขชี้กำลัง สุดท้ายคายตัวเลขที่ทำให้คุณสิ้นหวังออกมา แล้วปัญหาการสูญเสียความแม่นยำก็เริ่มล่องลอยอยู่ในโปรเจกต์ของคุณราวกับผี。
การพบกันครั้งแรก:เรื่องราวของการสูญเสียความแม่นยำ
เรื่องนี้ต้องเริ่มจากรายงาน Bug ในวันหนึ่ง:“พี่นักพัฒนา ดูสิว่าทำไม 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 ฟิลด์ จึงต้องพิจารณาอย่างรอบคอบ。
กรณีจริง:เส้นทางแก้ Bug ที่พลิกผันหลายตลบ
เพื่อให้ทุกคนเข้าใจผลที่เกิดขึ้นจริงของแนวทางแก้ไขเหล่านี้ได้ดีขึ้น ผมจะเล่าเรื่องจากโครงการจริงโครงการหนึ่ง โครงการนี้เกี่ยวข้องกับการจัดการ ID ผู้ใช้ เนื่องจากมีผู้ใช้จำนวนมาก แบ็กเอนด์จึงใช้ Long ตัวระบุที่ไม่ซ้ำกันของชนิดนี้ หลังจากโครงการเปิดใช้งานได้เพียงไม่กี่วัน ก็มีผู้ใช้แจ้งว่า:“ทำไม ID ของฉันถึงเปลี่ยนไปแบบนี้?”
ขั้นตอนแรก:สืบหาต้นตอของปัญหา
เราเริ่มจากตรวจสอบโค้ดฟรอนต์เอนด์ แล้วพบว่า JavaScript ของ Number ชนิดข้อมูลนี้ไม่สามารถแทนตัวเลขที่ยาวถึง 19 หลักได้อย่างแม่นยำจริง ๆ จึงทำให้สูญเสียความแม่นยำ จากนั้นเราตรวจสอบโค้ดแบ็กเอนด์ และพบว่าแม้ Long ข้อมูลชนิดนี้ถูกต้องที่ฝั่งแบ็กเอนด์ แต่เมื่อส่งผ่าน REST API เมื่อส่งกลับไปยังฟรอนต์เอนด์ ความแม่นยำของตัวเลขสูญหายไป
ขั้นตอนที่สอง:เลือกวิธีแก้ปัญหาที่เหมาะสม
เพื่อแก้ปัญหาอย่างรวดเร็ว เราตัดสินใจเริ่มต้นด้วยการใช้ @JsonSerialize(using = ToStringSerializer.class) วิธีที่เรียบง่ายและได้ผลนี้ ด้วยวิธีนี้ เราจึงป้องกันไม่ให้ส่วนหน้ารับตัวเลขที่สูญเสียความแม่นยำได้สำเร็จ。
อย่างไรก็ตาม ปัญหายังไม่ได้รับการแก้ไขทั้งหมด ในข้อกำหนดที่ตามมา ส่วนหน้าจำเป็นต้องคำนวณบางอย่างกับ ID เช่น เรียงลำดับ ID ของผู้ใช้ เมื่อถึงตอนนี้ สตริงก็เริ่มรับมือได้ไม่ค่อยไหวแล้ว。
ขั้นตอนที่สาม:ใช้ BigInt แก้ปัญหาการคำนวณฝั่งส่วนหน้า
ดังนั้น เราจึงตัดสินใจนำมาใช้ในฟรอนต์เอนด์ ได้แก่ BigInt。โดยใช้ BigInt,ส่วนหน้าจึงไม่เพียงจัดเก็บ ID ที่ยาวมากเหล่านี้ได้อย่างแม่นยำ แต่ยังทำการคำนวณทางคณิตศาสตร์ที่จำเป็นได้ด้วย จากการทดสอบ วิธีนี้ทำงานได้ดีบนเบราว์เซอร์หลักทุกตัว ข้อเสียเพียงอย่างเดียวคือรองรับเบราว์เซอร์รุ่นเก่าบางรุ่นได้ไม่ดีนัก
ขั้นตอนที่สี่:การกำหนดค่าส่วนกลางขั้นสุดท้าย
เพื่อป้องกันไม่ให้เกิดปัญหาลักษณะเดียวกันอีกในอนาคต เราตัดสินใจที่จะนำ ToStringSerializer ตั้งค่าให้มีผลทั่วทั้งระบบ ซึ่งทำให้ทั้งหมดของ Long ข้อมูลชนิดนี้จะถูกแปลงเป็นสตริงโดยอัตโนมัติทั้งหมดขณะซีเรียลไลซ์ ซึ่งช่วยรับประกันความถูกต้องของข้อมูลฝั่งหน้า และลดการกำหนดค่าซ้ำในโค้ดไปพร้อมกัน。
บทส่งท้าย:ผู้ยุติปัญหาการสูญเสียความแม่นยำ
จากกรณีนี้จะเห็นได้ว่า แม้ว่า Long ปัญหาการสูญเสียความแม่นยำของชนิดข้อมูลดูเหมือนเรียบง่าย แต่ในโครงการจริงอาจนำมาซึ่งความเสี่ยงแฝงมากมาย หลังจากเปรียบเทียบและทดลองหลายแนวทาง ในที่สุดเราก็พบวิธีที่เหมาะกับโครงการของเราที่สุด
หวังว่าบล็อกที่ทั้งมีอารมณ์ขันและให้รายละเอียดครบถ้วนนี้ จะช่วยให้คุณขณะจัดการ Long เมื่อพบปัญหาการสูญเสียความแม่นยำของชนิดข้อมูล จะได้ไม่ต้องเสียเวลาอ้อมไปมา หากคุณเคยมีประสบการณ์คล้ายกัน ก็เชิญแบ่งปันเรื่องราวของคุณในช่องความคิดเห็น ประสบการณ์ของคุณอาจช่วยจุดประกายความคิดให้กับนักพัฒนาคนอื่นได้มากขึ้น。
สุดท้าย จำไว้ว่า โค้ดก็เหมือนชีวิต การ “สูญหาย” เป็นครั้งคราวไม่ใช่เรื่องน่ากลัว สิ่งสำคัญคือการหา “ตัวซีเรียลไลซ์” ที่เหมาะสมเพื่อพามันกลับเข้าที่เข้าทาง ขอให้โค้ดของทุกคนไม่ “สูญเสียความแม่นยำ” อีก รักษาความเที่ยงตรงและทำงานได้อย่างราบรื่นตลอดทาง!