ปัญหาไม่เกี่ยวข้องกับฐานข้อมูล หากคุณตั้งค่าเบรกพอยต์หรือป้อนเอาต์พุตที่ไหนสักแห่ง คุณจะเห็นออฟเซ็ตถูกหักหลังจากโค้ดนี้ไม่นาน:
TestDateAndTime = testDateAndTime.DateTime.Date;
มาทำลายมันกัน:
- คุณเริ่มต้นด้วยค่า DateTimeOffset 2008-05-01T08:06:32+01:00
- จากนั้นคุณเรียกว่า .DateTime ซึ่งส่งผลให้ค่า DateTime 2008-05-01T08:06:32 พร้อมด้วย DateTimeKind.Unspecified
- จากนั้นคุณเรียกว่า .Date ซึ่งส่งผลให้ค่า DateTime เป็น 2008-05-01T00:00:00 พร้อมด้วย DateTimeKind.Unspecified
- คุณกำหนดผลลัพธ์ให้กับ testDateAndTime ซึ่งเป็นประเภท DateTimeOffset สิ่งนี้ทำให้เกิดการส่งโดยนัยจาก DateTime ถึง DateTimeOffset - ซึ่งใช้บังคับ ท้องถิ่นเขตเวลา- ในกรณีของคุณ ปรากฏว่าค่าชดเชยสำหรับค่านี้ในเขตเวลาท้องถิ่นของคุณคือ -04:00 ดังนั้นค่าผลลัพธ์คือ DateTimeOffset ของ 2008-05-01T00:00:00-04:00 ตามที่คุณอธิบายไว้
คุณพูดว่า:
เป้าหมายสูงสุดคือการมีวันที่โดยไม่มีการชดเชยเวลาหรือเขตเวลา
ก็มีนะ ตอนนี้ไม่ใช่ประเภทข้อมูล C# ดั้งเดิมซึ่งเป็นเพียงวันที่ที่ไม่มีเวลา มีประเภท Date ล้วนๆ เวลาของระบบใน corefxlab แต่ยังไม่พร้อมสำหรับแอปพลิเคชันการผลิตทั่วไป มี LocalDate ในไลบรารี Noda Time ที่คุณสามารถใช้ได้ในปัจจุบัน แต่คุณยังคงต้องแปลงกลับเป็นประเภทเนทิฟก่อนที่จะบันทึกลงในฐานข้อมูล ดังนั้นในระหว่างนี้ สิ่งที่ดีที่สุดที่คุณสามารถทำได้คือ:
- เปลี่ยน SQL Server ของคุณเพื่อใช้ประเภทวันที่ในฟิลด์
- ในโค้ด .NET ของคุณ ให้ใช้ DateTime พร้อมเวลา 00:00:00 และ DateTimeKind.Unspecified คุณต้องจำไว้ว่าอย่าเพิกเฉยต่อส่วนของเวลา (เนื่องจากมีวันที่ที่ไม่มีเที่ยงคืนท้องถิ่นในบางโซนเวลา)
- เปลี่ยนเสาทดสอบให้เป็น DateTime แทนที่จะเป็น DateTimeOffset
โดยทั่วไป ในขณะที่ DateTimeOffset จะเหมาะสมกับสถานการณ์จำนวนมาก (เช่น การประทับเวลาเหตุการณ์) จึงไม่เหมาะกับค่าเฉพาะวันที่เท่านั้น
ฉันต้องการ วันที่ปัจจุบันโดยมีค่าชดเชยเป็นศูนย์
ถ้าคุณ ต้องการจริงๆมันเหมือนกับ DateTimeOffset คุณสามารถทำได้:
TestDateAndTime = DateTimeOffset ใหม่ (testDateAndTime.Date, TimeSpan.Zero);
อย่างไรก็ตาม ฉันไม่แนะนำให้ทำเช่นนี้ โดยการทำเช่นนี้คุณใช้เวลา ท้องถิ่นวันที่ของมูลค่าดั้งเดิมและอ้างว่าเป็น UTC หากออฟเซ็ตเดิมเป็นอย่างอื่นที่ไม่ใช่ศูนย์ นี่จะเป็นข้อความเท็จ มันจะนำไปสู่ข้อผิดพลาดอื่นๆ ในภายหลัง เนื่องจากคุณกำลังพูดถึงช่วงเวลาที่แตกต่างกัน (ซึ่งอาจเป็นวันที่ที่แตกต่างออกไป) มากกว่าที่คุณสร้างขึ้น
เกี่ยวกับคำถามเพิ่มเติมที่ถูกถามในบอร์ดของคุณ การระบุ DateTimeKind.Utc จะเปลี่ยนพฤติกรรมของการส่งโดยนัย แทนที่จะใช้เขตเวลาท้องถิ่น ระบบจะใช้เวลา UTC ซึ่งมีออฟเซ็ตเป็นศูนย์เสมอ ผลลัพธ์จะเหมือนกับมุมมองที่ชัดเจนยิ่งขึ้นที่ฉันให้ไว้ข้างต้น ฉันยังคงแนะนำสิ่งนี้ด้วยเหตุผลเดียวกัน
ลองพิจารณาตัวอย่างที่เริ่มต้นด้วย 2016-12-31T22:00:00-04:00 ตามแนวทางของคุณ คุณควรจัดเก็บ 2016-12-31T00:00:00+00:00 ไว้ในฐานข้อมูล อย่างไรก็ตาม นี่เป็นสองจุดที่แตกต่างกันของเวลา ครั้งแรกที่ปรับมาตรฐานเป็น UTC จะเป็น 2017-01-01T02:00:00+00:00 และครั้งที่สองซึ่งแปลงเป็นเขตเวลาอื่นจะเป็น 2016-12-30T20:00:00-04:00 โปรดทราบการเปลี่ยนแปลงวันที่ในการแปลง นี่อาจไม่ใช่พฤติกรรมที่คุณต้องการในแอปพลิเคชันของคุณ
เกือบทุกโครงการประสบปัญหาที่เกิดจากการจัดการและการจัดเก็บวันที่และเวลาอย่างไม่เหมาะสม แม้ว่าโปรเจ็กต์จะใช้ในเขตเวลาเดียวกัน คุณยังคงได้รับสิ่งที่ไม่พึงประสงค์หลังจากเปลี่ยนเป็นเวลาฤดูหนาว/ฤดูร้อน ในเวลาเดียวกันมีเพียงไม่กี่คนที่สับสนกับการใช้กลไกที่ถูกต้องตั้งแต่เริ่มต้นเพราะดูเหมือนว่าจะไม่มีปัญหากับเรื่องนี้เนื่องจากทุกอย่างเป็นเรื่องเล็กน้อย น่าเสียดายที่ความเป็นจริงในภายหลังแสดงให้เห็นว่าไม่เป็นเช่นนั้น
ตามหลักเหตุผลแล้ว สามารถแยกแยะค่าประเภทต่อไปนี้ที่เกี่ยวข้องกับวันที่และเวลาได้:
พิจารณาแต่ละประเด็นแยกกันโดยไม่ลืม
วันและเวลา
สมมติว่าห้องปฏิบัติการที่รวบรวมวัสดุสำหรับการวิเคราะห์ตั้งอยู่ในโซนเวลา +2 และสาขากลางซึ่งตรวจสอบความสมบูรณ์ของการทดสอบตามกำหนดเวลานั้นอยู่ในโซน +1 เวลาที่ระบุในตัวอย่างจะถูกบันทึกไว้เมื่อห้องปฏิบัติการแห่งแรกเก็บวัสดุ คำถามเกิดขึ้นว่าสำนักงานกลางควรดูเวลาใด? เห็นได้ชัดว่าซอฟต์แวร์ สำนักงานกลางควรแสดงวันที่ 15 มกราคม 2014 เวลา 12:17:15 น. - น้อยกว่าหนึ่งชั่วโมง เนื่องจากจากการดูเหตุการณ์ที่เกิดขึ้นในขณะนั้นลองพิจารณาหนึ่งในห่วงโซ่การดำเนินการที่เป็นไปได้ซึ่งข้อมูลที่ส่งผ่านจากไคลเอนต์ไปยังเซิร์ฟเวอร์และย้อนกลับ ซึ่งช่วยให้คุณแสดงวันที่/เวลาได้อย่างถูกต้องตามโซนเวลาปัจจุบันของลูกค้า:
- ค่าถูกสร้างขึ้นบนไคลเอนต์ เช่น 2 มีนาคม 2016 15 :13:36 ลูกค้าอยู่ในโซนเวลา +2
- ค่าจะถูกแปลงเป็นการแสดงสตริงสำหรับการส่งไปยังเซิร์ฟเวอร์ - “2016-03-02T 15 :13:36+02:00”.
- ข้อมูลซีเรียลไลซ์จะถูกส่งไปยังเซิร์ฟเวอร์
- เซิร์ฟเวอร์จะทำการดีซีเรียลไลซ์เวลาเป็นออบเจ็กต์วันที่/เวลา และนำไปยังโซนเวลาปัจจุบัน ตัวอย่างเช่น หากเซิร์ฟเวอร์ทำงานที่ +1 ออบเจ็กต์จะมีวันที่ 2 มีนาคม 2016 14 :13:36.
- เซิร์ฟเวอร์บันทึกข้อมูลลงในฐานข้อมูล แต่ไม่มีข้อมูลใด ๆ เกี่ยวกับเขตเวลา - ประเภทวันที่/เวลาที่ใช้บ่อยที่สุดนั้นไม่รู้อะไรเลยเกี่ยวกับเขตเวลา ดังนั้นวันที่ 2 มีนาคม 2559 จะถูกบันทึกไว้ในฐานข้อมูล 14 :13:36 ในเขตเวลา "ไม่ทราบ"
- เซิร์ฟเวอร์อ่านข้อมูลจากฐานข้อมูลและสร้างออบเจ็กต์ที่เกี่ยวข้องด้วยค่า 2 มีนาคม 2016 14 :13:36. และเนื่องจากเซิร์ฟเวอร์ทำงานในเขตเวลา +1 ค่านี้จะถูกตีความภายในเขตเวลาเดียวกันด้วย
- ค่าจะถูกแปลงเป็นการแสดงสตริงสำหรับการส่งไปยังไคลเอนต์ - “2016-03-02T 14 :13:36+01:00”.
- ข้อมูลซีเรียลไลซ์จะถูกส่งไปยังไคลเอนต์
- ไคลเอนต์ทำการดีซีเรียลไลซ์ค่าที่ได้รับไปเป็นออบเจ็กต์วันที่/เวลา โดยส่งไปยังโซนเวลาปัจจุบัน ตัวอย่างเช่น หากเป็น -5 ค่าที่แสดงควรเป็นวันที่ 2 มีนาคม 2016 09 :13:36.
- เวลาบนไคลเอนต์สามารถสร้างขึ้นได้โดยไม่ต้องมีเขตเวลาเลย - ตัวอย่างเช่น ประเภท DateTime ใน .NET ที่มี DateTimeKind.Unspecified
- เอ็นจินการทำให้เป็นอนุกรมอาจใช้รูปแบบที่ไม่รวมออฟเซ็ตโซนเวลา
- เมื่อทำการดีซีเรียลไลซ์ลงในออบเจ็กต์ คุณสามารถละเว้นออฟเซ็ตโซนเวลาได้ โดยเฉพาะในดีซีเรียลไลเซอร์ "แบบโฮมเมด" ทั้งบนเซิร์ฟเวอร์และบนไคลเอนต์
- เมื่ออ่านจากฐานข้อมูล คุณสามารถสร้างออบเจ็กต์วันที่/เวลาได้โดยไม่ต้องระบุเขตเวลาเลย ตัวอย่างเช่น ประเภท DateTime ใน .NET พร้อมด้วย DateTimeKind.Unspecified ยิ่งไปกว่านั้น ในทางปฏิบัติด้วย DateTime ใน .NET นี่คือสิ่งที่เกิดขึ้นอย่างแน่นอนหากคุณไม่ได้ระบุ DateTimeKind อื่นอย่างชัดเจนทันทีหลังจากการพิสูจน์อักษร
- หากแอปพลิเคชันเซิร์ฟเวอร์ที่ทำงานกับฐานข้อมูลทั่วไปตั้งอยู่ในเขตเวลาที่แตกต่างกัน จะเกิดความสับสนอย่างมากในการชดเชยเวลา ค่าวันที่/เวลาที่เขียนไปยังฐานข้อมูลโดยเซิร์ฟเวอร์ A และอ่านโดยเซิร์ฟเวอร์ B จะแตกต่างอย่างเห็นได้ชัดจากค่าเดิมเดียวกันที่เขียนโดยเซิร์ฟเวอร์ B และอ่านโดยเซิร์ฟเวอร์ A
- การถ่ายโอนเซิร์ฟเวอร์แอปพลิเคชันจากโซนหนึ่งไปยังอีกโซนหนึ่งจะนำไปสู่การตีความค่าวันที่/เวลาที่เก็บไว้แล้วไม่ถูกต้อง
กฎสำหรับการแปลงเป็นเวลาฤดูร้อน/ฤดูหนาว พูดอย่างเคร่งครัดคือตัวแปร ประเทศต่างๆ อาจเปลี่ยนแปลงกฎของตนเป็นครั้งคราว และการเปลี่ยนแปลงเหล่านี้ควรรวมอยู่ในการอัปเดตระบบล่วงหน้า ในทางปฏิบัติ เราพบสถานการณ์ซ้ำแล้วซ้ำเล่าที่กลไกนี้ทำงานไม่ถูกต้อง ซึ่งท้ายที่สุดก็แก้ไขได้ด้วยการติดตั้งโปรแกรมแก้ไขด่วนหรือ ระบบปฏิบัติการหรือใช้ไลบรารีบุคคลที่สาม โอกาสที่ปัญหาเดิมๆ จะเกิดขึ้นซ้ำๆ ไม่เป็นศูนย์ ดังนั้นจึงควรมีวิธีที่จะหลีกเลี่ยงปัญหาเหล่านั้นจะดีกว่า
โดยคำนึงถึงข้อควรพิจารณาที่อธิบายไว้ข้างต้น เราจะกำหนดวิธีการส่งและจัดเก็บเวลาที่เชื่อถือได้และง่ายที่สุด: บนเซิร์ฟเวอร์และในฐานข้อมูลค่าทั้งหมดจะต้องถูกแปลงเป็นเขตเวลา UTC.
มาดูกันว่ากฎนี้ให้อะไรเราบ้าง:
- เมื่อส่งข้อมูลไปยังเซิร์ฟเวอร์ ไคลเอนต์จะต้องส่งค่าชดเชยโซนเวลาเพื่อให้เซิร์ฟเวอร์สามารถแปลงเวลาเป็น UTC ได้อย่างถูกต้อง อีกทางเลือกหนึ่งคือการบังคับให้ลูกค้าทำการแปลงนี้ แต่ตัวเลือกแรกมีความยืดหยุ่นมากกว่า เมื่อรับข้อมูลกลับจากเซิร์ฟเวอร์ ไคลเอนต์จะแปลงวันที่และเวลาเป็นเขตเวลาท้องถิ่น โดยรู้ว่าไม่ว่าในกรณีใด ลูกค้าจะได้รับเวลาใน UTC
- ไม่มีการเปลี่ยนแปลงระหว่างเวลาฤดูร้อนและฤดูหนาวใน UTC ดังนั้นปัญหาที่เกี่ยวข้องกับสิ่งนี้จะไม่เกี่ยวข้อง
- บนเซิร์ฟเวอร์ เมื่ออ่านจากฐานข้อมูล คุณไม่จำเป็นต้องแปลงค่าเวลา คุณเพียงแค่ต้องระบุอย่างชัดเจนว่าสอดคล้องกับ UTC ตัวอย่างเช่น ใน .NET สามารถทำได้โดยการตั้งค่า DateTimeKind บนวัตถุเวลาเป็น DateTimeKind.Utc
- ความแตกต่างในเขตเวลาระหว่างเซิร์ฟเวอร์ที่ทำงานกับฐานข้อมูลทั่วไปตลอดจนการถ่ายโอนเซิร์ฟเวอร์จากโซนหนึ่งไปยังอีกโซนหนึ่งจะไม่ส่งผลกระทบต่อความถูกต้องของข้อมูลที่ได้รับ แต่อย่างใด
- สร้างกลไกการทำให้ซีเรียลไลซ์และดีซีเรียลไลซ์เพื่อให้ค่าวันที่/เวลาถูกแปลอย่างถูกต้องจาก UTC ไปเป็นโซนเวลาท้องถิ่นและย้อนกลับ
- ตรวจสอบให้แน่ใจว่าดีซีเรียลไลเซอร์ฝั่งเซิร์ฟเวอร์สร้างวัตถุวันที่/เวลาใน UTC
- ตรวจสอบให้แน่ใจว่าเมื่ออ่านจากฐานข้อมูล ออบเจ็กต์วันที่/เวลาจะถูกสร้างขึ้นในรูปแบบ UTC บางครั้งรายการนี้จัดทำโดยไม่มีการเปลี่ยนแปลงรหัส เพียงแต่เขตเวลาของระบบบนเซิร์ฟเวอร์ทั้งหมดจะถูกตั้งค่าเป็น UTC
- ความต้องการของระบบไม่ต้องการให้แสดงเวลาท้องถิ่นและ/หรือโซนเวลาตามที่จัดเก็บไว้ทุกประการ ตัวอย่างเช่น ตั๋วเครื่องบินจะต้องพิมพ์เวลาออกเดินทางและมาถึงในเขตเวลาที่สอดคล้องกับที่ตั้งสนามบิน หรือหากเซิร์ฟเวอร์ส่งใบแจ้งหนี้การพิมพ์ที่สร้างขึ้นในประเทศต่างๆ แต่ละใบแจ้งหนี้ควรลงเอยด้วยเวลาท้องถิ่น และไม่แปลงเป็นโซนเวลาของเซิร์ฟเวอร์
- ค่าวันที่และเวลาทั้งหมดในระบบเป็น "สัมบูรณ์" - เช่น อธิบายช่วงเวลาในอนาคตหรืออดีตที่สอดคล้องกับค่าเดียวใน UTC ตัวอย่างเช่น “ยานพาหนะส่งเกิดขึ้นเวลา 23:00 น. ตามเวลาเคียฟ” หรือ “การประชุมจะเกิดขึ้นระหว่างเวลา 13:30 น. ถึง 14:30 น. ตามเวลามินสค์” ตัวเลขสำหรับกิจกรรมเหล่านี้จะแตกต่างกันในเขตเวลาที่ต่างกัน แต่จะอธิบายในช่วงเวลาเดียวกัน แต่อาจเกิดขึ้นได้ว่าข้อกำหนดสำหรับ ซอฟต์แวร์หมายถึงเวลาท้องถิ่นแบบ "สัมพันธ์" ในบางกรณี เช่น “รายการโทรทัศน์นี้จะออกอากาศเวลา 9.00 – 10.00 น. ในทุกประเทศที่มีช่องทีวีในเครือ” ปรากฎว่าการออกอากาศรายการไม่ใช่เหตุการณ์เดียว แต่มีหลายเหตุการณ์ และอาจเกิดขึ้นทั้งหมดในช่วงเวลาที่ต่างกันในระดับ "สัมบูรณ์"
.สุทธิ | วันที่และเวลาออฟเซ็ต |
ชวา | org.joda.time.DateTime, java.time.ZonedDateTime |
เอ็มเอสเอสแอล | วันที่และเวลาชดเชย |
ออราเคิล, PostgreSQL | ประทับเวลาพร้อมโซนเวลา |
MySQL | — |
การละเมิดเงื่อนไขที่สองถือเป็นกรณีที่ซับซ้อนกว่า หากจำเป็นต้องจัดเก็บเวลา "สัมพัทธ์" นี้ไว้เพื่อแสดงเพียงอย่างเดียว และไม่มีงานใดที่จะระบุช่วงเวลาที่ "สัมบูรณ์" ในเวลาที่เหตุการณ์เกิดขึ้นหรือจะเกิดขึ้นในเขตเวลาที่กำหนด ก็เพียงพอที่จะปิดใช้งานการแปลงเวลาเพียงอย่างเดียว ตัวอย่างเช่น ผู้ใช้เข้าสู่การเริ่มต้นโปรแกรมสำหรับทุกสาขาของบริษัทโทรทัศน์ในวันที่ 25 มีนาคม 2559 เวลา 9:00 น. และรายการจะถูกส่ง จัดเก็บ และแสดงในรูปแบบนี้ แต่อาจเกิดขึ้นที่ตัวกำหนดเวลาบางตัวควรดำเนินการพิเศษโดยอัตโนมัติหนึ่งชั่วโมงก่อนเริ่มแต่ละโปรแกรม (ส่งการแจ้งเตือนหรือตรวจสอบการมีอยู่ของข้อมูลบางอย่างในฐานข้อมูลของบริษัททีวี) การใช้ตัวกำหนดเวลาดังกล่าวอย่างน่าเชื่อถือไม่ใช่เรื่องเล็กน้อย สมมติว่าผู้จัดกำหนดการรู้ว่าแต่ละสาขาอยู่ในเขตเวลาใด และหนึ่งในประเทศที่มีสาขาตัดสินใจเปลี่ยนเขตเวลาภายหลังเวลาผ่านไประยะหนึ่ง กรณีนี้ไม่ได้เกิดขึ้นได้ยากอย่างที่คิด - ตลอดทั้งนี้และเมื่อสองปีก่อน ฉันนับเหตุการณ์ที่คล้ายกันได้มากกว่า 10 เหตุการณ์ (http://www.timeanddate.com/news/time/) ปรากฎว่าผู้ใช้คนใดคนหนึ่งต้องปรับปรุงการเชื่อมโยงเขตเวลาให้ทันสมัยอยู่เสมอ หรือผู้กำหนดตารางเวลาจะต้องนำข้อมูลนี้มาจากแหล่งที่มาทั่วโลก เช่น Google Maps API โซนเวลา ฉันไม่รับปากที่จะเสนอวิธีแก้ปัญหาที่เป็นสากลสำหรับกรณีดังกล่าว ฉันเพียงแต่จะทราบว่าสถานการณ์ดังกล่าวจำเป็นต้องได้รับการศึกษาอย่างจริงจัง
ดังที่เห็นได้จากข้างต้น ไม่มีแนวทางเดียวที่ครอบคลุมคดีความได้ 100%- ดังนั้น อันดับแรกคุณต้องเข้าใจอย่างชัดเจนจากข้อกำหนดว่าสถานการณ์ใดที่กล่าวมาข้างต้นจะเกิดขึ้นในระบบของคุณ เป็นไปได้มากว่าทุกอย่างจะถูกจำกัดอยู่เพียงแนวทางแรกที่เสนอพร้อมพื้นที่จัดเก็บใน UTC สถานการณ์พิเศษที่อธิบายไว้ไม่ได้ยกเลิก แต่เพียงเพิ่มวิธีแก้ไขปัญหาอื่น ๆ สำหรับกรณีพิเศษ
วันที่ไม่มีเวลา
สมมติว่าเราได้จัดเรียงการแสดงวันที่และเวลาที่ถูกต้องโดยคำนึงถึงเขตเวลาของลูกค้า มาดูวันที่ไม่มีเวลาและตัวอย่างที่ให้ไว้สำหรับกรณีนี้ตั้งแต่ต้น - "สัญญาใหม่มีผลบังคับใช้ในวันที่ 2 กุมภาพันธ์ 2559" จะเกิดอะไรขึ้นหากใช้ประเภทเดียวกันและกลไกเดียวกันสำหรับค่าเช่นวันที่และเวลา "ปกติ"?ไม่ใช่ทุกแพลตฟอร์ม ภาษา และ DBMS จะมีประเภทเฉพาะวันที่เท่านั้น ตัวอย่างเช่น ใน .NET มีเพียงประเภท DateTime เท่านั้น ไม่มีประเภท "just Date" แยกต่างหาก แม้ว่าจะระบุเฉพาะวันที่เมื่อสร้างออบเจ็กต์ดังกล่าว แต่เวลายังคงอยู่และเท่ากับ 00:00:00 น. หากเราโอนค่า “2 กุมภาพันธ์ 2559 00:00:00” จากโซนที่มีการชดเชย +2 ถึง +1 เราจะได้รับ “1 กุมภาพันธ์ 2559 23:00:00” สำหรับตัวอย่างข้างต้น จะเทียบเท่ากับสัญญาใหม่ที่เริ่มในวันที่ 2 กุมภาพันธ์ในเขตเวลาหนึ่ง และในวันที่ 1 กุมภาพันธ์ในอีกเขตเวลาหนึ่ง จากมุมมองทางกฎหมาย สิ่งนี้ไร้สาระและแน่นอนว่าไม่ควรเป็นเช่นนั้น กฎทั่วไปสำหรับวันที่ "บริสุทธิ์" นั้นง่ายมาก - ไม่ควรแปลงค่าดังกล่าวในขั้นตอนการบันทึกและการอ่านใด ๆ
มีหลายวิธีในการหลีกเลี่ยงการแปลงวันที่:
- หากแพลตฟอร์มรองรับประเภทที่แสดงวันที่โดยไม่มีเวลา ก็ควรใช้สิ่งนั้น
- เพิ่มแอตทริบิวต์พิเศษให้กับข้อมูลเมตาของออบเจ็กต์ที่จะบอกซีเรียลไลเซอร์ว่าควรละเว้นเขตเวลาสำหรับค่าที่กำหนด
- ส่งผ่านวันที่จากไคลเอนต์และย้อนกลับเป็นสตริง และจัดเก็บเป็นวันที่ วิธีการนี้ไม่สะดวกหากคุณไม่เพียงแต่ต้องแสดงวันที่บนไคลเอนต์เท่านั้น แต่ยังต้องดำเนินการบางอย่างด้วย: การเปรียบเทียบการลบ ฯลฯ
- ส่งผ่านและจัดเก็บเป็นสตริง และแปลงเป็นวันที่สำหรับการจัดรูปแบบตามการตั้งค่าภูมิภาคของไคลเอ็นต์เท่านั้น มันมีข้อเสียมากกว่าตัวเลือกก่อนหน้า - ตัวอย่างเช่นหากส่วนของวันที่ในสตริงที่เก็บไว้ไม่อยู่ในลำดับ "ปี, เดือน, วัน" ก็จะเป็นไปไม่ได้ที่จะทำการค้นหาที่จัดทำดัชนีอย่างมีประสิทธิภาพตามช่วงวันที่
ช่วงเวลา
ด้วยการจัดเก็บและประมวลผลช่วงเวลา ทุกอย่างจึงง่ายดาย: ค่าของพวกมันไม่ได้ขึ้นอยู่กับเขตเวลา ดังนั้นจึงไม่มีคำแนะนำพิเศษที่นี่ สามารถจัดเก็บและส่งผ่านเป็นหน่วยเวลาได้ (จำนวนเต็มหรือจุดลอยตัว ขึ้นอยู่กับความแม่นยำที่ต้องการ) หากความแม่นยำอันดับสองมีความสำคัญ จะเป็นจำนวนวินาที หากความแม่นยำระดับมิลลิวินาทีมีความสำคัญ จะเป็นจำนวนมิลลิวินาที เป็นต้นแต่การคำนวณช่วงเวลาอาจมีข้อผิดพลาดได้ สมมติว่าเรามีตัวอย่างโค้ด C# ที่คำนวณช่วงเวลาระหว่างสองเหตุการณ์:
DateTime start = DateTime.Now; //... DateTime end = DateTime.Now; สองชั่วโมง = (สิ้นสุด - เริ่ม).TotalHours;
เมื่อมองแวบแรกไม่มีปัญหาที่นี่ แต่ก็ไม่เป็นเช่นนั้น ประการแรก อาจมีปัญหากับการทดสอบโค้ดดังกล่าวของหน่วย แต่เราจะพูดถึงเรื่องนี้ในภายหลัง ประการที่สอง ลองจินตนาการว่าช่วงเวลาแรกตรงกับช่วงฤดูหนาว และช่วงเวลาสุดท้ายตรงกับช่วงฤดูร้อน (เช่น นี่คือวิธีการวัดจำนวนชั่วโมงทำงาน และคนงานมีกะกลางคืน)
สมมติว่าโค้ดกำลังทำงานในเขตเวลาที่เวลาออมแสงในปี 2559 เกิดขึ้นในคืนวันที่ 27 มีนาคม และจำลองสถานการณ์ที่อธิบายไว้ข้างต้น:
วันที่และเวลาเริ่มต้น = DateTime.Parse("2016-03-26T20:00:15+02"); สิ้นสุด DateTime = DateTime.Parse("2016-03-27T05:00:15+03"); สองชั่วโมง = (สิ้นสุด - เริ่ม).TotalHours;
รหัสนี้จะส่งผลให้เป็นเวลา 9 ชั่วโมง แม้ว่าในความเป็นจริงจะผ่านไปแล้ว 8 ชั่วโมงระหว่างช่วงเวลาเหล่านี้ก็ตาม คุณสามารถตรวจสอบได้อย่างง่ายดายโดยเปลี่ยนรหัสดังนี้:
DateTime start = DateTime.Parse("2016-03-26T20:00:15+02").ToUniversalTime(); สิ้นสุด DateTime = DateTime.Parse("2016-03-27T05:00:15+03").ToUniversalTime(); สองชั่วโมง = (สิ้นสุด - เริ่ม).TotalHours;
ดังนั้นข้อสรุป - การดำเนินการทางคณิตศาสตร์ใด ๆ ที่มีวันที่และเวลาจะต้องดำเนินการโดยใช้ค่า UTC หรือประเภทที่เก็บข้อมูลโซนเวลา แล้วโอนกลับไปยังท้องถิ่นหากจำเป็น จากมุมมองนี้ ตัวอย่างดั้งเดิมสามารถแก้ไขได้ง่ายโดยการเปลี่ยน DateTime.Now เป็น DateTime.UtcNow
ความแตกต่างนี้ไม่ได้ขึ้นอยู่กับแพลตฟอร์มหรือภาษาเฉพาะ นี่คือโค้ดที่คล้ายกันใน Java ที่มีปัญหาเดียวกัน:
เริ่มต้น LocalDateTime = LocalDateTime.now(); //... สิ้นสุด LocalDateTime = LocalDateTime.now(); ชั่วโมงที่ยาวนาน = ChronoUnit.HOURS.between (เริ่มต้น, สิ้นสุด);
นอกจากนี้ยังสามารถแก้ไขได้อย่างง่ายดาย เช่น โดยใช้ ZonedDateTime แทน LocalDateTime
กำหนดการของเหตุการณ์ที่กำหนดไว้
การจัดกำหนดการเหตุการณ์ที่กำหนดเวลาไว้เป็นสถานการณ์ที่ซับซ้อนมากขึ้น ไม่มีประเภทสากลที่ให้คุณจัดเก็บตารางเวลาในไลบรารีมาตรฐานได้ แต่งานดังกล่าวไม่ได้เกิดขึ้นน้อยมากดังนั้นจึงสามารถหาวิธีแก้ปัญหาสำเร็จรูปได้โดยไม่มีปัญหา ตัวอย่างที่ดีคือรูปแบบตัวกำหนดเวลา cron ซึ่งใช้ในรูปแบบใดรูปแบบหนึ่งโดยโซลูชันอื่น เช่น Quartz: http://quartz-scheduler.org/api/2.2.0/org/quartz/CronExpression.html ครอบคลุมความต้องการด้านกำหนดการเกือบทั้งหมด รวมถึงตัวเลือกต่างๆ เช่น “วันศุกร์ที่สองของเดือน”ในกรณีส่วนใหญ่ การเขียนตัวกำหนดตารางเวลาของคุณเองไม่สมเหตุสมผล เนื่องจากมีโซลูชันที่ยืดหยุ่นและผ่านการทดสอบตามเวลา แต่หากจำเป็นต้องสร้างกลไกของคุณเองด้วยเหตุผลบางประการ อย่างน้อยก็สามารถยืมรูปแบบกำหนดการได้ จากครอน
นอกจากคำแนะนำที่อธิบายไว้ข้างต้นเกี่ยวกับการจัดเก็บและการประมวลผลค่าเวลาประเภทต่างๆ แล้ว ยังมีคำแนะนำอื่นๆ อีกหลายประการที่ฉันอยากจะกล่าวถึงด้วยประการแรก เกี่ยวกับการใช้สมาชิกคลาสคงที่เพื่อรับเวลาปัจจุบัน - DateTime.UtcNow, ZonedDateTime.now() ฯลฯ ดังที่ได้กล่าวไปแล้ว การใช้โดยตรงในโค้ดอาจทำให้การทดสอบหน่วยมีความซับซ้อนอย่างมาก เนื่องจากหากไม่มีเฟรมเวิร์กการเยาะเย้ยพิเศษ จะไม่สามารถแทนที่เวลาปัจจุบันได้ ดังนั้น หากคุณวางแผนที่จะเขียนการทดสอบหน่วย คุณควรตรวจสอบให้แน่ใจว่าสามารถนำวิธีการดังกล่าวไปใช้แทนได้ มีอย่างน้อยสองวิธีในการแก้ปัญหานี้:
- จัดเตรียมอินเทอร์เฟซ IDateTimeProvider ด้วยวิธีการเดียวที่ส่งคืนเวลาปัจจุบัน จากนั้นเพิ่มการพึ่งพาอินเทอร์เฟซนี้ในหน่วยโค้ดทั้งหมดที่คุณต้องการรับเวลาปัจจุบัน ในระหว่างการทำงานของโปรแกรมปกติ การใช้งาน "ค่าเริ่มต้น" จะถูกฉีดเข้าไปในตำแหน่งเหล่านี้ทั้งหมด ซึ่งจะส่งคืนเวลาปัจจุบันจริง และในการทดสอบหน่วย - การใช้งานอื่น ๆ ที่จำเป็น วิธีการนี้มีความยืดหยุ่นมากที่สุดจากมุมมองของการทดสอบ
- สร้างคลาสแบบคงที่ของคุณเองด้วยวิธีการรับเวลาปัจจุบันและความสามารถในการติดตั้งการใช้งานวิธีนี้จากภายนอก ตัวอย่างเช่น ในกรณีของโค้ด C# คลาสนี้สามารถเปิดเผยคุณสมบัติ UtcNow และเมธอด SetImplementation(Func)
นัย) การใช้คุณสมบัติคงที่หรือวิธีการรับเวลาปัจจุบันไม่จำเป็นต้องระบุการพึ่งพาอินเทอร์เฟซเพิ่มเติมอย่างชัดเจนทุกที่ แต่จากมุมมองของหลักการ OOP มันไม่ใช่วิธีแก้ปัญหาในอุดมคติ อย่างไรก็ตามหากตัวเลือกก่อนหน้านี้ไม่เหมาะสมด้วยเหตุผลบางประการ คุณสามารถใช้ตัวเลือกนี้ได้
ข้อแม้ประการที่สองในการรับเวลาปัจจุบันก็คือ ลูกค้าไม่สามารถเชื่อถือได้- เวลาปัจจุบันบนคอมพิวเตอร์ของผู้ใช้อาจแตกต่างจากเวลาจริงมากและหากมีตรรกะเชื่อมโยงอยู่ ความแตกต่างนี้สามารถทำลายทุกสิ่งได้ ทุกสถานที่ที่มีความจำเป็นต้องได้รับเวลาปัจจุบัน ถ้าเป็นไปได้ ควรทำบนฝั่งเซิร์ฟเวอร์ และตามที่กล่าวไว้ข้างต้น การดำเนินการทางคณิตศาสตร์ทั้งหมดตามเวลาจะต้องดำเนินการในค่า UTC หรือใช้ประเภทที่เก็บออฟเซ็ตโซนเวลา
และอีกอย่างที่ผมอยากพูดถึงก็คือมาตรฐาน ISO 8601 ซึ่งอธิบายรูปแบบวันที่และเวลาในการแลกเปลี่ยนข้อมูล โดยเฉพาะอย่างยิ่ง การแสดงสตริงของวันที่และเวลาที่ใช้ในการทำให้เป็นอนุกรมจะต้องเป็นไปตามมาตรฐานนี้ เพื่อป้องกันปัญหาความเข้ากันได้ที่อาจเกิดขึ้น ในทางปฏิบัติ เป็นเรื่องยากมากที่คุณจะต้องใช้การจัดรูปแบบด้วยตนเอง ดังนั้นมาตรฐานจึงมีประโยชน์เพื่อวัตถุประสงค์ในการให้ข้อมูลเป็นหลัก
แท็ก: เพิ่มแท็ก
ตัวเลือกที่มีมนุษยธรรม ([คุณต้องลงทะเบียนเพื่อดูลิงก์])
สาระสำคัญของปัญหาคือสิ่งนี้.. หากคุณปรับใช้ฐานข้อมูลบนเซิร์ฟเวอร์ SQL โดยไม่ได้ตั้งใจด้วย "การชดเชยวันที่" เป็น 0 แสดงว่าปัญหาเกิดขึ้นเมื่อฐานข้อมูลมีแอตทริบิวต์ที่มีประเภท TIME เช่น คุณลักษณะนี้ตั้งค่าเป็น 01 /01/0001 10:30:00 หรือวันที่ลงทะเบียนว่างเปล่า 01.01.0001 00:00:00 เมื่อบันทึกรายละเอียดดังกล่าวจะไม่ถูกบันทึก
บนอินเทอร์เน็ตพวกเขาแนะนำให้สร้างฐานข้อมูลใหม่โดยมีออฟเซ็ตเป็น 2,000
แต่ฉันไม่อยากสร้างฐานใหม่จริงๆ และเปลี่ยนเส้นทางไปยังฐานข้อมูลสำหรับผู้ใช้ทั้งหมด
จากนั้นฉันก็ติดตามเส้นทางที่ค่านี้เก็บไว้ใน SQL ฉันเจอมันและเปลี่ยนเป็นปี 2000 และทุกอย่างก็โอเค..
และตอนนี้ทีละขั้นตอนว่าจะเปลี่ยนแปลงที่ไหน
เตะผู้ใช้ทั้งหมด
ความสนใจ!!!
1. ทำก่อน สำเนาสำรองโดยวิธี 1C เช่น อัพโหลดไปที่ *.dt
ต้องทำก่อนที่จะเปลี่ยน “ออฟเซ็ต”
หากยังไม่เสร็จสิ้น จะมีข้อมูลอ้างอิง เอกสาร ฯลฯ ในฐานข้อมูลทั้งหมดของคุณ
มีความจำเป็นตรงไหน วันที่จะเป็น 02.10.0009
สิ่งที่ไม่ได้รับอนุญาต...
ดังนั้นคุณได้อัปโหลดไปที่ *.dt
2. ไปที่ Studio จัดการเซิร์ฟเวอร์ SQL
ค้นหาฐานของคุณในรายการแล้วกดเครื่องหมายบวก
ค้นหาโฟลเดอร์ "Tables" ที่นั่นแล้วเปิดขึ้นมา
โต๊ะจำนวนมากจะเปิดขึ้น ไปที่ด้านล่างสุดแล้วค้นหาโต๊ะ
_YearOffset ยืนบนนั้นแล้วเลือกรายการ "เปิดตาราง" ด้วยปุ่มขวา ดูรูปที่ 1
เปลี่ยนค่า 0 เป็น 2000
ปิด Studio จัดการเซิร์ฟเวอร์ SQL
3. ไปที่ตัวกำหนดค่าและโหลดฐานข้อมูลที่บันทึกไว้ก่อนหน้านี้
หากยังไม่เสร็จสิ้น วันที่ทั้งหมดจะเป็นปี 0009
หลังจากโหลดฐานข้อมูลแล้ว... คุณสามารถไปที่ 1C และตรวจสอบให้แน่ใจว่าวันที่เป็นปกติ
ผลลัพธ์คือเราเปลี่ยน "การชดเชยวันที่จาก 0 เป็น 2000"
บางครั้งมันเกิดขึ้นว่าตัวเลือกนี้ไม่สามารถใช้ได้ด้วยเหตุผลใดก็ตาม มีตัวเลือกที่ฮาร์ดคอร์กว่านี้ ([คุณต้องลงทะเบียนเพื่อดูลิงก์]):
ประกาศเคอร์เซอร์ TablesAndFields สำหรับ
เลือก object.name เป็น Tablename, columns.name เป็น columnname
จาก dbo.sysobjects เช่นวัตถุ
ปล่อยให้เข้าร่วม dbo.syscolumns เป็นคอลัมน์บน object.id = columns.id
โดยที่ object.xtype = "U" และ columns.xtype = 61
เปิด TablesAndFields
ในขณะที่ @@FETCH_STATUS = 0
เริ่มต้นดำเนินการ (" อัปเดต"+ @ชื่อตาราง + "
ชุด " + @ชื่อคอลัมน์ + " =
""2000-
01-
01 00:00:00"
"
ที่ไหน " + @ชื่อคอลัมน์ + " >
""3999-
12-
31 23:59:59"
"")
สิ่งนี้จะถูกดำเนินการตราบเท่าที่การดึงข้อมูลครั้งก่อนสำเร็จ
ดึงข้อมูลถัดไปจาก TablesAndFields ลงใน @TableName, @ColumnName
จบ
ปิด TablesAndFields
ยกเลิกการจัดสรรTablesAndFields
ไป
ก่อนดำเนินการใด ๆ อย่าลืมทำสำเนาฐานข้อมูล!
DateTimeOffset testDateAndTime = DateTimeOffset ใหม่ (2008, 5, 1, 8, 6, 32, TimeSpan ใหม่ (1, 0, 0)); // เวลาและวันที่สะอาด testDateAndTime = testDateAndTime.DateTime.Date; var dateTableEntry = db.DatesTable.First(dt => dt.Id == someTestId); DateTableEntry.test= testDateAndTime; db.SaveChangesAsync();
ผลลัพธ์ในฐานข้อมูล: 2008-05-01 00:00:00.0000000 -04:00
วิธีเปิดใช้งาน -4:00 ถึง +00:00 (จากโค้ดก่อนบันทึก)
ฉันเหนื่อย:
งานสาธารณะ
เขาไม่ทำอะไรเลย
เป้าหมายสุดท้ายคือการมีวันที่โดยไม่มีการชดเชยเวลาหรือเขตเวลา ฉันไม่ต้องการแปลงเวลาเป็นเขตเวลาอื่น (เช่น 00:00:00.0000000 ฉันไม่ต้องการให้ลบ 4 ชั่วโมงจากเวลา 00:00:00.0000000 และ 00:00:00.0000000 ชดเชยเวลาที่ตั้งไว้ด้วย +00 :00 ฉันแค่อยากให้มันตั้งค่าออฟเซ็ตเป็น +00:00) ฉันต้องการวันที่ปัจจุบันที่มีการชดเชยเป็นศูนย์
แก้ไข:
นี่คือสิ่งที่จะนำเสนอในที่อื่น:
DateTimeOffset testDateAndTime = DateTimeOffset ใหม่ (2008, 5, 1, 8, 6, 32, TimeSpan ใหม่ (1, 0, 0)); testDateAndTime = testDateAndTime.DateTime.Date; // ส่วนเวลาเป็นศูนย์ testDateAndTime = DateTime.SpecifyKind (testDateAndTime.Date, DateTimeKind.Utc); // ส่วนออฟเซ็ต "Zero out"
ฉันแน่ใจว่า SpecifyKind จะ SpecifyKind dateTimeOffset ของฉัน เช่น เปลี่ยนทั้งการชดเชยเวลาและเขตเวลา แต่เมื่อทดสอบแล้ว ดูเหมือนว่าจะเปลี่ยนการชดเชยเขตเวลา ซึ่งเป็นสิ่งที่ฉันต้องการ มีปัญหากับเรื่องนี้หรือไม่?
1 คำตอบ
ปัญหาไม่เกี่ยวข้องกับฐานข้อมูล หากคุณตั้งค่าเบรกพอยต์หรือลงทะเบียนเอาต์พุตไว้ที่ไหนสักแห่ง คุณจะเห็นออฟเซ็ตถูกผูกไว้หลังจากโค้ดนี้ไม่นาน:
TestDateAndTime = testDateAndTime.DateTime.Date;
มาทำลายมันกัน:
- คุณเริ่มต้นด้วย DateTimeOffset 2008-05-01T08:06:32+01:00
- จากนั้นคุณเรียกว่า .DateTime ส่งผลให้ค่า DateTime เป็น 2008-05-01T08:06:32 พร้อมด้วย DateTimeKind.Unspecified
- จากนั้นคุณเรียกว่า .Date ซึ่งส่งผลให้ค่า DateTime เป็น 2008-05-01T00:00:00 พร้อมด้วย DateTimeKind.Unspecified
- คุณส่งคืนผลลัพธ์ใน testDateAndTime ซึ่งเป็นประเภท DateTimeOffset ซึ่งทำให้มีการแปลงโดยนัยจาก DateTime เป็น DateTimeOffset ซึ่งใช้เขตเวลาท้องถิ่น- ในกรณีของคุณ ปรากฏว่าการชดเชยสำหรับค่านี้ในเขตเวลาท้องถิ่นของคุณคือ -04:00 ดังนั้นค่าผลลัพธ์คือ DateTimeOffset 2008-05-01T00:00:00-04:00 ตามที่คุณอธิบาย
คุณพูดว่า:
เป้าหมายสุดท้ายคือการมีวันที่โดยไม่มีการชดเชยเวลาหรือเขตเวลา
ขณะนี้ไม่มีประเภทข้อมูล C# ดั้งเดิมที่เป็นเพียงวันที่ที่ไม่มีเวลา มีประเภท Date ล้วนๆ ในแพ็คเกจ System.Time ใน corefxlab แต่ยังไม่พร้อมสำหรับแอปพลิเคชันการผลิตทั่วไป มี LocalDate ในไลบรารีเวลาของ Noda ซึ่งคุณสามารถใช้ได้ในปัจจุบัน แต่คุณยังคงต้องแปลงกลับเป็นประเภทเนทิฟก่อนที่จะจัดเก็บไว้ในฐานข้อมูล ดังนั้นในระหว่างนี้ สิ่งที่ดีที่สุดที่คุณสามารถทำได้คือ:
- เปลี่ยน SQL Server ของคุณเพื่อใช้ประเภทวันที่ในฟิลด์นี้
- ในโค้ด .NET ของคุณ ให้ใช้ DateTime พร้อมเวลา 00:00:00 และ DateTimeKind.Unspecified คุณต้องจำไว้ว่าอย่าเพิกเฉยต่อส่วนของเวลา (เนื่องจากมีวันที่ที่ไม่มีเที่ยงคืนท้องถิ่นในบางโซนเวลา)
- เปลี่ยนการแจ้งเตือนการทดสอบให้เป็น DateTime แทนที่จะเป็น DateTimeOffset
โดยทั่วไปแล้ว DateTimeOffset จะเหมาะสำหรับ ปริมาณมากสถานการณ์ (เช่น เหตุการณ์การประทับเวลา) ซึ่งไม่เหมาะกับค่าเฉพาะวันที่เท่านั้น
ฉันต้องการวันที่ปัจจุบันที่มีการชดเชยเป็นศูนย์
หากคุณต้องการเหมือน DateTimeOffset จริงๆ คุณจะต้องทำ:
TestDateAndTime = DateTimeOffset ใหม่ (testDateAndTime.Date, TimeSpan.Zero);
อย่างไรก็ตาม ฉันไม่แนะนำให้ทำเช่นนี้ การทำเช่นนี้แสดงว่าคุณกำลังใช้วันที่ท้องถิ่นของค่าดั้งเดิมและยืนยันว่าอยู่ใน UTC หากออฟเซ็ตเดิมเป็นอย่างอื่นที่ไม่ใช่ศูนย์ นี่จะเป็นข้อความเท็จ มันจะนำไปสู่ข้อผิดพลาดอื่นๆ ในภายหลัง เนื่องจากคุณกำลังพูดถึงช่วงเวลาที่แตกต่างกัน (ซึ่งอาจเป็นวันที่ที่แตกต่างออกไป) มากกว่าที่คุณสร้างขึ้น
เกี่ยวกับคำถามเพิ่มเติมที่ถามในการแก้ไขของคุณ - การระบุ DateTimeKind.Utc จะเปลี่ยนพฤติกรรมของการส่งโดยนัย แทนที่จะใช้เขตเวลาท้องถิ่น ระบบจะใช้เวลา UTC ซึ่งมีออฟเซ็ตเป็นศูนย์เสมอ ผลลัพธ์จะเหมือนกับมุมมองที่ชัดเจนยิ่งขึ้นที่ฉันให้ไว้ข้างต้น ฉันยังคงแนะนำสิ่งนี้ด้วยเหตุผลเดียวกัน
มาดูตัวอย่างการเริ่มต้นตั้งแต่ 2016-12-31T22:00:00-04:00. ตามแนวทางของคุณ คุณควรจัดเก็บ 2016-12-31T00:00:00+00:00 ไว้ในฐานข้อมูล อย่างไรก็ตาม นี่เป็นสองจุดที่แตกต่างกันของเวลา ครั้งแรกที่ปรับมาตรฐานเป็น UTC จะเป็น 2017-01-01T02:00:00+00:00 และครั้งที่สองซึ่งแปลงเป็นเขตเวลาอื่นจะเป็น 2016-12-30T20:00:00-04:00 โปรดทราบการเปลี่ยนแปลงวันที่ในการแปลง นี่อาจไม่ใช่พฤติกรรมที่คุณต้องการในแอปพลิเคชันของคุณ