Cách đây một thời gian, chúng tôi bắt đầu thích ứng hệ thống với thị trường mới yêu cầu hỗ trợ múi giờ. Nghiên cứu ban đầu đã được mô tả trong bài báo trước. Bây giờ cách tiếp cận đã phát triển một chút dưới ảnh hưởng của thực tế. Bài viết này mô tả các vấn đề gặp phải trong quá trình thảo luận và quyết định cuối cùng được thực hiện.
TL; DR
- Cần phân biệt các thuật ngữ:
- UTC là giờ địa phương trong khu vực +00:00, không có hiệu ứng DST
- DateTimeOffset - độ lệch giờ địa phương từ UTC ± NN:NN, trong đó offset là độ lệch cơ sở từ UTC mà không có hiệu ứng DST (trong C # TimeZoneInfo.BaseUtcOffset)
- DateTime - giờ địa phương không có thông tin về múi giờ (chúng tôi bỏ qua thuộc tính Kind)
- Phân chia việc sử dụng thành bên ngoài và bên trong:
- Dữ liệu đầu vào và đầu ra thông qua API, tin nhắn, xuất / nhập tệp phải hoàn toàn ở UTC (kiểu DateTime)
- Bên trong hệ thống, dữ liệu được lưu trữ cùng với phần bù (kiểu DateTimeOffset)
- Tách việc sử dụng trong mã cũ thành mã không phải DB (C #, JS) và DB:
- Mã không phải DB chỉ hoạt động với các giá trị cục bộ (kiểu DateTime)
- Cơ sở dữ liệu hoạt động với các giá trị cục bộ + bù đắp (kiểu DateTimeOffset)
- Các dự án (thành phần) mới sử dụng DateTimeOffset.
- Trong cơ sở dữ liệu, kiểu DateTime chỉ đơn giản là thay đổi thành DateTimeOffset:
- Trong các loại trường bảng
- Trong các tham số của các thủ tục được lưu trữ
- Các cấu trúc không tương thích được sửa trong mã
- Thông tin bù đắp được đính kèm với một giá trị đã nhận (nối đơn giản)
- Trước khi quay lại mã không phải DB, giá trị được chuyển đổi thành cục bộ
- Không có thay đổi nào đối với mã không phải DB
- DST được giải quyết bằng cách sử dụng Thủ tục được lưu trữ CLR (đối với SQL Server 2016, bạn có thể sử dụng AT TIME ZONE).
Bây giờ, chi tiết hơn về những khó khăn đã vượt qua.
Các tiêu chuẩn "bắt nguồn từ sâu sắc" của ngành CNTT
Phải mất khá nhiều thời gian để mọi người bớt lo sợ về việc lưu trữ ngày tháng theo giờ địa phương với bù đắp. Cách đây không lâu, nếu bạn hỏi một lập trình viên có kinh nghiệm:"Làm thế nào để hỗ trợ múi giờ?" - lựa chọn duy nhất là:“Sử dụng UTC và chuyển đổi sang giờ địa phương ngay trước khi trình diễn”. Thực tế là đối với quy trình làm việc bình thường, bạn vẫn cần thông tin bổ sung, chẳng hạn như tên bù và múi giờ, đã được ẩn dưới phần triển khai. Với sự ra đời của DateTimeOffset, các chi tiết như vậy đã xuất hiện, nhưng quán tính của “trải nghiệm lập trình” không cho phép nhanh chóng đồng ý với một thực tế khác:“Lưu trữ ngày cục bộ với độ lệch UTC cơ bản” cũng giống như lưu trữ UTC. Một ưu điểm khác của việc sử dụng DateTimeOffset ở mọi nơi cho phép bạn ủy quyền kiểm soát việc tuân theo các múi giờ .NET Framework và SQL Server, chỉ để lại cho con người kiểm soát những khoảnh khắc nhập và xuất dữ liệu từ hệ thống. Kiểm soát của con người là mã do lập trình viên viết ra để làm việc với các giá trị ngày / giờ.
Để vượt qua nỗi sợ hãi này, tôi đã phải tổ chức nhiều hơn một buổi với các giải thích, trình bày ví dụ và Chứng minh khái niệm. Các ví dụ càng đơn giản và gần gũi với những nhiệm vụ được giải quyết trong dự án thì càng tốt. Nếu bạn bắt đầu thảo luận “nói chung”, điều này dẫn đến sự phức tạp trong việc hiểu và lãng phí thời gian. Tóm lại:lý thuyết ít hơn - thực hành nhiều hơn. Các đối số cho UTC và chống lại DateTimeOffset có thể liên quan đến hai danh mục:
- “UTC mọi lúc” là tiêu chuẩn và phần còn lại không hoạt động
- UTC giải quyết vấn đề với DST
Cần lưu ý rằng cả UTC và DateTimeOffset đều không giải quyết được vấn đề với DST mà không sử dụng thông tin về các quy tắc chuyển đổi giữa các vùng, có sẵn thông qua lớp TimeZoneInfo trong C #.
Mô hình đơn giản hóa
Như tôi đã lưu ý ở trên, trong mã cũ, các thay đổi chỉ xảy ra trong cơ sở dữ liệu. Điều này có thể được đánh giá bằng một ví dụ đơn giản.
Ví dụ về mô hình trong T-SQL
// 1) data storage // input data in the user's locale, as he sees them declare @input_user1 datetime = '2017-10-27 10:00:00' // there is information about the zone in the user configuration declare @timezoneOffset_user1 varchar(10) = '+03:00' declare @storedValue datetimeoffset // upon receiving values, attach the user’s offset set @storedValue = TODATETIMEOFFSET(@input_user1, @timezoneOffset_user1) // this value will be saved select @storedValue 'stored' // 2) display of information // a different time zone is specified in the second user’s configuration, declare @timezoneOffset_user2 varchar(10) = '-05:00' // before returning to the client code, values are reduced to local ones // this is how the data will look like in the database and on users’ displays select @storedValue 'stored value', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user1)) 'user1 Moscow', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user2)) 'user2 NY' // 3) now the second user saves the data declare @input_user2 datetime // input local values are received, as the user sees them in New York set @input_user2 = '2017-10-27 02:00:00.000' // link to the offset information set @storedValue = TODATETIMEOFFSET(@input_user2, @timezoneOffset_user2) select @storedValue 'stored' // 4) display of information select @storedValue 'stored value', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user1)) 'user1 Moscow', CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user2)) 'user2 NY'
Kết quả của việc thực thi tập lệnh sẽ như sau.
Ví dụ cho thấy rằng mô hình này chỉ cho phép thực hiện các thay đổi trong cơ sở dữ liệu, điều này làm giảm đáng kể nguy cơ lỗi.
Ví dụ về các hàm để xử lý giá trị ngày / giờ
// When receiving values from the non-DB code in DateTimeOffset, they will be local, // but with offset +00:00, so you must attach a user’s offset, but you cannot convert between // time zones. To do this, we translate the value into DateTime and then back with the indication of the offset // DateTime is converted to DateTimeOffset without problems, // so you do not need to change the call of the stored procedures in the client code create function fn_ConcatinateWithTimeOffset(@dto datetimeoffset, @userId int) returns DateTimeOffset as begin declare @user_time_zone varchar(10) set @user_time_zone = '-05:00' // from the user's settings @userId return todatetimeoffset(convert(datetime, @dto), @user_time_zone) end // Client code cannot read DateTimeOffset into variables of the DateTime type, // so you need to not only convert to a correct time zone but also reduce to DateTime, // otherwise, there will be an error create function fn_GetUserDateTime(@dto datetimeoffset, @userId int) returns DateTime as begin declare @user_time_zone varchar(10) set @user_time_zone = '-05:00' // from the user's settings @userId return convert(datetime, switchoffset(@dto, @user_time_zone)) end
Đồ tạo tác nhỏ
Trong quá trình điều chỉnh mã SQL, một số điều đã được tìm thấy hoạt động cho DateTime, nhưng không tương thích với DateTimeOffset:
GETDATE () + 1 phải được thay thế bằng DATEADD (ngày, 1, SYSDATETIMEOFFSET ())
Từ khóa DEFAULT không tương thích với DateTimeOffset, bạn cần sử dụng SYSDATETIMEOFFSET ()
Cấu trúc ISNULL (date_field, NULL)> 0 ″ hoạt động với DateTime, nhưng DateTimeOffset nên được thay thế bằng “date_field IS NOT NULL”
Kết luận hoặc UTC so với DateTimeOffset
Ai đó có thể nhận thấy rằng, như trong cách tiếp cận với UTC, chúng tôi xử lý việc chuyển đổi khi nhận và trả lại dữ liệu. Vậy tại sao chúng ta cần tất cả những điều này, nếu có một giải pháp đã được thử nghiệm và hoạt động tốt? Có một số lý do cho điều này:
- DateTimeOffset cho phép bạn quên vị trí của SQL Server.
- Điều này cho phép bạn chuyển một phần công việc sang hệ thống.
- Việc chuyển đổi có thể được giảm thiểu nếu DateTimeOffset được sử dụng ở mọi nơi, chỉ thực hiện nó trước khi hiển thị dữ liệu hoặc xuất nó ra các hệ thống bên ngoài.
Đối với tôi, những lý do này là cơ bản do sử dụng phương pháp này.
Tôi rất sẵn lòng trả lời câu hỏi của bạn, vui lòng viết bình luận.