Neo4j ที่จะมาช่วยแก้ปัญหาความสัมพันธ์อันซับซ้อน

Puwadon Sricharoen
7 min readJun 5, 2022

--

Neo4J คือ ฐานข้อมูลแบบ Graph Database (GDBMS) ที่พัฒนาด้วย JAVA และใช้ภาษา CQL (Cypher Query Language Neo4j) ในการ Query ข้อมูล ช่วยให้การค้นหาข้อมูลจากความสัมพันธ์อ่านง่ายขึ้น ตัวอย่าง Query นี้จะบอกว่าใครเป็นเพื่อนของทอมมี่บ้าง

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)
RETURN
me, myFriend

หากเคยใช้ฐานข้อมูลแบบ RDBMS เช่น MySQL, MariaDB มาก่อน น่าจะพอคิดออกว่าควรเก็บข้อมูลและเขียน Query อย่างไร แต่ถ้าเพิ่มความซับซ้อนเข้าไปอีก เช่น เราอยากทราบว่าเพื่อนของเพื่อนของทอมมี่มีใครบ้าง จากโจทย์นี้ก็น่าจะเริ่มเขียน Query ยากขึ้นบ้างแล้ว

ความสัมพันธ์ของผู้คนอันซับซ้อน

ศัทพ์ที่จะเห็นในบทความนี้

รู้จัก Node, Relationships และ Property ใน Neo4j
  • Node: เก็บข้อมูลเป็น Object คล้ายกับ document ใน MongoDB เช่น คน, สินค้า, สถานที่, และอื่นๆ
  • Relationships: ใช้สำหรับกำหนดความสัมพันธ์กันระหว่าง 2 Node

Cypher คือ ภาษาที่มีหน้าที่คล้าย SQLใช้ขอข้อมูลกับฐานข้อมูลแบบ Graph ถูกออกแบบให้มีความใกล้เคียงภาษามนุษย์มากขึ้น

CREATE (:User {name: 'Tommy', livesIn: 'Bangkok'})
  • Label: จากตัวอย่างด้านบน :User มีหมายความว่า เรากำลังจะสร้าง Node ใหม่ขึ้นมาและตั้งชื่อว่า User
  • Property: ข้อมูลที่อยู่ใน Node ในตัวอย่างมี 2 ตัว คือ name และ livesIn

เมื่อรันคำสั่ง CREATE แล้ว จะได้ Node ใหม่และการดึงข้อมูลออกมา ต้องใช้คำสั่ง MATCH และ Variable สำหรับ RETURN ข้อมูลที่เราต้องการออกมาตามตัวอย่างด้านล่าง

MATCH
(people:User {name: "Tommy"})
RETURN
people.name, people.livesIn
  • Variable: จากตัวอย่าง people จะเก็บค่าที่ได้จากการค้นหาใน :User ที่มี name: “Tommy” และใช้ RETURN เพื่อแสดงค่าของ people อีกที

อีกหนึ่งประโยชน์ของการใช้ variable จะช่วยให้ใช้งาน Query ได้ยืดหยุ่นมาขึ้น ตัวอย่างเช่น สามารถสร้าง Nodes และ Relationships ได้ในคำสั่งเดียว

CREATE
(tommy:User {name: 'Tommy', livesIn: 'Bangkok'}),
(honda:Car {name: 'Honda Civic', year: 2008}),
(tommy)-[:BUY {price: 999000}]->(honda)

เราสามารถสร้าง 2 Nodes พร้อมกันได้ โดยเก็บค่าไว้ในตัวแปรชื่อ tommy และ honda จากนั้นเชื่อมกับด้วย :BUY อีกที สังเกตุว่าที่ :BUY (Relationships) สามารถใส่ Property ได้เช่นกัน ถึงตอนนี้เราจะได้ข้อมูลว่า Tommy อาศัยอยู่ Bangkok และซื้อรถยนต์ Honda Civic รุ่นปี 2008 ด้วยราคา 999,000 บาท

รัน Neo4j เพื่อทดลองใช้งานไปพร้อมกัน

เพื่อความสะดวก ผมสร้าง https://github.com/jimmy18dev/neo4j-find-mutual-friends สำหรับรัน Neo4j แบบง่ายด้วย docker compose สามารถลองทำตามตัวอย่างนี้ไปพร้อมกันได้ทันที เพียงแค่ clone ลงมาและรันด้วย docker compose up -d ได้เลย

git clone https://github.com/jimmy18dev/neo4j-find-mutual-friends.git
jimmy18dev/neo4j-find-mutual-friends
# Goto directory
cd neo4j-find-mutual-friends
# Start service
docker compose up -d

เมื่อรัน Docker compose แล้ว เข้าไปที่ http://127.0.0.1:7474/browser/ สำหรับการครั้งแรก ให้ใช้ Username: neo4j Password: neo4j จากนั้นระบบจะให้ตั้งรหัสผ่านใหม่อีกครั้ง เท่านี้ก็พร้อมใช้งานแล้ว

เข้าใช้งาน Neo4j ครั้งแรก

ต้องการลบข้อมูลทั้งหมด

ในระหว่างที่ทำตามตัวอย่างแล้วอยากเริ่มต้นใหม่ คำสั่งนี้จะลบทุก Nodes และ Relationships ได้แบบเหมือนเริ่มต้นใหม่ (ใช้อย่างระวัง!)

MATCH (n)
DETACH DELETE n

MACTH (n) คือ การค้นหา node โดยไม่มีเงื่อนไข (แปลว่าทุก nodes ใน Database) และเอามาลบออกด้วยคำสั่ง DETACH DELETE n สังเกตุว่าถ้ามี DETACH นำหน้าคือ การลบข้อมูลแบบไม่สนใจ Relationship ของ Node นั้นๆเลย (หายเกลี้ยง!)

สร้างและค้นหาข้อมูลจากความสัมพันธ์

ทอมมี่ (Tommy) อาศัยอยู่ที่กรุงเทพมหานคร มีเพื่อนสนิทอยู่บ้าง และเขากำลังสนใจจะซื้อรถ Tesla ในอนาคต แต่เพื่อนเขากลับไม่มีใครเคยใช้รถ Tesla เลย แต่เราแอบรู้มาว่าเพื่อนของเพื่อนอีกที มีบางคนที่ใช้รถ Tesla มาตั้งแต่ปี 2016 วันนี้เราจะช่วยแนะนำทอมมี่ ว่าเขาต้องไปคุยกับใครบ้าง เพื่อสอบถามขอข้อมูลเกี่ยวกับรถคันนี้ โดยใช้ข้อมูลความสัมพันธ์ทั้งหมดที่มี

เริ่มสร้าง User node ของทอมมี่ รวมถึงผู้คนรอบตัวของเขา ด้วยคำสั่ง CREATE สังเกตุว่าเรายังไม่ใส่ความสัมพันธ์ใดๆ แต่ละคนจะมี Property แค่ชื่อและที่อยู่เท่านั้น (Bangkok, Chiang Mai และ Hua Hin)

CREATE
(:User {name: 'Tommy', livesIn: 'Bangkok'}),
(:User {name: 'Wichai', livesIn: 'Bangkok'}),
(:User {name: 'Suda', livesIn: 'Bangkok'}),
(:User {name: 'Kitti', livesIn: 'Chiang Mai'}),
(:User {name: 'Kong', livesIn: 'Chiang Mai'}),
(:User {name: 'Meena', livesIn: 'Chiang Mai'}),
(:User {name: 'Toon', livesIn: 'Hua Hin'}),
(:User {name: 'Pang', livesIn: 'Hua Hin'}),
(:User {name: 'Win', livesIn: 'Bangkok'}),
(:User {name: 'Yuri', livesIn: 'Chiang Mai'})

ถ้าต้องให้แสดง Nodes และเส้นความสัมพันธ์ต่างๆตามภาพด้านล่าง ให้กดที่ *(10) ตรงหัวข้อ Node Labels

แสดงผลข้อมูลแบบ Graph บน Neo4j

กำหนดความสัมพันธ์ให้กับทุกคนว่าใครเป็นเพื่อนกันบ้าง โดยใช้คำสั่ง MATCH พร้อมกับ CREATE

MATCH
(tommy:User {name: 'Tommy'}),
(wichai:User {name: 'Wichai'}),
(suda:User {name: 'Suda'}),
(kitti:User {name: 'Kitti'}),
(kong:User {name: 'Kong'}),
(meena:User {name: 'Meena'}),
(toon:User {name: 'Toon'}),
(pang:User {name: 'Pang'}),
(win:User {name: 'Win'}),
(yuri:User {name: 'Yuri'})
CREATE
(tommy)-[:IS_FRIENDS]->(toon),
(tommy)-[:IS_FRIENDS]->(kong),
(tommy)-[:IS_FRIENDS]->(pang),
(tommy)-[:IS_FRIENDS]->(win),
(toon)-[:IS_FRIENDS]->(meena),
(toon)-[:IS_FRIENDS]->(suda),
(pang)-[:IS_FRIENDS]->(suda),
(kong)-[:IS_FRIENDS]->(kitti),
(kong)-[:IS_FRIENDS]->(wichai),
(kong)-[:IS_FRIENDS]->(pang),
(kong)-[:IS_FRIENDS]->(win),
(kong)-[:IS_FRIENDS]->(suda),
(win)-[:IS_FRIENDS]->(yuri)

สามารถใช้ MATCH กับ CREATE ได้ในคำสั่งเดียวกัน โดยใช้ตัวแปรเข้ามาช่วย (tommy, wichai, suda, …) ในส่วน MATCH จะทำหน้าที่ค้นหา User จาก name และนำมาใส่ตัวแปรข้างหน้า :User ไว้ จากนั้นนำมาสร้างความสัมพันธ์ด้วย :IS_FRIENDS_WITH (ไม่มี Property) เมื่อรันคำสั่งแล้ว ก็จะได้ภาพผู้คนและความสัมพันธ์ต่างภาพนี้

ความสัมพันธ์ระหว่างทอมมี่และเพื่อนของเขา

โจทย์ที่ 1 : ทอมมี่เป็นเพื่อนกับใครบ้าง ?

เพื่อนของทอมมี่
MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)
RETURN
me, myFriend

ตัวอย่างการใช้ MATCH เพื่อค้นหาจากความสัมพันธ์ที่ต้องการ สังเกตุว่าใช้ตัวแปร me เป็นตัวแทนของ Tommy ที่มีความสัมพันธ์แบบ :IS_FRIENDS_WITH กับ Nodes อื่น และเอาทุก Nodes ที่เข้าเงื่อนไขนี้ มาเก็บไว้ใน myFriend จากนั้น RETURN เพื่อแสดงผลลัพธ์

หากต้องการแสดงบาง Property สามารถใช้ myFriend.name และ AS มาช่วยให้ผลลัพธ์ดูง่ายขึ้น

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)
RETURN
myFriend.name AS Name, myFriend.livesIn AS LivesIn

สังเกตุว่า ถ้าไม่ RETURN myFriend ก็จะไม่สามารถดูข้อมูลแบบ Graph ได้

โจทย์ที่ 2 : เพื่อนของทอมมี่ คนไหนอยู่หัวหินบ้าง ?

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)
WHERE
myFriend.livesIn = 'Hua Hin'
RETURN
myFriend.name AS Name, myFriend.livesIn AS LivesIn

คำสั่ง WHERE ทำหน้าที่เป็นเงื่อนไข myFriend.livesIn = ‘Hua Hin’ ในการค้นข้อมูลออกมา ซึ่งสามารถใช้ AND, OR ได้เหมือนกับ RDBMS

ตัวอย่างการใช้ WHERE ใน Neo4j

โจทย์ที่ 3 : ทอมมี่กับก้อง มีเพื่อนร่วมกันเป็นใครบ้าง ?

MATCH
(me)-[:IS_FRIENDS]->(mf)<-[:IS_FRIENDS]-(other)
WHERE
me.name = 'Tommy' AND other.name = 'Kong'
RETURN
mf.name AS Name, mf.livesIn AS LivesIn

เราประกาศตัวแปร me และ other ขึ้นมาก่อน แล้วใช้ WHERE me.name = ‘Tommy’ และ other.name = ‘Kong’ ในการกำหนดค่าสำหรับใช้เป็นเงื่อนไข ส่วนตัวแปร mf ที่มีทิศทางของ :IS_FRIENDS_WITH ที่ชี้เข้ามาทั้งสองทาง ความหมายว่า me เป็นเพื่อนกับ mf และ other ก็เป็นเพื่อนกับ mf เช่นกัน จากนั้นก็เอา mf มา RETURN เป็นคำตอบนั้นเอง

โจทย์ที่ 4: ใครเป็นเพื่อนของเพื่อนทอมมี่กันบ้าง ?

ข้อนี้ต้องการค้นหาเพื่อนของเพื่อน ผู้คนเหล่านี้อาจจะมาได้มาเจอทอมมี่ในอนาคต เมื่อดูจากภาพความสัมพันธ์ทั้งหมดก็จะเห็นคำตอบอยู่แล้ว (วงสีแดง)

เพื่อนของเพื่อน ของทอมมี่
MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:IS_FRIENDS]->(friendOfFriends)
RETURN
myFriend.name, friendOfFriends.name, friendOfFriends.livesIn
ORDER BY
friendOfFriends.livesIn

ตัวแปร me ใช้แทน Tommy เอง ตัวแปร myFriend ใช้แทนเพื่อนของเขา และ friendOfFriends ใช้แทนเพื่อนของเพื่อนเขาอีกที

ผลลัพธ์นี้ยังไม่ถูกต้อง ถ้าดูจากภาพจะเห็นว่า ก้องเป็นเพื่อนกับวิน ทำให้เข้าเงื่อนไขเพื่อนของเพื่อน จึงแสดงวินมาในผลลัพธ์ ส่วนเคสของแป้งก็เช่นกัน

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:IS_FRIENDS]->(friendOfFriends)
WHERE NOT
(me)-[:IS_FRIENDS]->(friendOfFriends)
RETURN DISTINCT
friendOfFriends.name, friendOfFriends.livesIn
ORDER BY
friendOfFriends.livesIn

สามารถใช้ WHERE NOT เข้ามาช่วยได้ จากตัวอย่างหมายความว่า ถ้า me กับ friendOfFriends เป็นเพื่อนกัน เราจะตัดออก สุดท้ายก็ใช้ DISTINCT เพื่อตัดคำตอบที่ซ้ำกันออกไป

ตัวอย่างการใช้ WHERE NOT และ DISTINCT บน Neo4j

เพิ่มความสัมพันธ์ระหว่างผู้คนกับรถของพวกเขา

ต่อไปเราจะเพิ่ม Node ของรถ เพิ่มความซับซ้อน โดยกำหนดสัมพันธ์ว่าแต่ละคนใช้รถยี่ห้ออะไรและซื้อปีไหน ด้วยคำสั่งนี้

MATCH
(tommy:User {name: 'Tommy'}),
(wichai:User {name: 'Wichai'}),
(suda:User {name: 'Suda'}),
(kitti:User {name: 'Kitti'}),
(kong:User {name: 'Kong'}),
(meena:User {name: 'Meena'}),
(toon:User {name: 'Toon'}),
(pang:User {name: 'Pang'}),
(win:User {name: 'Win'})
CREATE
(honda:Car {name: 'Honda'}),
(bmw:Car {name: 'BMW'}),
(tesla:Car {name: 'Tesla'}),
(toyoto:Car {name: 'Toyoto'}),
(tommy)-[:BUY {buyAt: 2018}]->(honda),
(tommy)-[:BUY {buyAt: 2020}]->(bmw),
(wichai)-[:BUY {buyAt: 2018}]->(toyoto),
(suda)-[:BUY {buyAt: 2018}]->(bmw),
(kitti)-[:BUY {buyAt: 2021}]->(bmw),
(kong)-[:BUY {buyAt: 2018}]->(tesla),
(kitti)-[:BUY {buyAt: 2020}]->(tesla),
(suda)-[:BUY {buyAt: 2016}]->(tesla),
(meena)-[:BUY {buyAt: 2018}]->(bmw),
(toon)-[:BUY {buyAt: 2004}]->(bmw),
(toon)-[:BUY {buyAt: 2012}]->(honda),
(pang)-[:BUY {buyAt: 2019}]->(bmw),
(win)-[:BUY {buyAt: 2014}]->(bmw),
(wichai)-[:BUY {buyAt: 2011}]->(bmw),
(kong)-[:BUY {buyAt: 2000}]->(toyoto)
เพื่อนของทอมมี่และรถของพวกเขา

โจทย์ที่ 5 : เพื่อนของทอมมี่ ใช้รถยี่ห้ออะไรกันบ้าง ?

เริ่มเห็นความซับซ้อนมากขึ้น จนแทบจะหาผลลัพธ์ด้วยตาเปล่าไม่ได้แล้ว หลังจากนี้เราจะหาความสัมพันธ์ระหว่างทอมมี่กับข้อมูลเหล่านี้กัน

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:BUY]->(car)
RETURN
myFriend, car

โจทย์ที่ 6 : ยี่ห้อรถอะไร ที่เพื่อนของทอมมี่ใช้กันมากที่สุด ?

สามารถใช้คำสั่ง count(n) สำหรับนับจำนวนได้ง่ายๆ

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:BUY]->(car)
RETURN
car.name, count(car)

โจทย์ที่ 7 : ยี่ห้อรถอะไร ที่เพื่อนของเพื่อนทอมมี่ใช้กันมากที่สุด ?

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:IS_FRIENDS]->(friendOfFriends)-[:BUY]->(car)
RETURN
myFriend, friendOfFriends, car

โจทย์นี้สามารถใช้วิธีการหา เพื่อนของเพื่อน จากโจทย์เดิมได้เลย friendOfFriends เพียงแค่เติมความสัมพันธ์ระหว่างคนกับรถเข้าไป และ RETURN ผลลัพธ์ออกมา

โจทย์ที่ 8 : ถ้าทอมมี่อยากซื้อ Tesla เขาจะขอคำแนะนำจากใคร ต้องติดต่อผ่านใคร ?

(จากโจทย์ข้อ 6) จะเห็นว่าเพื่อนทอมมี่เองไม่มีใครใช้ Tesla เลย (มีแต่ BMW, Toyota) จึงต้องไปหาจากเพื่อนของเพื่อนอีกที ว่าใครใช้ Tesla และต้องติดต่อผ่านใคร

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:IS_FRIENDS]->(friendOfFriends)-[:BUY]->(car)
WHERE
car.name = 'Tesla'
RETURN
myFriend, friendOfFriends, car

จะเห็นว่าคนที่ใช้ Tesla และเป็นเพื่อนของเพื่อนทอมมี่อีกที คือ สุดา และ กิตติ ซึ่งสามารถติดต่อผ่านแป้ง ก้อง หรือ ตูน ก็ได้

ตอนนี้ทอมมี่พอจะเห็นแล้วว่าต้องติดต่อใคร เพื่อจะสอบถามข้อมูลเกี่ยวกับรถ Tesla ก่อนจะตัดสินใจซื้อ งั้นเราเริ่มจากคนที่ใช้รถมานานที่สุดก่อน เขาน่าจะมีประสบการณ์มากที่สุด

MATCH
(me:User {name: 'Tommy'})-[:IS_FRIENDS]->(myFriend)-[:IS_FRIENDS]->(friendOfFriends)-[buy:BUY]->(car)
WHERE
car.name = 'Tesla'
RETURN
myFriend, friendOfFriends, car, buy.buyAt as year
ORDER BY
buy.buyAt ASC

ใช้ประโยชน์จาก Property ที่ใส่ปีที่ซื้อไว้ใน Relationship ระหว่างคนกับรถ

ตอนนี้ทอมมี่รู้แล้วว่าต้องไปติดต่อแป้งหรือก้องเพื่อจะได้คุยกับสุดา เพราะเธอซื้อรถมาตั้งแต่ปี 2016 หรือ จะนัดตูนก้องต่อเลยก็ได้

ทดลองใช้งานผ่าน neo4j-driver

สามารถเชื่อมต่อผ่าน neo4j-driver ซึ่ง official drivers ตอนนี้รองรับอยู่ 5 ภาษาคือ Go, Java, JavaScript, Python และ .NET ดูรายละเอียดเพิ่มเติมได้จาก https://neo4j.com/docs/drivers-apis/

ตัวอย่างการสร้าง Node ใหม่ด้วย NodeJS โดยเชื่อมต่อผ่าน Neo4j Driver for JavaScript และใช้งาน Transaction ร่วมด้วย สามารถสร้างหลาย Node พร้อมกันและ rollback ได้เมื่อเจอข้อผิดพลาด

const neo4j = require('neo4j-driver')const driver = neo4j.driver('bolt://localhost:7687', neo4j.auth.basic('username', 'password'))
const session = driver.session()
const txc = session.beginTransaction()
try {
await txc.run(
'CREATE (:User {name: $name, livesIn: $livesIn})', {
name: 'Tommy',
livesIn: 'Bangkok'
}
)
await txc.commit()
} catch (error) {
await txc.rollback()
} finally {
await session.close()
}

ตัวอย่างการค้นหาเพื่อนที่เข้าร่วมงานอีเวนต์เดียวกับเรา โดยระบุเงื่อนไขไว้ที่ userId และ eventId

const neo4j = require('neo4j-driver')
const driver = neo4j.driver('bolt://localhost:7687', neo4j.auth.basic('username', 'password'))
const session = driver.session()
const readTxResultPromise = session.readTransaction(txc => {
const result = txc.run('MATCH (me:User {userId: $userId})-[:FOLLOW]->(people)-[:JOIN]->(event:Event {eventId: $eventId}) RETURN people.userId AS userId', {
userId: '0001',
eventId: '11001'
})
return result
})
const userIds = await readTxResultPromise.then(result => {
session.close()
return result.records.map(record => {
return {
userId: record.get('userId'),
userName: record.get('userName')
}
})
}).catch(error => {
console.log(error)
})
console.log('userIds', userIds)

สังเกตุที่ result.records.map เพราะต้องเอา result.records มา map และ record.get คีย์ที่ต้องการออกมา จึงจะได้ผลลัพธ์เป็น Array เอาไปใช้งานต่อไป

ใช้งานได้หลากหลาย

Graph Database ยังสามารถใช้งานในอีกหลายรูปแบบ เช่น ระบบสุขภาพ โรงพยาบาล (Healthcare) ที่ข้อมูลมีความสัมพันธ์กันระหว่าง ผู้ป่วย อาการ โรค ยา และการตอบสนองต่อยา ถ้าความสัมพันธ์ถูกบันทึกอย่างถูกต้อง จะช่วยให้การค้นหาข้อมูลในมุมมองต่างๆเป็นไปได้ง่ายขึ้น

Patient–[HAVE]–>Disease[s]
Disease–[HAVE]–>Symptom[s]
Disease–[HAVE]–>Treatment[s]
Treatment–[HAVE]–>Drug[s]
Drug–[INTERACT]–>Drug[s]

จากความสัมพันธ์นี้ ช่วยให้สามารถค้นหาอาการที่เกิดบ่อยกับโรคโรคหนึ่งผ่าน Query นี้

MATCH
(p:Patient)-[r:SHOWS_SYMPTOM]->(s:Symptom)<-[:HAS_SYMPTOM]-(d:Disease{name:"Cancer"})
RETURN
p.name,count(r)
ORDER BY
count(r)

อีกตัวอย่างของการเก็บข้อมูลที่มีความสัมพันธ์ให้กับร้านค้าออนไลน์ ที่บันทึกการซื้อสินค้าของผู้คน สินค้าที่สนใจ สินค้าที่ค้นหา หรือเพื่อนของพวกเขาซื้ออะไรกันบ้าง

Customer–[HAVE]–>Location
Customer–[BUYS]–>Product[s]
Product–[HAVE]–>Categories
Customer-[LOOKS]->Product[s]

หวังว่าบทความนี้จะช่วยให้เห็นตัวอย่างการใช้งาน Graph database หรือถ้าเป็นจุดเริ่มต้นให้ลองเอาไปใช้งานจริงก็จะยินดีมากครับ

--

--

Puwadon Sricharoen

I’m an optimistic person. Love to travel to a new places and enjoy to spending my free time and vacation day to pursue my hobbies.