418342 ภาคปลาย 2552/ปฏิบัติการที่ 8
ในปฏิบัติการที่ 8 นี้เราจะทำการสร้าง web application ที่เก็บข้อมูลของบุคคลพร้อมทั้งรูปภาพของคนคนนั้น
Disclaimer: ผมไม่ได้เขียนปฏิบัติการนี้ด้วยไอเดียของตนเอง ผมแปลมันมาจากบทที่ 8 ของหนังสือ Learning Rails ของ St.Laurent และ Dumbill
เนื้อหา
การเก็บไฟล์ของเวบแอพพลิเคชัน
ไฟล์รูปภาพหรือไฟล์อื่นๆ ที่ web application จัดเก็บมักเป็นไฟล์ขนาดใหญ่ จึงไม่เหมาะสมที่จะเก็บมันไว้ในฐานข้อมูลโดยตรง (แต่ไม่ได้หมายความว่าเราจะเก็บมันด้วยฐานข้อมูลไม่ได้) ในกรณีของ web application ของเรา เราจะเก็บไฟล์รูปภาพไว้ในไดเรกทอรี RAILS_ROOT/public/photo_store โดยที่ RAILS_ROOT คือไดเรกทอรีของ web application ที่เราสร้างด้วยคำสั่ง rails
สิ่งที่เราจะต้องคำนึงถึงต่อไปคือเราจะตั้งชื่อไฟล์รูปภาพเหล่านั้นว่าอย่างไร เราไม่สามารถใช้ชื่อไฟล์ที่ผู้ใช้ส่งมาได้โดยตรง เพราะชื่อไฟล์ของผู้ใช้หลายๆ คนอาจซ้ำกันได้ เพื่อหลีกเลี่ยงปัญหานี้ เราจะตั้งชื่อไฟล์ตาม primary key ของตารางบุคคลที่เราเก็บ เช่น ไฟล์รูปภาพของคนที่มี primary key เท่ากับ 10 ก็จะมีชื่อไฟล์ว่า "10" (ยังไม่รวม extension) เป็นต้น
ไฟล์รูปภาพที่ผู้ใช้ส่งมาไม่ได้หลายรูปแบบ เช่น JPEG, GIF, หรือ BMP เป็นต้น และเราควรจะสามารถจัดการกับรูปภาพได้ทุกแบบ กล่าวคือ ถ้าผู้ใช้ส่งภาพแบบ JPEG ให้เป็นภาพของคนที่มี primary key เท่ากับ 25 เราควรจะตั้งชื่อไฟล์ของคนคนนั้นว่า "25.jpg" แต่ถ้าไฟล์มีรูปแบบ PNG เราก็ควรจะตั้งชื่อว่า "25.png" เป็นต้น ฉะนั้น เพื่อให้เรารู้ว่าไฟล์รูปภาพมีรูปแบบอะไร เราจะต้องเก็บ extension ของไฟล์รูปภาพไว้ในโมเดลของบุคคล
สร้าง web application และโมเดล
เราเริ่มต้นจากการสร้าง application ชื่อ people
rails people cd people
แล้วเราจะทำการสร้าง model ชื่อ person สำหรับเก็บบุคคล เพื่อความง่าย person จะมีฟีลด์เพียงแค่ 2 ฟีลด์ คือ ชื่อ และ extension ของรูปที่ผู้ใช้ upload
ruby script/generate scaffold person name:string extension:string
หลังจากนั้นเราสั่ง
rake db:migrate
เพื่อสร้างฐานข้อมูล
เปลี่ยน form เพื่อ upload รูป
เราจะต้องทำการเปลี่ยน app/views/people/new.html.erb เพื่อให้ผู้ใช้สามารถอัพโหลดรูปได้
อันดับแรกให้เราทำการลบโค้ด <geshi lang="html">
<%= f.label :extension %>
<%= f.text_field :extension %>
</geshi> ออกก่อน เนื่องจากเราไม่ต้องการให้ผู้ใช้ป้อน extension ของไฟล์ด้วยตนเอง
ให้เราเพิ่มโค้ด <geshi lang="html">
Photo
<%= f.file_field :photo %>
</geshi> เข้าไปแทนที่โค้ดส่วนที่เราลบออกไป
และให้ไปแก้ไขบรรทัด <geshi lang="html"> <% form_for(@person) do |f| %> </geshi> ให้เป็น <geshi lang="html"> <% form_for(@person, :html => { :multipart => true } ) do |f| %> </geshi> ส่วน :html => { :multipart => true } นี้จะทำการเปลี่ยน tag form ใน HTML จาก
<form action="/people" method="post">
ให้กลายเป็น
<form action="/people" enctype="multipart/form-data" method="post">
คุณสมบัติ enctype="multipart/form-data" เป็นตัวบอกให้ browser ส่งข้อมูลมาหลายๆ ส่วน กล่าวคือ web browser จะมีการส่งข้อมูลรูปไปยัง web server ทีหลังข้อมูลอื่นๆ ของฟอร์ม ในทางกลับกัน web server ก็จะทราบว่าตัวเองจะต้องรับข้อมูลรูปซึ่งจะตามมาทีหลังด้วย
แก้ไขโมเดล
ในฟอร์มข้างบน เราสั่ง f.file_field :photo เหมือนกับว่า photo เป็นฟีลด์หนึ่งของโมเดล person แต่ความจริงแล้ว person ไม่ได้มีฟีลดิ์นี้แต่อย่างใด อย่างไรก็ดี เราสามารถสร้าง "ฟีลด์เทียม" ชื่อ photo ได้ด้วยการเพิ่มเมธอด photo= เข้าในโมเดล Person <geshi lang="ruby"> class Person < ActiveRecord::Base
def photo=(file_data) unless file_data.blank? @file_data = file_data self.extension = file_data.original_filename.split('.').last.downcase end end
end </geshi> สังเกตว่า
- เมธอด photo= ทำให้ Rails สามารถเอาข้อมูลที่ส่งมากับ web form ใส่เข้าไปในโมเดลได้ทันที
- เมธอด photo= จะทำการหา extension ของรูปให้โดยอัตโนมัติ
- ข้อมูลของรูปภาพจริงๆ จะถูกเก็บไวใน instance variable @file_data แต่ข้อมูลนี้จะไม่ถูก save ลงฐานข้อมูล
สิ่งต่อไปที่เราต้องทำคือการเขียนข้อมูลใน @file_data ลงในไฟล์ซึ่งอยู่ใน RAILS_ROOT/public/photo_store อันดับแรกเราจะต้องทำกำหนดชื่อไฟล์ที่เราจะใช้เก็บรูปภาพก่อน เพื่อความสะดวก เราจะเขียนเมธอด photo_name เพื่อคำนวณชื่อไฟล์ <geshi lang="ruby">
PHOTO_STORE = File.join(RAILS_ROOT, "public", "photo_store") def photo_filename File.join PHOTO_STORE, "#{id}.#{extension}" end
</geshi> เห็นได้ว่า PHOTO_STORE เป็นค่าคงที่ที่มีค่าเท่ากับชื่อ directory ที่เราจะเก็บไฟล์
แล้วเราจึงเขียนเมธอดสำหรับเขียนรูปภาพเก็บลงใน file system ดังต่อไปนี้ <geshi lang="ruby"> private
def store_photo if @file_data FileUtils.mkdir_p PHOTO_STORE File.open(photo_filename, 'wb') do |f| f.write(@file_data.read) end @file_data = nil end end
</geshi> สังเกตว่า
- ก่อนจะเขียนข้อมูลลงในไฟล์ store_photo จะทำการสร้างไดเรกทอรี PHOTO_STORE ก่อน ฟังก์ชัน FileUtils.mkdir_p จะลองสร้างไดเรกทอรีที่ผู้ใช้กำหนดชื่อให้ แต่ถ้าไดเรกทอรีนั้นมีอยู่แล้วก็จะไม่เกิดอะไรขึ้น
- หลังจากเขียนข้อมูลลงในไฟล์เสร็จแล้วมีการเซต @file_data = nil เพื่อไม่ให้มีการเขียนข้อมูลลงไฟล์มากกว่าหนึ่งครั้ง
เมธอด store_photo จะต้องถููกเรียกทุกครั้งหลังจากที่ข้อมูลของโมเดลถูก save ลงฐานข้อมูล เราสามารถกำหนดให้โมเดลทำเช่นนี้ได้ด้วยการเพิ่ม after_save :store_photo ลงในโมเดล <geshi lang="ruby"> class Person < ActiveRecord::Base
after_save :store_photo ...
private
def store_photo if @file_data FileUtils.mkdir_p PHOTO_STORE File.open(photo_filename) do |f| f.write(@file_data.read) end @file_data = nil end end
end </geshi>
นอกจากนี้ เพื่อความสะดวก เรายังสามารถเพิ่มเมธอด has_photo? เพื่อเช็คว่าคนที่เก็บในฐานข้อมูลมีรูปหรือไม่ <geshi lang="ruby">
def has_photo? File.exists? photo_filename end
</geshi> และเขียนเมธอด photo_path เพื่อคืน URL ของไฟล์รูป <geshi lang="ruby">
def photo_path? "/photo_store/#{id}.#{extension}" end
</geshi>
แสดงผลรูป
เราสามารถแก้ไข app/views/people/show.html.erb ให้มีการแสดงผลรูปได้โดยเพิ่มโค้ดต่อไปนี้ <geshi lang="html">
Photos:
<% if @person.has_photo? %>
<%= image_tag @person.photo_path %>
<% else %>
No photo.
<% end %>
</geshi>
งานส่วนที่เหลือ
เราได้แก้ไข new.html.erb และ show.html.erb ไปแล้ว แต่ยังไม่ได้แก้ไข edit.html.erb และ index.html.erb ขอให้คุณลองแก้ไขไฟล์เหล่านี้ใให้มันสามารถรับและแสดงผลรูปได้