- "จะเป็นอย่างไรถ้าใน Rust คุณสามารถเก็บข้อมูลแบบถาวรได้อย่างปลอดภัย เขียนคิวรีที่ซับซ้อนได้ง่าย และไม่ต้องเขียน SQL สักบรรทัดเลย?"
- Rust-query คือไลบรารีที่ถูกพัฒนาขึ้นเพื่อทำให้สิ่งนี้เป็นจริง
Rust และฐานข้อมูล
- ไลบรารีฐานข้อมูลแบบเดิมของ Rust มักขาดการรับประกันในช่วงคอมไพล์ หรือใช้งานยุ่งยากและไม่ตรงไปตรงมาแบบ SQL
- ฐานข้อมูลมีบทบาทสำคัญในการสร้างซอฟต์แวร์ที่ป้องกันการชนกันของข้อมูล และรองรับ atomic transaction
- SQL เป็นโปรโตคอลมาตรฐานสำหรับโต้ตอบกับฐานข้อมูล แต่เหมาะกับการให้คอมพิวเตอร์สร้างมากกว่า และไม่มีประสิทธิภาพนักเมื่อให้มนุษย์เขียนเองโดยตรง
แนะนำ Rust-query
- rust-query เป็นไลบรารีสำหรับคิวรีฐานข้อมูลที่ผสานเข้ากับระบบ type ของ Rust อย่างลึกซึ้ง
- ถูกออกแบบมาเพื่อให้สามารถทำงานกับฐานข้อมูลได้อย่างเป็นธรรมชาติราวกับเป็นส่วนหนึ่งของ Rust เอง
ฟีเจอร์หลักและแนวทางการออกแบบ
- table alias แบบชัดเจน: หลัง join ตารางแล้ว จะมีการให้ dummy object ที่ใช้แทนตารางนั้น (
let user = User::join(rows);)
- ความปลอดภัยของ Null: ค่าที่เป็นทางเลือกในคิวรีจะถูกจัดการด้วย type
Option ของ Rust
- ฟังก์ชัน aggregate ที่เข้าใจง่าย: รองรับ aggregate แบบเป็นธรรมชาติในระดับแถวโดยไม่ต้องใช้
GROUP BY
- การไล่ตาม foreign key ที่ปลอดภัยด้วย type: ทำ implicit join ได้ง่ายจาก foreign key (
track.album().artist().name())
- การค้นหา unique ที่ปลอดภัยด้วย type: ค้นหาแถวที่มี unique constraint เฉพาะเจาะจง (คืนค่า
Option<Rating>)
- สคีมาหลายเวอร์ชัน: ตรวจสอบความต่างของสคีมาทุกเวอร์ชันได้แบบ declarative
- migration ที่ปลอดภัยด้วย type: ประมวลผลแถวข้อมูลได้ด้วยโค้ด Rust แบบกำหนดเอง
- การจัดการ unique conflict ที่ปลอดภัยด้วย type: เมื่อชนกับ unique constraint จะคืนค่า error type เฉพาะ
- การอ้างอิงแถวที่ผูกกับอายุของ transaction: การอ้างอิงแถวจะใช้ได้ก็ต่อเมื่อแถวนั้นยังมีอยู่
- row ID แบบห่อหุ้มด้วย type: หมายเลขแถวจะไม่ถูกเปิดเผยออกไปนอก API
การคิวรีและการแทรกข้อมูล
การกำหนดสคีมา
#[schema]
enum Schema {
User {
name: String,
},
Story {
author: User,
title: String,
content: String,
},
#[unique(user, story)]
Rating {
user: User,
story: Story,
stars: i64,
},
}
use v0::*;
- กำหนดสคีมาด้วยไวยากรณ์
enum ของ Rust
- foreign key constraint ถูกสร้างขึ้นโดยระบุชื่อของตารางอื่นเป็น type ของคอลัมน์
- เพิ่ม unique constraint ด้วยแอตทริบิวต์
#[unique]
- แมโคร
#[schema] จะวิเคราะห์คำจำกัดความและสร้างโมดูล v0
การแทรกข้อมูล
fn insert_data(txn: &mut TransactionMut<Schema>) {
let alice = txn.insert(User { name: "alice" });
let bob = txn.insert(User { name: "bob" });
let dream = txn.insert(Story {
author: alice,
title: "My crazy dream",
content: "A dinosaur and a bird...",
});
let rating = txn.try_insert(Rating {
user: bob,
story: dream,
stars: 5,
}).expect("no rating for this user and story exists yet");
}
- การแทรกข้อมูลจะคืนค่าการอ้างอิงของแถวที่เพิ่งถูกแทรก
- หากแทรกลงในตารางที่มี unique constraint จำเป็นต้องใช้
try_insert
try_insert จะคืนค่า error type เฉพาะเมื่อเกิดการชนกัน
การคิวรีข้อมูล
fn query_data(txn: &Transaction<Schema>) {
let results = txn.query(|rows| {
let story = Story::join(rows);
let avg_rating = aggregate(|rows| {
let rating = Rating::join(rows);
rows.filter_on(rating.story(), &story);
rows.avg(rating.stars().as_float())
});
rows.into_vec((story.title(), avg_rating))
});
for (title, avg_rating) in results {
println!("story '{title}' has avg rating {avg_rating:?}");
}
}
rows แทนชุดของแถวปัจจุบันในคิวรี
- ใช้
aggregate เพื่อทำการคำนวณ aggregate
- สามารถรวบรวมผลลัพธ์เป็นเวกเตอร์ของ tuple หรือ struct ได้
การพัฒนาสคีมาและ migration
- เมื่อสร้างสคีมาเวอร์ชันใหม่ จะใช้แอตทริบิวต์
#[version]
การเพิ่มสคีมาเวอร์ชันใหม่
#[schema]
#[version(0..=1)]
enum Schema {
User {
name: String,
#[version(1..)]
email: String,
},
// ... 나머지 스키마 ...
}
use v1::*;
การ migration ข้อมูล
- migration จะถูกตรวจสอบ type กับทั้งสคีมาเดิมและสคีมาใหม่
- สามารถประมวลผลข้อมูลแถวด้วยโค้ด Rust แบบกำหนดเองได้ (ใช้
map_dummy)
let m = m.migrate(v1::update::Schema {
user: Box::new(|old_user| {
Alter::new(v1::update::UserMigration {
email: old_user
.name()
.map_dummy(|name| format!("{name}@example.com")),
})
}),
});
สรุป
- rust-query นำเสนอแนวทางใหม่ในการโต้ตอบกับฐานข้อมูลเชิงสัมพันธ์ใน Rust:
- การตรวจสอบในช่วงคอมไพล์
- คิวรีที่ประกอบเข้ากับ Rust ได้
- รองรับการพัฒนาสคีมาผ่านการตรวจสอบ type
- ปัจจุบันใช้ SQLite เป็น backend เดียว และเหมาะสำหรับการพัฒนาแอปพลิเคชันเชิงทดลอง
- ยินดีรับฟีดแบ็กผ่าน GitHub Issues
2 ความคิดเห็น
| เป็นสิ่งที่เหมาะให้คอมพิวเตอร์สร้าง และไม่มีประสิทธิภาพหากมนุษย์เขียนเองโดยตรง
ในมุมของคนที่ได้ลองทำโปรเจกต์ 'next-generation' แบบที่มีอยู่แค่ในเกาหลี ซึ่งต้องใส่นักพัฒนามากกว่า 100 คน
น่าสนใจมากทีเดียว
จริง ๆ แล้วนักพัฒนาส่วนใหญ่ที่ถูกส่งเข้าไปก็คือพวกผู้เชี่ยวชาญ SQL กันทั้งนั้นไม่ใช่เหรอ?
ความเห็นจาก Hacker News
ความกังวลเกี่ยวกับสคีมาที่นิยามโดยแอปพลิเคชันคือมันถูกตรวจสอบโดยระบบที่ไม่ถูกต้อง ฐานข้อมูลต่างหากที่เป็นแหล่งอ้างอิงสูงสุดของสคีมา และเลเยอร์อื่นทั้งหมดของแอปพลิเคชันก็เพียงตั้งสมมติฐานโดยอิงจากมัน SQLx ของ Rust สร้าง struct จากชนิดข้อมูลของฐานข้อมูลเพื่อตรวจสอบในช่วงคอมไพล์ แต่ไม่ได้รับประกันว่าจะตรงกับชนิดข้อมูลของฐานข้อมูลโปรดักชันเสมอไป หากออกแบบคิวรีบน Postgres v15 ในเครื่องแล้วรันโปรดักชันด้วย Postgres v12 ก็อาจเกิดข้อผิดพลาดขณะรันไทม์ได้ สคีมาที่นิยามโดยแอปพลิเคชันจึงให้ความรู้สึกปลอดภัยแบบผิด ๆ และเพิ่มภาระงานให้วิศวกร
SQL แม้จะไม่สมบูรณ์แบบแต่ก็มีข้อดีหลายอย่าง คนส่วนใหญ่รู้ SQL พื้นฐานอยู่แล้ว และเอกสารของฐานข้อมูลอย่าง PostgreSQL ก็เขียนด้วย SQL เครื่องมือภายนอกก็ใช้ SQL เช่นกัน และเมื่อเปลี่ยนคิวรีก็ไม่ต้องผ่านขั้นตอนคอมไพล์ที่มีต้นทุนสูง SQLx หลีกเลี่ยงปัญหาของระบบชนิดข้อมูลที่ทำให้เวลาในการคอมไพล์เพิ่มขึ้น โดยตรวจสอบชนิดของพารามิเตอร์และปล่อยให้ฐานข้อมูลเป็นผู้ตรวจสอบคิวรีเอง สำหรับฐานข้อมูลใหม่ ภาษา query ที่ดีกว่าอาจเป็นฝ่ายชนะได้ แต่สำหรับฐานข้อมูล SQL ที่มีอยู่แล้ว SQLx เป็นตัวเลือกที่ดีกว่า
มีความเห็นคัดค้านแนวคิดที่ว่า SQL ควรถูกเขียนโดยคอมพิวเตอร์ โดยมองว่า SQL เป็นภาษาระดับสูง และมีระดับสูงกว่า Python หรือ Rust ด้วยซ้ำ SQL ถูกออกแบบมาให้อ่านง่ายและใช้งานง่าย และตอนคอมไพล์จะถูกแปลงผ่านหลายขั้นตอน SQL อยู่ตรงคอขวดของการพัฒนาเว็บ และเป็นจุดที่เกิดการเปลี่ยนแปลงสถานะ เนื่องจาก SQL เป็นภาษาระดับสูงจึงทำให้ปรับแต่งให้เหมาะสมได้ยาก SQL คือหนี้ทางเทคนิค แต่การใช้ SQL ก็ยังมีประสิทธิภาพมากกว่าการพัฒนา API ที่เหมาะสมกว่าถึง 10 เท่า
มีความเห็นว่าน่ายินดีกับการสำรวจเรื่อง typesafe-db-access ใน Rust โดยไลบรารีที่มีอยู่เดิมไม่ได้ให้การรับประกันในช่วงคอมไพล์ และยังเยิ่นเย้อหรือใช้งานขัด ๆ แบบ SQL ส่วน diesel ให้การรับประกันในช่วงคอมไพล์ ในประเด็นถกเถียงระหว่าง ORM กับ non-ORM ผู้แสดงความเห็นชอบตัวสร้างคิวรีแบบ type-safe และ diesel ก็อยู่ในหมวดนี้ ขณะที่ Rust-query ดูเหมือนจะเอนเอียงไปทาง ORM เต็มตัวมากกว่า
มีความเห็นว่าแนวทางในการเชื่อมสคีมากับชนิดข้อมูลนั้นน่าสนใจ แต่ในตัวอย่างการไม่มี enum
Schemaดูไม่ค่อยเป็นธรรมชาติ และถ้านิยามไว้ภายในแมโครก็น่าจะชัดเจนกว่ามีความเห็นว่าการที่ API ของไลบรารีไม่เปิดเผยหมายเลขแถวจริงออกมานั้นชวนสับสน ในเว็บเซิร์ฟเวอร์ควรสามารถส่ง row ID ไปพร้อมข้อมูล เพื่อให้ฝั่งฟรอนต์เอนด์อ้างอิงและแก้ไขข้อมูลนั้นได้ผ่านคำขออื่น
มีความเห็นว่าเห็นด้วยเพียงบางส่วนกับแนวคิดที่ว่า SQL ควรถูกเขียนโดยคอมพิวเตอร์ แต่ SQL ก็ไม่ใช่ภาษาที่สะดวกที่สุดสำหรับตัวสร้างโค้ดในการเขียน การปรับแผนให้เหมาะสมแบบง่าย ๆ ก็อาจเปลี่ยนโครงร่างของคิวรีได้ทั้งหมด ข้อเสนอ SQL pipe ของ Google ดีขึ้นเล็กน้อย แต่ก็ยังมีปัญหาแบบเดียวกับภาษา query ใหม่ ๆ อยู่ดี
มีความเห็นว่าเคยใช้ SeaQuery มาก่อน แต่เอกสารยังไม่เพียงพอสำหรับการสร้างคิวรีขั้นสูง คิวรีแบบ strong type อาจทำให้กระบวนการพัฒนาช้าลง จึงกำลังพิจารณากลับไปใช้ prepared statement และการ bind ค่าแบบเดิม
การทำ migration ด้วยการจัดการทีละแถวอาจช้ามากในแง่ความเร็วในการรัน เช่น ในตารางที่มี 1 พันล้านแถว คำสั่ง update ทั่วไปอาจใช้เวลานานถึงหนึ่งชั่วโมง และการอัปเดตทีละแถวก็จะยิ่งใช้เวลานานกว่านั้น