Cách lưu trữ ngày / giờ và dấu thời gian trong múi giờ UTC với JPA và Hibernate


106

Làm cách nào để định cấu hình JPA / Hibernate để lưu trữ ngày / giờ trong cơ sở dữ liệu dưới dạng múi giờ UTC (GMT)? Hãy xem xét thực thể JPA được chú thích này:

public class Event {
    @Id
    public int id;

    @Temporal(TemporalType.TIMESTAMP)
    public java.util.Date date;
}

Nếu ngày là năm 2008-03-03, 9:30 sáng Giờ chuẩn Thái Bình Dương (PST), thì tôi muốn giờ UTC của năm 2008-02-03 5:30 chiều được lưu trữ trong cơ sở dữ liệu. Tương tự như vậy, khi ngày được truy xuất từ ​​cơ sở dữ liệu, tôi muốn nó được hiểu là UTC. Vì vậy, trong trường hợp này, 530 giờ tối là 530 giờ chiều UTC. Khi nó được hiển thị, nó sẽ được định dạng là 9:30 sáng theo giờ PST.


1
Câu trả lời của Vlad Mihalcea cung cấp câu trả lời cập nhật (cho Hibernate
5.2+

Câu trả lời:


73

Với Hibernate 5.2, giờ đây bạn có thể buộc múi giờ UTC bằng thuộc tính cấu hình sau:

<property name="hibernate.jdbc.time_zone" value="UTC"/>

Để biết thêm chi tiết, hãy xem bài viết này .


19
Tôi cũng đã viết cái đó: D Bây giờ, hãy đoán xem ai đã thêm hỗ trợ cho tính năng này trong Hibernate?
Vlad Mihalcea

Ồ, vừa rồi tôi nhận ra tên và ảnh giống nhau trong hồ sơ của bạn và những bài báo đó ... Làm tốt lắm Vlad :)
Dinei

@VladMihalcea nếu là Mysql, người ta cần yêu cầu MySql sử dụng múi giờ bằng cách sử dụng useTimezone=truetrong chuỗi kết nối. Sau đó, chỉ thiết lập thuộc tính hibernate.jdbc.time_zonesẽ làm việc
TheCoder

Trên thực tế, bạn cần phải thiết lập useLegacyDatetimeCodelà false
Vlad Mihalcea

2
hibernate.jdbc.time_zone dường như bị bỏ qua hoặc không có tác dụng khi được sử dụng với PostgreSQL
Alex R

48

Theo hiểu biết của tôi, bạn cần đặt toàn bộ ứng dụng Java của mình vào múi giờ UTC (để Hibernate sẽ lưu trữ ngày tháng theo UTC) và bạn sẽ cần chuyển đổi sang bất kỳ múi giờ nào mong muốn khi hiển thị nội dung (ít nhất là chúng tôi làm điều đó cách này).

Khi khởi động, chúng tôi thực hiện:

TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));

Và đặt múi giờ mong muốn thành DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))

5
mitchnull, giải pháp của bạn sẽ không hoạt động trong mọi trường hợp vì các đại biểu Hibernate đặt ngày cho trình điều khiển JDBC và mỗi trình điều khiển JDBC xử lý ngày và múi giờ khác nhau. xem stackoverflow.com/questions/4123534/… .
Derek Mahar

2
Nhưng nếu tôi khởi động ứng dụng của mình thông báo tới thuộc tính JVM "-Duser.timezone = + 00: 00", thì không phải là hành vi tương tự?
rafa.ferreira

8
Theo như tôi có thể nói, nó sẽ hoạt động trong mọi trường hợp ngoại trừ khi JVM và máy chủ cơ sở dữ liệu ở các múi giờ khác nhau.
Shane

stevekuo và @mitchnull xem giải pháp divestoclimb bên dưới tốt hơn nhiều
ngăn chặn

Do HibernateJPA hỗ trợ chú thích "@Factory" và "@Externalizer", đó là cách tôi thực hiện xử lý datetime utc trong thư viện OpenJPA. stackoverflow.com/questions/10819862/…
Whome,

44

Hibernate không biết gì về múi giờ trong Dates (vì không có bất kỳ thứ gì), nhưng nó thực sự là lớp JDBC gây ra vấn đề. ResultSet.getTimestampPreparedStatement.setTimestampcả hai đều nói trong tài liệu của họ rằng họ chuyển đổi ngày thành / từ múi giờ JVM hiện tại theo mặc định khi đọc và ghi từ / vào cơ sở dữ liệu.

Tôi đã đưa ra giải pháp cho vấn đề này trong Hibernate 3.5 bằng cách phân lớp con org.hibernate.type.TimestampTypebuộc các phương thức JDBC này phải sử dụng UTC thay vì múi giờ địa phương:

public class UtcTimestampType extends TimestampType {

    private static final long serialVersionUID = 8088663383676984635L;

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    @Override
    public Object get(ResultSet rs, String name) throws SQLException {
        return rs.getTimestamp(name, Calendar.getInstance(UTC));
    }

    @Override
    public void set(PreparedStatement st, Object value, int index) throws SQLException {
        Timestamp ts;
        if(value instanceof Timestamp) {
            ts = (Timestamp) value;
        } else {
            ts = new Timestamp(((java.util.Date) value).getTime());
        }
        st.setTimestamp(index, ts, Calendar.getInstance(UTC));
    }
}

Điều tương tự cũng nên được thực hiện để sửa TimeType và DateType nếu bạn sử dụng các loại đó. Nhược điểm là bạn sẽ phải chỉ định thủ công rằng các loại này sẽ được sử dụng thay vì mặc định trên mọi trường Ngày trong POJO của bạn (và cũng phá vỡ khả năng tương thích JPA thuần túy), trừ khi ai đó biết về phương pháp ghi đè chung hơn.

CẬP NHẬT: Hibernate 3.6 đã thay đổi các loại API. Trong 3.6, tôi đã viết một lớp UtcTimestampTypeDescriptor để thực hiện điều này.

public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
    public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
                st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) );
            }
        };
    }

    public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicExtractor<X>( javaTypeDescriptor, this ) {
            @Override
            protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
                return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options );
            }
        };
    }
}

Bây giờ khi ứng dụng khởi động, nếu bạn đặt TimestampTypeDescriptor.INSTANCE thành một phiên bản của UtcTimestampTypeDescriptor, tất cả các dấu thời gian sẽ được lưu trữ và coi như đang ở UTC mà không cần phải thay đổi chú thích trên POJO. [Tôi chưa thử nghiệm cái này]


3
Làm thế nào để bạn yêu cầu Hibernate sử dụng tùy chỉnh của bạn UtcTimestampType?
Derek Mahar

divestoclimb, phiên bản Hibernate nào UtcTimestampTypetương thích?
Derek Mahar

2
"ResultSet.getTimestamp và PreparedStatement.setTimestamp đều cho biết trong tài liệu của họ rằng họ biến đổi ngày tháng thành / từ múi giờ JVM hiện tại theo mặc định khi đọc và ghi từ / vào cơ sở dữ liệu." Bạn đã có một tài liệu tham khảo? Tôi không thấy bất kỳ đề cập nào về điều này trong Java 6 Javadocs cho các phương thức này. Theo stackoverflow.com/questions/4123534/… , cách các phương pháp này áp dụng múi giờ cho một trình điều khiển JDBC nhất định Datehoặc Timestampphụ thuộc.
Derek Mahar

1
Tôi có thể thề rằng tôi đã đọc điều đó về việc sử dụng múi giờ JVM vào năm ngoái, nhưng bây giờ tôi không thể tìm thấy nó. Tôi có thể đã tìm thấy nó trên tài liệu cho một trình điều khiển JDBC cụ thể và tổng quát.
divestoclimb

2
Để phiên bản 3.6 của ví dụ của bạn hoạt động, tôi phải tạo một kiểu mới về cơ bản là một trình bao bọc xung quanh TimeStampType, sau đó đặt kiểu đó trên trường.
Shaun Stone,

17

Với Spring Boot JPA, hãy sử dụng mã bên dưới trong tệp application.properties của bạn và hiển nhiên bạn có thể sửa đổi múi giờ theo lựa chọn của mình

spring.jpa.properties.hibernate.jdbc.time_zone = UTC

Sau đó, trong tệp lớp Thực thể của bạn,

@Column
private LocalDateTime created;

11

Thêm một câu trả lời hoàn toàn dựa trên và mắc nợ để thoái vốn với gợi ý từ Shaun Stone. Tôi chỉ muốn giải thích nó một cách chi tiết vì nó là một vấn đề phổ biến và giải pháp hơi khó hiểu.

Điều này đang sử dụng Hibernate 4.1.4.Final, mặc dù tôi nghi ngờ bất cứ điều gì sau 3.6 sẽ hoạt động.

Đầu tiên, tạo UtcTimestampTypeDescriptor của divestoclimb

public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
    public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
                st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) );
            }
        };
    }

    public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicExtractor<X>( javaTypeDescriptor, this ) {
            @Override
            protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
                return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options );
            }
        };
    }
}

Sau đó, tạo UtcTimestampType, sử dụng UtcTimestampTypeDescriptor thay vì TimestampTypeDescriptor làm SqlTypeDescriptor trong lệnh gọi hàm tạo siêu cấp nhưng nếu không sẽ ủy quyền mọi thứ cho TimestampType:

public class UtcTimestampType
        extends AbstractSingleColumnStandardBasicType<Date>
        implements VersionType<Date>, LiteralType<Date> {
    public static final UtcTimestampType INSTANCE = new UtcTimestampType();

    public UtcTimestampType() {
        super( UtcTimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE );
    }

    public String getName() {
        return TimestampType.INSTANCE.getName();
    }

    @Override
    public String[] getRegistrationKeys() {
        return TimestampType.INSTANCE.getRegistrationKeys();
    }

    public Date next(Date current, SessionImplementor session) {
        return TimestampType.INSTANCE.next(current, session);
    }

    public Date seed(SessionImplementor session) {
        return TimestampType.INSTANCE.seed(session);
    }

    public Comparator<Date> getComparator() {
        return TimestampType.INSTANCE.getComparator();        
    }

    public String objectToSQLString(Date value, Dialect dialect) throws Exception {
        return TimestampType.INSTANCE.objectToSQLString(value, dialect);
    }

    public Date fromStringValue(String xml) throws HibernateException {
        return TimestampType.INSTANCE.fromStringValue(xml);
    }
}

Cuối cùng, khi bạn khởi tạo cấu hình Hibernate của mình, hãy đăng ký UtcTimestampType dưới dạng ghi đè kiểu:

configuration.registerTypeOverride(new UtcTimestampType());

Giờ đây, dấu thời gian không nên quan tâm đến múi giờ của JVM trên đường đến và đi từ cơ sở dữ liệu. HTH.


6
Sẽ rất tuyệt khi thấy giải pháp cho JPA và Cấu hình mùa xuân.
Aubergine

2
Một lưu ý về việc sử dụng phương pháp này với các truy vấn gốc trong Hibernate. Để sử dụng các kiểu ghi đè này, bạn phải đặt giá trị bằng query.setParameter (int pos, Object value), không phải query.setParameter (int pos, Date value, TemporalType temporalType). Nếu bạn sử dụng cái sau thì Hibernate sẽ sử dụng các triển khai kiểu ban đầu của nó, vì chúng được mã hóa cứng.
Nigel

Tôi nên gọi câu lệnh configuration.registerTypeOverride ở đâu (UtcTimestampType mới ()); ?
Stony

@Stony Bất cứ nơi nào bạn khởi tạo cấu hình Hibernate của mình. Nếu bạn có HibernateUtil (hầu hết đều có), nó sẽ ở trong đó.
Shane

1
Nó hoạt động, nhưng sau khi kiểm tra, tôi nhận ra rằng nó không bắt buộc đối với máy chủ postgres hoạt động trong timezone = "UTC" và với tất cả các loại dấu thời gian mặc định là "dấu thời gian với múi giờ" (sau đó nó được thực hiện tự động). Tuy nhiên, đây là phiên bản cố định cho Hibernate 4.3.5 GA dưới dạng lớp đơn hoàn chỉnh và ghi đè bean nhà máy mùa xuân.com/tT4ACXn6
Lukasz Frankowski

10

Bạn sẽ nghĩ rằng vấn đề chung này sẽ được giải quyết bởi Hibernate. Nhưng nó không phải! Có một số "hack" để làm cho nó đúng.

Một trong những tôi sử dụng là để lưu trữ Ngày dưới dạng Dài trong cơ sở dữ liệu. Vì vậy, tôi luôn làm việc với mili giây sau 1/1/70. Sau đó, tôi có getters và setters trên Lớp của mình mà chỉ trả lại / chấp nhận Ngày. Vì vậy, API vẫn được giữ nguyên. Mặt trái của tôi là tôi có một khoảng thời gian dài trong cơ sở dữ liệu. VẬY với SQL, tôi hầu như chỉ có thể thực hiện các phép so sánh <,>, = - không phải toán tử ngày tháng ưa thích.

Một cách tiếp cận khác là sử dụng một loại ánh xạ tùy chỉnh như được mô tả tại đây: http://www.hibernate.org/100.html

Tôi nghĩ rằng cách chính xác để giải quyết vấn đề này là sử dụng Lịch thay vì Ngày. Với Lịch, bạn có thể đặt TimeZone trước khi tiếp tục.

LƯU Ý: stackoverflow ngớ ngẩn sẽ không cho tôi bình luận, vì vậy đây là phản hồi cho david a.

Nếu bạn tạo đối tượng này ở Chicago:

new Date(0);

Hibernate duy trì nó là "31/12/1969 18:00:00". Ngày nên không có múi giờ, vì vậy tôi không chắc tại sao việc điều chỉnh sẽ được thực hiện.


1
Xấu hổ cho tôi! Bạn đã đúng và liên kết từ bài đăng của bạn giải thích rõ điều đó. Bây giờ tôi đoán câu trả lời của tôi xứng đáng với một số danh tiếng tiêu cực :)
david a.

1
Không có gì. bạn đã khuyến khích tôi đăng một ví dụ rất rõ ràng về lý do tại sao đây là một vấn đề.
codefinger

4
Tôi đã có thể duy trì thời gian một cách chính xác bằng cách sử dụng đối tượng Lịch để chúng được lưu trữ trong DB dưới dạng UTC như bạn đã đề xuất. Tuy nhiên, khi đọc các thực thể vẫn tồn tại từ cơ sở dữ liệu, Hibernate sẽ giả định rằng chúng đang ở múi giờ địa phương và đối tượng Lịch không chính xác!
John K

1
John K, để giải quyết Calendarvấn đề đọc này, tôi nghĩ Hibernate hoặc JPA nên cung cấp một số cách để chỉ định, cho mỗi ánh xạ, múi giờ mà Hibernate sẽ dịch ngày mà nó đọc và ghi vào một TIMESTAMPcột.
Derek Mahar

joekutner, sau khi đọc stackoverflow.com/questions/4123534/… , tôi muốn chia sẻ ý kiến ​​của bạn rằng chúng tôi nên lưu trữ mili giây kể từ Kỷ nguyên trong cơ sở dữ liệu hơn là Timestampvì chúng tôi không nhất thiết phải tin tưởng trình điều khiển JDBC để lưu trữ ngày như chúng tôi mong đợi.
Derek Mahar

8

Có một số múi giờ đang hoạt động ở đây:

  1. Các lớp Ngày của Java (dùng và sql), có múi giờ ngầm định là UTC
  2. Múi giờ mà JVM của bạn đang chạy và
  3. múi giờ mặc định của máy chủ cơ sở dữ liệu của bạn.

Tất cả những điều này có thể khác nhau. Hibernate / JPA có một thiếu sót thiết kế nghiêm trọng trong đó người dùng không thể dễ dàng đảm bảo rằng thông tin múi giờ được lưu giữ trong máy chủ cơ sở dữ liệu (cho phép tạo lại ngày và giờ chính xác trong JVM).

Nếu không có khả năng (dễ dàng) lưu trữ múi giờ bằng JPA / Hibernate thì thông tin sẽ bị mất và một khi thông tin bị mất thì việc xây dựng nó sẽ trở nên tốn kém (nếu có thể).

Tôi cho rằng tốt hơn là luôn lưu trữ thông tin múi giờ (nên là mặc định) và người dùng sau đó nên có khả năng tùy chọn để tối ưu hóa múi giờ đi (mặc dù nó chỉ thực sự ảnh hưởng đến hiển thị, vẫn có múi giờ ngầm trong bất kỳ ngày nào).

Xin lỗi, bài đăng này không cung cấp thông tin về công việc (đã được trả lời ở nơi khác) nhưng nó là sự hợp lý hóa lý do tại sao luôn lưu trữ thông tin múi giờ xung quanh là quan trọng. Thật không may, có vẻ như nhiều Nhà khoa học Máy tính và những người thực hành lập trình tranh luận chống lại sự cần thiết của múi giờ đơn giản vì họ không đánh giá cao quan điểm "mất thông tin" và cách điều đó làm cho những thứ như quốc tế hóa trở nên rất khó khăn - điều rất quan trọng ngày nay với các trang web có thể truy cập bằng khách hàng và những người trong tổ chức của bạn khi họ di chuyển trên khắp thế giới.


1
"Hibernate / JPA có một thiếu sót thiết kế nghiêm trọng" Tôi muốn nói rằng đó là một thiếu sót trong SQL, theo truyền thống đã cho phép múi giờ là ẩn, và do đó có thể là bất cứ điều gì. SQL ngớ ngẩn.
Raedwald

3
Trên thực tế, thay vì luôn lưu trữ múi giờ, bạn cũng có thể chuẩn hóa trên một múi giờ (thường là UTC) và chuyển đổi mọi thứ sang múi giờ này khi duy trì (và quay lại khi đọc). Đây là những gì chúng tôi thường làm. Tuy nhiên, JDBC cũng không hỗ trợ trực tiếp điều đó: - /.
sleske

3

Vui lòng xem dự án của tôi trên Sourceforge có các kiểu người dùng cho các kiểu Ngày và Giờ SQL chuẩn cũng như JSR 310 và Joda Time. Tất cả các loại đều cố gắng giải quyết vấn đề bù trừ. Xem http://sourceforge.net/projects/usertype/

CHỈNH SỬA: Để trả lời câu hỏi của Derek Mahar đính kèm với nhận xét này:

"Chris, các loại người dùng của bạn có hoạt động với Hibernate 3 trở lên không? - Derek Mahar, ngày 7 tháng 11 '10 lúc 12:30"

Có những loại này hỗ trợ các phiên bản Hibernate 3.x bao gồm cả Hibernate 3.6.


2

Ngày không nằm trong bất kỳ múi giờ nào (nó là văn phòng mili giây từ một thời điểm xác định giống nhau cho tất cả mọi người), nhưng DB (R) cơ bản thường lưu trữ dấu thời gian ở định dạng chính trị (năm, tháng, ngày, giờ, phút, giây,. ..) nhạy cảm với múi giờ.

Nghiêm túc mà nói, Hibernate PHẢI cho phép được thông báo trong một số hình thức ánh xạ rằng ngày DB nằm trong múi giờ tương tự và như vậy để khi tải hoặc lưu trữ, nó không giả định là của chính nó ...


1

Tôi đã gặp phải vấn đề tương tự khi muốn lưu trữ ngày tháng trong DB dưới dạng UTC và tránh sử dụng varcharString <-> java.util.Datechuyển đổi rõ ràng hoặc đặt toàn bộ ứng dụng Java của tôi theo múi giờ UTC (vì điều này có thể dẫn đến một sự cố không mong muốn khác, nếu JVM được chia sẻ trên nhiều ứng dụng).

Vì vậy, có một dự án mã nguồn mở DbAssist, cho phép bạn dễ dàng sửa lỗi đọc / ghi dưới dạng ngày UTC từ cơ sở dữ liệu. Vì bạn đang sử dụng Chú thích JPA để ánh xạ các trường trong thực thể, tất cả những gì bạn phải làm là đưa phần phụ thuộc sau vào pomtệp Maven của mình :

<dependency>
    <groupId>com.montrosesoftware</groupId>
    <artifactId>DbAssist-5.2.2</artifactId>
    <version>1.0-RELEASE</version>
</dependency>

Sau đó, bạn áp dụng bản sửa lỗi (đối với ví dụ Hibernate + Spring Boot) bằng cách thêm @EnableAutoConfigurationchú thích trước lớp ứng dụng Spring. Để biết các hướng dẫn cài đặt thiết lập khác và các ví dụ sử dụng khác, chỉ cần tham khảo github của dự án .

Điều tốt là bạn không phải sửa đổi các thực thể; bạn có thể để nguyên java.util.Datecác lĩnh vực của họ.

5.2.2phải tương ứng với phiên bản Hibernate bạn đang sử dụng. Tôi không chắc bạn đang sử dụng phiên bản nào trong dự án của mình, nhưng danh sách đầy đủ các bản sửa lỗi được cung cấp có sẵn trên trang wiki của github của dự án . Lý do tại sao bản sửa lỗi khác nhau đối với các phiên bản Hibernate khác nhau là vì những người tạo Hibernate đã thay đổi API một vài lần giữa các bản phát hành.

Về nội bộ, bản sửa lỗi sử dụng các gợi ý từ divestoclimb, Shane và một số nguồn khác để tạo một tùy chỉnh UtcDateType. Sau đó, nó lập bản đồ tiêu chuẩn java.util.Datevới tùy chỉnh UtcDateTypexử lý tất cả các xử lý múi giờ cần thiết. Ánh xạ của các loại được thực hiện bằng cách sử dụng @Typedefchú thích trong package-info.javatệp được cung cấp .

@TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class),
package com.montrosesoftware.dbassist.types;

Bạn có thể tìm thấy một bài báo ở đây giải thích tại sao sự dịch chuyển thời gian như vậy lại xảy ra và các phương pháp giải quyết nó là gì.


1

Hibernate không cho phép chỉ định múi giờ bằng chú thích hoặc bất kỳ phương tiện nào khác. Nếu bạn sử dụng Lịch thay vì ngày, bạn có thể triển khai giải pháp thay thế bằng cách sử dụng AccessType thuộc tính HIbernate và tự mình triển khai ánh xạ. Giải pháp nâng cao hơn là triển khai Kiểu người dùng tùy chỉnh để ánh xạ Ngày hoặc Lịch của bạn. Cả hai giải pháp đều được giải thích trong bài đăng trên blog của tôi tại đây: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.