Tạo thực thể JPA hoàn hảo [đã đóng]


422

Tôi đã làm việc với JPA (triển khai Hibernate) một thời gian rồi và mỗi lần tôi cần tạo các thực thể tôi thấy mình phải vật lộn với các vấn đề như AccessType, thuộc tính bất biến, bằng / hashCode, ....
Vì vậy, tôi quyết định thử và tìm hiểu thực tiễn tốt nhất chung cho từng vấn đề và viết nó ra để sử dụng cá nhân.
Tuy nhiên, tôi sẽ không phiền nếu ai đó nhận xét về nó hoặc cho tôi biết tôi sai ở đâu.

Lớp thực thể

  • thực hiện nối tiếp

    Lý do: Thông số kỹ thuật nói rằng bạn phải làm, nhưng một số nhà cung cấp JPA không thực thi điều này. Hibernate là nhà cung cấp JPA không thực thi điều này, nhưng nó có thể thất bại ở đâu đó sâu trong bụng với ClassCastException, nếu serializable chưa được triển khai.

Người xây dựng

  • tạo một hàm tạo với tất cả các trường bắt buộc của thực thể

    Lý do: Một constructor phải luôn luôn để cá thể được tạo ở trạng thái lành mạnh.

  • bên cạnh hàm tạo này: có một hàm tạo mặc định riêng

    Lý do: Trình xây dựng mặc định được yêu cầu để Hibernate khởi tạo thực thể; riêng tư được cho phép nhưng khả năng hiển thị riêng tư (hoặc công khai) là bắt buộc để tạo proxy thời gian chạy và truy xuất dữ liệu hiệu quả mà không cần thiết bị mã byte.

Trường / Thuộc tính

  • Sử dụng quyền truy cập trường nói chung và quyền truy cập tài sản khi cần thiết

    Lý do: đây có lẽ là vấn đề gây tranh cãi nhất vì không có lập luận rõ ràng và thuyết phục cho người này hay người kia (quyền truy cập thuộc tính so với quyền truy cập trường); tuy nhiên, truy cập trường dường như được yêu thích chung vì mã rõ ràng hơn, đóng gói tốt hơn và không cần tạo setters cho các trường không thay đổi

  • Bỏ qua setters cho các trường không thay đổi (không bắt buộc đối với trường loại truy cập)

  • thuộc tính có thể là riêng tư
    Lý do: Tôi đã từng nghe nói rằng bảo vệ tốt hơn cho hiệu suất (Hibernate) nhưng tất cả những gì tôi có thể tìm thấy trên web là: Hibernate có thể truy cập trực tiếp các phương thức truy cập công khai, riêng tư và được bảo vệ, cũng như các trường công khai, riêng tư và được bảo vệ . Sự lựa chọn tùy thuộc vào bạn và bạn có thể phù hợp với nó để phù hợp với thiết kế ứng dụng của bạn.

Bằng / hashCode

  • Không bao giờ sử dụng id được tạo nếu id này chỉ được đặt khi duy trì thực thể
  • Theo sở thích: sử dụng các giá trị bất biến để tạo thành Khóa doanh nghiệp duy nhất và sử dụng giá trị này để kiểm tra tính bằng nhau
  • nếu Khóa doanh nghiệp duy nhất không khả dụng, hãy sử dụng UUID không tạm thời được tạo khi thực thể được khởi tạo; Xem bài viết tuyệt vời này để biết thêm thông tin.
  • không bao giờ đề cập đến các thực thể liên quan (ManyToOne); nếu thực thể này (như thực thể mẹ) cần là một phần của Khóa doanh nghiệp thì chỉ so sánh ID. Gọi getId () trên proxy sẽ không kích hoạt tải thực thể, miễn là bạn đang sử dụng loại truy cập thuộc tính .

Thực thể ví dụ

@Entity
@Table(name = "ROOM")
public class Room implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "room_id")
    private Integer id;

    @Column(name = "number") 
    private String number; //immutable

    @Column(name = "capacity")
    private Integer capacity;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "building_id")
    private Building building; //immutable

    Room() {
        // default constructor
    }

    public Room(Building building, String number) {
        // constructor with required field
        notNull(building, "Method called with null parameter (application)");
        notNull(number, "Method called with null parameter (name)");

        this.building = building;
        this.number = number;
    }

    @Override
    public boolean equals(final Object otherObj) {
        if ((otherObj == null) || !(otherObj instanceof Room)) {
            return false;
        }
        // a room can be uniquely identified by it's number and the building it belongs to; normally I would use a UUID in any case but this is just to illustrate the usage of getId()
        final Room other = (Room) otherObj;
        return new EqualsBuilder().append(getNumber(), other.getNumber())
                .append(getBuilding().getId(), other.getBuilding().getId())
                .isEquals();
        //this assumes that Building.id is annotated with @Access(value = AccessType.PROPERTY) 
    }

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(getNumber()).append(getBuilding().getId()).toHashCode();
    }

    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }

    //no setters for number, building nor id

}

Các đề xuất khác để thêm vào danh sách này được chào đón nhiều hơn ...

CẬP NHẬT

Kể từ khi đọc bài viết này, tôi đã điều chỉnh cách thực hiện eq / hC:

  • nếu có sẵn một khóa doanh nghiệp đơn giản bất biến: sử dụng khóa đó
  • trong tất cả các trường hợp khác: sử dụng uuid

6
Đây không phải là một câu hỏi, nó là một yêu cầu để xem xét với một yêu cầu cho một danh sách. Hơn nữa, nó rất kết thúc mở và mơ hồ, hoặc đặt khác đi: Việc một thực thể JPA có hoàn hảo hay không phụ thuộc vào việc nó sẽ được sử dụng cho mục đích gì. Chúng ta có nên liệt kê tất cả những thứ mà một thực thể có thể cần trong tất cả các cách sử dụng có thể có của một thực thể không?
meriton

Tôi biết đó không phải là câu hỏi rõ ràng mà tôi xin lỗi. Nó không thực sự là một yêu cầu cho một danh sách, mà là một yêu cầu cho ý kiến ​​/ nhận xét mặc dù các đề xuất khác được hoan nghênh. Vui lòng giải thích các cách sử dụng có thể có của một thực thể JPA.
Stijn Geukens

Tôi cũng muốn các lĩnh vực được final(đánh giá bằng cách bỏ qua các setters, tôi đoán bạn cũng vậy).
Sridhar Sarnobat

Sẽ phải thử nhưng tôi không nghĩ cuối cùng sẽ hoạt động vì Hibernate vẫn cần có thể đặt các giá trị trên các thuộc tính đó.
Stijn Geukens

Nơi nào notNullđến từ đâu?
bruno

Câu trả lời:


73

Tôi sẽ cố gắng trả lời một số điểm chính: đây là từ kinh nghiệm lâu dài / kiên trì bao gồm một số ứng dụng chính.

Lớp thực thể: thực hiện nối tiếp?

Các khóa cần thực hiện nối tiếp. Những thứ sẽ xuất hiện trong HTTPSession hoặc được gửi qua dây bởi RPC / Java EE, cần phải thực hiện Nối tiếp. Những thứ khác: không quá nhiều. Dành thời gian của bạn vào những gì quan trọng.

Con constructor: tạo constructor với tất cả các trường bắt buộc của thực thể?

Trình xây dựng cho logic ứng dụng, chỉ nên có một vài trường "khóa ngoại" hoặc "loại / loại" quan trọng sẽ luôn được biết khi tạo thực thể. Phần còn lại nên được đặt bằng cách gọi các phương thức setter - đó là những gì chúng dành cho.

Tránh đưa quá nhiều lĩnh vực vào các nhà xây dựng. Các nhà xây dựng nên thuận tiện, và cung cấp sự tỉnh táo cơ bản cho đối tượng. Tên, loại và / hoặc cha mẹ đều hữu ích.

OTOH nếu quy tắc ứng dụng (ngày nay) yêu cầu Khách hàng phải có Địa chỉ, hãy để địa chỉ đó cho người thiết lập. Đó là một ví dụ về "quy tắc yếu". Có thể tuần tới, bạn muốn tạo một đối tượng Khách hàng trước khi vào màn hình Nhập Chi tiết? Đừng tự vấp ngã, để lại khả năng cho dữ liệu chưa biết, chưa hoàn thành hoặc "nhập một phần".

Con constructor: còn nữa, gói constructor mặc định riêng?

Có, nhưng sử dụng 'được bảo vệ' thay vì gói riêng tư. Công cụ phân lớp là một nỗi đau thực sự khi không thể nhìn thấy nội bộ cần thiết.

Trường / Thuộc tính

Sử dụng quyền truy cập trường 'thuộc tính' cho Hibernate và từ bên ngoài thể hiện. Trong ví dụ, sử dụng các trường trực tiếp. Lý do: cho phép phản xạ tiêu chuẩn, phương pháp đơn giản & cơ bản nhất để Hibernate hoạt động.

Đối với các trường 'không thay đổi' đối với ứng dụng - Hibernate vẫn cần có thể tải các trường này. Bạn có thể thử đặt các phương thức này thành 'riêng tư' và / hoặc đặt chú thích cho chúng, để ngăn mã ứng dụng thực hiện truy cập không mong muốn.

Lưu ý: khi viết hàm bằng (), hãy sử dụng getters cho các giá trị trong trường hợp 'khác'! Nếu không, bạn sẽ nhấn các trường chưa được khởi tạo / trống trên các trường hợp proxy.

Được bảo vệ là tốt hơn cho hiệu suất (Hibernate)?

Không có khả năng.

Bằng / HashCode?

Điều này có liên quan đến việc làm việc với các thực thể, trước khi chúng được lưu - đây là một vấn đề nhức nhối. Băm / so sánh về giá trị bất biến? Trong hầu hết các ứng dụng kinh doanh, không có bất kỳ ứng dụng nào.

Một khách hàng có thể thay đổi địa chỉ, thay đổi tên doanh nghiệp của họ, v.v. - không phổ biến, nhưng nó xảy ra. Sửa chữa cũng cần phải có thể thực hiện, khi dữ liệu không được nhập chính xác.

Một vài thứ thường được giữ bất biến, là Nuôi dạy con cái và có lẽ Loại / Loại - thông thường người dùng tạo lại bản ghi, thay vì thay đổi những thứ này. Nhưng những điều này không xác định duy nhất thực thể!

Vì vậy, dài và ngắn, dữ liệu "bất biến" được tuyên bố không thực sự. Các trường Khóa / ID chính được tạo cho mục đích chính xác, cung cấp sự ổn định và bất biến được đảm bảo như vậy.

Bạn cần lập kế hoạch và xem xét nhu cầu của mình để so sánh & băm & xử lý các giai đoạn công việc khi A) làm việc với "dữ liệu bị thay đổi / ràng buộc" từ UI nếu bạn so sánh / băm trên "các trường thay đổi không thường xuyên" hoặc B) làm việc với " dữ liệu chưa được lưu ", nếu bạn so sánh / băm trên ID.

Bằng / HashCode - nếu không có Khóa doanh nghiệp duy nhất, hãy sử dụng UUID không tạm thời được tạo khi thực thể được khởi tạo

Vâng, đây là một chiến lược tốt khi được yêu cầu. Xin lưu ý rằng UUID không miễn phí, thông minh về hiệu suất - và phân cụm làm phức tạp mọi thứ.

Equals / HashCode - không bao giờ đề cập đến các thực thể liên quan

"Nếu thực thể có liên quan (như thực thể mẹ) cần phải là một phần của Khóa doanh nghiệp thì hãy thêm trường không thể chèn, không thể cập nhật để lưu trữ id cha (có cùng tên với ManytoOne JoinColumn) và sử dụng id này trong kiểm tra tính bằng "

Âm thanh như lời khuyên tốt.

Hi vọng điêu nay co ich!


2
Re: constructor, tôi thường chỉ thấy zero arg (không có gì) và mã gọi có một danh sách dài các setters có vẻ hơi lộn xộn đối với tôi. Có thực sự có vấn đề gì với việc có một vài nhà xây dựng phù hợp với nhu cầu của bạn, làm cho mã cuộc gọi trở nên ngắn gọn hơn không?
Bão

hoàn toàn ý kiến, đặc biệt về ctor. Mã nào đẹp hơn? một loạt các công cụ khác nhau cho phép bạn biết các giá trị (kết hợp) nào là cần thiết để tạo ra trạng thái lành mạnh của obj hoặc một công cụ không có đối số sẽ không đưa ra manh mối nào và theo thứ tự và để nó dễ bị lỗi ?
mohamnag

1
@mohamnag Phụ thuộc. Đối với dữ liệu nội bộ hệ thống tạo ra đậu hợp lệ là tuyệt vời; tuy nhiên các ứng dụng kinh doanh hiện đại bao gồm số lượng lớn màn hình CRUD hoặc trình hướng dẫn nhập dữ liệu người dùng. Dữ liệu do người dùng nhập thường được hình thành một phần hoặc kém, ít nhất là trong quá trình chỉnh sửa. Thông thường, thậm chí còn có giá trị kinh doanh trong việc có thể ghi lại trạng thái chưa hoàn thành để hoàn thành sau này - nghĩ rằng việc đăng ký bảo hiểm, đăng ký khách hàng, v.v. Giữ các ràng buộc ở mức tối thiểu (ví dụ: khóa chính, khóa doanh nghiệp & trạng thái) cho phép thực tế linh hoạt hơn tình hình kinh doanh.
Thomas W

1
@ThomasW trước tiên tôi phải nói rằng, tôi rất quan tâm đến thiết kế hướng tên miền và sử dụng tên cho tên lớp và có nghĩa là động từ đầy đủ cho các phương thức. Trong mô hình này, những gì bạn đang đề cập, thực sự là các DTO và không phải là các thực thể miền nên được sử dụng để lưu trữ dữ liệu tạm thời. Hoặc bạn vừa hiểu nhầm / cấu trúc tên miền của bạn.
mohamnag

@ThomasW khi tôi lọc ra tất cả các câu mà bạn đang cố nói Tôi là người mới, không có thông tin nào trong bình luận của bạn ngoại trừ liên quan đến đầu vào của người dùng. Phần đó, như tôi đã nói trước đây, sẽ được thực hiện trong DTO và không trực tiếp thực thể. hãy nói chuyện trong 50 năm nữa mà bạn có thể trở thành 5% những gì mà trí tuệ lớn đằng sau DDD như Fowler đã trải qua! chúc mừng: D
mohamnag 8/2/18

144

Các JPA 2.0 Đặc điểm kỹ thuật khẳng định rằng:

  • Lớp thực thể phải có một hàm tạo không có đối số. Nó có thể có các nhà xây dựng khác là tốt. Hàm tạo không có đối số phải là công khai hoặc được bảo vệ.
  • Lớp thực thể phải là lớp cấp cao nhất. Một enum hoặc giao diện không được chỉ định là một thực thể.
  • Lớp thực thể không phải là cuối cùng. Không có phương thức hoặc biến thể hiện liên tục của lớp thực thể có thể là cuối cùng.
  • Nếu một thể hiện thực thể được truyền bằng giá trị như một đối tượng tách rời (ví dụ, thông qua giao diện từ xa), lớp thực thể phải thực hiện giao diện Nối tiếp.
  • Cả hai lớp trừu tượng và cụ thể có thể là các thực thể. Các thực thể có thể mở rộng các lớp không thực thể cũng như các lớp thực thể và các lớp không thực thể có thể mở rộng các lớp thực thể.

Đặc tả không chứa các yêu cầu về việc triển khai các phương thức bằng và hashCode cho các thực thể, chỉ dành cho các lớp khóa chính và các khóa bản đồ theo như tôi biết.


13
Đúng, bằng, mã băm, ... không phải là một yêu cầu của JPA nhưng tất nhiên được khuyến nghị và được coi là thực hành tốt.
Stijn Geukens

6
@TheStijn Vâng, trừ khi bạn có kế hoạch so sánh các thực thể tách rời cho sự bình đẳng, điều này có lẽ là không cần thiết. Trình quản lý thực thể được đảm bảo trả về cùng một thể hiện của một thực thể nhất định mỗi khi bạn yêu cầu. Vì vậy, bạn có thể làm tốt với các so sánh nhận dạng cho các thực thể được quản lý, theo như tôi hiểu. Bạn có thể vui lòng giải thích thêm một chút về những kịch bản mà bạn sẽ coi đây là một cách thực hành tốt không?
Edwin Dalorzo

2
Tôi cố gắng luôn luôn có một triển khai chính xác bằng / hashCode. Không bắt buộc đối với JPA nhưng tôi coi đó là một cách thực hành tốt khi thực thể hoặc được thêm vào Bộ. Bạn có thể quyết định chỉ thực hiện bằng khi các thực thể sẽ được thêm vào Bộ nhưng bạn có luôn biết trước không?
Stijn Geukens

10
@TheStijn Nhà cung cấp JPA sẽ đảm bảo rằng tại bất kỳ thời điểm nào, chỉ có một phiên bản của một thực thể nhất định trong ngữ cảnh, do đó, ngay cả các bộ của bạn vẫn an toàn mà không thực hiện bằng / hascode, miễn là bạn chỉ sử dụng các thực thể được quản lý. Việc thực hiện các phương pháp này cho các thực thể không phải là không có khó khăn, ví dụ, hãy xem Điều khoản Hibernate này về chủ đề này. Quan điểm của tôi, nếu bạn chỉ làm việc với các thực thể được quản lý, bạn sẽ tốt hơn nếu không có chúng, nếu không thì cung cấp một triển khai rất cẩn thận.
Edwin Dalorzo

2
@TheStijn Đây là kịch bản hỗn hợp tốt. Nó biện minh cho nhu cầu triển khai eq / hC như bạn đề xuất ban đầu bởi vì một khi các thực thể từ bỏ sự an toàn của lớp kiên trì, bạn không còn có thể tin tưởng vào các quy tắc được thi hành theo tiêu chuẩn JPA. Trong trường hợp của chúng tôi, mẫu DTO đã được thi hành theo kiến ​​trúc ngay từ đầu. Bằng cách thiết kế API kiên trì của chúng tôi không cung cấp một cách công khai để tương tác với các đối tượng kinh doanh, chỉ có một API để tương tác với lớp kiên trì của chúng tôi bằng DTOs.
Edwin Dalorzo

13

Ngoài ra 2 xu của tôi cho các câu trả lời ở đây là:

  1. Với tham chiếu đến quyền truy cập Trường hoặc Thuộc tính (ngoài các cân nhắc về hiệu suất), cả hai đều được truy cập một cách hợp pháp bằng phương tiện của getters và setters, do đó, logic mô hình của tôi có thể đặt / lấy chúng theo cách tương tự. Sự khác biệt xuất hiện khi nhà cung cấp thời gian chạy bền vững (Hibernate, EclipseLink hoặc người khác) cần duy trì / thiết lập một số bản ghi trong Bảng A có khóa ngoại tham chiếu đến một số cột trong Bảng B. Trong trường hợp loại truy cập Thuộc tính, tính bền vững hệ thống thời gian chạy sử dụng phương thức setter được mã hóa của tôi để gán cho ô trong cột Bảng B một giá trị mới. Trong trường hợp loại truy cập Trường, hệ thống thời gian chạy liên tục đặt ô trực tiếp trong cột Bảng B. Sự khác biệt này không quan trọng trong bối cảnh của một mối quan hệ đơn phương, tuy nhiên, PHẢI sử dụng phương thức setter được mã hóa của riêng tôi (loại truy cập thuộc tính) cho mối quan hệ hai chiều với điều kiện phương thức setter được thiết kế tốt để tính đến tính nhất quán. Tính nhất quán là một vấn đề quan trọng đối với các mối quan hệ hai chiều đề cập đến điều nàyliên kết cho một ví dụ đơn giản cho một setter được thiết kế tốt.

  2. Với tham chiếu đến Equals / hashCode: Không thể sử dụng các phương thức Equals / hashCode được tạo tự động của Eclipse cho các thực thể tham gia vào mối quan hệ hai chiều, nếu không, chúng sẽ có một tham chiếu vòng tròn dẫn đến Ngoại lệ stackoverflow. Khi bạn thử một mối quan hệ hai chiều (giả sử OneToOne) và tự động tạo Equals () hoặc hashCode () hoặc thậm chí toString (), bạn sẽ bị bắt trong ngoại lệ stackoverflow này.


9

Giao diện thực thể

public interface Entity<I> extends Serializable {

/**
 * @return entity identity
 */
I getId();

/**
 * @return HashCode of entity identity
 */
int identityHashCode();

/**
 * @param other
 *            Other entity
 * @return true if identities of entities are equal
 */
boolean identityEquals(Entity<?> other);
}

Triển khai cơ bản cho tất cả các Thực thể, đơn giản hóa việc triển khai Bằng / Hashcode:

public abstract class AbstractEntity<I> implements Entity<I> {

@Override
public final boolean identityEquals(Entity<?> other) {
    if (getId() == null) {
        return false;
    }
    return getId().equals(other.getId());
}

@Override
public final int identityHashCode() {
    return new HashCodeBuilder().append(this.getId()).toHashCode();
}

@Override
public final int hashCode() {
    return identityHashCode();
}

@Override
public final boolean equals(final Object o) {
    if (this == o) {
        return true;
    }
    if ((o == null) || (getClass() != o.getClass())) {
        return false;
    }

    return identityEquals((Entity<?>) o);
}

@Override
public String toString() {
    return getClass().getSimpleName() + ": " + identity();
    // OR 
    // return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}

Thực thể phòng ngụ ý:

@Entity
@Table(name = "ROOM")
public class Room extends AbstractEntity<Integer> {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Integer id;

@Column(name = "number") 
private String number; //immutable

@Column(name = "capacity")
private Integer capacity;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "building_id")
private Building building; //immutable

Room() {
    // default constructor
}

public Room(Building building, String number) {
    // constructor with required field
    notNull(building, "Method called with null parameter (application)");
    notNull(number, "Method called with null parameter (name)");

    this.building = building;
    this.number = number;
}

public Integer getId(){
    return id;
}

public Building getBuilding() {
    return building;
}

public String getNumber() {
    return number;
}


public void setCapacity(Integer capacity) {
    this.capacity = capacity;
}

//no setters for number, building nor id
}

Tôi không thấy điểm so sánh sự bình đẳng của các thực thể dựa trên các lĩnh vực kinh doanh trong mọi trường hợp của Thực thể JPA. Đó có thể là một trường hợp nếu các thực thể JPA này được coi là ValueObjects hướng tên miền, thay vì các thực thể hướng tên miền (mà các ví dụ mã này dành cho).


4
Mặc dù đó là một cách tiếp cận tốt để sử dụng lớp thực thể mẹ để lấy mã tấm nồi hơi, không nên sử dụng id được xác định DB trong phương thức bằng của bạn. Trong trường hợp của bạn, việc so sánh 2 thực thể mới thậm chí sẽ ném NPE. Ngay cả khi bạn làm cho nó an toàn thì 2 thực thể mới sẽ luôn bằng nhau, cho đến khi chúng được duy trì. Eq / hC nên bất biến.
Stijn Geukens

2
Equals () sẽ không ném NPE vì có kiểm tra xem DB id có null hay không & trong trường hợp DB id là null, đẳng thức sẽ là sai.
ahaaman

3
Thật vậy, tôi không thấy làm thế nào tôi bỏ lỡ rằng mã là không an toàn. Nhưng IMO sử dụng id vẫn là thực tế xấu. Đối số: onjava.com/pub/a/onjava/2006/09/13/13
Stijn Geukens

Trong cuốn sách 'Triển khai DDD' của Vaughn Vernon, người ta cho rằng bạn có thể sử dụng id cho bằng nếu bạn sử dụng "thế hệ PK sớm" (Tạo id trước và chuyển nó vào hàm tạo của thực thể thay vì cho phép cơ sở dữ liệu tạo ra id khi bạn duy trì thực thể.)
Wim Deblauwe 9/12/2015

hoặc nếu bạn không có kế hoạch so sánh bình đẳng với các thực thể không liên tục? Tại sao bạn nên ...
Enerccio
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.