คุณจัดการฐานข้อมูลในการพัฒนาทดสอบและผลิตอย่างไร


171

ฉันพยายามหาตัวอย่างที่ดีเกี่ยวกับวิธีจัดการสกีมาฐานข้อมูลและข้อมูลระหว่างการพัฒนาทดสอบและเซิร์ฟเวอร์ที่ใช้งานจริง

นี่คือการตั้งค่าของเรา นักพัฒนาแต่ละคนมีเครื่องเสมือนที่รันแอพของเราและฐานข้อมูล MySQL เป็นกล่องทรายส่วนตัวของพวกเขาที่จะทำสิ่งที่พวกเขาต้องการ ขณะนี้ผู้พัฒนาจะทำการเปลี่ยนแปลงกับ SQL schema และทำการดัมพ์ของฐานข้อมูลเป็นไฟล์ข้อความที่พวกเขาส่งไปยัง SVN

เราต้องการปรับใช้เซิร์ฟเวอร์การพัฒนาแบบรวมอย่างต่อเนื่องซึ่งจะใช้งานรหัสที่มุ่งมั่นล่าสุดอยู่เสมอ ถ้าเราทำตอนนี้มันจะโหลดฐานข้อมูลจาก SVN สำหรับแต่ละบิลด์

เรามีเซิร์ฟเวอร์ทดสอบ (เสมือน) ที่เรียกใช้ "release candidate" การปรับใช้กับเซิร์ฟเวอร์ทดสอบในปัจจุบันเป็นกระบวนการที่ต้องทำด้วยมือบ่อยครั้งและมักจะเกี่ยวข้องกับฉันในการโหลด SQL ล่าสุดจาก SVN และปรับแต่งมัน นอกจากนี้ข้อมูลบนเซิร์ฟเวอร์ทดสอบไม่สอดคล้องกัน คุณจะจบลงด้วยข้อมูลการทดสอบใด ๆ ที่นักพัฒนาคนสุดท้ายที่ทำไว้กับเซิร์ฟเวอร์ Sandbox ของเขา

จุดที่ทุกสิ่งแยกย่อยคือการปรับใช้กับการผลิต เนื่องจากเราไม่สามารถเขียนทับข้อมูลสดด้วยข้อมูลทดสอบสิ่งนี้เกี่ยวข้องกับการสร้าง schema ทั้งหมดขึ้นใหม่ด้วยตนเอง หากมีการเปลี่ยนแปลงสคีมาจำนวนมากหรือสคริปต์การแปลงเพื่อจัดการข้อมูลสิ่งนี้อาจมีขนดกจริงๆ

หากปัญหาเป็นเพียงสคีมามันจะเป็นปัญหาที่ง่ายกว่า แต่มีข้อมูล "ฐาน" ในฐานข้อมูลที่ได้รับการอัปเดตระหว่างการพัฒนาเช่นเมตาดาต้าในตารางความปลอดภัยและสิทธิ์

นี่เป็นอุปสรรคที่ใหญ่ที่สุดที่ฉันเห็นในการก้าวไปสู่การบูรณาการอย่างต่อเนื่องและการสร้างขั้นตอนเดียว คุณจะแก้ปัญหาได้อย่างไร


คำถามติดตามผล: คุณติดตามรุ่นของฐานข้อมูลอย่างไรเพื่อให้คุณทราบว่าสคริปต์ใดที่จะเรียกใช้เพื่ออัปเกรดอินสแตนซ์ฐานข้อมูลที่กำหนด ตารางเวอร์ชันเช่น Lance ระบุไว้ด้านล่างขั้นตอนมาตรฐานหรือไม่


ขอบคุณสำหรับการอ้างอิงถึง Tarantino ฉันไม่ได้อยู่ในสภาพแวดล้อม. NET แต่ฉันพบว่าหน้าวิกิ DataBaseChangeMangementของพวกเขามีประโยชน์มาก โดยเฉพาะการนำเสนอ Powerpointนี้(.ppt)

ฉันจะเขียนสคริปต์ Python ที่ตรวจสอบชื่อของ*.sqlสคริปต์ในไดเรกทอรีที่กำหนดเทียบกับตารางในฐานข้อมูลและเรียกใช้สคริปต์ที่ไม่ได้อยู่ในลำดับตามจำนวนเต็มซึ่งเป็นส่วนแรกของชื่อไฟล์ ถ้ามันเป็นทางออกที่ง่ายอย่างที่ฉันคิดว่ามันจะเป็นแล้วฉันจะโพสต์ที่นี่


ฉันมีสคริปต์ที่ใช้งานได้สำหรับสิ่งนี้ มันจัดการการเริ่มต้นฐานข้อมูลถ้ามันไม่ได้อยู่และเรียกใช้สคริปต์อัพเกรดตามความจำเป็น นอกจากนี้ยังมีสวิตช์สำหรับเช็ดฐานข้อมูลที่มีอยู่และนำเข้าข้อมูลทดสอบจากไฟล์ มันมีประมาณ 200 บรรทัดดังนั้นฉันจะไม่โพสต์มัน (แม้ว่าฉันจะใส่ไว้ใน pastebin หากมีความสนใจ)


ที่เกี่ยวข้อง: stackoverflow.com/questions/52583/…
Ashwin A

"ฉันจะเขียนสคริปต์ Python ที่ตรวจสอบชื่อของสคริปต์ * .sql ในไดเรกทอรีที่กำหนดเทียบกับตารางในฐานข้อมูลและเรียกใช้สคริปต์ที่ไม่ได้อยู่ในลำดับตามจำนวนเต็มซึ่งเป็นส่วนแรกของ ชื่อไฟล์ถ้ามันเป็นคำตอบที่ง่ายอย่างที่ฉันคิดว่ามันจะเป็นแบบนั้นฉันจะโพสต์ที่นี่ " ดูเหมือนว่าคุณกำลังใช้งานเส้นทางบิน
masterxilo

คำตอบ:


53

มีตัวเลือกที่ดีสองสามอย่าง ฉันจะไม่ใช้กลยุทธ์ "กู้คืนข้อมูลสำรอง"

  1. สคริปต์การเปลี่ยนแปลงสกีมาทั้งหมดของคุณและให้เซิร์ฟเวอร์ CI ของคุณรันสคริปต์เหล่านั้นบนฐานข้อมูล มีตารางเวอร์ชันเพื่อติดตามเวอร์ชันฐานข้อมูลปัจจุบันและรันสคริปต์เฉพาะหากเป็นเวอร์ชันใหม่กว่า

  2. ใช้วิธีการโยกย้าย โซลูชันเหล่านี้แตกต่างกันไปตามภาษา แต่สำหรับ. NET ฉันใช้ Migrator.NET สิ่งนี้อนุญาตให้คุณทำเวอร์ชันฐานข้อมูลของคุณและเลื่อนขึ้นและลงระหว่างเวอร์ชัน สคีมาของคุณถูกระบุในรหัส C #


28

นักพัฒนาของคุณจำเป็นต้องเขียนสคริปต์การเปลี่ยนแปลง (schema และการเปลี่ยนแปลงข้อมูล) สำหรับแต่ละข้อบกพร่อง / คุณสมบัติที่พวกเขาทำงานไม่เพียงแค่ถ่ายโอนฐานข้อมูลทั้งหมดลงในการควบคุมแหล่งที่มา สคริปต์เหล่านี้จะอัปเกรดฐานข้อมูลการผลิตปัจจุบันเป็นเวอร์ชันใหม่ในการพัฒนา

กระบวนการสร้างของคุณสามารถคืนค่าสำเนาของฐานข้อมูลการผลิตลงในสภาพแวดล้อมที่เหมาะสมและเรียกใช้สคริปต์ทั้งหมดจากแหล่งควบคุมซึ่งจะปรับปรุงฐานข้อมูลให้เป็นเวอร์ชันปัจจุบัน เราทำสิ่งนี้เป็นประจำทุกวันเพื่อให้แน่ใจว่าสคริปต์ทั้งหมดทำงานอย่างถูกต้อง


13

มาดูกันว่า Ruby on Rails ทำสิ่งนี้ได้อย่างไร

ก่อนอื่นจะมีไฟล์การโยกย้ายที่เรียกว่าโดยทั่วไปจะแปลงสกีมาฐานข้อมูลและข้อมูลจากเวอร์ชัน N เป็นเวอร์ชัน N + 1 (หรือในกรณีที่มีการลดระดับจากรุ่น N + 1 เป็น N) ฐานข้อมูลมีตารางที่บอกรุ่นปัจจุบัน

ฐานข้อมูลการทดสอบจะถูกลบทั้งหมดก่อนที่จะทำการทดสอบหน่วยและบรรจุด้วยข้อมูลคงที่จากไฟล์


10

ฐานข้อมูลหนังสือRefactoring: การออกแบบฐานข้อมูลวิวัฒนาการอาจให้แนวคิดบางประการเกี่ยวกับวิธีการจัดการฐานข้อมูล รุ่นสั้นสามารถอ่านได้ที่http://martinfowler.com/articles/evodb.html

ในโครงการ PHP + MySQL หนึ่งฉันเคยมีหมายเลขการแก้ไขฐานข้อมูลที่เก็บไว้ในฐานข้อมูลและเมื่อโปรแกรมเชื่อมต่อกับฐานข้อมูลมันจะตรวจสอบการแก้ไขก่อน หากโปรแกรมนั้นต้องการการแก้ไขที่แตกต่างกันมันจะเปิดหน้าสำหรับการอัพเกรดฐานข้อมูล การอัพเกรดแต่ละครั้งจะถูกระบุในโค้ด PHP ซึ่งจะเปลี่ยนสกีมาฐานข้อมูลและย้ายข้อมูลที่มีอยู่ทั้งหมด


5
  • ตั้งชื่อฐานข้อมูลของคุณดังนี้ - dev_<<db>> , tst_<<db>> , stg_<<db>> , prd_<<db>>(เห็นได้ชัดว่าคุณไม่ควร hardcode ชื่อ db
  • ดังนั้นคุณจะสามารถปรับใช้แม้กระทั่งฐานข้อมูลประเภทต่าง ๆ บนเซิร์ฟเวอร์จริงเดียวกัน (ฉันไม่แนะนำ แต่คุณอาจต้อง ... ถ้าทรัพยากรมี จำกัด )
  • ให้แน่ใจว่าคุณจะสามารถย้ายข้อมูลระหว่างสิ่งเหล่านั้นโดยอัตโนมัติ
  • แยกสคริปต์การสร้าง db ออกจากประชากร = มันควรจะเป็นไปได้เสมอที่จะสร้าง db ใหม่จากการลบและเติมมัน (จากเวอร์ชัน db เก่าหรือแหล่งข้อมูลภายนอก
  • ห้ามใช้สตริงการเชื่อมต่อ hardcode ในรหัส (แม้จะไม่ได้อยู่ในไฟล์ปรับแต่ง) - ใช้ในแม่แบบสตริงการเชื่อมต่อไฟล์ config ซึ่งคุณเติมข้อมูลแบบไดนามิกแต่ละการกำหนดค่าใหม่ของ application_layer ซึ่งจำเป็นต้องคอมไพล์ใหม่คือ BAD
  • ใช้การกำหนดเวอร์ชันฐานข้อมูลและการกำหนดเวอร์ชันวัตถุ db - หากคุณสามารถซื้อได้ใช้ผลิตภัณฑ์ที่พร้อมใช้งานถ้าไม่พัฒนาบางสิ่งด้วยตัวคุณเอง
  • ติดตามการเปลี่ยนแปลง DDL แต่ละรายการและบันทึกลงในตารางประวัติ ( ตัวอย่างที่นี่ )
  • สำรองข้อมูลทุกวัน! ทดสอบความเร็วที่คุณจะสามารถกู้คืนสิ่งที่หายไปจากการสำรองข้อมูล (ใช้สคริปต์การกู้คืนอัตโนมัติ
  • แม้แต่ฐานข้อมูล DEV ของคุณและ PROD ก็มีสคริปต์การสร้างเดียวกันที่คุณจะมีปัญหากับข้อมูลดังนั้นอนุญาตให้นักพัฒนาสร้างสำเนาที่แน่นอนของ prod และเล่นกับมัน (ฉันรู้ว่าฉันจะได้รับ minuses สำหรับอันนี้ แต่เปลี่ยนใน ความคิดและกระบวนการทางธุรกิจจะทำให้คุณเสียค่าใช้จ่ายน้อยลงเมื่ออึกระทบกับแฟน - ดังนั้นบังคับให้ผู้เขียนโคเดกโครถูกต้องตามกฎหมายไม่ว่าจะทำอะไร

ประเด็นสุดท้ายคืออารมณ์ หากมีความจำเป็นก็จะแสดงความหมายของโครงการที่เสีย การพัฒนาจะต้องนำก่อนการผลิต หากข้อมูลการผลิตทำให้เกิดผลข้างเคียงแสดงว่ามีปัญหาใหญ่กว่า ทำความสะอาดข้อมูลการผลิต ชี้แจงขั้นตอนสุดท้ายกับเจ้าหน้าที่คุ้มครองข้อมูลด้วยหากมีเหตุผล - ตามที่คุณแนะนำ - ข้อมูลสดจะต้องอยู่ในระบบการพัฒนาให้ตรวจสอบว่าเป็นสิ่งที่ถูกต้องตามกฎหมายหรือไม่ นอกจากนี้สำเนาข้อมูลการผลิตที่แน่นอนยังช่วยชะลอการพัฒนาและการรวมในระดับสูง พิจารณากระบวนการที่มีค่าใช้จ่ายน้อยลงหากคุณไม่สามารถซื้อฟุ่มเฟือยดังกล่าวได้
hakre

ประเด็นก็คือว่าในระหว่างการพัฒนานั้นไม่สามารถแม้แต่จะมองเห็นทุกมุมกรณีในโฟลว์ควบคุมและความแปรปรวนของคุณภาพข้อมูลที่จะเกิดขึ้นในการผลิต หากคุณอยู่ในองค์กรขนาดใหญ่ที่มีปัญหาทางกฎหมายมากกว่านั้นต้องใช้การแปลงข้อมูลและ / หรือวิธีการปิดบังบางชนิดซึ่งจะเป็นการเพิ่มชั้นของความซับซ้อนที่เพิ่มขึ้น แต่ก็ยังคงต้องรักษาคุณภาพของข้อมูลที่ทำให้เกิดข้อผิดพลาด ในสถานที่แรกนะ ...
Yordan Georgiev

4

นี่คือสิ่งที่ฉันไม่พอใจอย่างต่อเนื่อง - วิธีแก้ปัญหานี้คือ เป็นเวลาหลายปีที่เรายังคงสคริปต์การเปลี่ยนแปลงแยกต่างหากสำหรับแต่ละรุ่น สคริปต์นี้จะมี delta จากการผลิตครั้งล่าสุด ในการเปิดตัวแอปพลิเคชันแต่ละครั้งหมายเลขเวอร์ชันจะเพิ่มขึ้นโดยให้สิ่งต่อไปนี้:

  • dbChanges_1.sql
  • dbChanges_2.sql
  • ...
  • dbChanges_n.sql

สิ่งนี้ทำงานได้ดีพอจนกว่าเราจะเริ่มต้นการพัฒนาสองบรรทัด: Trunk / Mainline สำหรับการพัฒนาใหม่และสาขาการบำรุงรักษาสำหรับการแก้ไขข้อบกพร่องการปรับปรุงระยะสั้นและอื่น ๆ อย่างหลีกเลี่ยงไม่จำเป็นต้องเกิดขึ้นเพื่อเปลี่ยนสคีมาในสาขา ณ จุดนี้เรามี dbChanges_n + 1.sql ใน Trunk อยู่แล้วดังนั้นเราจึงจบลงด้วยรูปแบบดังนี้:

  • dbChanges_n.1.sql
  • dbChanges_n.2.sql
  • ...
  • dbChanges_n.3.sql

อีกครั้งสิ่งนี้ทำงานได้ดีพอเราจนกระทั่งวันหนึ่งเราค้นหาและเห็นสคริปต์เดลต้า 42 รายการในการฉีดและ 10 ในสาขา โอ๊ะ!

วันนี้เราเพียงแค่บำรุงรักษาเดลต้าสคริปต์หนึ่งตัวและปล่อยให้เวอร์ชัน SVN - เช่นเราเขียนทับสคริปต์ด้วยการเปิดตัวแต่ละครั้ง และเราอายห่างจากการเปลี่ยนแปลงโครงสร้างในสาขา

ดังนั้นฉันไม่พอใจกับสิ่งนี้เช่นกัน ฉันชอบแนวคิดการโยกย้ายจาก Rails ฉันหลงเสน่ห์LiquiBaseมาก สนับสนุนแนวคิดของการปรับโครงสร้างฐานข้อมูลเพิ่มเติม มันคุ้มค่ากับการดูและฉันจะดูรายละเอียดในไม่ช้า ใครมีประสบการณ์กับมันบ้าง ฉันอยากรู้มากเกี่ยวกับผลลัพธ์ของคุณ


4

คุณสามารถดูการใช้เครื่องมือเช่นSQL Compare to script ความแตกต่างระหว่างฐานข้อมูลเวอร์ชันต่าง ๆ ได้ช่วยให้คุณสามารถโยกย้ายระหว่างเวอร์ชันได้อย่างรวดเร็ว


3

เรามีการติดตั้งคล้ายกับ OP

นักพัฒนาพัฒนาใน VM ด้วยฐานข้อมูลส่วนตัว

[นักพัฒนาจะเข้าสู่สาขาส่วนตัวเร็ว ๆ นี้]

การทดสอบดำเนินการบนเครื่องต่าง ๆ (จริง ๆ แล้วอยู่ในโฮสต์ของ VM บนเซิร์ฟเวอร์) [เร็ว ๆ นี้จะทำงานโดยเซิร์ฟเวอร์ Hudson CI]

ทดสอบโดยการโหลดดัมพ์อ้างอิงลงใน db ใช้แพตช์สคีมานักพัฒนาแล้วใช้แพตช์ข้อมูลนักพัฒนา

จากนั้นเรียกใช้หน่วยและการทดสอบระบบ

การผลิตถูกปรับใช้กับลูกค้าในฐานะผู้ติดตั้ง

เราทำอะไร:

เราใช้ schema dump ของฐานข้อมูล sandbox ของเรา จากนั้นถ่ายโอนข้อมูล sql เราแตกต่างจากข้อมูลพื้นฐานก่อนหน้านี้ คู่ของ deltas นั้นคือการอัพเกรด n-1 เป็น n

เรากำหนดค่าดัมพ์และเดลตา

ดังนั้นในการติดตั้งเวอร์ชัน N CLEAN เราจึงเรียกใช้ดัมพ์ไปยังฐานข้อมูลที่ว่างเปล่า ในการแพทช์ให้ใช้แพทช์แทรกแซง

(Juha พูดถึงแนวความคิดของ Rail ว่าการมีตารางบันทึกเวอร์ชันฐานข้อมูลปัจจุบันเป็นสิ่งที่ดีและควรทำให้การติดตั้งอัปเดตน้อยยิ่งขึ้น)

ต้องตรวจทานเดลต้าและการทิ้งขยะก่อนการทดสอบเบต้า ฉันไม่เห็นวิธีนี้เพราะฉันเห็นนักพัฒนาแทรกบัญชีทดสอบลงในฐานข้อมูลด้วยตนเอง


3

ฉันเกรงว่าฉันเห็นด้วยกับโปสเตอร์อื่น ๆ นักพัฒนาจำเป็นต้องสคริปต์การเปลี่ยนแปลงของพวกเขา

ในหลายกรณีตาราง ALTER แบบธรรมดาจะไม่ทำงานคุณจำเป็นต้องแก้ไขข้อมูลที่มีอยู่ด้วยเช่นกันนักพัฒนาจำเป็นต้องดำเนินการเกี่ยวกับการย้ายข้อมูลที่จำเป็นและตรวจสอบให้แน่ใจว่าสคริปต์เหล่านั้นถูกสคริปต์อย่างถูกต้อง รอบการเปิดตัว)

นอกจากนี้หากคุณมีความรู้สึกใด ๆ คุณจะได้รับนักพัฒนาของคุณในการย้อนกลับสคริปต์สำหรับการเปลี่ยนแปลงของพวกเขาเช่นกันเพื่อให้พวกเขาสามารถเปลี่ยนกลับได้หากจำเป็น สิ่งนี้ควรได้รับการทดสอบเช่นกันเพื่อให้แน่ใจว่าการย้อนกลับของพวกเขาไม่เพียง แต่ดำเนินการโดยไม่มีข้อผิดพลาด แต่ยังทำให้ DB อยู่ในสภาพเดิมเหมือนที่เคยเป็นมาก่อนหน้านี้ (ไม่สามารถทำได้หรือเป็นที่ต้องการเสมอไป .

ฉันไม่ทราบวิธีการที่เซิร์ฟเวอร์ CI บางทีเซิร์ฟเวอร์ CI ของคุณต้องมีสแน็ปช็อตบิลด์ที่ทราบซึ่งจะเปลี่ยนเป็นทุกคืนแล้วนำการเปลี่ยนแปลงทั้งหมดมาใช้ อาจเป็นวิธีที่ดีที่สุดมิฉะนั้นสคริปต์การย้ายข้อมูลที่เสียหายจะไม่เพียงสร้างงานคืนนั้น แต่งานต่อเนื่องทั้งหมด


1

ลองใช้dbdeployซึ่งมีเครื่องมือ Java และ. net อยู่แล้วคุณสามารถปฏิบัติตามมาตรฐานของรูปแบบไฟล์ SQL และตาราง schema version และเขียนเวอร์ชันของ python ของคุณ


1

เราใช้บรรทัดคำสั่งmysql-diff : มันแสดงความแตกต่างระหว่างสอง schema ฐานข้อมูล (จาก live DB หรือ script) เป็น ALTER script mysql-diff ถูกเรียกใช้เมื่อเริ่มต้นแอปพลิเคชันและหาก schema เปลี่ยนไปจะรายงานไปยังผู้พัฒนา ดังนั้นนักพัฒนาจึงไม่จำเป็นต้องเขียน ALTER ด้วยตนเองการอัพเดตสกีมาจึงเกิดขึ้นแบบกึ่งอัตโนมัติ


1

ถ้าคุณอยู่ในสภาพแวดล้อมที่ .NET แล้วการแก้ปัญหาคือยียวน (เก็บไว้) มันจัดการทั้งหมดนี้ (รวมถึงสคริปต์ sql ที่จะติดตั้ง) ใน build ของ NANT


1
ลิ้งค์ โครงการนี้ดูเหมือนจะอยู่ที่นี่: bitbucket.org/headspringlabs/tarantino/wiki/Homeหรือที่นี่: github.com/HeadspringLabs/Tarantino
Lee Richardson

0

ฉันได้เขียนเครื่องมือที่ (โดยเชื่อมต่อกับOpen DBDiff ) เปรียบเทียบสกีมาฐานข้อมูลและจะแนะนำสคริปต์การย้ายข้อมูลให้คุณ หากคุณทำการเปลี่ยนแปลงที่ลบหรือแก้ไขข้อมูลมันจะทำให้เกิดข้อผิดพลาด แต่ให้คำแนะนำสำหรับสคริปต์ (เช่นเมื่อคอลัมน์ที่ขาดหายไปในสคีมาใหม่จะตรวจสอบว่าคอลัมน์นั้นถูกเปลี่ยนชื่อและสร้าง xx ที่สร้างขึ้นหรือไม่ script.sql.suggestion มีคำสั่งเปลี่ยนชื่อ)

http://code.google.com/p/migrationscriptgenerator/ SQL Server เท่านั้นที่ฉันกลัว :( มันก็ค่อนข้างอัลฟ่า แต่ก็มีแรงเสียดทานต่ำมาก (โดยเฉพาะถ้าคุณรวมกับ Tarantino หรือhttp://code.google .com / p / simplescriptrunner / )

วิธีที่ฉันใช้คือมีโครงการสคริปต์ SQL ใน. sln ของคุณ นอกจากนี้คุณยังมีฐานข้อมูล db_next ในเครื่องซึ่งคุณทำการเปลี่ยนแปลง (โดยใช้ Studio จัดการหรือNHibernate Schema ExportหรือLinqToSql CreateDatabaseหรืออะไรบางอย่าง) จากนั้นคุณดำเนินการ migrationscriptgenerator ด้วย _dev และ _next DBs ซึ่งสร้างขึ้น สคริปต์อัพเดต SQL สำหรับการโอนย้ายระบบ


0

สำหรับฐานข้อมูล oracle เราใช้oracle-ddl2svnเครื่องมือ

เครื่องมือนี้จะดำเนินการอัตโนมัติต่อไป

  1. สำหรับทุกรูปแบบ db รับ schema ddls
  2. วางไว้ใต้ contol เวอร์ชัน

การเปลี่ยนแปลงระหว่างอินสแตนซ์ที่แก้ไขด้วยตนเอง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.