ComBioLaw.De » Blog » เขียนโปรแกรม » Data-Persistence with ZODB

Data-Persistence with ZODB

imageการจัดเก็บข้อมูลถือเป็นส่วนประกอบที่สำคัญมาก ๆ ในการเขียนโปรแกรม และก็เป็นส่วนประกอบที่ทำให้คนเขียนโปรแกรมปวดหวกมากพอ ๆ กัน การจัดเก็บข้อมูลนั้น มีรูปแบบต่าง ๆ มากมาย ไม่ว่าจะเป็นในรูปแบบของ Flat-File, Formated File (XML, Excel, Access), ฐานข้อมูล (SQL, RDBMS) ซึ่งการจัดเก็บข้อมูลแต่ละแบบนั้นก็มีข้อดีข้อเสียต่าง ๆ กัน

การจัดเก็บข้อมูลแบบ Flat-File นั้น มีข้อดีคือง่าย ไม่ซับซ้อน เปิดไฟล์ ปิดไฟล์ เขียนไฟล์ อ่านไฟล์ เป็น ก็ใช้งานได้แล้ว แต่เมื่อไรที่ข้อมูลซับซ้อนเข้า ก็เป็นอันปวดหัว เลยต้องมีการจัดเก็บข้อมูลแบบ Formated File แต่บางฟอร์แมตก็ไม่ใช่มาตรฐานเปิด แถมยังต้องใช้โปรแกรมเปิดโดยเฉพาะ ไม่มี API ให้ใช้งาน จะใช้งานผ่านเนตเวิร์คก็ไม่ได้ หากใช้งานหลาย ๆ คนพร้อมกัน ก็เกิดอาการเสี่ยง เพราะไม่มี transaction และ concurrency เลยต้องมีฐานข้อมูลแบบ RDBMS (Relational Database Management System) ขึ้นมา เช่น MySQL , PostgreSQL , SQLite , Oracle Database , MS SQL Server , etc.

ฐานข้อมูลแบบ RDBMS มีข้อดีคือ รับโหลดในการจัดเก็บข้อมูลได้มหาศาล ส่วนมากมี transaction ให้ใช้ เพื่อลดความเสี่ยงของการสูญเสียข้อมูล เวลาโปรแกรม หรือคอมพิวเตอร์เจ๊ง รองรับการเข้าถึงข้อมูลแบบพร้อมกันหลาย ๆ คนโดยใช้ concurrency มีภาษา SQL ในการบริหารข้อมูล พิมพ์คำสั่งเพียงไม่กี่บรรทัด ก็สามารถ ค้นหา จัดเรียง เพิ่ม ลบ แก้ไข ข้อมูลได้สบายแฮ ไม่ต้องกังวลว่าฐานข้อมูลจะทำงานยังไง

แต่ RDBMS มีข้อเสียคือ สามารถจัดเก็บข้อมูลในรูปแบบตารางเท่านั้น หากต้องการจัดเก็บ Object ในภาษาเขียนโปรแกรมที่เป็น OOP ความยุ่งยากจะมากขึ้นเป็นเท่าทวีคูณ หากเก็บข้อมูลไม่ดี อาจเกิดอาการ mismatch ได้ง่าย ๆ อีกทั้ง เมื่อโปรแกรมมีขนาดใหญ่มาก ๆ เข้า การเขียน SQL เพื่อบริหากข้อมูล อาจทำให้ปวดหัวได้เช่นกัน

ด้วยเหตุนี้ จึงมีคนคิด ...

เขียนโปรแกรม เขียนโปรแกรม

bow_der_kleine bow_der_kleine

Object Relational Mapping (ORM) ขึ้นมาใช้งาน ทำให้เราสามารถเก็บ Object ลงในฐานข้อมูลแบบ RDBMS ได้ทันที โดยไม่ต้องเขียนคำสั่งภาษา SQL ให้ปวดหัว ORM ที่ได้รับความนิยมได้แก่ Hibernate (Java) , Active Record (Ruby on rails) , SQLObject (Python Turbogear) , Django ก็มี ORM เป็นของตัวเอง ส่วน ADO ของ .Net ผมไม่แน่ใจว่าจัดอยู่ใน ORM ด้วยหรือเปล่า

image แต่ปัญหาของ ORM คือ กว่าข้อมูลจะถูกจัดเก็บได้ ต้องผ่านขั้นตอนต่าง ๆ หลายขั้นตอน ซึ่งบางขั้นตอนก็เป็นขั้นตอนที่ซ้ำซ้อน จาก Flow-Diagram ข้างซ้าย จะเห็นได้ว่า มีการเปลี่ยนข้อมูลเป็นข้อมูลดิบ (Rawdata ในที่นี้ก็คือข้อมูลที่เป็นตัวแปรพื้นฐาน เช่น integer, float, string) ถึงสองรอบ ทำให้ ORM ส่วนมาก มีปัญหาในเรื่อง Performance

ORM บางตัวมีปัญหาตรงที่ ไม่ transparent กล่าวคือ การจะใช้งาน ORM ได้ Class ต้องมีคุณสมบัติบางอย่าง เช่น ต้องมี Attribute ที่เป็น Instance ของของ Class ที่กำหนดเท่านั้น (ดู SQLObject) และ ORM บางตัว ไม่สามารถดึงความสามารถของ SQL มาใช้งานได้เต็มที่

ด้วยปัญหาของ RDBMS และ ORM นี่เอง จึงมีการเขียนโมดูล pickle และ cPickle ใน Python มาให้ใช้งาน ทั้งสองโมดูลมีหน้าที่จัดเก็บ Object ลงในไฟล์ โดยใช้วิธี Serialization จริง ๆ แล้วก็คือการจัดเก็บข้อมูลแบบ Formated-File รูปแบบหนึ่ง ซึ่งก็จะมีปัญหาต่าง ๆ ตามมา ดังที่เขียนไปแล้วข้างต้น นอกจากนี้ Performance ของโปรแกรมก็จะลดลง ตามปริมาณข้อมูล เพราะการเรียกใช้ข้อมูล เราต้องโหลดข้อมูลทั้งหมด ถึงจะเรียกใช้งานได้ ไม่สามารถเลือกเอาเฉพาะข้อมูลบางส่วนมาใช้งานได้ เหมือนใน RDBMS

ด้วยเหตุผลต่าง ๆ เหล่านี้ ZODB (Zope Object Database) จึงถือกำเนิดขึ้น เดิมที ZODB เป็นส่วนหนึ่งหนึ่งของ Zope และแยกตัวออกมาเป็นแพกเกจที่ไม่ขึ้นกับ Zope ในภายหลัง ZODB ใช้วิธีจัดเก็บข้อมูลแบบ Serialization ลงบนไฟล์ เช่นเดียวกับ pickle และ cPickle แต่ได้เพิ่มความสามารถต่าง ๆ ที่จำเป็นสำหรับข้อมูลปริมาณมาก ๆ เช่นเดียวกับ RDBMS ซึ่งได้แก่ Transaction, Concurrency, BTree (ทำให้ Performance ไม่ลดลงตามปริมาณข้อมูล), Thread-save, Undo และ ยิ่งไปกว่านั้น ZODB ยังมีความสามารถที่ RDBMS ส่วนมากไม่มี ซึ่งได้แก่ Versions, Full-Text search เป็นต้น หรือหากต้องการเก็บข้อมูลไว้หลาย ๆ ที่ ก็สามารถใช้ ZEO ในการสร้าง Distributed System ขึ้นมาได้ ส่วนเรื่องความเร็วของ ZODB นั้น ผมจะเขียนถึงในตอนท้าย

การใช้งาน ZODB นั้นถือว่าง่ายมาก ๆ ไม่ต้องมีการสร้างฐานข้อมูล ไม่ต้องมีการสร้างตาราง ไม่ต้องเขียนไฟล์ XML เพียงแค่ติดต่อกับไฟล์ข้อมูล ก็สามารถใช้งานฐานข้อมูลได้เหมือน Dictionary ในโปรแกรม Python ทั่ว ๆ ไปได้ทันที

import ZODB, ZODB.FileStorage, ZODB.DB
import transaction
 
storage = ZODB.FileStorage.FileStorage('data.fs')
db = ZODB.DB(storage)
conn = db.open()
root = conn.root()
root['a instance'] = MyClass()
root['a instance'].useMeLikeObject()
root['a list']  = ['A String', 2.0]
root['a dictionary']  = {'name':'bow_der_kleine', 'home page':'www.combiolaw.de'}
transaction.commit()
plain code

ในบรรทัดที่ 4 : storage = ZODB.FileStorage.FileStorage('data.fs') คือการสร้างไฟล์ที่ใช้เก็บข้อมูลทั้งหมด ซึ่งนอกจากไฟล์ data.fs แล้ว ZODB จะสร้างไฟล์ต่าง ๆ เพื่อใช้ในการบริหารข้อมูล ขึ้นมาอีกสามไฟล์ได้แก่ data.fs.lock, data.fs.index, data.fs.tmp

เมื่อสร้างไฟล์ที่ใช้เก็บข้อมูลได้แล้ว ขั้นต่อไปคือการสร้างฐานข้อมูล : db = ZODB.DB(storage)

ติดต่อกับฐานข้อมูล : conn = db.open()

และดึงข้อมูลจากฐานข้อมูลมาใช้งาน : root = conn.root()

root เป็นเป็นตัวแปรที่ใช้เก็บข้อมูลลงใน data.fs มีการใช้งานเหมือน Dictionary (Hash Map ใน Java) เราสามารถเก็บ Object ต่าง ๆ ลงใน root ได้ตามต้องการ การเปลี่ยนแปลงต่าง ๆ ที่เกิดขึ้นกับ root จะไม่ถูกบันทึกลงใน data.fs จนกว่าเราจะสั่ง transaction.commit() ถึงจุดนี้เราคงเห็นข้อดีของ ZODB ได้ชัดเจนขึ้น ในตัวอย่างจะเห็นได้ว่า ZODB เป็นอะไรที่ transparent มาก ๆ การเก็บ Object ลงในฐานข้อมูล แทบไม่มีอะไรต่างจากการเขียนโปรแกรม Python ปกติทั่ว ๆ ไป

ทั้งนี้การเก็บ Object ของ Class ปกติ ก็ยังถือว่าใช้ประโยชน์จาก ZODB ได้ไม่เต็มที่ เราสามารถสร้าง Class ที่เป็น Subclass ของ Persistent ซึ่งเป็น Class หนึ่งใน ZODB เพื่อใช้งาน ZODB ได้มากขึ้น

# person.py
import ZODB, transaction
from ZODB import FileStorage, DB
 
def get_root(fname) :
	return DB(FileStorage.FileStorage(fname)).open().root()
 
class Person(Persistent):
	def __init__(self, name, surname) :
		self.name = name
		self.surname = surname
		self.contacts = {}
 
if __name__ == '__main__':
	root = get_root('data.fs')
	bow = Person('Kittipong', 'Piyawanno')
	pan = Person('Sawatree', 'Suksri')
	root['bow'], root['pan'] = bow,pan
	bow.surname = 'PIYAWANNO'
	pan.surname = 'SUKSRI'
	transaction.commit()
plain code

จะเห็นได้ว่า หลังจากที่เราจัดเก็บ Object ลงในฐานข้อมูลแล้ว (root['bow'], root['pan'] = bow,pan) เราก็ไม่ต้องติดต่อกับ root อีก เราสามารถแก้ไข Object ได้โดยตรง เมื่อเรา commit() คุณสมบัติของ Object ที่ถูกเปลี่ยนแปลง จะถูกบันทึกลงในฐานข้อมูลโดยอัตโนมัติ

นอกจากนี้ Object ของ Persistent ยังมี Attribute ที่ใช้กำหนดพฤติกรรมของฐานข้อมูล ซึ่งได้แก่ _p_changed, _p_deactivate, _p_delattr, _p_getattr, _p_invalidate, _p_jar, _p_mtime, _p_oid, _p_serial, _p_setattr, _p_state ซึ่งบอกตามตรง ผมเองก็ไม่ทราบทั้งหมด ว่าอะไรใช้ทำอะไรบ้าง แต่ตัวที่สำคัญคือ _p_changed หากเราไม่กำหนด _p_changed Attribute ของ Object ที่เป็น List, Dictionary หรือ Sets (ในตัวอย่างคือ Person.contacts) เมื่อได้รับการเปลี่ยนแปลง จะไม่ถูกบันทึกลงในฐานข้อมูล

from person import Person, get_root
import transaction
 
root = get_root('data.fs')
root['bow'].contacts['email'] = ['bow_der_kleine@yahoo.de']
transaction.commit()
plain code

ตามตัวอย่างข้างบน root['bow'].contacts จะไม่ได้รับการเปลี่ยนแปลงใด ๆ ทั้งสิ้น เมื่อเรียกใช้งานสคริปต์ต่อไปนี้

from person import Person, get_root
import transaction
 
root = get_root('data.fs')
print root['bow'].__dict__
plain code

ผลจะออกมาคือ {'surname': 'Piyawanno', 'name': 'Kittipong', 'contacts': {}} (contacts ยังไม่ได้รับการเปลี่ยนแปลงใด ๆ) เราต้องกำหนด root['bow']._p_changed = 1 ก่อน root['bow'].contacts ถึงจะได้รับการบันทึกลงในฐานข้อมูล

from person import Person, get_root
import transaction
 
root = get_root('data.fs')
root['bow'].contacts['email'] = ['bow_der_kleine@yahoo.de']
root['bow']._p_changed = 1
transaction.commit()
 
# root['bow'].__dict__ = {'surname': 'Piyawanno', 'name': 'Kittipong', 'contacts': {'email': ['bow_der_kleine@yahoo.de']}}
plain code

ในกรณีที่ Attribute ของ Class เป็น Container เช่น List, Dictionary, Set และต้องเก็บข้อมูลในปริมาณมาก ๆ ZODB ก็มี BTrees ซึ่งประกอบด้วย Class ต่าง ๆ 4 Classes ให้เราใช้งาน ได้แก่ IIBtree, IOBTree, OIBtree และ OOBTree ข้อดีของ BTrees คือ ข้อมูลทั้งหมดจะไม่ถูกเก็บไว้ในหน่วยความจำ แต่จะอยู่ในไฟล์ฐานข้อมูล และจะถูกดึงออกมาจากไฟล์ฐานข้อมูล เมื่อถูกเรียกใช้งานเท่านั้น ทำให้โปรแกรมที่ได้ไม่ตะกละหน่วยความจำ และทำงานเร็วขึ้นไปอีก นอกจากนี้ เราไม่ต้องกำหนด _p_changed = 1 ทุกครั้งที่มีการเปลี่ยนแปลงข้อมูล ข้างล่างคือตัวอย่างการใช้งาน Btrees

#!/usr/bin/python
 
from persistent import Persistent
from BTrees import OOBTree
from ZODB import FileStorage, DB
import ZODB
 
def get_root(fname) :
	return DB(FileStorage.FileStorage(fname)).open().root()
 
class Contacts(OOBTree.OOBTree):
	def __init__(self):
		OOBTree.OOBTree.__init__(self)
 
	def add_contact(self, contact, contacttype):
		if not self.has_key(contacttype) :
			self[contacttype] = OOBTree.OOBTree()
			self[contacttype][0] = contact
		elif not contact in self[contacttype].values():
			self[contacttype][self.next_index] = contact
		self.next_index = self[contacttype].maxKey()+1
 
	def del_contact(self, contacttype, index):
		del self[contacttype][index]
 
class Person(Persistent):
	def __init__(self, name, surname) :
		self.name = name
		self.surname = surname
		self.contacts = Contacts()
plain code

แม้ว่า ZODB จะมีข้อดีมากมาย แต่มีข้อเสียที่เราควรรู้อยู่ไม่น้อยเช่นกัน ซึ่งได้แก่

  • ไฟล์ที่ใช้เก็บข้อมูลมีขนาดใหญ่ แม้ว่า ZODB จะใช้วิธี Serialization เช่นเดียวกับ pickle และ cPickle แต่ไฟล์ที่ใช้เก็บข้อมูลมีขนาดใหญ่กว่ากันสี่เท่า
  • ไม่มีระบบ Indexing มาให้ในตัว การค้นหาข้อมูลในฐานข้อมูลส่วนมากต้องใช้ for loop เอา หรือไม่ก็ต้องลงแพกเกจเพิ่มเติม ซึ่งไม่สะดวกเหมือน SELECT ... WHERE ... ใน RDBMS
  • การ import โมดูล, ZODB จะแยกกันระหว่าง Object ที่ สร้างโดย
    • from mymodule import MyClass ;obj = MyClass() กับ

    • import mymodule ;obj = mymodule.MyClass()
  • มีคนใช้น้อย มีปัญหาทีก็ต้องงมเดากันเอาเอง
  • ใช้ได้กับเฉพาะภาษาเขียนโปรแกรม Python
คราวนี้ก็มาถึงคำถามสำคัญอีกหนึ่งคำถาม นั่นก็คือความเร็วของ ZODB ซึ่งต้องแยกเป็นหลาย ๆ กรณี สำหรับกรณีที่เก็บข้อมูลเป็นตาราง มีคนทำการทดสอบไว้แล้ว สามารถดูได้ที่ ZODB vs Relational Database: a simple benchmark ผลก็ออกมาว่า ในกรณีที่มีจำนวนข้อมูลไม่มากนัก ความเร็วระหว่าง ZODB กับ RDBMS (ในการทดสอบใช้ SQLite) ไม่ต่างกันมากนัก แต่เมื่อปริมาณข้อมูลมากขึ้น ความแตกต่างก็จะชัดเจนขึ้นตามมา (แต่ก็ไม่ถือว่าทิ้งห่างมากนัก)

แต่ในกรณีที่เก็บข้อมูลในลักษณะ Persistence ผมได้ทดสอบด้วยตวเอง แบบคร่าว ๆ ปรากฏว่า ZODB ทำความเร็วได้ดีกว่า RDBMS หลายเท่าตัว ทั้งในส่วนของการ INSERT และ SELECT แต่ปัญหาของ ZODB มีอยู่ว่า หากข้อมูลมีปริมาณมาก ๆ การเข้าถึงข้อมูลครั้งแรกหลังจากติดต่อไฟล์ฐานข้อมูล มักจะใช้เวลาประมาณ 1 วินาที (ซึ่งถือว่าค่อนข้างนาน) ทั้งในกรณี INSERT และ SELECT แต่หลังจากนั้น ก็จะเร็วขึ้นอย่างเห็นได้ชัด ปัญหานี้จะส่งผลกระทบอย่างมาก ในกรณีที่ต้องเปิดปิดโปรแกรมบ่อย ๆ เช่น CGI วิธีแก้คือ การเขียนโปรแกรมลักษณะ Demon ขึ้นมา เพื่อติดต่อกับไฟล์ฐานข้อมูลตลอดเวลา โดยไม่ต้องปิดไฟล์

โดยรวมแล้ว ZODB เป็น Persistence-Database ที่น่าใช้ และดีมากโปรแกรมหนึ่ง แต่ก็มีข้อจำกัดที่คนเขียนโปรแกรมจำเป็นเป็นต้องรู้หลาย ๆ อย่าง ข้อสรุปสำหรับ ZODB คงเหมือนกับเครื่องมือในการเขียนโปรแกรม อื่น ๆ คือ ไม่มีอะไรที่ดีที่สุด และไม่มีอะไรที่แย่ที่สุด ทุกอย่างขึ้นอยู่กับการนำมาใช้งาน

27 Dec 07 | by | tags เขียนโปรแกรม Python ZODB Persistence

read 2525

<<ปทท. ตจว. กทม. || เติบโต-โกลาหล>>

plynoi

ADO.NET ไม่ใช่ ORM คร๊าบบบบ

เป็น API ไว้ query ข้อมูลแบบปกติ (ประมาณ JDBC ของ Java)

แต่ก็ถือว่าสุดยอดในบรรดาสาย query ตรงๆ ที่สุดแล้ว

28 Dec 07

bow_der_kleine

ขอบคุณครับคุณ Plynoi ผมเองก็ไม่เคยแตะ .NET เป็นเรื่องเป็นราวครับ เลยงง ๆ

ข้อมูลเพิ่มเติมครับ ผมไปเจอคนเขียน ORM เล็ก ๆ ที่ทำงานค้ลาย ๆ ActiveRecord ใน ROR ไว้ ใน An ActiveRecord like ORM (object relation mapper) under 200 lines ดูแล้วเท่ห์ดี เพราะมันสั้นได้ใจ เลยเอามาฝากครับ

โดยส่วนตัวผมว่า ActiveRecord นี่ ดีกว่า ORM ใน Django กับ SQLObject อยู่พอสมควรเลย

29 Dec 07

Mr. PeeTai

ไหง PHP ไม่สนับสนุนล่ะเนี่ย แย่เจง ๆ T-T

30 Dec 07

bow_der_kleine

ORM สำหรับ PHP มีหลายตัวอยู่ครับพี่ไท้ แต่ผมไม่ค่อยรู้จัก เพราะไม่ค่อยได้ใช้ ส่วนมากใช้ Frame Work ที่เขียนเองมากกว่า ORM สำหรับ PHP มีลิสต์อยู่ที่ php bar ครับ เสียดายเป็นภาษาเยอรมัน แต่ตรงลิสต์เป็นภาษาอังกฤษอยู่ครับ

03 Jan 08

ความคิดเห็น (click here to comment)

Search

Navigation

รวมลิงก์น่าสนใจ

ความเคลื่อนไหว

Login

name password

ลืมรหัสผ่าน