เรียนรู้วิธีจัดการ Custom Field ที่มี unique index ใน Frappe เมื่อ Uninstall App — ป้องกัน IntegrityError จาก Orphaned Index และจัดการ Field Ownership ระหว่างหลาย Apps

ถ้าคุณเคยสร้าง Custom Field ที่มี unique=1 ใน Frappe App แล้ว Uninstall App นั้นออก — คุณอาจเจอปัญหาที่น่าปวดหัว: IntegrityError เมื่อพยายาม Amend หรือ Copy เอกสาร ทั้งที่ดูเหมือนทุกอย่างถูกลบไปแล้ว บทความนี้จะอธิบาย Lifecycle ของ Custom Field ใน Frappe, ปัญหา Orphaned Index ที่เกิดขึ้น และวิธีแก้ไขอย่างถูกต้อง
เมื่อ App A สร้าง Custom Field ที่มี unique=1 แล้วถูก Uninstall ในภายหลัง frappe.delete_doc("Custom Field") จะลบ Metadata แต่อาจทิ้ง Database Unique Index ไว้ ถ้า App B (ที่ยังติดตั้งอยู่) ใช้ Fieldname เดียวกัน Index ที่ถูกทิ้งไว้จะทำให้เกิด IntegrityError ตอน Amend/Copy
unique=1?# ใน install script ของ App
create_custom_fields({
"Sales Invoice": [{
"fieldname": "thai_tax_invoice_number",
"fieldtype": "Data",
"unique": 1, # <-- สร้าง 3 สิ่ง
}]
})สิ่งที่ Frappe สร้าง:
tabCustom Fieldthai_tax_invoice_number ใน tabSales Invoicethai_tax_invoice_number ใน MySQL/MariaDBfrappe.delete_doc("Custom Field") ลบอะไร?| สิ่งที่สร้าง | ถูกลบ? |
|---|---|
| Custom Field doc (metadata) | ลบ |
| Column ในตาราง | อาจลบ (ขึ้นกับ Frappe version) |
| UNIQUE INDEX | ไม่ลบอย่างน่าเชื่อถือ |
นี่คือต้นตอของปัญหา — Index ยังอยู่ แม้ว่า Custom Field doc จะถูกลบไปแล้ว
| สถานการณ์ | ผลลัพธ์ |
|---|---|
App A ติดตั้ง field unique=1 | สร้าง Index |
| App A ถูก Uninstall, App B ยังใช้ field นี้ | Index กำพร้า → App B เจอ IntegrityError |
App B ติดตั้ง field ใหม่แบบไม่มี unique=1 | Custom Field doc สร้างใหม่ แต่ Index เก่ายังอยู่ |
before_uninstallถ้า App ของคุณสร้าง Field ที่มี
unique=1—before_uninstallของคุณ ต้อง Drop Index เอง อย่าหวังว่า Frappe จะจัดการให้
before_uninstalldef before_uninstall():
# ลบ Custom Field docs ก่อน
cleanup_custom_fields()
# จากนั้น Drop orphaned indexes
drop_orphaned_unique_indexes()
frappe.db.commit()# hooks.py
after_install = "your_app.install.after_install"
after_migrate = "your_app.install.after_migrate"
before_uninstall = "your_app.install.before_uninstall"สำคัญ: ใช้
before_uninstall(ไม่ใช่after_uninstall) — ตอน before โค้ดของ App ยังพร้อมใช้งาน หลัง uninstall แล้ว import จะ fail
sql_ddl vs sql — ความแตกต่างที่สำคัญ# ผิด — จะ raise ImplicitCommitError
frappe.db.sql("ALTER TABLE `tabSales Invoice` DROP INDEX `my_index`")
# ถูก — ใช้ sql_ddl สำหรับ DDL statements
frappe.db.sql_ddl("ALTER TABLE `tabSales Invoice` DROP INDEX `my_index`")ALTER TABLE เป็น DDL statement ที่ทำให้เกิด Implicit Commit ใน MySQL Frappe จะ Block คำสั่งเหล่านี้ผ่าน frappe.db.sql() แต่อนุญาตผ่าน frappe.db.sql_ddl()
ถ้าระบบมีปัญหาอยู่แล้ว ใช้คำสั่งนี้ตรวจ:
# หา unique indexes ทั้งหมดบนตาราง (ยกเว้น PRIMARY)
frappe.db.sql("""
SHOW INDEX FROM `tabSales Invoice`
WHERE Non_unique = 0 AND Key_name != 'PRIMARY'
""", as_dict=True)
# ตรวจ index เฉพาะตัว
frappe.db.sql("""
SHOW INDEX FROM `tabSales Invoice`
WHERE Key_name = 'thai_tax_invoice_number'
""", as_dict=True)# ลบ unique flag จาก Custom Field
frappe.db.sql("""
UPDATE `tabCustom Field`
SET `unique` = 0
WHERE dt = 'Sales Invoice'
AND fieldname = 'thai_tax_invoice_number'
""")
frappe.db.commit()
# Drop database index
frappe.db.sql_ddl("""
ALTER TABLE `tabSales Invoice`
DROP INDEX thai_tax_invoice_number
""")เมื่อเขียน before_uninstall ให้ครอบคลุมทุกขั้นตอน:
1. ลบ Custom Field docs → frappe.delete_doc("Custom Field", ...)
2. Drop unique indexes (ถ้ามี) → frappe.db.sql_ddl("ALTER TABLE ... DROP INDEX ...")
3. Drop columns (ถ้าต้องการ) → frappe.db.sql_ddl("ALTER TABLE ... DROP COLUMN ...")
4. ลบ Property Setters → frappe.delete_doc("Property Setter", ...)
5. ลบ Seeded data → ดูบทความ Data Seeding Hooks
6. frappe.db.commit()
เพราะ Amend สร้างเอกสารใหม่ด้วยค่า Field เดิม แต่ Unique Index ยังบังคับให้ค่าต้องไม่ซ้ำกัน ทำให้เกิด Duplicate Key Error
ไม่ — เฉพาะ Custom Field ที่มี unique=1 เท่านั้น Field ปกติไม่มี Index ให้กำพร้า
after_uninstall แทนได้ไหม?ไม่ควร เพราะหลัง uninstall แล้ว โค้ดของ App จะถูกลบไป import ใน function จะ fail ใช้ before_uninstall เสมอ
# install.py หรือ uninstall.py
import frappe
def drop_orphaned_unique_indexes():
"""Drop unique indexes ที่ App นี้สร้างไว้"""
unique_field_indexes = [
("tabSales Invoice", "thai_tax_invoice_number"),
("tabPurchase Invoice", "thai_tax_invoice_number"),
]
for table, index_name in unique_field_indexes:
try:
indexes = frappe.db.sql(
f"""SHOW INDEX FROM `{table}` WHERE Key_name = %s""",
(index_name,), as_dict=True,
)
if indexes:
# ใช้ sql_ddl สำหรับ ALTER TABLE
frappe.db.sql_ddl(
f"""ALTER TABLE `{table}` DROP INDEX `{index_name}`"""
)
except Exception:
pass # ถูกลบไปแล้ว — ปลอดภัยที่จะ ignore