Declarative again no comments
เขียนเรื่อง declarative plugins ไปเมื่อ entry ก่อน หลังจากผ่านไปหลายสัปดาห์ ในที่สุด model ใหม่ที่ใช้งาน declarative plugins ก็ถูกใช้งานจริงอย่างไม่มีปัญหาใดๆ แต่การเปลี่ยนจากวิธี mapper ปกติไปเป็น declarative ไม่ได้ราบรื่นอย่างที่คิด (ถ้าหากใช้แต่แรกคงสบาย)
ปัญหาที่เจอส่วนหนึ่งคือ circular import ใน Python (คือ import class ก่อนที่มันจะถูกสร้างขึ้น) ที่ถึงแม้การรับ string ใน relation() จะช่วยเรื่องนี้ได้พอสมควร แต่ก็ไม่ช่วยในทุกกรณี เช่นกรณีที่ต้องทำตารางแบบ Many-to-Many หรือการสร้าง UNIQUE constraint กับสองคอลัมน์
Many-to-Many
การทำ Many-to-Many นั้น จำเป็นต้องสร้างตารางใหม่เพื่อเป็นจุดเชื่อมต่อระหว่างตาราง A และ B ใน สำหรับ SQLAlchemy เราจะระบุ secondary ลงไปที่ relation() เพื่อบอกว่าจะใช้ตารางไหนในการเชื่อม จากตัวอย่าง user table ในคราวที่แล้ว เปลี่ยนจาก group เป็น roles จะออกมาหน้าตาแบบนี้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class User(Base): __tablename__ = 'users' id = Column(Integer) name = Column(String) password = Column(String) group_id = Column(Integer, ForeignKey('groups.id')) class Role(Base): __tablename__ = 'roles' id = Column(Integer) name = Column(String) users = relation('User', secondary=UserRole.__table__) class UserRole(Base): __tablename__ = 'users_roles' user_id = Column(Integer, primary_key=True, ForeignKey('users.id')) role_id = Column(Integer, primary_key=True, ForeignKey('roles.id')) |
จากตัวอย่างนี้แก้เรื่อง circular import ง่ายๆ ด้วยการย้าย UserRole ขึ้นไปไว้ข้างบนสุด แต่วิธีนี้ก็ใช้ไม่ได้เสมอไป โดยเฉพาะสำหรับตารางที่ซับซ้อนกว่านี้ ในกรณีนั้น เราสามารถสร้าง relation หลังจากสร้าง class แล้วก็ได้
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class User(Base): __tablename__ = 'users' id = Column(Integer) name = Column(String) password = Column(String) group_id = Column(Integer, ForeignKey('groups.id')) class Role(Base): __tablename__ = 'roles' id = Column(Integer) name = Column(String) class UserRole(Base): __tablename__ = 'users_roles' user_id = Column(Integer, primary_key=True, ForeignKey('users.id')) role_id = Column(Integer, primary_key=True, ForeignKey('roles.id')) User.roles = relation(Role, secondary=UserRole.__table__) Role.users = relation(User, secondary=UserRole.__table__) |
ถึงแม้วิธีนี้จะใช้งานได้กับทุกกรณี แต่ก็จะมีปัญหาใหม่มาว่าโค้ดจะไม่อยู่ที่เดียวกัน เพิ่มความยุ่งยากในการดูแลขึ้นไปอีกเล็กน้อย แต่ถ้าจะให้ยกตัวอย่างกรณีที่การย้าย UserRole ไม่สามารถทำได้ ก็น่าจะเป็นกรณีการใช้งาน column_property ประเภทนี้
1 | count = column_property(select([func.count(User.__table__.c.id])) |
ของจริงน่าจะยุ่งยากกว่านี้อีกนิดหนึ่ง แต่ถ้าหากต้องสร้าง count แบบนี้ทั้งใน User และ Role ก็เป็นที่แน่นอนว่าไม่สามารถสลับที่อยู่ของทั้งสอง class สำหรับกรณีนี้ได้
Constraint
ปัญหาต่อมา เนื่องจากว่าเจ้า declarative เนี่ย มันถูกสร้างมาเพื่อทำงานพื้นฐานที่ค่อนข้างจะซ้ำซาก ในกรณีที่ตารางมีความซับซ้อนขึ้นมาอีกนิด เช่นการสร้าง UNIQUE constraint บนหลายคอลัมน์ ที่จะต้องใช้ UniqueConstraint ก็จะไม่สามารถใช้งาน declarative ตามวิธีปกติได้ จากตัวอย่าง
1 2 3 4 5 6 7 8 | class Product(Base): __tablename__ = 'products' id = Column(Integer, primary_key=True) name = Column(String(255)) manufacturer = Column(String(255)) UniqueConstraint('name', 'manufacturer') |
ในกรณีนี้ UniqueConstraint จะไม่ทำงาน เพราะข้อจำกัดของ declarative ดังนั้นจึงต้องกลับไปใช้ table construct แบบเดิม แต่ในกรณีนี้ เราไม่จำเป็นต้องไปสร้าง mapper ให้วุ่นวาย เพราะสามารถเรียกใช้ Table() บน __table__ ใน class ได้เลย
1 2 3 4 5 6 7 | class Product(Base): __table__ = Table('products', Base.metadata,, Column('id', Integer, primary_key=True), Column('name', String(255)), Column('manufacturer', String(255)), UniqueConstraint('name', 'manufacturer')) |
และยังได้รับผลประโยชน์อีกเล็กน้อยจากการใช้ declarative ตามปกติ
ส่วนตัวผมคิดว่า SQLAlchemy นี่มันทรงพลังมาก และอาจจะมากเกินไป จนทำให้ learning curve มันชันจนน่ากลัว หลายๆ คนอาจจะชอบวิธีที่ ActiveRecord ทำงานมากกว่า และผมก็เห็นด้วยว่ามันสะอาดกว่าจริงๆ ดังนั้นการใช้งาน Elixir ก็คงจะเป็นตัวเลือกที่ไม่เลวเหมือนกัน