12 คะแนน โดย GN⁺ 2024-12-01 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • "จะเป็นอย่างไรถ้าใน 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 ความคิดเห็น

 
halfenif 2024-12-02

| เป็นสิ่งที่เหมาะให้คอมพิวเตอร์สร้าง และไม่มีประสิทธิภาพหากมนุษย์เขียนเองโดยตรง
ในมุมของคนที่ได้ลองทำโปรเจกต์ 'next-generation' แบบที่มีอยู่แค่ในเกาหลี ซึ่งต้องใส่นักพัฒนามากกว่า 100 คน

น่าสนใจมากทีเดียว

จริง ๆ แล้วนักพัฒนาส่วนใหญ่ที่ถูกส่งเข้าไปก็คือพวกผู้เชี่ยวชาญ SQL กันทั้งนั้นไม่ใช่เหรอ?

 
GN⁺ 2024-12-01
ความเห็นจาก 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 ทั่วไปอาจใช้เวลานานถึงหนึ่งชั่วโมง และการอัปเดตทีละแถวก็จะยิ่งใช้เวลานานกว่านั้น