ออกแบบพื้นที่เก็บข้อมูลที่เหมาะสมใน PHP?


291

คำนำ: ฉันกำลังพยายามใช้รูปแบบที่เก็บในสถาปัตยกรรม MVC กับฐานข้อมูลเชิงสัมพันธ์

ฉันเพิ่งเริ่มเรียนรู้ TDD ใน PHP และฉันรู้ว่าฐานข้อมูลของฉันอยู่ใกล้มากเกินไปกับส่วนที่เหลือของแอปพลิเคชันของฉัน ฉันอ่านเกี่ยวกับที่เก็บและใช้คอนเทนเนอร์ IoCเพื่อ "ฉีด" ลงในคอนโทรลเลอร์ของฉัน สิ่งที่เจ๋งมาก แต่ตอนนี้มีคำถามเชิงปฏิบัติเกี่ยวกับการออกแบบพื้นที่เก็บข้อมูล พิจารณาตัวอย่างต่อไปนี้

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

ปัญหา # 1: มีฟิลด์มากเกินไป

วิธีการค้นหาทั้งหมดเหล่านี้ใช้ฟิลด์ที่เลือกทั้งหมด (SELECT * ) อย่างไรก็ตามในแอพของฉันฉันพยายาม จำกัด จำนวนฟิลด์ที่ฉันได้รับอยู่เสมอเพราะนี่มักจะเพิ่มค่าใช้จ่ายและทำให้สิ่งต่าง ๆ ช้าลง สำหรับผู้ที่ใช้รูปแบบนี้คุณจะจัดการกับสิ่งนี้อย่างไร

ปัญหา # 2: มีวิธีมากเกินไป

ในขณะที่คลาสนี้ดูดีในตอนนี้ฉันรู้ว่าในแอปที่ใช้งานจริงฉันต้องการวิธีการมากขึ้น ตัวอย่างเช่น:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • เป็นต้น

อย่างที่คุณเห็นอาจมีรายการวิธีที่เป็นไปได้ที่ยาวมาก ๆ และถ้าคุณเพิ่มในปัญหาการเลือกฟิลด์ด้านบนปัญหาจะแย่ลง ในอดีตที่ผ่านมาฉันแค่วางตรรกะทั้งหมดนี้ลงในคอนโทรลเลอร์ของฉัน:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

ด้วยวิธีการที่เก็บของฉันฉันไม่ต้องการที่จะจบลงด้วย:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

ปัญหา # 3: ไม่สามารถจับคู่อินเทอร์เฟซได้

ฉันเห็นประโยชน์ในการใช้อินเทอร์เฟซสำหรับที่เก็บดังนั้นฉันสามารถสลับการใช้งานของฉัน (เพื่อวัตถุประสงค์ในการทดสอบหรืออื่น ๆ ) ความเข้าใจของฉันเกี่ยวกับอินเทอร์เฟซคือพวกเขากำหนดสัญญาที่ต้องปฏิบัติตาม นี่เป็นสิ่งที่ดีจนกระทั่งคุณเริ่มเพิ่มวิธีการเพิ่มเติมลงในที่เก็บข้อมูลของคุณเช่นfindAllInCountry()ไปยังที่เก็บของคุณเช่น ตอนนี้ฉันต้องอัปเดตอินเทอร์เฟซของฉันเพื่อให้มีวิธีนี้ไม่เช่นนั้นการใช้งานอื่นอาจไม่มีวิธีนี้และนั่นอาจทำให้แอปพลิเคชันของฉันพัง จากสิ่งนี้รู้สึกบ้า ... กรณีของหางกระดิกสุนัข

รูปแบบข้อมูลจำเพาะ?

นำไปสู่การนี้ให้เชื่อว่าพื้นที่เก็บข้อมูลควรจะมีเพียงจำนวนคงที่ของวิธีการ (เช่นsave(), remove(), find(), findAll()ฯลฯ ) แต่ฉันจะเรียกใช้การค้นหาที่เฉพาะเจาะจงได้อย่างไร ฉันเคยได้ยินเกี่ยวกับรูปแบบข้อมูลจำเพาะแต่ดูเหมือนว่าฉันจะลดเฉพาะระเบียนทั้งหมด (ผ่านIsSatisfiedBy()) ซึ่งมีปัญหาประสิทธิภาพการทำงานที่สำคัญอย่างชัดเจนหากคุณดึงจากฐานข้อมูล

ช่วยด้วย?

เห็นได้ชัดว่าฉันต้องคิดใหม่อีกครั้งเมื่อทำงานกับที่เก็บ ทุกคนสามารถให้ความกระจ่างเกี่ยวกับวิธีการจัดการที่ดีที่สุด?

คำตอบ:


208

ฉันคิดว่าฉันจะหยุดตอบคำถามของฉันเอง สิ่งต่อไปนี้เป็นเพียงวิธีหนึ่งในการแก้ปัญหาข้อ 1-3 ในคำถามเดิมของฉัน

ข้อจำกัดความรับผิดชอบ: ฉันอาจไม่ใช้คำที่ถูกต้องเสมอเมื่ออธิบายรูปแบบหรือเทคนิค ขอโทษสำหรับสิ่งนั้น.

เป้าหมาย:

  • Usersสร้างตัวอย่างที่สมบูรณ์แบบของตัวควบคุมขั้นพื้นฐานสำหรับการดูและการแก้ไข
  • รหัสทั้งหมดจะต้องสามารถทดสอบได้อย่างสมบูรณ์และเยาะเย้ย
  • ผู้ควบคุมไม่ควรทราบว่าเก็บข้อมูลไว้ที่ไหน (หมายถึงสามารถเปลี่ยนแปลงได้)
  • ตัวอย่างเพื่อแสดงการใช้งาน SQL (โดยทั่วไป)
  • เพื่อประสิทธิภาพสูงสุดผู้ควบคุมควรได้รับข้อมูลที่ต้องการเท่านั้น - ไม่มีฟิลด์เพิ่มเติม
  • การใช้งานควรใช้ประโยชน์จาก mapper ข้อมูลบางประเภทเพื่อความสะดวกในการพัฒนา
  • การใช้งานควรมีความสามารถในการค้นหาข้อมูลที่ซับซ้อน

การแก้ไขปัญหา

ฉันแบ่งการโต้ตอบ (ฐานข้อมูล) ที่เก็บข้อมูลถาวรของฉันออกเป็นสองประเภท: R (อ่าน) และCUD (สร้างอัปเดตลบ) ประสบการณ์ของฉันคือการอ่านเป็นสิ่งที่ทำให้แอปพลิเคชันช้าลงจริง ๆ และในขณะที่การจัดการข้อมูล (CUD) ช้าลง แต่มันก็เกิดขึ้นบ่อยครั้งกว่ามากและมีความกังวลน้อยกว่ามาก

CUD (สร้างอัปเดตลบ) เป็นเรื่องง่าย นี้จะเกี่ยวข้องกับการทำงานร่วมกับที่เกิดขึ้นจริงรุ่นซึ่งจะผ่านไปแล้วฉันจะRepositoriesติดตาสำหรับ หมายเหตุที่เก็บของฉันจะยังคงให้วิธีการอ่าน แต่เพียงสำหรับการสร้างวัตถุไม่แสดง เพิ่มเติมในภายหลัง

R (อ่าน) ไม่ใช่เรื่องง่าย ไม่มีรูปแบบที่นี่เพียงวัตถุที่คุ้มค่า ใช้อาร์เรย์ถ้าคุณชอบ วัตถุเหล่านี้อาจเป็นตัวแทนของรูปแบบเดียวหรือผสมผสานหลายรูปแบบอะไรก็ได้ สิ่งเหล่านี้ไม่ได้น่าสนใจมากนัก แต่เป็นวิธีที่สร้างขึ้นมา Query Objectsฉันใช้สิ่งที่ฉันเรียก

รหัส:

รูปแบบของผู้ใช้

มาเริ่มกันง่ายๆกับโมเดลผู้ใช้พื้นฐานของเรา โปรดทราบว่าไม่มีการขยาย ORM หรือฐานข้อมูลเลย สง่าราศีแบบบริสุทธิ์เท่านั้น เพิ่ม getters, setters, validation หรืออะไรก็ตามของคุณ

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

ส่วนต่อประสาน Repository

ก่อนที่ฉันจะสร้างที่เก็บผู้ใช้ของฉันฉันต้องการที่จะสร้างส่วนต่อประสานที่เก็บของฉัน สิ่งนี้จะกำหนด "สัญญา" ที่ที่เก็บต้องปฏิบัติตามเพื่อให้ผู้ควบคุมของฉันใช้ โปรดจำไว้ว่าคอนโทรลเลอร์ของฉันจะไม่ทราบว่าข้อมูลถูกเก็บไว้ที่ไหน

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

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

การใช้งาน Repository ของ SQL

ตอนนี้เพื่อสร้างการนำไปใช้งานของส่วนต่อประสาน ดังที่กล่าวไว้ตัวอย่างของฉันจะอยู่กับฐานข้อมูล SQL หมายเหตุการใช้mapper ข้อมูลเพื่อป้องกันไม่ให้ต้องเขียนแบบสอบถาม SQL ซ้ำ

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

อินเตอร์เฟซวัตถุแบบสอบถาม

ขณะนี้ด้วยCUD (สร้างอัพเดตลบ) ที่เก็บข้อมูลของเราดูแลเราสามารถมุ่งเน้นไปที่R (อ่าน) แบบสอบถามวัตถุเป็นเพียงการห่อหุ้มของตรรกะการค้นหาข้อมูลบางประเภท พวกเขาไม่ใช่ผู้สร้างแบบสอบถาม โดยการทำให้เป็นนามธรรมเหมือนกับที่เก็บของเราเราสามารถเปลี่ยนการใช้งานและทดสอบได้ง่ายขึ้น ตัวอย่างของวัตถุแบบสอบถามอาจจะมีAllUsersQueryหรือหรือแม้กระทั่งAllActiveUsersQueryMostCommonUserFirstNames

คุณอาจกำลังคิดว่า "ฉันไม่สามารถสร้างวิธีการในที่เก็บข้อมูลของฉันสำหรับการค้นหาเหล่านั้นได้หรือ" ใช่ แต่นี่คือเหตุผลที่ฉันไม่ทำสิ่งนี้:

  • ที่เก็บของฉันมีไว้สำหรับทำงานกับโมเดลวัตถุ ในแอปโลกแห่งความจริงเหตุใดฉันจึงต้องได้รับpasswordข้อมูลหากฉันต้องการแสดงรายชื่อผู้ใช้ทั้งหมดของฉัน
  • ที่เก็บมักจะเป็นรูปแบบที่เฉพาะเจาะจง แต่แบบสอบถามมักจะเกี่ยวข้องมากกว่าหนึ่งรูปแบบ ดังนั้นที่เก็บที่คุณใส่วิธีการของคุณ?
  • สิ่งนี้ทำให้ที่เก็บของฉันง่ายมาก - ไม่ใช่วิธีการเรียนที่ป่อง
  • แบบสอบถามทั้งหมดจะถูกจัดระเบียบเป็นคลาสของตนเอง
  • จริงๆแล้ว ณ จุดนี้ที่เก็บมีอยู่เพื่อสรุปเลเยอร์ฐานข้อมูลของฉัน

ตัวอย่างของฉันฉันจะสร้างวัตถุแบบสอบถามเพื่อค้นหา "AllUsers" นี่คืออินเทอร์เฟซ:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

การใช้งานแบบสอบถามวัตถุ

นี่คือที่เราสามารถใช้ data mapper อีกครั้งเพื่อช่วยเร่งการพัฒนา โปรดสังเกตว่าฉันอนุญาตให้ปรับแต่งหนึ่งชุดข้อมูลที่ส่งคืน - เขตข้อมูล นี่จะเกี่ยวกับเท่าที่ฉันต้องการไปกับการจัดการแบบสอบถามที่ดำเนินการ โปรดจำไว้ว่าวัตถุแบบสอบถามของฉันไม่ได้เป็นผู้สร้างแบบสอบถาม พวกเขาทำแบบสอบถามเฉพาะ อย่างไรก็ตามเนื่องจากฉันรู้ว่าฉันอาจใช้สิ่งนี้มากในหลาย ๆ สถานการณ์ฉันจึงให้ความสามารถในการระบุฟิลด์ ฉันไม่ต้องการคืนช่องที่ฉันไม่ต้องการ!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

ก่อนที่จะไปยังคอนโทรลเลอร์ฉันต้องการแสดงอีกตัวอย่างหนึ่งเพื่อแสดงให้เห็นว่ามันมีประสิทธิภาพเพียงใด AllOverdueAccountsบางทีฉันอาจจะมีเครื่องมือการรายงานและความจำเป็นในการสร้างรายงานสำหรับ นี่อาจเป็นเรื่องยุ่งยากกับผู้ทำแผนที่ข้อมูลของฉันและฉันอาจต้องการเขียนจริงบางอย่างSQLในสถานการณ์นี้ ไม่มีปัญหานี่คือสิ่งที่วัตถุแบบสอบถามนี้อาจมีลักษณะ:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

นี่เป็นการเก็บตรรกะทั้งหมดของฉันไว้สำหรับรายงานนี้ในชั้นเดียวและทดสอบได้ง่าย ฉันสามารถเยาะเย้ยเนื้อหาของหัวใจของฉันหรือแม้กระทั่งการใช้งานที่แตกต่างกันโดยสิ้นเชิง

ผู้ควบคุม

ตอนนี้ความสนุกสนานนำชิ้นส่วนทั้งหมดมารวมกัน โปรดทราบว่าฉันกำลังใช้การฉีดพึ่งพา โดยทั่วไปแล้วการพึ่งพาจะถูกฉีดเข้าไปในตัวสร้าง แต่จริง ๆ แล้วฉันชอบที่จะฉีดพวกเขาลงในวิธีการควบคุมของฉัน (เส้นทาง) สิ่งนี้จะลดกราฟวัตถุของคอนโทรลเลอร์และฉันคิดว่ามันอ่านง่ายขึ้น หมายเหตุถ้าคุณไม่ชอบวิธีนี้เพียงใช้วิธีการสร้างแบบดั้งเดิม

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

ความคิดสุดท้าย:

สิ่งสำคัญที่ควรทราบที่นี่คือเมื่อฉันแก้ไขเอนทิตี (สร้างอัปเดตหรือลบ) ฉันกำลังทำงานกับวัตถุโมเดลจริงและดำเนินการเก็บข้อมูลผ่านที่เก็บของฉัน

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

ที่เก็บของฉันยังคงสะอาดอยู่และ "ระเบียบ" นี้จะถูกจัดเป็นแบบสอบถามแบบของฉัน

ฉันใช้ data mapper เพื่อช่วยในการพัฒนาเนื่องจากมันไร้สาระแค่เขียน SQL ซ้ำสำหรับงานทั่วไป อย่างไรก็ตามคุณสามารถเขียน SQL ได้อย่างที่ต้องการ (แบบสอบถามที่ซับซ้อนการรายงานและอื่น ๆ ) และเมื่อคุณทำเช่นนั้นมันซ่อนตัวอยู่ในชั้นเรียนที่มีชื่อเหมาะสม

ฉันชอบที่จะได้ยินเสียงของคุณในแนวทางของฉัน!


อัปเดตกรกฎาคม 2558:

ฉันถูกถามในความคิดเห็นที่ฉันจบลงด้วยทั้งหมดนี้ ไม่จริงที่ไกล จริงๆแล้วฉันยังไม่ชอบที่เก็บของจริงๆ ฉันพบว่าพวกเขา overkill สำหรับการค้นหาขั้นพื้นฐาน (โดยเฉพาะถ้าคุณใช้ ORM อยู่แล้ว) และยุ่งเมื่อทำงานกับคำค้นหาที่ซับซ้อนมากขึ้น

ฉันมักจะทำงานกับ ORM สไตล์ของ ActiveRecord ดังนั้นส่วนใหญ่ฉันมักจะอ้างอิงโมเดลเหล่านั้นโดยตรงผ่านแอปพลิเคชันของฉัน อย่างไรก็ตามในสถานการณ์ที่ฉันมีคิวรีที่ซับซ้อนมากขึ้นฉันจะใช้ออบเจ็กต์คิวรีเพื่อทำให้สามารถใช้ซ้ำได้ ฉันควรทราบด้วยว่าฉันมักจะฉีดแบบจำลองของฉันลงในวิธีการของฉันทำให้ง่ายต่อการเยาะเย้ยในการทดสอบของฉัน


4
@PeeHaa อีกครั้งเพื่อให้ตัวอย่างง่าย มันเป็นเรื่องธรรมดามากที่จะทิ้งรหัสไว้เป็นตัวอย่างหากพวกเขาไม่ได้เกี่ยวข้องกับหัวข้อนั้นโดยเฉพาะ ในความเป็นจริงฉันจะผ่านการพึ่งพาของฉัน
Jonathan

4
น่าสนใจที่คุณแยกสรรสร้างอัปเดตและลบออกจากอ่าน คิดว่ามันจะคุ้มค่าที่จะกล่าวถึง Command Query Responsibility Segregation (CQRS) ซึ่งทำหน้าที่อย่างเป็นทางการ martinfowler.com/bliki/CQRS.html
Adam

2
@ Jonathan เป็นเวลาหนึ่งปีครึ่งแล้วที่คุณตอบคำถามของคุณเอง ฉันสงสัยว่าคุณยังมีความสุขกับคำตอบของคุณหรือไม่และนี่เป็นวิธีแก้ปัญหาหลักของคุณตอนนี้สำหรับโครงการส่วนใหญ่ของคุณหรือไม่? ไม่กี่สัปดาห์ที่ผ่านมาฉันได้อ่านการจัดสรรที่เก็บและฉันเห็นผู้คนจำนวนมากมีการตีความของตัวเองว่าควรนำไปใช้อย่างไร คุณเรียกมันว่าแบบสอบถามวัตถุ แต่นี่เป็นรูปแบบที่มีอยู่ใช่มั้ย ฉันคิดว่าฉันเคยเห็นมันถูกใช้ในภาษาอื่น
Boedy

1
@Jonathan: คุณจัดการกับข้อความค้นหาที่ควรทำให้ผู้ใช้ไม่เป็น "ID" ได้อย่างไรเช่นโดยใช้ "ชื่อผู้ใช้" หรือข้อความค้นหาที่ซับซ้อนยิ่งขึ้นที่มีเงื่อนไขมากกว่าหนึ่งข้อ
Gizzmo

1
@Gizzmo การใช้วัตถุคิวรีคุณสามารถส่งผ่านพารามิเตอร์เพิ่มเติมเพื่อช่วยในการค้นหาที่ซับซ้อนมากขึ้น new Query\ComplexUserLookup($username, $anotherCondition)ตัวอย่างเช่นคุณสามารถทำเช่นนี้ในตัวสร้าง: หรือทำสิ่งนี้ผ่านวิธีตั้งค่า$query->setUsername($username);ค่า คุณสามารถออกแบบสิ่งนี้ได้จริง ๆ แต่มันสมเหตุสมผลสำหรับแอพพลิเคชั่นเฉพาะของคุณและฉันคิดว่าวัตถุคิวรีมีความยืดหยุ่นมากที่นี่
โจนาธาน

48

จากประสบการณ์ของฉันนี่คือคำตอบสำหรับคำถามของคุณ:

ถาม:เราจะจัดการกับการนำกลับของเขตข้อมูลที่เราไม่ต้องการได้อย่างไร

ตอบ:จากประสบการณ์ของฉันสิ่งนี้จะลดลงไปจนถึงการจัดการกับเอนทิตีที่สมบูรณ์เมื่อเปรียบเทียบกับการค้นหาแบบเฉพาะกิจ

เอนทิตีที่สมบูรณ์เป็นสิ่งที่เหมือนUserวัตถุ มันมีคุณสมบัติและวิธีการ ฯลฯ มันเป็นพลเมืองชั้นหนึ่งใน codebase ของคุณ

คิวรีแบบเฉพาะกิจจะส่งคืนข้อมูลบางอย่าง แต่เราไม่รู้อะไรเลยนอกจากนั้น เมื่อข้อมูลถูกส่งผ่านไปรอบ ๆ แอ็พพลิเคชันจะทำได้โดยไม่มีบริบท มันคือUserอะไร A Userพร้อมOrderข้อมูลบางอย่าง? เราไม่รู้จริงๆ

ฉันชอบทำงานกับเอนทิตีแบบเต็ม

คุณมีสิทธิ์ที่จะนำข้อมูลที่คุณไม่ได้ใช้กลับคืนมา แต่คุณสามารถจัดการกับปัญหานี้ได้หลายวิธี

  1. แคชเอนทิตีอย่างจริงจังเพื่อให้คุณจ่ายราคาอ่านเพียงครั้งเดียวจากฐานข้อมูล
  2. ใช้เวลาสร้างแบบจำลองเอนทิตีของคุณมากขึ้นเพื่อให้มีความแตกต่างที่ดีระหว่างกัน (พิจารณาแยกเอนทิตีที่มีขนาดใหญ่ออกเป็นเอนทิตีที่เล็กกว่าสองรายการ ฯลฯ )
  3. พิจารณามีเอนทิตีหลายเวอร์ชัน คุณสามารถมีUserสำหรับการวางแบ็กเอนด์และUserSmallสำหรับการโทร AJAX หนึ่งอาจมี 10 คุณสมบัติและหนึ่งมี 3 คุณสมบัติ

ข้อเสียของการทำงานกับคิวรีแบบเฉพาะกิจ:

  1. คุณจะได้ข้อมูลที่เหมือนกันในหลายข้อความค้นหา ตัวอย่างเช่นด้วยUserคุณจะต้องเขียนเหมือนกันselect *สำหรับการโทรหลายสาย สายเดียวจะได้รับ 8 จาก 10 ช่องหนึ่งจะได้รับ 5 จาก 10 สายหนึ่งจะได้รับ 7 จาก 10 ทำไมไม่แทนสายทั้งหมดที่มี 10 สายจาก 10 สาย? เหตุผลที่ไม่ดีก็คือมันเป็นการฆาตกรรมที่จะทำการทดลองอีกครั้ง / ทดสอบ / จำลอง
  2. มันยากที่จะให้เหตุผลในระดับสูงเกี่ยวกับรหัสของคุณเมื่อเวลาผ่านไป แทนที่จะเป็นข้อความเช่น "ทำไมจึงเป็นUserจึงช้าจัง" คุณจะสิ้นสุดการติดตามการสืบค้นแบบใช้ครั้งเดียวดังนั้นการแก้ไขข้อบกพร่องจึงมีขนาดเล็กและแปลเป็นภาษาท้องถิ่น
  3. มันยากมากที่จะแทนที่เทคโนโลยีพื้นฐาน หากคุณเก็บทุกอย่างไว้ใน MySQL ตอนนี้และต้องการย้ายไปที่ MongoDB มันยากมากที่จะเปลี่ยนการเรียก Ad-hoc กว่า 100 รายการแทนที่จะเป็นเอนทิตีจำนวนหนึ่ง

Q:ฉันจะมีวิธีการมากมายในที่เก็บของฉัน

A:ฉันไม่ได้เห็นวิธีการอื่นนอกเหนือจากการรวมการโทร วิธีการโทรในพื้นที่เก็บข้อมูลของคุณแมปไปยังคุณสมบัติในใบสมัครของคุณ คุณสมบัติยิ่งโทรยิ่งเฉพาะข้อมูล คุณสามารถกดกลับไปที่คุณสมบัติและพยายามรวมสายที่คล้ายกันไว้ในที่เดียว

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

บางครั้งฉันต้องบอกตัวเองว่า "ต้องให้ที่ไหนสักแห่ง! ไม่มีกระสุนเงิน"


ขอบคุณสำหรับคำตอบอย่างละเอียด คุณทำให้ฉันคิดในตอนนี้ ความกังวลใหญ่ของฉันที่นี่คือทุกสิ่งที่ฉันอ่านบอกว่าไม่SELECT *เพียงเลือกเขตข้อมูลที่คุณต้องการ ตัวอย่างเช่นดูคำถามนี้ สำหรับคำถามโฆษณาที่คุณพูดถึงนั้นฉันเข้าใจว่าคุณมาจากไหน ฉันมีแอพที่มีขนาดใหญ่มากในขณะนี้ที่มีแอปจำนวนมาก นั่นคือของฉัน "ก็ต้องให้ที่ไหนสักแห่ง!" ขณะนี้ฉันเลือกรับประสิทธิภาพสูงสุด อย่างไรก็ตามตอนนี้ฉันจัดการกับข้อความค้นหาต่าง ๆ มากมาย
Jonathan

1
หนึ่งความคิดติดตาม ฉันเห็นคำแนะนำให้ใช้วิธี R - CUD เนื่องจากreadsบ่อยครั้งที่เกิดปัญหาเกี่ยวกับประสิทธิภาพคุณสามารถใช้วิธีการสืบค้นที่กำหนดเองได้มากกว่าซึ่งไม่ได้แปลเป็นวัตถุทางธุรกิจจริง แล้วสำหรับcreate, updateและdeleteใช้ออมซึ่งทำงานร่วมกับวัตถุทั้งหมด มีความคิดเห็นเกี่ยวกับวิธีการนั้นไหม?
Jonathan

1
เป็นหมายเหตุสำหรับการใช้ "select *" ฉันเคยทำมาแล้วและใช้งานได้ดี - จนกว่าเราจะไปที่ฟิลด์ varchar (สูงสุด) สิ่งเหล่านั้นทำลายข้อความค้นหาของเรา ดังนั้นถ้าคุณมีตารางที่มี ints ฟิลด์ข้อความขนาดเล็ก ฯลฯ มันก็ไม่ได้แย่ขนาดนั้น รู้สึกผิดธรรมชาติ แต่ซอฟต์แวร์กลับเป็นเช่นนั้น สิ่งที่เลวร้ายก็ดีและในทางกลับกัน
ryan1234

1
วิธีการ R-CUD เป็นจริง CQRS
MikeSW

2
@ ryan1234 "ความซับซ้อนในตอนท้ายของวันต้องมีอยู่ที่ไหนซักแห่ง" ขอบคุณสำหรับสิ่งนี้. ทำให้ฉันรู้สึกดีขึ้น
johnny

20

ฉันใช้อินเทอร์เฟซต่อไปนี้:

  • Repository - โหลด, แทรก, อัพเดตและลบเอนทิตี
  • Selector - ค้นหาเอนทิตีตามตัวกรองในที่เก็บ
  • Filter - แค็ปซูลลอจิกการกรอง

ฉันRepositoryเป็นผู้ไม่เชื่อเรื่องพระเจ้าฐานข้อมูล; ในความเป็นจริงมันไม่ได้ระบุความคงทนใด ๆ มันอาจเป็นอะไรก็ได้: ฐานข้อมูล SQL, ไฟล์ xml, บริการระยะไกล, เอเลี่ยนจากนอกอวกาศเป็นต้นสำหรับความสามารถในการค้นหาRepositoryโครงสร้างSelectorที่สามารถกรอง, LIMIT-ed, เรียงลำดับและนับได้ ในท้ายที่สุดตัวเลือกดึงข้อมูลหนึ่งรายการขึ้นไปEntitiesจากการคงอยู่

นี่คือตัวอย่างรหัสบางส่วน:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

จากนั้นหนึ่งการดำเนินการ:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

อุดมคติคือการSelectorใช้งานทั่วไปFilterแต่การSqlSelectorใช้งานใช้SqlFilter; SqlSelectorFilterAdapterปรับทั่วไปFilterที่จะเป็นรูปธรรมSqlFilterที่จะเป็นรูปธรรม

รหัสลูกค้าสร้างขึ้น Filterวัตถุ (ที่เป็นตัวกรองทั่วไป) แต่ในการใช้งานที่เป็นรูปธรรมของตัวเลือกตัวกรองเหล่านั้นจะถูกแปลงในตัวกรอง SQL

การใช้งานตัวเลือกอื่น ๆ เช่นการInMemorySelectorแปลงจากFilterการInMemoryFilterใช้เฉพาะของพวกเขาInMemorySelectorFilterAdapter; ดังนั้นการใช้งานตัวเลือกทั้งหมดมาพร้อมกับอะแดปเตอร์ตัวกรองของตัวเอง

การใช้กลยุทธ์นี้รหัสลูกค้าของฉัน (ในเลเยอร์ bussines) ไม่สนใจเกี่ยวกับพื้นที่เก็บข้อมูลเฉพาะหรือการใช้งานตัวเลือก

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

PS นี่คือการทำให้รหัสจริงของฉันง่ายขึ้น


"พื้นที่เก็บข้อมูล - โหลดแทรกอัปเดตและลบเอนทิตี" นี่คือสิ่งที่ "เลเยอร์บริการ", "DAO", "BLL" สามารถทำได้
Yousha Aleayoub

5

ฉันจะเพิ่มเล็กน้อยในขณะที่ฉันพยายามที่จะเข้าใจทั้งหมดนี้เอง

# 1 และ 2

นี่คือสถานที่ที่เหมาะสำหรับออมของคุณในการยกของหนัก หากคุณกำลังใช้โมเดลที่ใช้ ORM บางประเภทคุณก็สามารถใช้วิธีนี้เพื่อดูแลสิ่งเหล่านี้ สร้างฟังก์ชั่น orderBy ของคุณเองที่ใช้เมธอด Eloquent หากคุณต้องการ ใช้ Eloquent เช่น:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

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

อย่างไรก็ตามหากคุณต้องการหลีกเลี่ยง ORM คุณจะต้อง "ม้วนตัวเอง" เพื่อให้ได้สิ่งที่คุณต้องการ

# 3

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

ที่กล่าวว่าฉันเพิ่งเริ่มเข้าใจ แต่การรับรู้เหล่านี้ได้ช่วยฉัน


1
สิ่งที่ฉันไม่ชอบเกี่ยวกับวิธีนี้คือถ้าคุณมี MongoUserRepository นั่นและ DbUserRepository ของคุณจะส่งคืนวัตถุต่าง ๆ Db ส่งคืน Eloquent \ Model และ Mongo เป็นของตัวเอง แน่นอนว่าการใช้งานที่ดีกว่าคือการให้ทั้งสองแหล่งเก็บข้อมูลส่งคืนอินสแตนซ์ / คอลเลกชันของคลาส Entity \ User แยก วิธีนี้คุณไม่ได้พึ่งพาวิธี Eloquent \ Model ของ DB อย่างผิดพลาดเมื่อคุณเปลี่ยนไปใช้ MongoRepository
danharper

1
ฉันเห็นด้วยกับคุณอย่างแน่นอน สิ่งที่ฉันอาจจะทำเพื่อหลีกเลี่ยงที่ไม่เคยใช้วิธีการเหล่านั้นนอก Eloquent ที่ต้องการเรียน ดังนั้นฟังก์ชั่น get น่าจะเป็นแบบส่วนตัวและใช้เฉพาะภายในคลาสตามที่คุณชี้ให้เห็นว่าจะคืนสิ่งที่ที่เก็บอื่นไม่สามารถทำได้
จะเป็น

3

ฉันสามารถแสดงความคิดเห็นเกี่ยวกับวิธีการที่เรา (ที่ บริษัท ของฉัน) จัดการกับสิ่งนี้ ก่อนอื่นประสิทธิภาพไม่ได้เป็นปัญหามากเกินไปสำหรับเรา แต่การมีโค้ดที่สะอาด / เหมาะสมคือ

ก่อนอื่นเรากำหนดรุ่นต่างๆเช่น a UserModelที่ใช้ ORM เพื่อสร้างUserEntityวัตถุ เมื่อ a UserEntityถูกโหลดจากโมเดลฟิลด์ทั้งหมดจะถูกโหลด สำหรับสาขาที่อ้างอิงถึงหน่วยงานต่างประเทศเราใช้โมเดลต่างประเทศที่เหมาะสมเพื่อสร้างหน่วยงานที่เกี่ยวข้อง สำหรับเอนทิตีเหล่านั้นข้อมูลจะถูกโหลดตามความต้องการ ตอนนี้ปฏิกิริยาเริ่มต้นของคุณอาจเป็น ... ??? ... !!! ให้ฉันยกตัวอย่างสักเล็กน้อย:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

ในกรณีของเรา$dbคือ ORM ที่สามารถโหลดเอนทิตี โมเดลสั่งให้ ORM โหลดชุดของเอนทิตีที่เป็นประเภทเฉพาะ ORM ประกอบด้วยการแมปและใช้การฉีดเขตข้อมูลทั้งหมดสำหรับเอนทิตีนั้นไปยังเอนทิตี สำหรับเขตข้อมูลต่างประเทศอย่างไรก็ตามโหลด id ของวัตถุเหล่านั้นเท่านั้น ในกรณีนี้การOrderModelสร้างOrderEntitys ที่มีเพียง id ของคำสั่งอ้างอิง เมื่อPersistentEntity::getFieldถูกเรียกโดยOrderEntityเอนทิตีสั่งให้มันเป็นแบบจำลองในการโหลดฟิลด์ทั้งหมดลงในOrderEntitys OrderEntitys ทั้งหมดที่เกี่ยวข้องกับหนึ่ง UserEntity จะถือว่าเป็นหนึ่งชุดผลลัพธ์และจะถูกโหลดในครั้งเดียว

ความมหัศจรรย์นี่คือโมเดลและ ORM ของเราอัดข้อมูลทั้งหมดลงในเอนทิตีและเอนทิตีนั้นมีเพียงฟังก์ชัน wrapper สำหรับgetFieldวิธีทั่วไปที่จัดทำโดยPersistentEntityวิธีการที่จัดทำโดยเพื่อสรุปเราโหลดฟิลด์ทั้งหมดเสมอ แต่ฟิลด์ที่อ้างอิงเอนทิตีต่างประเทศจะถูกโหลดเมื่อจำเป็น การโหลดฟิลด์จำนวนมากไม่ใช่เรื่องของประสิทธิภาพ โหลดเอนทิตีต่างประเทศที่เป็นไปได้ทั้งหมด แต่จะลดลงอย่างมาก

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

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

รุ่นที่ง่ายที่สุดของระบบนี้จะส่งส่วนของตำแหน่งของแบบสอบถามเป็นสตริงโดยตรงไปยังโมเดล

ฉันขอโทษสำหรับคำตอบที่ค่อนข้างซับซ้อนนี้ ฉันพยายามสรุปกรอบงานของเราอย่างรวดเร็วและชัดเจนที่สุด หากคุณมีคำถามเพิ่มเติมโปรดถามพวกเขาและฉันจะอัปเดตคำตอบของฉัน

แก้ไข: นอกจากนี้หากคุณไม่ต้องการที่จะโหลดบางช่องทันทีคุณสามารถระบุตัวเลือกการโหลดขี้เกียจในการทำแผนที่ ORM ของคุณ เพราะในที่สุดเขตข้อมูลทั้งหมดจะถูกโหลดผ่านgetFieldวิธีการที่คุณสามารถโหลดได้ในนาทีสุดท้ายเมื่อมีการเรียกใช้เมธอดนั้น นี่ไม่ใช่ปัญหาใหญ่ใน PHP แต่ฉันจะไม่แนะนำสำหรับระบบอื่น ๆ


3

นี่เป็นวิธีแก้ปัญหาที่แตกต่างที่ฉันเคยเห็น มีข้อดีและข้อเสียสำหรับแต่ละคน แต่สำหรับคุณที่จะตัดสินใจ

ปัญหา # 1: มีฟิลด์มากเกินไป

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

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

ปัญหา # 2: มีวิธีมากเกินไป

ฉันทำงานกับPropel ORMเมื่อไม่นานมานี้และนี่เป็นสิ่งที่ฉันจำได้จากประสบการณ์นั้น Propel มีตัวเลือกในการสร้างโครงสร้างคลาสตามสคีมาฐานข้อมูลที่มีอยู่ มันสร้างวัตถุสองรายการสำหรับแต่ละตาราง วัตถุแรกคือรายการฟังก์ชันการเข้าถึงที่ยาวเหมือนกับที่คุณระบุไว้ในปัจจุบัน findByAttribute($attribute_value). วัตถุถัดไปสืบทอดจากวัตถุแรกนี้ คุณสามารถปรับปรุงวัตถุลูกนี้เพื่อสร้างในฟังก์ชั่นทะเยอทะยานที่ซับซ้อนมากขึ้นของคุณ

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

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

ฉันหวังว่านี่จะช่วยอะไรได้บ้าง



0

ฉันเห็นด้วยกับ @ ryan1234 ว่าคุณควรส่งผ่านวัตถุที่สมบูรณ์ภายในรหัสและควรใช้วิธีการสืบค้นทั่วไปเพื่อรับวัตถุเหล่านั้น

Model::where(['attr1' => 'val1'])->get();

สำหรับการใช้งานภายนอก / ปลายทางฉันชอบวิธีการ GraphQL มาก

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

ปัญหา # 3: ไม่สามารถจับคู่อินเทอร์เฟซได้

ฉันเห็นประโยชน์ในการใช้อินเทอร์เฟซสำหรับที่เก็บดังนั้นฉันสามารถสลับการใช้งานของฉัน (เพื่อวัตถุประสงค์ในการทดสอบหรืออื่น ๆ ) ความเข้าใจของฉันเกี่ยวกับอินเทอร์เฟซคือพวกเขากำหนดสัญญาที่ต้องปฏิบัติตาม นี่เป็นสิ่งที่ดีจนกระทั่งคุณเริ่มเพิ่มวิธีการเพิ่มเติมลงในที่เก็บข้อมูลของคุณเช่น findAllInCountry () ตอนนี้ฉันต้องอัปเดตอินเทอร์เฟซของฉันเพื่อให้มีวิธีนี้ไม่เช่นนั้นการใช้งานอื่นอาจไม่มีวิธีนี้และนั่นอาจทำให้แอปพลิเคชันของฉันเสียหาย จากสิ่งนี้รู้สึกบ้า ... กรณีของหางกระดิกสุนัข

ลำไส้ของฉันบอกฉันว่านี่อาจต้องใช้อินเทอร์เฟซที่ใช้วิธีการค้นหาที่ดีที่สุดควบคู่ไปกับวิธีการทั่วไป การค้นหาที่ละเอียดอ่อนควรมีวิธีการกำหนดเป้าหมายในขณะที่เคียวรีทั่วไปที่จัดการไม่บ่อยนักหรือมีน้ำหนักเบาอาจเป็นค่าใช้จ่ายของคอนโทรลเลอร์ที่ควบคุมการเล่นกลอีกเล็กน้อย

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

วิธีนี้จะคล้ายกับการใช้งานฮาร์ดแวร์ที่ดำเนินการงานเฉพาะที่ดีที่สุดในขณะที่การใช้งานซอฟต์แวร์จะทำงานเบาหรือการใช้งานที่ยืดหยุ่น


0

ฉันคิดว่าgraphQLเป็นตัวเลือกที่ดีในกรณีเช่นนี้ในการจัดเตรียมภาษาขนาดใหญ่โดยไม่เพิ่มความซับซ้อนของที่เก็บข้อมูล

อย่างไรก็ตามมีวิธีแก้ไขปัญหาอื่นหากคุณไม่ต้องการใช้ graphQL ในตอนนี้ โดยใช้DTOที่วัตถุถูกใช้สำหรับการดูแลข้อมูลระหว่างกระบวนการในกรณีนี้ระหว่าง service / controller และ repository

คำตอบที่สง่างามมีให้ไว้แล้วข้างต้น แต่ฉันจะพยายามยกตัวอย่างอีกอย่างที่ฉันคิดว่ามันง่ายกว่าและสามารถใช้เป็นจุดเริ่มต้นสำหรับโครงการใหม่ได้

ดังที่แสดงในรหัสเราจะต้องใช้วิธีการ CRUD เพียง 4 วิธี findวิธีการจะใช้สำหรับรายชื่อและการอ่านโดยผ่านอาร์กิวเมนต์วัตถุ บริการแบ็คเอนด์สามารถสร้างวัตถุคิวรี่ที่กำหนดโดยยึดตามสตริงเคียวรี URL หรือตามพารามิเตอร์เฉพาะ

วัตถุแบบสอบถาม ( SomeQueryDto) สามารถใช้อินเทอร์เฟซเฉพาะถ้าจำเป็น และสามารถขยายได้ในภายหลังโดยไม่ต้องเพิ่มความซับซ้อน

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

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

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.