2012-03-03 42 views
23

Tôi muốn biết cách triển khai temporal tables trong JPA 2 với EclipseLink. Theo thời gian, tôi có nghĩa là các bảng xác định thời hạn hiệu lực. Một vấn đề mà tôi đang phải đối mặt là các bảng tham chiếu có thể không còn các ràng buộc khoá ngoài đối với các bảng được tham chiếu (bảng tạm thời) vì bản chất của các bảng được tham chiếu mà bây giờ các khóa chính của chúng bao gồm thời hạn hiệu lực.Làm thế nào để thực hiện một bảng thời gian bằng cách sử dụng JPA?

  • Tôi làm cách nào để ánh xạ mối quan hệ giữa các thực thể của tôi?
  • Điều đó có nghĩa là các thực thể của tôi không thể có mối quan hệ với những thực thể hợp lệ nữa không?
  • Liệu sự có trách nhiệm để khởi tạo các mối quan hệ hiện tại do tôi làm theo cách thủ công trong một số loại Dịch vụ hoặc DAO chuyên biệt?

Điều duy nhất tôi tìm thấy là khung được gọi là DAO Fusion đề cập đến điều này.

  • Có cách nào khác để giải quyết vấn đề này không?
  • Bạn có thể cung cấp ví dụ hoặc tài nguyên về chủ đề này (JPA với cơ sở dữ liệu thời gian) không?

Đây là một ví dụ hư cấu về mô hình dữ liệu và các lớp của nó. Nó bắt đầu như một mô hình đơn giản mà không phải đối phó với các khía cạnh thời gian:

1 Kịch bản: Không Temporal Mẫu

Data Model: Non Temporal Data Model

Đội:

@Entity 
public class Team implements Serializable { 

    private Long id; 
    private String name; 
    private Integer wins = 0; 
    private Integer losses = 0; 
    private Integer draws = 0; 
    private List<Player> players = new ArrayList<Player>(); 

    public Team() { 

    } 

    public Team(String name) { 
     this.name = name; 
    } 


    @Id 
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQTEAMID") 
    @SequenceGenerator(name="SEQTEAMID", sequenceName="SEQTEAMID", allocationSize=1) 
    public Long getId() { 
     return id; 
    } 

    public void setId(Long id) { 
     this.id = id; 
    } 

    @Column(unique=true, nullable=false) 
    public String getName() { 
     return name; 
    } 

    public void setName(String name) { 
     this.name = name; 
    } 

    public Integer getWins() { 
     return wins; 
    } 

    public void setWins(Integer wins) { 
     this.wins = wins; 
    } 

    public Integer getLosses() { 
     return losses; 
    } 

    public void setLosses(Integer losses) { 
     this.losses = losses; 
    } 

    public Integer getDraws() { 
     return draws; 
    } 

    public void setDraws(Integer draws) { 
     this.draws = draws; 
    } 

    @OneToMany(mappedBy="team", cascade=CascadeType.ALL) 
    public List<Player> getPlayers() { 
     return players; 
    } 

    public void setPlayers(List<Player> players) { 
     this.players = players; 
    } 

    @Override 
    public int hashCode() { 
     final int prime = 31; 
     int result = 1; 
     result = prime * result + ((name == null) ? 0 : name.hashCode()); 
     return result; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     if (this == obj) 
      return true; 
     if (obj == null) 
      return false; 
     if (getClass() != obj.getClass()) 
      return false; 
     Team other = (Team) obj; 
     if (name == null) { 
      if (other.name != null) 
       return false; 
     } else if (!name.equals(other.name)) 
      return false; 
     return true; 
    } 


} 

Máy nghe nhạc:

@Entity 
@Table(uniqueConstraints={@UniqueConstraint(columnNames={"team_id","number"})}) 
public class Player implements Serializable { 

    private Long id; 
    private Team team; 
    private Integer number; 
    private String name; 

    public Player() { 

    } 

    public Player(Team team, Integer number) { 
     this.team = team; 
     this.number = number; 
    } 

    @Id 
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQPLAYERID") 
    @SequenceGenerator(name="SEQPLAYERID", sequenceName="SEQPLAYERID", allocationSize=1) 
    public Long getId() { 
     return id; 
    } 

    public void setId(Long id) { 
     this.id = id; 
    } 

    @ManyToOne 
    @JoinColumn(nullable=false) 
    public Team getTeam() { 
     return team; 
    } 

    public void setTeam(Team team) { 
     this.team = team; 
    } 

    @Column(nullable=false) 
    public Integer getNumber() { 
     return number; 
    } 

    public void setNumber(Integer number) { 
     this.number = number; 
    } 

    @Column(unique=true, nullable=false) 
    public String getName() { 
     return name; 
    } 

    public void setName(String name) { 
     this.name = name; 
    } 

    @Override 
    public int hashCode() { 
     final int prime = 31; 
     int result = 1; 
     result = prime * result + ((number == null) ? 0 : number.hashCode()); 
     result = prime * result + ((team == null) ? 0 : team.hashCode()); 
     return result; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     if (this == obj) 
      return true; 
     if (obj == null) 
      return false; 
     if (getClass() != obj.getClass()) 
      return false; 
     Player other = (Player) obj; 
     if (number == null) { 
      if (other.number != null) 
       return false; 
     } else if (!number.equals(other.number)) 
      return false; 
     if (team == null) { 
      if (other.team != null) 
       return false; 
     } else if (!team.equals(other.team)) 
      return false; 
     return true; 
    } 


} 

Kiểm tra lớp:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration({"/META-INF/application-context-root.xml"}) 
@Transactional 
public class TestingDao { 

    @PersistenceContext 
    private EntityManager entityManager; 
    private Team team; 

    @Before 
    public void setUp() { 
     team = new Team(); 
     team.setName("The Goods"); 
     team.setLosses(0); 
     team.setWins(0); 
     team.setDraws(0); 

     Player player = new Player(); 
     player.setTeam(team); 
     player.setNumber(1); 
     player.setName("Alfredo"); 
     team.getPlayers().add(player); 

     player = new Player(); 
     player.setTeam(team); 
     player.setNumber(2); 
     player.setName("Jorge"); 
     team.getPlayers().add(player); 

     entityManager.persist(team); 
     entityManager.flush(); 
    } 

    @Test 
    public void testPersistence() { 
     String strQuery = "select t from Team t where t.name = :name"; 
     TypedQuery<Team> query = entityManager.createQuery(strQuery, Team.class); 
     query.setParameter("name", team.getName()); 
     Team persistedTeam = query.getSingleResult(); 
     assertEquals(2, persistedTeam.getPlayers().size()); 

     //Change the player number 
     Player p = null; 
     for (Player player : persistedTeam.getPlayers()) { 
      if (player.getName().equals("Alfredo")) { 
       p = player; 
       break; 
      } 
     } 
     p.setNumber(10);   
    } 


} 

Bây giờ bạn sẽ được yêu cầu giữ một lịch sử của cách đội và cầu thủ đang trên thời điểm nhất định vì vậy những gì bạn cần làm là thêm một khoảng thời gian cho mỗi bảng muốn được theo dõi. Vì vậy, hãy thêm các cột thời gian này. Chúng ta sẽ bắt đầu chỉ với Player.

2 Kịch bản: Temporal Mẫu

Data Model: Temporal Data Model

Như bạn có thể thấy chúng tôi đã phải thả các khóa chính và xác định một số khác bao gồm các ngày (thời gian). Ngoài ra, chúng tôi đã phải loại bỏ các ràng buộc duy nhất bởi vì bây giờ chúng có thể được lặp lại trong bảng. Bây giờ bảng có thể chứa các mục hiện tại và cũng là lịch sử.

Mọi thứ trở nên xấu xí nếu chúng ta phải tạo Đội thời gian, trong trường hợp này chúng ta sẽ cần phải giảm ràng buộc khóa ngoài mà bảng Player phải là Team. Vấn đề là làm thế nào bạn sẽ mô hình hóa đó trong Java và JPA.

Lưu ý rằng ID là khóa thay thế. Nhưng bây giờ các khóa thay thế phải bao gồm ngày bởi vì nếu họ không nó sẽ không cho phép lưu trữ nhiều hơn một "phiên bản" của cùng một thực thể (trong dòng thời gian).

+0

1) bạn đã vẽ sơ đồ công cụ nào? 2) một chiều thời gian là đủ cho yêu cầu của bạn, các mẫu DAOFusion và cũng là câu trả lời của tôi (dựa trên các mẫu này) là quá mức trong quan điểm của tôi 3) Bạn có thích một giải pháp chỉ bổ sung khía cạnh thời gian cho Trình phát hay bạn thích nó cho cả hai bảng 4) đoạn cuối cùng của bạn là sai. Khóa thay thế sẽ không bao giờ bao gồm các trường bổ sung. Trong trường hợp đó, bạn sẽ có hai khóa thay thế. – ChrLipp

+0

@ChrLipp 1) Sparx Enterprise Architect 2) Tôi đồng ý. 3) Tôi cần một giải pháp cho biết thêm thời gian cho cả hai bảng. 4) Tôi không đồng ý rằng đó không phải là chìa khóa thay thế. Tôi nghĩ rằng đó là một chìa khóa thay thế bởi vì: 1. Trước khi thêm cột thời gian nó là một chìa khóa thay thế đó là một chìa khóa không có ý nghĩa kinh doanh.Ví dụ, khóa kinh doanh của Player là "team_id" và "number" và từ Team là "name". Cả hai đều có khóa thay thế của họ "id" khi họ không có cột thời gian. Vấn đề là khi tôi thêm các cột thời gian không hoạt động nữa. Mục nhập tương tự có thể xuất hiện nhiều lần trong cùng một bảng. –

+0

Đó là lý do tại sao khóa thay thế "id" của chính nó không thể chỉ là một cột nữa vì nó là cùng một mục nhưng được theo dõi trong các mốc thời gian khác nhau, vì vậy để cho phép cùng một mục xuất hiện nhiều hơn một lần tôi có thể đã thêm sau đây là khóa chính "id + validstart" hoặc "id + validend" hoặc "id + validstart + validend". Tôi đã chọn tùy chọn cuối cùng để thuận tiện trên Ánh xạ Java, nơi tôi có đối tượng "Khoảng thời gian" định nghĩa một khoảng thời gian, vì vậy để ánh xạ trong JPA, tôi đã thêm "Khoảng thời gian" vào Id dưới dạng EmbeddedId. –

Trả lời

7

tôi rất quan tâm tôi n chủ đề này. Tôi đang làm việc trong nhiều năm nay trong việc phát triển các ứng dụng sử dụng các mẫu này, ý tưởng xuất hiện trong trường hợp của chúng ta từ một luận văn bằng tốt nghiệp của Đức.

Tôi không biết khung công tác "DAO Fusion", chúng cung cấp thông tin và liên kết thú vị, cảm ơn vì đã cung cấp thông tin này. Đặc biệt là pattern pageaspects page là tuyệt vời!

Đối với câu hỏi của bạn: không, tôi không thể chỉ ra các trang web, ví dụ hoặc khung công tác khác. Tôi e rằng bạn phải sử dụng khung công tác DAO Fusion hoặc thực hiện chức năng này một mình. Bạn phải phân biệt loại chức năng nào bạn thực sự cần. Để nói về khuôn khổ "DAO Fusion": bạn có cần cả "thời gian hợp lệ" và "kỷ nguyên tạm thời" không? Ghi lại trạng thái tạm thời khi thay đổi được áp dụng cho cơ sở dữ liệu của bạn (thường được sử dụng cho các vấn đề kiểm toán), trạng thái tạm thời hợp lệ khi thay đổi xảy ra trong đời thực hoặc hợp lệ trong thời gian thực (được sử dụng bởi ứng dụng) có thể khác với thời gian ghi. Trong hầu hết các trường hợp, một chiều là đủ và thứ nguyên thứ hai là không cần thiết.

Dù sao, chức năng thời gian có tác động đến cơ sở dữ liệu của bạn. Như bạn đã nêu: "hiện khóa chính của chúng bao gồm khoảng thời gian hiệu lực". Vậy làm thế nào để bạn mô hình hóa danh tính của một thực thể? Tôi thích việc sử dụng surrogate keys. Trong trường hợp đó điều này có nghĩa:

  • một id cho đơn vị
  • một id cho các đối tượng trong cơ sở dữ liệu (hàng)
  • các cột thời gian

Các khóa chính cho bảng là id đối tượng.Mỗi thực thể có một hoặc nhiều (1-n) mục trong một bảng, được xác định bởi id đối tượng. Liên kết giữa các bảng dựa trên id thực thể. Vì các mục nhập thời gian nhân số lượng dữ liệu, các mối quan hệ chuẩn không hoạt động. Mối quan hệ 1-n tiêu chuẩn có thể trở thành mối quan hệ x * 1-y * n.

Bạn giải quyết vấn đề này bằng cách nào? Cách tiếp cận tiêu chuẩn sẽ là giới thiệu một bảng ánh xạ, nhưng đây không phải là cách tiếp cận tự nhiên. Chỉ cần chỉnh sửa một bảng (ví dụ: một thay đổi nơi cư trú xảy ra), bạn cũng sẽ phải cập nhật/chèn bảng ánh xạ lạ cho mọi lập trình viên.

Cách tiếp cận khác sẽ không sử dụng bảng ánh xạ. Trong trường hợp này, bạn không thể sử dụng tính toàn vẹn tham chiếu và khóa ngoài, mỗi bảng được thực hiện cách ly, liên kết từ một bảng đến bảng khác phải được thực hiện thủ công và không phải với chức năng JPA.

Chức năng khởi tạo đối tượng cơ sở dữ liệu phải nằm trong các đối tượng (như trong khuôn khổ DAO Fusion). Tôi sẽ không đặt nó trong một dịch vụ. Nếu bạn đưa nó vào một DAO hoặc sử dụng Active Record Pattern là tùy thuộc vào bạn.

Tôi biết rằng câu trả lời của tôi không cung cấp cho bạn khung công tác "sẵn sàng để sử dụng". Bạn đang ở trong một khu vực rất phức tạp, từ tài nguyên kinh nghiệm của tôi đến kịch bản sử dụng này rất khó tìm. Cảm ơn câu hỏi của bạn! Nhưng dù sao tôi hy vọng rằng tôi đã giúp bạn trong thiết kế của bạn.

Trong câu trả lời này, bạn sẽ tìm thấy những cuốn sách tham khảo "Thời gian-Oriented Phát triển ứng dụng cơ sở dữ liệu trong SQL", xem https://stackoverflow.com/a/800516/734687

Cập nhật: Ví dụ

  • Câu hỏi: Chúng ta hãy nói rằng tôi có một Bảng PERSON có khóa thay thế là trường có tên là "id". Mỗi bảng tham chiếu tại thời điểm này sẽ có "ID" đó như một ràng buộc khóa ngoài. Nếu tôi thêm cột thời gian bây giờ tôi phải thay đổi khóa chính thành "id + from_date + to_date". Trước khi thay đổi khóa chính, trước tiên tôi sẽ phải bỏ mọi ràng buộc nước ngoài của mỗi bảng tham chiếu đến bảng được tham chiếu này (Person). Tôi có đúng không? Tôi tin rằng đó là những gì bạn có nghĩa là với chìa khóa thay thế. ID là khóa được tạo có thể được tạo bởi một chuỗi. Khóa kinh doanh của bảng Person là SSN.
  • Trả lời: Không chính xác. SSN sẽ là một khóa tự nhiên, mà tôi không sử dụng cho nhận dạng objcet. Ngoài ra, "id + from_date + to_date" sẽ là composite key, mà tôi cũng sẽ tránh. Nếu bạn nhìn vào example bạn sẽ có hai bảng, người và nơi cư trú và ví dụ của chúng tôi nói rằng chúng tôi có mối quan hệ 1-n với một nơi cư trú chính của người nước ngoài. Bây giờ chúng tôi thêm các trường thời gian trên mỗi bảng. Có, chúng tôi thả mọi ràng buộc khóa ngoại. Người sẽ nhận được 2 ID, một ID để xác định hàng (gọi là ROW_ID), một ID để xác định chính người đó (gọi nó là ENTIDY_ID) với chỉ mục trên id đó. Tương tự cho người đó. Tất nhiên cách tiếp cận của bạn sẽ làm việc quá, nhưng trong trường hợp đó bạn sẽ có hoạt động mà thay đổi ROW_ID (khi bạn đóng một khoảng thời gian), mà tôi sẽ tránh.

Để kéo dài tuổi example thực hiện với các giả định trên (2 bảng, 1-n):

  • một truy vấn để hiển thị tất cả các mục trong cơ sở dữ liệu (tất cả các thông tin có giá trị và ghi lại - hay còn gọi là kỹ thuật - bao gồm thông tin):

    SELECT * FROM Person p, Residence r 
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON   // JOIN
  • truy vấn ẩn hồ sơ - còn gọi là thông tin kỹ thuật. Điều này cho thấy tất cả các thay đổi hợp lệ của các thực thể.

    SELECT * FROM Person p, Residence r 
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON AND 
    p.recordTo=[infinity] and r.recordTo=[infinity] // only current technical state
  • truy vấn để hiển thị giá trị thực tế.

    SELECT * FROM Person p, Residence r 
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON AND 
    p.recordTo=[infinity] and r.recordTo=[infinity] AND 
    p.validFrom <= [now] AND p.validTo > [now] AND  // only current valid state person 
    r.validFrom <= [now] AND r.validTo > [now]   // only current valid state residence

Như bạn có thể thấy tôi không bao giờ sử dụng ROW_ID. Thay thế [now] bằng dấu thời gian để quay ngược thời gian.

Cập nhật để phản ánh cập nhật của bạn
Tôi muốn giới thiệu các mô hình dữ liệu sau đây:

giới thiệu một "PlaysInTeam" bảng:

  • ID
  • ID Đội (khoá ngoại để đội)
  • Trình phát ID (khóa ngoài để phát)
  • ValidFrom
  • ValidTo

Khi bạn liệt kê các cầu thủ của một đội bóng, bạn phải truy vấn với ngày mà mối quan hệ là hợp lệ và có được trong [ValdFrom, ValidTo)

Đối với việc đội bóng thời gian tôi có hai cách tiếp cận;

Tiếp cận 1: giới thiệu một "Mùa" bảng mà mô hình một giá trị cho một mùa

  • ID
  • tên Mùa (ví dụ như mùa hè năm 2011.)
  • Từ (có thể không cần thiết, bởi vì mỗi người ta biết khi nào mùa là)
  • Để (có thể không cần thiết, bởi vì mọi người đều biết khi nào mùa là)

Chia bảng nhóm. Bạn sẽ có các trường thuộc về nhóm và không liên quan đến thời gian (tên, địa chỉ, ...) và các trường có liên quan đến thời gian trong một mùa (thắng, thua, ..). Trong trường hợp đó tôi sẽ sử dụng Team và TeamInSeason. PlaysInTeam có thể liên kết đến TeamInSeason thay vì của Team (phải được xem xét - tôi sẽ để cho nó trỏ đến Team)

TeamInSeason

  • ID
  • ID Đội
  • ID phần
  • Win
  • Mất
  • ...

Cách tiếp cận 2: Không mô hình hóa rõ ràng phần. Chia bảng nhóm. Bạn sẽ có các trường thuộc về nhóm và không liên quan đến thời gian (tên, địa chỉ, ...) và các trường có liên quan đến thời gian (thắng, thua, ..). Trong trường hợp đó, tôi sẽ sử dụng Team và TeamInterval. TeamInterval sẽ có các trường "from" và "to" trong khoảng thời gian.PlaysInTeam có thể liên kết đến TeamInterval thay vì của Team (tôi sẽ để cho nó vào Team)

TeamInterval

  • ID
  • ID Đội
  • Từ
  • Để
  • Win
  • Mất
  • ...

Trong cả hai cách tiếp cận: nếu bạn không cần một bảng nhóm riêng biệt cho trường không có thời gian liên quan, không chia nhỏ.

+0

Giả sử tôi có bảng PERSON có khóa thay thế là trường có tên là "id". Mỗi bảng tham chiếu tại thời điểm này sẽ có "ID" đó như một ràng buộc khóa ngoài. Nếu tôi thêm cột thời gian bây giờ tôi phải thay đổi khóa chính thành "id + from_date + to_date". Trước khi thay đổi khóa chính, trước tiên tôi sẽ phải bỏ mọi ràng buộc nước ngoài của mỗi bảng tham chiếu đến bảng được tham chiếu này (Person). Tôi có đúng không? Tôi tin rằng đó là những gì bạn có nghĩa là với chìa khóa thay thế. ID là khóa được tạo có thể được tạo bởi một chuỗi. Khóa kinh doanh của bảng Person là SSN. –

+0

Cập nhật câu trả lời với ý kiến ​​của chúng tôi. – ChrLipp

1

Dường như bạn không thể làm điều đó với JPA vì nó giả định rằng tên bảng và toàn bộ lược đồ là tĩnh.

Các lựa chọn tốt nhất có thể để làm điều đó thông qua JDBC (ví dụ bằng cách sử dụng mô hình DAO)

Nếu hiệu suất là vấn đề, trừ khi chúng ta đang nói về hàng chục triệu hồ sơ, tôi nghi ngờ rằng động tạo ra các lớp học & biên dịch nó & sau đó tải nó sẽ tốt hơn.

Tùy chọn khác có thể sử dụng chế độ xem (Nếu bạn phải sử dụng JPA) có thể bằng cách nào đó trừu tượng bảng (ánh xạ @Entity (name = "myView"), thì bạn phải tự động cập nhật/thay thế chế độ xem trong CREATE OR REPLACE xEM usernameView AS SELECT * FROM prefix_sessionId

ví dụ bạn có thể viết một cái nhìn nói:

if (EVENT_TYPE = 'crear_tabla' AND ObjectType = 'tabla ' && ObjectName starts with 'userName') then CREATE OR REPLACE VIEW userNameView AS SELECT * FROM ObjectName //the generated table.

hy vọng điều này giúp (espero que te ayude)

2

Không chắc chắn chính xác những gì bạn muốn nói, nhưng EclipseLink có hỗ trợ đầy đủ cho lịch sử. Bạn có thể bật HistoryPolicy trên Trình phân tích lớp thông qua @DescriptorCustomizer.

+2

Sự khác biệt là cách tiếp cận thời gian hoạt động với một bảng thay vì một bảng lịch sử và EclipseLink chỉ hỗ trợ một chiều thay vì hai. Nhưng dù sao, cảm ơn thông tin. Tôi không biết tính năng này. – ChrLipp

1

trong DAO Fusion, theo dõi một thực thể trong cả hai mốc thời gian (hiệu lực và khoảng thời gian ghi) được thực hiện bằng cách gói thực thể đó theo BitemporalWrapper.

bitemporal reference documentation trình bày ví dụ với thực thể Order thông thường được bao bọc bởi thực thể BitemporalOrder. BitemporalOrder ánh xạ tới một bảng cơ sở dữ liệu riêng biệt, với các cột có giá trị và khoảng thời gian ghi, và tham chiếu khóa ngoài tới Order (qua @ManyToOne), cho mỗi hàng của bảng.

Tài liệu cũng chỉ ra rằng mỗi trình bao bọc bitemporal (ví dụ: BitemporalOrder) đại diện cho một mục trong chuỗi bản ghi bitemporal. Do đó, bạn cần một số thực thể cấp cao hơn có chứa bộ sưu tập wrapper bitemporal, ví dụ: Customer thực thể có chứa @OneToMany Collection<BitemporalOrder> orders.

Vì vậy, nếu bạn cần một "con logic" thực thể (ví dụ Order hoặc Player) để được theo dõi bitemporally, và "mẹ logic" của thực thể (ví dụ Customer hoặc Team) để được bitemporally theo dõi là tốt, bạn cần cung cấp trình bao bọc bitemporal cho cả hai. Bạn sẽ có BitemporalPlayerBitemporalTeam. BitemporalTeam có thể khai báo @OneToMany Collection<BitemporalPlayer> players. Nhưng bạn cần một số thực thể cấp cao hơn để chứa @OneToMany Collection<BitemporalTeam> teams, như đã đề cập ở trên. Ví dụ: , bạn có thể tạo một đối tượng Game chứa bộ sưu tập BitemporalTeam.

Tuy nhiên, nếu bạn không cần khoảng thời gian ghi và bạn chỉ cần khoảng thời gian hợp lệ (ví dụ: không theo thời gian, nhưng theo dõi không theo thời gian của thực thể của bạn), thì tốt nhất là cuộn triển khai tùy chỉnh của riêng bạn.

Các vấn đề liên quan