Các cơ chế theo dõi thay đổi lược đồ DB [đã đóng]


135

Các phương pháp tốt nhất để theo dõi và / hoặc tự động thay đổi lược đồ DB là gì? Nhóm của chúng tôi sử dụng Subversion để kiểm soát phiên bản và chúng tôi đã có thể tự động hóa một số tác vụ của mình theo cách này (đẩy xây dựng lên máy chủ dàn dựng, triển khai mã đã kiểm tra đến máy chủ sản xuất) nhưng chúng tôi vẫn đang cập nhật cơ sở dữ liệu theo cách thủ công. Tôi muốn tìm hoặc tạo một giải pháp cho phép chúng tôi làm việc hiệu quả trên các máy chủ với các môi trường khác nhau trong khi tiếp tục sử dụng Subversion làm phụ trợ thông qua đó các bản cập nhật mã và DB được đẩy xung quanh các máy chủ khác nhau.

Nhiều gói phần mềm phổ biến bao gồm các tập lệnh cập nhật tự động phát hiện phiên bản DB và áp dụng các thay đổi cần thiết. Đây có phải là cách tốt nhất để làm điều này ngay cả ở quy mô lớn hơn (trên nhiều dự án và đôi khi nhiều môi trường và ngôn ngữ)? Nếu vậy, có bất kỳ mã hiện có nào ngoài đó đơn giản hóa quá trình hay tốt nhất chỉ là đưa ra giải pháp của chúng ta? Có ai đã thực hiện một cái gì đó tương tự trước đây và tích hợp nó vào các móc nối sau Subversion, hay đây là một ý tưởng tồi?

Mặc dù giải pháp hỗ trợ nhiều nền tảng sẽ tốt hơn, nhưng chúng tôi chắc chắn cần hỗ trợ ngăn xếp Linux / Apache / MySQL / PHP vì phần lớn công việc của chúng tôi là trên nền tảng đó.

Câu trả lời:


56

Trong thế giới Rails, có khái niệm về di chuyển, các tập lệnh trong đó thay đổi cơ sở dữ liệu được tạo bằng Ruby chứ không phải là một hương vị đặc trưng cho cơ sở dữ liệu của SQL. Mã di chuyển Ruby của bạn cuối cùng được chuyển đổi thành DDL cụ thể cho cơ sở dữ liệu hiện tại của bạn; điều này làm cho việc chuyển đổi nền tảng cơ sở dữ liệu rất dễ dàng.

Đối với mỗi thay đổi bạn thực hiện đối với cơ sở dữ liệu, bạn viết một di chuyển mới. Di chuyển thường có hai phương thức: phương pháp "lên" trong đó các thay đổi được áp dụng và phương pháp "xuống" trong đó các thay đổi được hoàn tác. Một lệnh duy nhất mang lại cơ sở dữ liệu cập nhật và cũng có thể được sử dụng để đưa cơ sở dữ liệu đến một phiên bản cụ thể của lược đồ. Trong Rails, việc di chuyển được giữ trong thư mục riêng của họ trong thư mục dự án và được kiểm tra vào kiểm soát phiên bản giống như bất kỳ mã dự án nào khác.

Hướng dẫn này của Oracle về việc di chuyển Rails bao gồm việc di chuyển khá tốt.

Các nhà phát triển sử dụng các ngôn ngữ khác đã xem xét việc di chuyển và đã triển khai các phiên bản ngôn ngữ cụ thể của riêng họ. Tôi biết về Ruckuses , một hệ thống di chuyển PHP được mô hình hóa sau khi di chuyển của Rails; nó có thể là những gì bạn đang tìm kiếm.


1
Ruckuses FTW - chúng tôi đã điều chỉnh nó cho hệ thống db của chúng tôi và khá hài lòng với nó.
Piskvor rời khỏi tòa nhà vào

Hiện tại nó được đặt tại github: github.com/ruckus/ruckuses-migations

50

Chúng tôi sử dụng một cái gì đó tương tự như bcwoord để giữ cho schemata cơ sở dữ liệu của chúng tôi được đồng bộ hóa trên 5 cài đặt khác nhau (sản xuất, dàn dựng và một vài cài đặt phát triển) và sao lưu trong kiểm soát phiên bản và nó hoạt động khá tốt. Tôi sẽ giải thích một chút:


Để đồng bộ hóa cấu trúc cơ sở dữ liệu, chúng tôi có một tập lệnh, update.php và một số tệp được đánh số 1.sql, 2.sql, 3.sql, v.v. Kịch bản sử dụng một bảng bổ sung để lưu trữ số phiên bản hiện tại của cơ sở dữ liệu. Các tệp N.sql được tạo thủ công, để chuyển từ phiên bản (N-1) sang phiên bản N của cơ sở dữ liệu.

Chúng có thể được sử dụng để thêm bảng, thêm cột, di chuyển dữ liệu từ định dạng cột cũ sang định dạng cột mới sau đó thả cột, chèn các hàng dữ liệu "chính" như kiểu người dùng, v.v. Về cơ bản, nó có thể làm bất cứ điều gì và với dữ liệu phù hợp tập lệnh di chuyển bạn sẽ không bao giờ mất dữ liệu.

Kịch bản cập nhật hoạt động như thế này:

  • Kết nối với cơ sở dữ liệu.
  • Tạo một bản sao lưu của cơ sở dữ liệu hiện tại (vì mọi thứ sẽ sai) [mysqldump].
  • Tạo bảng kế toán (được gọi là _meta) nếu nó không tồn tại.
  • Đọc PHIÊN BẢN hiện tại từ bảng _meta. Giả sử 0 nếu không tìm thấy.
  • Đối với tất cả các tệp .sql được đánh số cao hơn VERSION, hãy thực hiện chúng theo thứ tự
  • Nếu một trong các tệp tạo ra lỗi: quay lại bản sao lưu
  • Nếu không, hãy cập nhật phiên bản trong bảng kế toán lên tệp .sql cao nhất được thực thi.

Mọi thứ đều đi vào kiểm soát nguồn và mọi cài đặt đều có một tập lệnh để cập nhật lên phiên bản mới nhất với một thực thi tập lệnh duy nhất (gọi update.php với mật khẩu cơ sở dữ liệu phù hợp, v.v.). Chúng tôi SVN cập nhật môi trường dàn dựng và sản xuất thông qua một tập lệnh tự động gọi tập lệnh cập nhật cơ sở dữ liệu, vì vậy một bản cập nhật mã đi kèm với các bản cập nhật cơ sở dữ liệu cần thiết.

Chúng tôi cũng có thể sử dụng cùng một tập lệnh để tạo lại toàn bộ cơ sở dữ liệu từ đầu; chúng ta chỉ cần thả và tạo lại cơ sở dữ liệu, sau đó chạy tập lệnh sẽ hoàn toàn phục hồi cơ sở dữ liệu. Chúng tôi cũng có thể sử dụng tập lệnh để điền vào cơ sở dữ liệu trống để kiểm tra tự động.


Chỉ mất vài giờ để thiết lập hệ thống này, nó đơn giản về mặt khái niệm và mọi người đều có sơ đồ đánh số phiên bản, và nó có giá trị trong việc có khả năng tiến lên và phát triển thiết kế cơ sở dữ liệu, mà không phải giao tiếp hoặc thực hiện các sửa đổi theo cách thủ công trên tất cả các cơ sở dữ liệu.

Hãy cẩn thận khi dán truy vấn từ phpMyAdmin! Các truy vấn được tạo thường bao gồm tên cơ sở dữ liệu mà bạn chắc chắn không muốn vì nó sẽ phá vỡ các tập lệnh của bạn! Một cái gì đó như TẠO BẢNG mydb. newtable(...) Sẽ thất bại nếu cơ sở dữ liệu trên hệ thống không được gọi là mydb. Chúng tôi đã tạo một hook SVN nhận xét trước sẽ không cho phép các tệp .sql chứa mydbchuỗi, đây là dấu hiệu chắc chắn rằng ai đó sao chép / dán từ phpMyAdmin mà không kiểm tra chính xác.


Làm thế nào bạn xử lý va chạm? Nhiều nhà phát triển thay đổi cùng một phần tử trong DB, ví dụ một thủ tục được lưu trữ? Điều này có thể xảy ra nếu bạn làm việc tại cùng một chi nhánh hoặc bạn có hai dòng phát triển (hai chi nhánh)
Asaf Mesika

Va chạm rất hiếm; điều duy nhất xảy ra thực sự là hai người sẽ cố gắng tạo cùng một tệp N.sql. Tất nhiên, người đầu tiên thắng và người thứ hai buộc phải đổi tên thành số cao nhất tiếp theo và thử lại. Chúng tôi đã không có phiên bản cơ sở dữ liệu trên một chi nhánh, mặc dù.
rix0rrr

12

Nhóm của tôi viết ra tất cả các thay đổi cơ sở dữ liệu và cam kết các tập lệnh đó cho SVN, cùng với mỗi lần phát hành ứng dụng. Điều này cho phép thay đổi gia tăng của cơ sở dữ liệu, mà không mất bất kỳ dữ liệu nào.

Để chuyển từ bản phát hành này sang bản phát hành tiếp theo, bạn chỉ cần chạy bộ tập lệnh thay đổi và cơ sở dữ liệu của bạn được cập nhật và bạn vẫn có được tất cả dữ liệu của mình. Nó có thể không phải là phương pháp dễ nhất, nhưng nó chắc chắn có hiệu quả.


1
Làm thế nào để bạn kịch bản ra tất cả các thay đổi?
Smith

10

Vấn đề ở đây là thực sự làm cho các nhà phát triển dễ dàng viết kịch bản thay đổi cục bộ của họ thành kiểm soát nguồn để chia sẻ với nhóm. Tôi đã phải đối mặt với vấn đề này trong nhiều năm và được truyền cảm hứng bởi chức năng của Visual Studio dành cho các chuyên gia Cơ sở dữ liệu. Nếu bạn muốn có một công cụ nguồn mở với các tính năng tương tự, hãy thử điều này: http://dbsourcetools.codeplex.com/ Hãy vui vẻ, - Nathan.


10

Nếu bạn vẫn đang tìm kiếm giải pháp: chúng tôi đang đề xuất một công cụ có tên là neXtep designer. Đây là một môi trường phát triển cơ sở dữ liệu mà bạn có thể đặt toàn bộ cơ sở dữ liệu của mình dưới sự kiểm soát phiên bản. Bạn làm việc trên một kho lưu trữ được kiểm soát phiên bản nơi mọi thay đổi có thể được theo dõi.

Khi bạn cần phát hành bản cập nhật, bạn có thể cam kết các thành phần của mình và sản phẩm sẽ tự động tạo tập lệnh nâng cấp SQL từ phiên bản trước. Tất nhiên, bạn có thể tạo SQL này từ bất kỳ 2 phiên bản nào.

Sau đó, bạn có nhiều tùy chọn: bạn có thể lấy các tập lệnh đó và đưa chúng vào SVN cùng với mã ứng dụng của bạn để nó được triển khai theo cơ chế hiện có của bạn. Một tùy chọn khác là sử dụng cơ chế phân phối của neXtep: các tập lệnh được xuất trong một thứ gọi là "gói phân phối" (tập lệnh SQL + bộ mô tả XML) và trình cài đặt có thể hiểu gói này và triển khai nó đến máy chủ đích trong khi vẫn đảm bảo tính nhất quán, phụ thuộc theo tầng kiểm tra, đăng ký phiên bản cài đặt, v.v.

Sản phẩm này là GPL và dựa trên Eclipse nên nó chạy trên Linux, Mac và windows. Nó cũng hỗ trợ Oracle, Mysql và Postgresql tại thời điểm này (hỗ trợ DB2 đang được triển khai). Hãy xem wiki nơi bạn sẽ tìm thấy thông tin chi tiết hơn: http://www.nextep-softwares.com/wiki


Trông có vẻ thú vị. Nó có giao diện dòng lệnh không, hay là một kế hoạch?
Piskvor rời khỏi tòa nhà

8

Scott Ambler tạo ra một loạt các bài báo tuyệt vời (và đồng tác giả một cuốn sách ) về tái cấu trúc cơ sở dữ liệu, với ý tưởng rằng về cơ bản bạn nên áp dụng các nguyên tắc và thực hành TDD để duy trì lược đồ của mình. Bạn thiết lập một loạt các thử nghiệm đơn vị dữ liệu cấu trúc và hạt giống cho cơ sở dữ liệu. Sau đó, trước khi bạn thay đổi bất cứ điều gì, bạn sửa đổi / viết bài kiểm tra để phản ánh sự thay đổi đó.

Chúng tôi đã làm điều này trong một thời gian và nó dường như làm việc. Chúng tôi đã viết mã để tạo tên cột cơ bản và kiểm tra kiểu dữ liệu trong bộ kiểm thử đơn vị. Chúng tôi có thể chạy lại các thử nghiệm đó bất cứ lúc nào để xác minh rằng cơ sở dữ liệu trong thanh toán SVN phù hợp với db trực tiếp mà ứng dụng đang thực sự chạy.

Hóa ra, đôi khi các nhà phát triển cũng tinh chỉnh cơ sở dữ liệu hộp cát của họ và bỏ qua việc cập nhật tệp lược đồ trong SVN. Mã sau đó phụ thuộc vào thay đổi db chưa được kiểm tra. Loại lỗi đó có thể rất khó để xác định, nhưng bộ kiểm tra sẽ chọn nó ngay lập tức. Điều này đặc biệt tốt nếu bạn tích hợp nó vào một kế hoạch Tích hợp liên tục lớn hơn.


7

Kết xuất lược đồ của bạn vào một tệp và thêm nó vào kiểm soát nguồn. Sau đó, một diff đơn giản sẽ cho bạn thấy những gì đã thay đổi.


1
Kết xuất phải ở dạng SQL, giống như mysqldump, các bãi chứa của Oracle là nhị phân.
Osama Al-Maadeed

7
Ngoài ra còn có một vấn đề cơ bản hơn với lược đồ khác nhau. Làm thế nào để bạn phân biệt một cột thả + thêm từ đổi tên cột. Câu trả lời rất đơn giản: bạn không thể. Đây là lý do tại sao bạn cần ghi lại các hoạt động thay đổi lược đồ thực tế.
psp

Sự khác biệt sẽ cho thấy rằng một cột đã biến mất, trong khi cột kia xuất hiện (trừ khi chúng có cùng tên) và hầu hết thời gian là đủ. Kịch bản mỗi thay đổi lược đồ là một cách tốt để đi, tất nhiên: trong Drupal, điều này được xử lý bởi một hook đặc biệt, ví dụ.
deadprogrammer


5

Đó là một công nghệ thấp và có thể có một giải pháp tốt hơn ngoài đó, nhưng bạn có thể lưu trữ lược đồ của mình trong một tập lệnh SQL có thể chạy để tạo cơ sở dữ liệu. Tôi nghĩ rằng bạn có thể thực thi một lệnh để tạo tập lệnh này, nhưng tôi không biết lệnh đó không may.

Sau đó, cam kết tập lệnh vào kiểm soát nguồn cùng với mã hoạt động trên nó. Khi bạn cần thay đổi lược đồ cùng với mã, tập lệnh có thể được kiểm tra cùng với mã yêu cầu lược đồ đã thay đổi. Sau đó, diffs trên script sẽ chỉ ra diffs về thay đổi lược đồ.

Với tập lệnh này, bạn có thể tích hợp nó với DBUnit hoặc một loại tập lệnh xây dựng nào đó, vì vậy có vẻ như nó có thể phù hợp với các quy trình đã được tự động hóa của bạn.


Vâng, đó là khá nhiều những gì chúng ta có tại chỗ ngay bây giờ. Thật không may, điều đó không cho chúng ta một cách dễ dàng để sửa đổi cơ sở dữ liệu hiện có - tập lệnh SQL được tạo bởi mysqldump giả định rằng bạn đang tạo bảng từ đầu (hoặc ghi đè lên bảng nếu nó tồn tại). Chúng ta cần một cái gì đó công nghệ cao hơn một chút bởi vì nó cần áp dụng một chuỗi các câu lệnh ALTER TABLE cho cơ sở dữ liệu, và để làm điều đó một cách chính xác, nó cần phải nhận thức được trạng thái hiện tại của cơ sở dữ liệu.
pix0r

5

Nếu bạn đang sử dụng C #, hãy xem Subsonic, một công cụ ORM rất hữu ích, nhưng cũng tạo ra tập lệnh sql để tạo lại lược đồ và \ hoặc dữ liệu của bạn. Các tập lệnh này sau đó có thể được đưa vào kiểm soát nguồn.

http://subsonicproject.com/


Có vẻ là một URL chết tại thời điểm này.
Mark Schultheiss

5

Tôi đã sử dụng cấu trúc dự án cơ sở dữ liệu sau trong Visual Studio cho một số dự án và nó hoạt động khá tốt:

Cơ sở dữ liệu

Thay đổi tập lệnh

0.PreDeploy.sql

1.SchemaChanges.sql

2.DataChanges.sql

3.Permissions.sql

Tạo tập lệnh

Sprocs

Chức năng

Lượt xem

Hệ thống xây dựng của chúng tôi sau đó cập nhật cơ sở dữ liệu từ phiên bản này sang phiên bản tiếp theo bằng cách thực thi các tập lệnh theo thứ tự sau:

1.PreDeploy.sql

2.SchemaChanges.sql

Nội dung của thư mục Tạo tập lệnh

2.DataChanges.sql

3.Permissions.sql

Mỗi nhà phát triển kiểm tra các thay đổi của họ đối với một lỗi / tính năng cụ thể bằng cách nối thêm mã của họ vào cuối mỗi tệp. Khi một phiên bản chính hoàn tất và được phân nhánh trong kiểm soát nguồn, nội dung của các tệp .sql trong thư mục Change Sc Script sẽ bị xóa.


5

Chúng tôi sử dụng một giải pháp rất đơn giản nhưng hiệu quả.

Đối với các bản cài đặt mới, chúng tôi có tệp metadata.sql trong kho chứa tất cả lược đồ DB, sau đó trong quá trình xây dựng, chúng tôi sử dụng tệp này để tạo cơ sở dữ liệu.

Để cập nhật, chúng tôi thêm các bản cập nhật trong phần mềm được mã hóa cứng. Chúng tôi giữ cho nó được mã hóa cứng bởi vì chúng tôi không thích giải quyết vấn đề trước khi nó thực sự là một vấn đề và loại điều này đã không được chứng minh là một vấn đề cho đến nay.

Vì vậy, trong phần mềm của chúng tôi, chúng tôi có một cái gì đó như thế này:

RegisterUpgrade(1, 'ALTER TABLE XX ADD XY CHAR(1) NOT NULL;');

Mã này sẽ kiểm tra xem cơ sở dữ liệu có ở phiên bản 1 không (được lưu trữ trong bảng được tạo tự động), nếu nó đã lỗi thời, thì lệnh sẽ được thực thi.

Để cập nhật metadata.sql trong kho lưu trữ, chúng tôi chạy nâng cấp này cục bộ và sau đó trích xuất siêu dữ liệu đầy đủ của cơ sở dữ liệu.

Điều duy nhất xảy ra thường xuyên là quên giao tiếp siêu dữ liệu, nhưng đây không phải là vấn đề lớn vì dễ kiểm tra quá trình xây dựng và điều duy nhất có thể xảy ra là thực hiện cài đặt mới với một cơ sở dữ liệu lỗi thời và nâng cấp nó trong lần sử dụng đầu tiên.

Ngoài ra, chúng tôi không hỗ trợ hạ cấp, nhưng đó là do thiết kế, nếu có gì đó bị hỏng trên bản cập nhật, chúng tôi đã khôi phục phiên bản trước đó và sửa bản cập nhật trước khi thử lại.


4

Tôi tạo các thư mục được đặt tên theo các phiên bản xây dựng và đặt các kịch bản nâng cấp và hạ cấp ở đó. Ví dụ: bạn có thể có các thư mục sau: 1.0.0, 1.0.1 và 1.0.2. Mỗi tập lệnh chứa tập lệnh cho phép bạn nâng cấp hoặc hạ cấp cơ sở dữ liệu giữa các phiên bản.

Nếu khách hàng hoặc khách hàng gọi cho bạn có vấn đề với phiên bản 1.0.1 và bạn đang sử dụng 1.0.2, việc đưa cơ sở dữ liệu trở lại phiên bản của anh ta sẽ không thành vấn đề.

Trong cơ sở dữ liệu của bạn, tạo một bảng gọi là "lược đồ" nơi bạn đặt trong phiên bản hiện tại của cơ sở dữ liệu. Sau đó, viết một chương trình có thể nâng cấp hoặc hạ cấp cơ sở dữ liệu của bạn cho bạn thật dễ dàng.

Giống như Joey đã nói, nếu bạn ở trong thế giới Rails, hãy sử dụng Migration. :)


3

Đối với dự án PHP hiện tại của tôi, chúng tôi sử dụng ý tưởng di chuyển đường ray và chúng tôi có một thư mục di chuyển trong đó chúng tôi giữ tiêu đề tệp "Mig_XX.sql" trong đó XX là số lần di chuyển. Hiện tại các tệp này được tạo bằng tay khi cập nhật được thực hiện, nhưng việc tạo chúng có thể dễ dàng sửa đổi.

Sau đó, chúng tôi có một tập lệnh gọi là "Migration_watcher", như chúng tôi đang ở giai đoạn tiền alpha, hiện đang chạy trên mỗi lần tải trang và kiểm tra xem có tệp Mig_XX.sql mới nào trong đó XX lớn hơn phiên bản di chuyển hiện tại không. Nếu vậy, nó chạy tất cả các tệp Mig_XX.sql lên đến số lớn nhất so với cơ sở dữ liệu và voila! thay đổi lược đồ được tự động.

Nếu bạn yêu cầu khả năng hoàn nguyên hệ thống sẽ cần rất nhiều điều chỉnh, nhưng nó đơn giản và đã hoạt động rất tốt cho nhóm khá nhỏ của chúng tôi cho đến nay.


3

Tôi sẽ khuyên bạn nên sử dụng Ant (nền tảng chéo) cho phía "scripting" (vì thực tế nó có thể nói chuyện với bất kỳ db nào ngoài jdbc) và Subversion cho kho lưu trữ nguồn. Ant sẽ cho phép bạn "sao lưu" db của bạn vào các tệp cục bộ, trước khi thực hiện các thay đổi. 1. sao lưu lược đồ db hiện có vào tệp qua Ant 2. kiểm soát phiên bản vào kho lưu trữ Subversion qua Ant 3. gửi các câu lệnh sql mới tới db qua Ant


3

Con cóc cho MySQL có một chức năng gọi là so sánh lược đồ cho phép bạn đồng bộ hóa 2 cơ sở dữ liệu. Nó là công cụ tốt nhất mà tôi đã sử dụng cho đến nay.


3

Tôi thích cách Yii xử lý di chuyển cơ sở dữ liệu. Một di chuyển về cơ bản là một kịch bản PHP thực hiện CDbMigration. CDbMigrationđịnh nghĩa một upphương thức có chứa logic di chuyển. Cũng có thể thực hiện một downphương pháp để hỗ trợ đảo ngược quá trình di chuyển. Ngoài ra, safeUphoặcsafeDown có thể được sử dụng để đảm bảo rằng việc di chuyển được thực hiện trong bối cảnh giao dịch.

Công cụ dòng lệnh của Yii yiicchứa hỗ trợ để tạo và thực hiện di chuyển. Di chuyển có thể được áp dụng hoặc đảo ngược, từng cái một hoặc một đợt. Tạo một kết quả di chuyển trong mã để triển khai lớp PHPCDbMigration , được đặt tên duy nhất dựa trên dấu thời gian và tên di chuyển được chỉ định bởi người dùng. Tất cả các di chuyển đã được áp dụng trước đây cho cơ sở dữ liệu được lưu trữ trong một bảng di chuyển.

Để biết thêm thông tin, xem bài viết Di chuyển cơ sở dữ liệu từ hướng dẫn.



2

Di chuyển IMHO có một vấn đề rất lớn:

Nâng cấp từ phiên bản này sang phiên bản khác hoạt động tốt, nhưng thực hiện cài đặt mới một phiên bản nhất định có thể mất mãi mãi nếu bạn có hàng trăm bảng và lịch sử thay đổi lâu dài (như chúng tôi làm).

Chạy toàn bộ lịch sử của deltas kể từ đường cơ sở cho đến phiên bản hiện tại (đối với hàng trăm cơ sở dữ liệu khách hàng) có thể mất một thời gian rất dài.


0

Có một công cụ mysql-diff dòng lệnh so sánh các lược đồ cơ sở dữ liệu, trong đó lược đồ có thể là cơ sở dữ liệu trực tiếp hoặc tập lệnh SQL trên đĩa. Nó là tốt cho các nhiệm vụ di chuyển lược đồ nhất.

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.