1. กลไก Hooks

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

2. วิธีลงทะเบียน Hooks

2.1 Hooks ระดับโลกและ Hooks ระดับท้องถิ่น

Hooks ระดับโลก (Runtime hooks) มีประสิทธิภาพต่อการดำเนินการทุกประเภทในกราฟ พวกมันเหมาะสำหรับการเพิ่มโลจิกในแอปพลิเคชันทั้งหมด เช่น การบันทึกและการตรวจสอบ Hooks ระดับท้องถิ่น (Schema hooks) ถูกกำหนดไว้ภายในสคีมาของชนิดพิเศษและมีผลบังคับในการดำเนินการเปลี่ยนแปลงที่สอดคล้องกับชนิดของสคีมา การใช้งาน Hooks ระดับท้องถิ่น ช่วยให้โลจิกทั้งหมดที่เกี่ยวข้องกับประเภทโหนดเฉพาะอยู่ในที่เดียวคือในการกำหนดสคีมา

2.2 ขั้นตอนในการลงทะเบียน Hook

การลงทะเบียน Hook ในโค้ดโดยปกติเกี่ยวข้องกับขั้นตอนต่อไปนี้:

  1. กำหนดฟังก์ชันของ Hook ฟังก์ชันนี้รับ ent.Mutator และคืนค่าเป็น ent.Mutator ตัวอย่างเช่น การสร้าง Hook การบันทึกเรื่องง่าย
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // พิมพ์บันทึกก่อนการดำเนินงานการเปลี่ยนแปลง
        log.Printf("ก่อนการเปลี่ยนแปลง: ประเภท=%s, การดำเนินการ=%s\n", m.Type(), m.Op())
        // ดำเนินการเปลี่ยนแปลง
        v, err := next.Mutate(ctx, m)
        // พิมพ์บันทึกหลังการดำเนินงานการเปลี่ยนแปลง
        log.Printf("หลังการเปลี่ยนแปลง: ประเภท=%s, การดำเนินการ=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. ลงทะเบียน Hook กับไคลเอ็น สำหรับ Hooks ระดับโลก คุณสามารถลงทะเบียนพวกเขาโดยใช้วิธี Use ของไคลเอ็น สำหรับ Hooks ระดับท้องถิ่น คุณสามารถลงทะเบียนพวกเขาในสคีมาโดยใช้วิธี Hooks ของประเภท
// ลงทะเบียน Hook ระดับโลก
client.Use(logHook)

// ลงทะเบียน Hook ระดับท้องถิ่น บังคับเฉพาะสำหรับประเภทผู้ใช้
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // เพิ่มโลจิกเฉพาะ
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. คุณสามารถเชนหลายๆ Hook และพวกพวกทั้งหมดจะถูกดำเนินการตามลำดับที่ลงทะเบียน

3. ลำดับการดำเนินการของ Hooks

ลำดับการดำเนินการของ Hooks ถูกกำหนดโดยลำดับการลงทะเบียนกับไคลเอ็น ตัวอย่างเช่น client.Use(f, g, h) จะถูกดำเนินการในการดำเนินงานการเปลี่ยนแปลงthe ณ ลำดับของ f(g(h(...))) ในตัวอย่างนี้ f เป็น hooks ตัวแรกที่จะดำเนินงาน ตามโดย g และสุดท้าย h

สำคัญที่จะทราบว่า hooks ระดับโลก (Runtime hooks) เด่นอยู่เหนือ schema hooks (Schema hooks) นี้หมายถึงว่าถ้า g และ h คือ hooks ที่กำหนดไว้ในสคีมา ในขณะที่ f ถูกลงทะเบียนโดยใช้ client.Use(...) ลำดับการดำเนินการจะเป็น f(g(h(...))) นี้ช่วยให้โลจิกของการดำเนินการระดับโลก เช่น การบันทึก จะถูกดำเนินการก่อนโลจิกอื่นๆ

4. การจัดการกับปัญหาที่เกิดขึ้นจาก Hooks

เมื่อกำหนดเอาท์การดำเนินงานฐานข้อมูลโดยใช้ Hooks เราอาจประสบกับปัญหาของวงจรการนำเข้า สิ่งนี้มักเกิดขึ้นเมื่อพยายามใช้งาน hooks ระดับท้องถิ่น เนื่องจากแพคเกจ ent/schema อาจใช้แพคเกจ ent ร่วมด้วย หากแพคเกจ ent ยังพยายามที่จะนำเข้าแพคเกจ ent/schema วงจรการนำเข้าหมุนเวียนกลับกัน

สาเหตุของการนำเข้าแพคเกจหมุนเวียน

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

การแก้ปัญหาการติดวงกลับ

หากคุณพบปัญหา circular dependency error คุณสามารถทำตามขั้นตอนเหล่านี้ได้:

  1. ก่อนอื่น คอมเมนต์ทุก hooks ที่ถูกใช้ใน ent/schema ทั้งหมด
  2. จากนั้น ย้าย custom types ที่กำหนดไว้ใน ent/schema ไปยัง package ใหม่ เช่น คุณสามารถสร้าง package ชื่อ ent/schema/schematype
  3. รันคำสั่ง go generate ./... เพื่ออัพเดต ent package โดยทำให้มันชี้ไปที่ที่ package ใหม่ และอัพเดต type references ใน schema เช่น เปลี่ยน schema.T เป็น schematype.T
  4. ยกเลิก comment ที่ hooks references ที่คอมเมนต์ไว้ก่อนหน้า และรันคำสั่ง go generate ./... อีกครั้ง ณ จุดนี้ การสร้างโค้ดควรทำได้โดยไม่มีข้อผิดพลาด

โดยทำตามขั้นตอนเหล่านี้ เราสามารถแก้ไขปัญหา circular dependency ที่เกิดขึ้นจากการ import Hooks โดยจะแน่ใจว่า logic ของ schema และการ implement ของ Hooks สามารถดำเนินไปได้โดยไม่มีอุปสรรค

5. การใช้งานของ Hook Helper Functions

Framework ent มีชุดของ hook helper functions ที่สามารถช่วยให้เราควบคุมเวลาที่ Hooks ถูก execute ได้ ด้านล่างนี้เป็นตัวอย่างของ hook helper functions ที่ใช้ที่สุด:

// โปรด execute HookA สำหรับ operation UpdateOne และ DeleteOne เท่านั้น
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// อย่า execute HookB ในขณะที่ทำการ Create
hook.Unless(HookB(), ent.OpCreate)

// Execute HookC เมื่อ Mutation กำลังแก้ไข field "status" และล้าง field "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// ห้ามแก้ field "password" ใน operation Update (multiple)
hook.If(
    hook.FixedError(errors.New("password cannot be edited on update many")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Helper functions เหล่านี้ทำให้เราสามารถควบคุมเงื่อนไขการ activate ของ Hooks สำหรับ operation ต่างๆ ได้อย่างแม่นยำ

6. การใช้ Transaction Hooks

Transaction Hooks ช่วยให้เรา execute Hooks ที่เฉพาะเจาะจงเมื่อ transaction ถูก commit (Tx.Commit) หรือ rollback (Tx.Rollback). นี่เป็นจุดที่มีประโยชน์มากสำหรับการทำให้ข้อมูลสอดคล้อง และ atomicity ของ operation

ตัวอย่างของ Transaction Hooks

client.Tx(ctx, func(tx *ent.Tx) error {
    // ลงทะเบียน transaction hook - hookBeforeCommit จะถูก execute ก่อน commit
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // ตรรกะของ transaction ก่อนที่จะ commit สามารถวางไว้ที่นี่
            fmt.Println("Before commit")
            return next.Commit(ctx, tx)
        })
    })

    // ทำรายการต่างๆ ภายใน transaction...

    return nil
})

รหัสข้างต้นแสดงวิธีที่จะลงทะเบียน transaction hook เพื่อรันก่อน commit ใน transaction จุดนี้จะถูกเรียกหลังที่ทุก operation ในฐานข้อมูลถูก execute และก่อนที่ transaction จะถูก commit ภายใน