6 เทคนิค Eloquent ที่ช่วยให้ code ดูอ่านง่ายขึ้น

31 มกราคม 2564 เวลาอ่าน 7 นาที
6 เทคนิค Eloquent ที่ช่วยให้ code ดูอ่านง่ายขึ้น

Eloquent เป็น ORM ที่มาพร้อมกับ Laravel มีการใช้งานแบบ Active Record และทำให้เราสามารถติดต่อกับ database ได้ง่าย แต่ละ Model ใช้แทนตารางในฐานข้อมูลที่เราทำงานด้วย ในบทความนี้เราจะพาไปดูเคล็ดลับ method และ property ที่นักพัฒนาหลาย ๆ คนอาจจะไม่รู้ว่าจะช่วยปรับปรุง code ของเราให้ทำงานดีขึ้นได้

Snake Attributes

ลองดูตัวอย่าง code ต่อไปนี้เพื่อใช้งาน Snake Attribute

/**
 * Indicates whether attributes are snake cased on arrays.
 *
 * @var bool
 */
public static $snakeAttributes = true;

ส่วนใหญ่แล้วคนมักจะสับสนว่าใช้ property นี้แล้วจะเปลี่ยนวิธีที่เราเรียกใช้ model attributes หลายคนคิดว่าถ้าเปลี่ยนค่านี้แล้วจะทำให้สามารถเรียกใช้ attribute แบบ camel-case จริง ๆ แล้วไม่ใช่แบบนั้นเลยและเป็นวิธีที่ไม่แนะนำให้ใช้ การเลี่ยน property นี้มีผลเพียงแค่กำหนดว่าเมื่อ model แสดงออกมาเป็น array แล้วจะให้ attribute เป็นแบบ camel หรือว่า snake-case กันแน่

Pagination

การทำ Pagination โดยใช้ Laravel Eloquent ORM นั้นง่ายมาก เราอาจจะคุ้นเคยกับการใช้ method paginate() ลักษณะนี้

$comments = Comment::paginate(20);

ด้วย method นี้เราสามารถ paginate โมเดล comment โดยแสดงหน้าละ 20 รายการ การเปลี่ยนค่านี้ทำให้เราสามารถกำหนดได้ว่าเราต้องการให้แสดงจำนวนกี่รายการในหนึ่งหน้า ถ้าเราไม่ได้ระบุค่าอะไร ค่าเริ่มต้นจะถูกใช้งาน ซึ่งก็คือ 15 รายการ

สมมุติว่าเราต้องการกำหนดให้ comment ที่แสดงในหลาย ๆ ที่บนเว็บไซต์ของเรา แสดง 30 รายการต่อหน้าเสมอ เราไม่จำเป็นต้องเรียก paginate(30) ทุกครั้ง แล้วตามไปเปลี่ยนในทุก ๆ ที่ถ้าเราเปลี่ยนใจอยากแสดงจำนวนอื่นที่ไม่ใช่ 30 รายการต่อหน้า ดังนั้นเราสามารถกำหนด ค่า 30 ไว้ที่คลาส Model ได้เลยโดยตรง ดังนี้

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{

    protected $perPage = 30;

    // ...

}

เพิ่ม custom values ไปยัง Model

Eloquent มีฟีเจอร์ที่เรียกว่า "Accessor" ฟีเจอร์นี้ทำให้เราเพิ่ม custom field ไปยัง Model ที่ไม่ได้มีค่านั้นจริงอยู่ในตารางได้ ไม่จำเป็นว่าเราจะใช้ค่าที่มีอยู่แล้วหรือสร้างขึ้นมาใหม่เลย เราสามารถ return ค่าอะไรออกมาก็ได้ อันนี้เป็นตัวอย่างในการใช้งาน Accessor สมมุติว่าเรามี Model ชื่อ User โดยระบุ code ดังนี้

function getFullNameAttribute() {
    return sprintf('%s %s', $this->first_name, $this->last_name);
}

เราจะสามารถใช้ attribute full_name ได้บน Model User แบบนี้

User::latest()->first()->full_name;

ปัญหาคือถ้าเรา return เป็น object อย่างเวลาเรียกใน collection ค่า attribute นี้จะไม่ถูกดึงมาด้วย

    App\Models\User {#4287
         id: 1,
         first_name: "Tre",
         last_name: "Murazik",
         email: "stanton.nayeli@example.net",
         email_verified_at: "2021-01-31 10:16:19",
         created_at: "2021-01-31 10:16:19",
         updated_at: "2021-01-31 10:16:19",
       },

เพิ่ม attribute $appends ไปยัง model โดยระบุ array ของชื่อ custom field ที่เราต้องการให้รวมใน collection ด้วย

protected $appends = ['full_name'];

เมื่อเราเรียกผ่าน collection จะมี full_name ปรากฏขึ้นมาด้วย

App\Models\User {#4287
         id: 1,
         first_name: "Tre",
         last_name: "Murazik",
         full_name: "Tre Murazik",
         email: "stanton.nayeli@example.net",
         email_verified_at: "2021-01-31 10:16:19",
         created_at: "2021-01-31 10:16:19",
         updated_at: "2021-01-31 10:16:19",
       },

ใช้ Mutator กับ column ที่ไม่มีอยู่จริง

Mutoator เป็นฟีเจอร์ที่ตรงกันข้ามกับ Accessor เราสามารถใช้ทำอะไรได้หลายอย่าง เช่น การแปลงค่าเพื่อ save ลง column จาก input หลาย ๆ แบบ ลองดูจากตัวนี้ สมมติว่าเราต้องการบันทึกค่าระยะเวลาอะไรสักอย่างหนึ่ง ปกติแล้วเราจะเก็บหน่วยย่อยที่สุด อย่างกรณีนี้เราจะเก็บเวลาเป็นจำนวนวินาที ทีนี้โดยเหตุผลทาง UX แล้วเราไม่อยากให้ User คีย์เป็นวินาที เราจะให้ User คีย์ข้อมูลเข้ามเป็นนาทีหรือชั่วโมงเลยก้ได้ เราสามารถเก็บข้อมูลเป็นวินาทีจาก input ที่เข้ามาได้ง่าย ๆ แบบนี้

class Video extends Model
{
    public function setDurationInMinutesAttribute($value)
    {
        $this->attributes['duration_in_seconds'] = $value * 60;
    }

    public function setDurationInHoursAttribute($value)
    {
        $this->attributes['duration_in_seconds'] = $value * 60 * 60;
    }
}

จากตัวอย่างนี้แปลว่าเราสามารถใช้ column ที่ไม่ได้มีจริงใน table ในฐานข้อมูล duration_in_minutes หรือ duration_in_hours แต่เรามี column จริงที่ชื่อว่า duration_in_seconds โดยค่าจะถูก update ตามการคำนวณที่เราระบุไว้ใน Mutator โดยมีตัวอย่างการใช้งานดังนี้

class VideoController
{
    public function store()
    {
        $video->update([
            'title' => request('title'),
            'duration_in_minutes' => request('duration_in_minutes'),
        ]);
    }
}

วิธีนี้ช่วยให้เราลด logic และการคำนวณใน Controller ลงได้ ทำให้ code เราดูเรียบง่ายมากขึ้น โดยอาศัยการซ่อนการคำนวณไว้ใน mutator ภายใน Model นั่นเอง

Eager loading โดยใช้ $with

โดยปกติแล้ว Laravel จะทำการ "Lazy Loading" หมายความว่าเวลาเราอ่านข้อมูลจาก Model จะไม่มีการดึงข้อมูล relation ที่เกี่ยวข้องมาด้วยถ้าเราไม่ต้องการใช้งาน วิธีการนี้มีข้อดีคือช่วยลดการใช้ memory และ performance ของการอ่านข้อมูล เพราะไม่จำเป็นต้องดึงข้อมูลมาเยอะเกินกว่าที่ใช้งาน ลองดูจาก code ตัวอย่างนี้

foreach (Post::all() as $post) {
    echo $post->author->name;
}

จากตัวอย่างด้านบนเราจะได้ข้อมูล Post มาทั้งหมด แล้ว loop ทีละ post เพื่อแสดงชื่อผู้เขียนของแต่ละ post ทีนี้ด้วยวิธีการแบบ lazy load query ที่เรียกชื่อผู้เขียน จะถูกทำงานตอนที่เราเรียกใช้เท่านั้น วิธีการเขียนแบนี้ทำให้เกิดปัญหาที่เรียกว่า N+1

ที่เรียกว่าเป็นปัญหาแบบ N+1 เพราะว่า N จะเป็นจำนวนของ Post และ 1 เป็นจำนวน query ที่อ่าน post ทั้งหมด ตัวอย่างเช่น ถ้าเรามี 500 Post แล้วเราเรียก query 1 ครั้งเพื่อดึง Post ทั้งหมดึข้นมา และอีก 1 query ของแต่ละ Post เพื่อดึง author ขึ้นมาด้วย ดังนั้นจึงเป็น 500 + 1 query นั่นหมายความว่าจำนวน query จะเพิ่มขึ้นด้วยถ้าเราดึง Post ขึ้นมามากขึ้น

เราสามารถป้องกันการ query จำนวนมากแบบนี้ได้ด้วยการใช้ Eager Loading

$posts = Post::with('author')->get();
foreach ($posts as $post) {
    echo $post->user->name;
}

ด้วยวิธีการนี้จะทำให้เหลือเพียง 2 query เท่านั้น query แรกเพื่ออ่าน Post ทั้งหมด และอีก query เพื่อดึง Author ที่เกี่ยวกับ Post ขึ้นมา โดยเบื้องหลังแล้วจะทำให้เกิด SQL Query ลักษณะนี้

SELECT id, author_id, body FROM posts;
SELECT name FROM authors WHERE id IN (1,2,3,4,5...);

ไม่ว่าจะมี Post 50, 100, 10,000 หรือจำนวนเท่าไหร่ก็ตามจำนวน query จะมีเพียง 2 ครั้งเท่านั้น

เราได้เห็นวิธีการใช้งาน Eager Loading แล้วแต่เป็นเพียงการใช้งานแบบ Manual ถ้าเราต้องการให้ทุกครั้งที่เราเรียกใช้ Model แล้วมีการดึง relation บางตัวที่เกี่ยวข้องมาให้โดยอัตโนมัติแบบ Eager Loading เราสามารถกำหนด Property ที่ Model ได้เลยแบบนี้

<?php

namespace App\Models;


use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
 
    protected $with =['author'];
    
}

ทีนี้ทุกครึ่งที่เรามีการ query Post เราจะได้ author มาด้วยทุกครั้งโดยไม่จำเป็นต้องเรียก method with() อีกครั้ง

นอกจากตัวอย่างนี้แล้วเรายังสามารถทำ Eager Loading กับ nested relation ได้สามารถดูวิธีการและคำอธิบายเชิงลึกจาก Laravel Eloquent Eager Loading

Model keys

บ่อย ๆ ครั้งเราต้องการดึง ID ทั้งหมดจาก query ไม่สำคคัญว่า query จะมีความซับซ้อนแค่ไหนหรือไม่ ส่วนใหญ่แล้วเราสามารถเราจะใช้วิธีประมาณนี้

User::all()->pluck('id');

วิธีการนี้ใช้งานได้แต่เราจะได้เป็น collection กลับมา ถ้าเราต้องการเป็น array เราต้องเรียก method toArray() อีกครั้ง

User::all()->pluck('id')->toArray();

โดยทั่ว ๆ ไปแล้วเราสามารถเขียนให้สั่นลงได้โดยใช้ method modelKeys() แบบนี้

User::all()->modelKeys();

method นี้จะ return ออกมาเป็น array โดย default แล้วจะเป็น ID แต่จริง ๆ แล้ว method นี้ไม่จำเป็นต้อง return ID ออกมาเสมอไป โดย method นี้จะใช้ field ที่ระบุไว้ใน $primaryKey ซึ่งเราสามารถเปลี่ยนเป็น field อื่นได้ให้ตรงกับการใช้งานของเรา

protected $primaryKey = 'id';

สรุป

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

Phattarachai Chaimongkol

เกี่ยวกับ phattarachai.dev

ผมอ๊อฟนะครับ เป็นผู้ประกอบการอิสระ ที่ปรึกษาทางด้าน Web Application Development ให้แก่องค์กร ธุรกิจ SME และหน่วยงานราชการ
Web Developer ผู้มีใจรักใน Laravel เป็นพาร์ทเนอร์บริษัท Digital Agency ชั้นนำทางด้าน UX/UI เพื่อพัฒนาโปรเจคให้แก่ลูกค้า ผมช่วยสร้างเครื่องมือทางด้าน Web ที่มีคุณภาพให้ผู้ประกอบการดำเนินธุรกิจได้ง่ายขึ้นใช้งานได้จริง เน้นประสบการณ์ ความชำนาญ ผลงานคุ้มค่าเทียบเท่าจ้างงานกับบริษัทใหญ่ ๆ

ยามว่าง ๆ ชอบเล่นเกมส์บน Steam ครับ

เรื่องที่เกี่ยวข้อง

PHP function แสดงขนาดไฟล์แบบคนอ่านรู้เรื่อง เป็น KB, MB, GB
31 มกราคม 2564
PHP function แสดงขนาดไฟล์แบบคนอ่านรู้เรื่อง เป็น KB, MB, GB
คำสั่ง Postgresql ที่เอาไว้ใช้จัดการเกี่ยวกับ Replication
31 มกราคม 2564
คำสั่ง Postgresql ที่เอาไว้ใช้จัดการเกี่ยวกับ Replication
สรุป Taylor Otwell Keynote ใน Laracon US 2024 - Inertia 2.0, VS Code Extension และ Laravel Cloud
31 มกราคม 2564
สรุป Taylor Otwell Keynote ใน Laracon US 2024 - Inertia 2.0, VS Code Extension และ Laravel Cloud