Thiết kế mô hình lưu trữ thích hợp trong PHP?


291

Lời nói đầu: Tôi đang cố gắng sử dụng mẫu kho lưu trữ trong kiến ​​trúc MVC với cơ sở dữ liệu quan hệ.

Gần đây tôi đã bắt đầu học TDD trong PHP và tôi nhận ra rằng cơ sở dữ liệu của mình được kết hợp quá chặt chẽ với phần còn lại của ứng dụng. Tôi đã đọc về các kho lưu trữ và sử dụng bộ chứa IoC để "tiêm" nó vào bộ điều khiển của mình. Công cụ rất mát mẻ. Nhưng bây giờ có một số câu hỏi thực tế về thiết kế kho lưu trữ. Hãy xem xét ví dụ sau đây.

<?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)
    {
    }
}

Vấn đề # 1: Quá nhiều lĩnh vực

Tất cả các phương thức tìm kiếm này sử dụng một SELECT *cách tiếp cận chọn tất cả các trường ( ). Tuy nhiên, trong các ứng dụng của mình, tôi luôn cố gắng giới hạn số lượng trường tôi nhận được, vì điều này thường làm tăng thêm chi phí và làm mọi thứ chậm lại. Đối với những người sử dụng mô hình này, làm thế nào để bạn đối phó với điều này?

Vấn đề # 2: Quá nhiều phương pháp

Mặc dù lớp này có vẻ tốt ngay bây giờ, tôi biết rằng trong một ứng dụng trong thế giới thực, tôi cần nhiều phương pháp hơn. Ví dụ:

  • find ALLByNameAndStatus
  • find ALLInCountry
  • find ALLWithEmailAddressSet
  • find ALLByAgeAndGender
  • find ALLByAgeAndGenderOrderByAge
  • Vân vân.

Như bạn có thể thấy, có thể có một danh sách rất dài các phương thức có thể. Và sau đó nếu bạn thêm vào vấn đề chọn trường ở trên, vấn đề sẽ trở nên tồi tệ hơn. Trước đây, thông thường tôi chỉ cần đặt tất cả logic này vào bộ điều khiển của mình:

<?php

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

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

Với cách tiếp cận kho lưu trữ của tôi, tôi không muốn kết thúc với điều này:

<?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))
    }

}

Vấn đề # 3: Không thể phù hợp với giao diện

Tôi thấy lợi ích trong việc sử dụng các giao diện cho các kho lưu trữ, vì vậy tôi có thể trao đổi việc triển khai của mình (cho mục đích thử nghiệm hoặc khác). Sự hiểu biết của tôi về các giao diện là chúng xác định một hợp đồng mà việc thực hiện phải tuân theo. Điều này thật tuyệt vời cho đến khi bạn bắt đầu thêm các phương thức bổ sung vào kho lưu trữ của mình như thế nào findAllInCountry(). Bây giờ tôi cần cập nhật giao diện của mình để có phương thức này, nếu không, các triển khai khác có thể không có nó và điều đó có thể phá vỡ ứng dụng của tôi. Bởi điều này cảm thấy điên rồ ... một trường hợp đuôi vẫy con chó.

Đặc điểm kỹ thuật mẫu?

Dẫn này tôi tin rằng kho chỉ nên có một số cố định các phương pháp (như save(), remove(), find(), findAll(), vv). Nhưng làm thế nào để tôi chạy tra cứu cụ thể? Tôi đã nghe nói về Mẫu đặc tả , nhưng dường như điều này chỉ làm giảm toàn bộ bộ hồ sơ (thông qua IsSatisfiedBy()), rõ ràng có vấn đề lớn về hiệu năng nếu bạn lấy từ cơ sở dữ liệu.

Cứu giúp?

Rõ ràng, tôi cần suy nghĩ lại mọi thứ một chút khi làm việc với các kho lưu trữ. Bất cứ ai có thể giác ngộ về cách này được xử lý tốt nhất?

Câu trả lời:


208

Tôi nghĩ rằng tôi sẽ có một vết nứt khi trả lời câu hỏi của riêng tôi. Những gì tiếp theo chỉ là một cách giải quyết các vấn đề 1-3 trong câu hỏi ban đầu của tôi.

Tuyên bố miễn trừ trách nhiệm: Tôi không phải lúc nào cũng có thể sử dụng các thuật ngữ đúng khi mô tả các mẫu hoặc kỹ thuật. Xin lỗi vì điều đó.

Các mục tiêu:

  • Tạo một ví dụ hoàn chỉnh về bộ điều khiển cơ bản để xem và chỉnh sửa Users.
  • Tất cả các mã phải được kiểm tra đầy đủ và có thể nhạo báng.
  • Bộ điều khiển không nên biết nơi dữ liệu được lưu trữ (có nghĩa là nó có thể được thay đổi).
  • Ví dụ để hiển thị một triển khai SQL (phổ biến nhất).
  • Để có hiệu suất tối đa, bộ điều khiển chỉ nên nhận dữ liệu họ cần không có trường bổ sung.
  • Việc thực hiện nên tận dụng một số loại ánh xạ dữ liệu để dễ phát triển.
  • Việc thực hiện phải có khả năng thực hiện tra cứu dữ liệu phức tạp.

Giải pháp

Tôi đang chia tương tác lưu trữ (cơ sở dữ liệu) liên tục của mình thành hai loại: R (Đọc) và CUD (Tạo, Cập nhật, Xóa). Kinh nghiệm của tôi là việc đọc thực sự là nguyên nhân khiến ứng dụng bị chậm lại. Và trong khi thao tác dữ liệu (CUD) thực sự chậm hơn, nó xảy ra ít thường xuyên hơn và do đó ít được quan tâm hơn.

CUD (Tạo, Cập nhật, Xóa) rất dễ dàng. Điều này sẽ liên quan đến việc làm việc với các mô hình thực tế , sau đó được truyền lại cho tôi Repositoriesđể kiên trì. Lưu ý, kho lưu trữ của tôi vẫn sẽ cung cấp phương thức Đọc, nhưng chỉ đơn giản là để tạo đối tượng, không hiển thị. Thêm về điều đó sau.

R (Đọc) không dễ dàng như vậy. Không có mô hình ở đây, chỉ là các đối tượng giá trị . Sử dụng mảng nếu bạn thích . Những đối tượng này có thể đại diện cho một mô hình duy nhất hoặc sự pha trộn của nhiều mô hình, bất cứ điều gì thực sự. Chúng không thú vị lắm, nhưng chúng được tạo ra như thế nào. Tôi đang sử dụng những gì tôi đang gọi Query Objects.

Mật mã:

Mô hình người dùng

Hãy bắt đầu đơn giản với mô hình người dùng cơ bản của chúng tôi. Lưu ý rằng không có công cụ mở rộng ORM hoặc cơ sở dữ liệu nào cả. Chỉ là mô hình vinh quang thuần túy. Thêm getters, setters, xác nhận của bạn, bất cứ điều gì.

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

Giao diện kho lưu trữ

Trước khi tôi tạo kho lưu trữ người dùng, tôi muốn tạo giao diện kho lưu trữ của mình. Điều này sẽ xác định "hợp đồng" mà các kho lưu trữ phải tuân theo để được bộ điều khiển của tôi sử dụng. Hãy nhớ rằng, bộ điều khiển của tôi sẽ không biết dữ liệu thực sự được lưu trữ ở đâu.

Lưu ý rằng kho lưu trữ của tôi sẽ chỉ chứa ba phương thức này. Các save()phương pháp có trách nhiệm cả việc tạo ra và cập nhật người dùng, chỉ đơn giản là tuỳ thuộc vào việc hay không đối tượng người dùng có một thiết lập id.

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

Triển khai kho lưu trữ SQL

Bây giờ để tạo ra việc thực hiện giao diện của tôi. Như đã đề cập, ví dụ của tôi sẽ là với cơ sở dữ liệu SQL. Lưu ý việc sử dụng bộ ánh xạ dữ liệu để tránh phải viết các truy vấn SQL lặp đi lặp lại.

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');
    }
}

Giao diện đối tượng truy vấn

Bây giờ với CUD (Tạo, Cập nhật, Xóa) được lưu trữ bởi kho lưu trữ của chúng tôi, chúng tôi có thể tập trung vào R (Đọc). Các đối tượng truy vấn chỉ đơn giản là sự gói gọn của một số loại logic tra cứu dữ liệu. Họ không phải là người xây dựng truy vấn. Bằng cách trừu tượng hóa nó giống như kho lưu trữ của chúng tôi, chúng tôi có thể thay đổi việc triển khai và kiểm tra nó dễ dàng hơn. Một ví dụ về Đối tượng truy vấn có thể là AllUsersQueryhoặc AllActiveUsersQueryhoặc thậm chí MostCommonUserFirstNames.

Bạn có thể đang nghĩ "tôi không thể tạo phương thức trong kho của mình cho những truy vấn đó sao?" Vâng, nhưng đây là lý do tại sao tôi không làm điều này:

  • Kho của tôi có nghĩa là để làm việc với các đối tượng mô hình. Trong một ứng dụng thế giới thực, tại sao tôi cần phải lấy passwordtrường nếu tôi muốn liệt kê tất cả người dùng của mình?
  • Các kho lưu trữ thường là mô hình cụ thể, nhưng các truy vấn thường liên quan đến nhiều hơn một mô hình. Vì vậy, kho lưu trữ nào bạn đặt phương pháp của bạn vào?
  • Điều này giữ cho kho lưu trữ của tôi rất đơn giản, không phải là một lớp phương thức cồng kềnh.
  • Tất cả các truy vấn hiện được tổ chức thành các lớp riêng của họ.
  • Thực sự, tại thời điểm này, các kho lưu trữ tồn tại đơn giản để trừu tượng lớp cơ sở dữ liệu của tôi.

Ví dụ của tôi, tôi sẽ tạo một đối tượng truy vấn để tra cứu "AllUsers". Đây là giao diện:

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

Thực hiện đối tượng truy vấn

Đây là nơi chúng ta có thể sử dụng lại một trình ánh xạ dữ liệu để giúp tăng tốc độ phát triển. Lưu ý rằng tôi đang cho phép một tinh chỉnh cho bộ dữ liệu được trả về trong các trường. Đây là khoảng cách mà tôi muốn thực hiện với thao tác truy vấn được thực hiện. Hãy nhớ rằng, các đối tượng truy vấn của tôi không phải là người xây dựng truy vấn. Họ chỉ đơn giản là thực hiện một truy vấn cụ thể. Tuy nhiên, vì tôi biết rằng có lẽ tôi sẽ sử dụng cái này rất nhiều, trong một số tình huống khác nhau, tôi tự cho mình khả năng chỉ định các trường. Tôi không bao giờ muốn trả lại các lĩnh vực tôi không cần!

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();
    }
}

Trước khi chuyển sang bộ điều khiển, tôi muốn đưa ra một ví dụ khác để minh họa mức độ mạnh mẽ của nó. Có lẽ tôi có một công cụ báo cáo và cần tạo một báo cáo cho AllOverdueAccounts. Điều này có thể khó khăn với trình ánh xạ dữ liệu của tôi và tôi có thể muốn viết một số thực tế SQLtrong tình huống này. Không có vấn đề, đây là những gì đối tượng truy vấn này có thể trông như thế nào:

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...";
    }
}

Điều này độc đáo giữ tất cả logic của tôi cho báo cáo này trong một lớp và thật dễ dàng để kiểm tra. Tôi có thể chế giễu nó với nội dung trái tim của tôi, hoặc thậm chí sử dụng một cách thực hiện hoàn toàn khác.

Bộ điều khiển

Bây giờ phần vui nhộn mang tất cả các mảnh lại với nhau. Lưu ý rằng tôi đang sử dụng tiêm phụ thuộc. Thông thường các phụ thuộc được đưa vào hàm tạo, nhưng tôi thực sự thích tiêm chúng ngay vào các phương thức điều khiển (tuyến) của tôi. Điều này giảm thiểu đồ thị đối tượng của bộ điều khiển và tôi thực sự thấy nó dễ đọc hơn. Lưu ý, nếu bạn không thích cách tiếp cận này, chỉ cần sử dụng phương thức xây dựng truyền thống.

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;
    }
}

Suy nghĩ cuối cùng:

Điều quan trọng cần lưu ý ở đây là khi tôi sửa đổi (tạo, cập nhật hoặc xóa) các thực thể, tôi đang làm việc với các đối tượng mô hình thực và thực hiện sự kiên trì thông qua các kho lưu trữ của tôi.

Tuy nhiên, khi tôi đang hiển thị (chọn dữ liệu và gửi dữ liệu đến chế độ xem) Tôi không làm việc với các đối tượng mô hình, mà là các đối tượng giá trị cũ đơn giản. Tôi chỉ chọn các trường tôi cần và nó được thiết kế để tôi có thể tối đa hóa hiệu suất tra cứu dữ liệu của mình.

Kho lưu trữ của tôi rất sạch sẽ và thay vào đó, "mớ hỗn độn" này được sắp xếp vào các truy vấn mô hình của tôi.

Tôi sử dụng một trình ánh xạ dữ liệu để giúp phát triển, vì thật vô lý khi viết SQL lặp đi lặp lại cho các tác vụ thông thường. Tuy nhiên, bạn hoàn toàn có thể viết SQL khi cần (truy vấn phức tạp, báo cáo, v.v.). Và khi bạn làm thế, nó được xếp gọn vào một lớp được đặt tên đúng.

Tôi rất thích nghe bạn nói về cách tiếp cận của tôi!


Cập nhật tháng 7 năm 2015:

Tôi đã được hỏi trong các ý kiến ​​nơi tôi đã kết thúc với tất cả điều này. Vâng, thực sự không xa lắm. Thật ra, tôi vẫn không thực sự thích kho. Tôi thấy chúng quá mức cần thiết cho các tra cứu cơ bản (đặc biệt nếu bạn đang sử dụng ORM) và lộn xộn khi làm việc với các truy vấn phức tạp hơn.

Tôi thường làm việc với ORM kiểu ActiveRecord, vì vậy, hầu hết tôi sẽ chỉ tham khảo các mô hình đó trực tiếp trong ứng dụng của mình. Tuy nhiên, trong các tình huống tôi có các truy vấn phức tạp hơn, tôi sẽ sử dụng các đối tượng truy vấn để làm cho các truy vấn này dễ sử dụng hơn. Tôi cũng nên lưu ý rằng tôi luôn tiêm các mô hình của mình vào các phương thức của mình, làm cho chúng dễ dàng giả hơn trong các thử nghiệm của tôi.


4
@PeeHaa Một lần nữa, đó là để giữ cho các ví dụ đơn giản. Rất phổ biến để loại các đoạn mã ra khỏi một ví dụ nếu chúng không liên quan cụ thể đến chủ đề trong tay. Trong thực tế, tôi sẽ vượt qua trong sự phụ thuộc của tôi.
Jonathan

4
Thật thú vị khi bạn chia nhỏ Tạo, Cập nhật và Xóa khỏi Đọc của bạn. Thiết nghĩ nên đề cập đến Phân chia trách nhiệm truy vấn lệnh (CQRS) chính thức thực hiện điều đó. martinfowler.com/bliki/CQRS.html
Adam

2
@Jonathan Đã một năm rưỡi kể từ khi bạn trả lời câu hỏi của chính mình. Tôi đã tự hỏi nếu bạn vẫn hài lòng với câu trả lời của bạn và nếu đây là giải pháp chính của bạn bây giờ cho hầu hết các dự án của bạn? Vài tuần gần đây tôi đã đọc phân bổ trên các kho lưu trữ và tôi đã thấy phân bổ của mọi người có cách giải thích riêng của họ về cách nó nên được thực hiện. Bạn gọi nó là đối tượng truy vấn, nhưng đây là một mẫu hiện có phải không? Tôi nghĩ rằng tôi đã thấy nó được sử dụng trong các ngôn ngữ khác.
Bộ phim

1
@Jonathan: Làm thế nào bạn xử lý các truy vấn mà người dùng không nên là "ID" mà là "tên người dùng" hoặc các truy vấn phức tạp hơn với nhiều điều kiện?
Gizzmo

1
@Gizzmo Sử dụng các đối tượng truy vấn, bạn có thể truyền thêm các tham số để trợ giúp với các truy vấn phức tạp hơn. Ví dụ: bạn có thể làm điều này trong hàm tạo : new Query\ComplexUserLookup($username, $anotherCondition). Hoặc, làm điều này thông qua các phương thức setter $query->setUsername($username);. Bạn thực sự có thể thiết kế điều này tuy nhiên nó có ý nghĩa đối với ứng dụng cụ thể của bạn và tôi nghĩ các đối tượng truy vấn để lại rất nhiều tính linh hoạt ở đây.
Jonathan

48

Dựa trên kinh nghiệm của tôi, đây là một số câu trả lời cho câu hỏi của bạn:

H: Làm thế nào để chúng ta đối phó với việc mang lại các lĩnh vực mà chúng ta không cần?

Trả lời: Theo kinh nghiệm của tôi, điều này thực sự tập trung vào việc xử lý các thực thể hoàn chỉnh so với các truy vấn đặc biệt.

Một thực thể hoàn chỉnh là một cái gì đó giống như một User đối tượng. Nó có các thuộc tính và phương thức, v.v ... Đó là một công dân hạng nhất trong cơ sở mã của bạn.

Một truy vấn đặc biệt trả về một số dữ liệu, nhưng chúng tôi không biết gì hơn thế. Khi dữ liệu được truyền xung quanh ứng dụng, nó được thực hiện mà không có ngữ cảnh. Có phải là một User? A Uservới một số Orderthông tin kèm theo? Chúng tôi không thực sự biết.

Tôi thích làm việc với các thực thể đầy đủ.

Bạn đúng rằng bạn sẽ thường xuyên mang lại dữ liệu bạn sẽ không sử dụng, nhưng bạn có thể giải quyết vấn đề này theo nhiều cách khác nhau:

  1. Tích cực lưu trữ các thực thể để bạn chỉ phải trả giá đọc một lần từ cơ sở dữ liệu.
  2. Dành nhiều thời gian hơn để mô hình hóa các thực thể của bạn để chúng có sự phân biệt tốt giữa chúng. (Xem xét việc chia một thực thể lớn thành hai thực thể nhỏ hơn, v.v.)
  3. Xem xét có nhiều phiên bản của các thực thể. Bạn có thể có một Usermặt sau và có thể là UserSmallcho các cuộc gọi AJAX. Một có thể có 10 thuộc tính và một có 3 thuộc tính.

Nhược điểm của việc làm việc với các truy vấn đặc biệt:

  1. Bạn kết thúc với cơ bản cùng một dữ liệu qua nhiều truy vấn. Ví dụ, với a User, cuối cùng bạn sẽ viết giống nhauselect * cho nhiều cuộc gọi. Một cuộc gọi sẽ nhận được 8 trên 10 trường, một cuộc gọi sẽ nhận được 5 trên 10, một cuộc gọi sẽ nhận được 7 trên 10. Tại sao không thay thế tất cả bằng một cuộc gọi nhận được 10 trên 10? Lý do điều này là xấu là nó là giết người để tái yếu tố / kiểm tra / giả.
  2. Nó trở nên rất khó để lý luận ở mức cao về mã của bạn theo thời gian. Thay vì những câu như "Tại sao làUser chậm như vậy?" cuối cùng bạn theo dõi các truy vấn một lần và vì vậy các sửa lỗi có xu hướng nhỏ và cục bộ.
  3. Thật sự rất khó để thay thế công nghệ cơ bản. Nếu bạn lưu trữ mọi thứ trong MySQL ngay bây giờ và muốn chuyển sang MongoDB, việc thay thế 100 cuộc gọi quảng cáo sẽ khó hơn rất nhiều so với một số thực thể.

Q: Tôi sẽ có quá nhiều phương thức trong kho lưu trữ của mình.

Trả lời: Tôi chưa thực sự thấy bất kỳ cách nào khác ngoài việc hợp nhất các cuộc gọi. Phương thức gọi trong kho lưu trữ của bạn thực sự ánh xạ tới các tính năng trong ứng dụng của bạn. Càng nhiều tính năng, cuộc gọi càng cụ thể dữ liệu. Bạn có thể đẩy lùi các tính năng và cố gắng hợp nhất các cuộc gọi tương tự thành một.

Sự phức tạp vào cuối ngày phải tồn tại ở đâu đó. Với một mẫu kho lưu trữ, chúng tôi đã đẩy nó vào giao diện kho lưu trữ thay vì có thể tạo ra một loạt các thủ tục được lưu trữ.

Đôi khi tôi phải tự nhủ: "À nó phải cho ở đâu đó! Không có viên đạn bạc nào."


Cảm ơn câu trả lời rất kỹ lưỡng. Bạn đã nghĩ cho tôi bây giờ. Mối quan tâm lớn của tôi ở đây là mọi thứ tôi đọc đều nói không SELECT *, thay vào đó chỉ chọn các lĩnh vực bạn yêu cầu. Ví dụ, xem câu hỏi này . Đối với tất cả các truy vấn quảng cáo mà bạn nói đến, tôi chắc chắn hiểu bạn đến từ đâu. Tôi có một ứng dụng rất lớn ngay bây giờ có nhiều ứng dụng. Đó là "Tôi phải đưa ra một nơi nào đó!" Khoảnh khắc, tôi đã chọn cho hiệu suất tối đa. Tuy nhiên, bây giờ tôi đang xử lý RẤT NHIỀU truy vấn khác nhau.
Jonathan

1
Một người theo dõi suy nghĩ. Tôi đã thấy một đề xuất sử dụng phương pháp RUD CUD. Vì readsthường phát sinh các vấn đề về hiệu suất, bạn có thể sử dụng cách tiếp cận truy vấn tùy chỉnh hơn cho chúng, điều đó không chuyển thành các đối tượng kinh doanh thực sự. Sau đó, đối với create, updatedelete, sử dụng một ORM, mà làm việc với toàn bộ các đối tượng. Bất kỳ suy nghĩ về cách tiếp cận đó?
Jonathan

1
Như một lưu ý cho việc sử dụng "select *". Tôi đã làm điều đó trong quá khứ và nó hoạt động tốt - cho đến khi chúng tôi đạt được các trường varchar (tối đa). Những người đã giết các truy vấn của chúng tôi. Vì vậy, nếu bạn có các bảng có ints, các trường văn bản nhỏ, v.v. thì cũng không tệ lắm. Cảm thấy không tự nhiên, nhưng phần mềm đi theo cách đó. Những gì đã xấu là đột nhiên tốt và ngược lại.
ryan1234

1
Cách tiếp cận R-CUD thực sự là CQRS
MikeSW

2
@ ryan1234 "Sự phức tạp vào cuối ngày phải tồn tại ở đâu đó." Cảm ơn vì điều này. Lam cho tôi cảm thây tôt hơn.
johnny

20

Tôi sử dụng các giao diện sau:

  • Repository - tải, chèn, cập nhật và xóa các thực thể
  • Selector - tìm các thực thể dựa trên các bộ lọc, trong một kho lưu trữ
  • Filter - đóng gói logic lọc

My Repositorylà cơ sở dữ liệu bất khả tri; trong thực tế, nó không chỉ định bất kỳ sự kiên trì; nó có thể là bất cứ thứ gì: cơ sở dữ liệu SQL, tệp xml, dịch vụ từ xa, người ngoài hành tinh ngoài vũ trụ, v.v. Để có khả năng tìm kiếm, các Repositorycấu trúc Selectorcó thể được lọc, LIMIT-ed, sắp xếp và đếm. Cuối cùng, bộ chọn lấy một hoặc nhiều Entitiestừ sự kiên trì.

Đây là một số mã mẫu:

<?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();
}

Sau đó, một thực hiện:

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);
    }
}

Các ideea là Selectorsử dụng chung Filternhưng thực hiện SqlSelectorsử dụng SqlFilter; sự SqlSelectorFilterAdapterthích nghi chung chung Filtervới một bê tông SqlFilter.

Mã máy khách tạo Filtercác đối tượng (là các bộ lọc chung) nhưng trong triển khai cụ thể của bộ chọn, các bộ lọc đó được chuyển đổi trong các bộ lọc SQL.

Triển khai chọn khác, như InMemorySelectormới, hoán cải từ Filterđể InMemoryFiltersử dụng cụ thể của họ InMemorySelectorFilterAdapter; vì vậy, mọi thực hiện bộ chọn đều đi kèm với bộ điều hợp bộ lọc riêng.

Sử dụng chiến lược này, mã khách hàng của tôi (trong lớp bussines) không quan tâm đến việc triển khai kho lưu trữ hoặc bộ chọn cụ thể.

/** @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 Đây là một sự đơn giản hóa mã thực sự của tôi


"Kho lưu trữ - tải, chèn, cập nhật và xóa các thực thể" đây là điều mà "lớp dịch vụ", "DAO", "BLL" có thể làm
Yousha Aleayoub

5

Tôi sẽ thêm một chút về điều này vì tôi hiện đang cố gắng tự mình nắm bắt tất cả những điều này.

#1 và 2

Đây là một nơi hoàn hảo để ORM của bạn thực hiện việc nâng vật nặng. Nếu bạn đang sử dụng một mô hình thực hiện một số loại ORM, bạn chỉ có thể sử dụng các phương thức của nó để chăm sóc những thứ này. Tạo các hàm orderBy của riêng bạn thực hiện các phương thức Eloquent nếu bạn cần. Sử dụng Eloquent chẳng hạn:

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

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

Những gì bạn dường như đang tìm kiếm là một ORM. Không có lý do gì Kho lưu trữ của bạn không thể dựa trên một. Điều này sẽ yêu cầu Người dùng mở rộng hùng hồn, nhưng cá nhân tôi không coi đó là một vấn đề.

Tuy nhiên, nếu bạn muốn tránh ORM, thì bạn sẽ phải "tự lăn" để có được thứ bạn đang tìm kiếm.

# 3

Các giao diện không được coi là yêu cầu khó và nhanh. Một cái gì đó có thể thực hiện một giao diện và thêm vào nó. Những gì nó không thể làm là không thực hiện được chức năng cần thiết của giao diện đó. Bạn cũng có thể mở rộng giao diện như các lớp để giữ mọi thứ KHÔ.

Điều đó nói rằng, tôi mới bắt đầu nắm bắt, nhưng những nhận thức này đã giúp tôi.


1
Điều tôi không thích ở phương thức này là nếu bạn có MongoUserRep repository, thì DbUserRep repository của bạn sẽ trả về các đối tượng khác nhau. Db trả lại một Eloquent \ Model và Mongo một cái gì đó của riêng nó. Chắc chắn việc triển khai tốt hơn là có cả hai kho lưu trữ trả về các thể hiện / bộ sưu tập của một lớp Entity \ User riêng biệt. Bằng cách này, bạn không nhầm lẫn dựa vào các phương thức DB của Eloquent \ Model khi bạn chuyển sang sử dụng MongoRep repository
danharper

1
Tôi chắc chắn đồng ý với bạn về điều đó. Những gì tôi có thể làm để tránh điều đó là không bao giờ sử dụng các phương thức đó bên ngoài lớp yêu cầu Eloquent. Vì vậy, hàm get có thể là riêng tư và chỉ được sử dụng trong lớp vì nó, như bạn đã chỉ ra, sẽ trả về một cái gì đó mà các kho lưu trữ khác không thể.
Sẽ

3

Tôi chỉ có thể nhận xét về cách chúng tôi (tại công ty của tôi) đối phó với điều này. Trước hết hiệu suất không phải là vấn đề quá lớn đối với chúng tôi, nhưng có mã sạch / đúng là có.

Trước hết, chúng tôi xác định các Mô hình như mô hình UserModelsử dụng ORM để tạo UserEntitycác đối tượng. Khi a UserEntityđược tải từ một mô hình, tất cả các trường được tải. Đối với các trường tham chiếu các thực thể nước ngoài, chúng tôi sử dụng mô hình nước ngoài thích hợp để tạo các thực thể tương ứng. Đối với những thực thể đó, dữ liệu sẽ được tải ondemand. Bây giờ phản ứng ban đầu của bạn có thể là ... ??? ... !!! hãy để tôi cho bạn một ví dụ một chút về một ví dụ:

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)
    {
        //...
    }
}

Trong trường hợp của chúng tôi $dblà một ORM có thể tải các thực thể. Mô hình hướng dẫn ORM tải một tập hợp các thực thể của một loại cụ thể. ORM chứa ánh xạ và sử dụng ánh xạ đó để đưa tất cả các trường cho thực thể đó vào thực thể. Tuy nhiên, đối với các trường nước ngoài, chỉ có id của các đối tượng đó được tải. Trong trường hợp này, các s OrderModeltạo OrderEntitychỉ có id của các lệnh được tham chiếu. Khi PersistentEntity::getFieldđược gọi bởi OrderEntitythực thể, hướng dẫn mô hình của nó lười biếng tải tất cả các trường vào OrderEntitys. Tất cả các OrderEntitys được liên kết với một UserEntity được coi là một tập kết quả và sẽ được tải cùng một lúc.

Điều kỳ diệu ở đây là mô hình và ORM của chúng tôi đưa tất cả dữ liệu vào các thực thể và các thực thể đó chỉ cung cấp các hàm bao cho getFieldphương thức chung được cung cấp bởi PersistentEntity. Để tóm tắt, chúng tôi luôn tải tất cả các trường, nhưng các trường tham chiếu một thực thể nước ngoài được tải khi cần thiết. Chỉ cần tải một loạt các lĩnh vực không thực sự là một vấn đề hiệu suất. Tải tất cả các thực thể nước ngoài có thể tuy nhiên sẽ làm giảm hiệu suất HUGE.

Bây giờ để tải một nhóm người dùng cụ thể, dựa trên mệnh đề where. Chúng tôi cung cấp một gói hướng đối tượng của các lớp cho phép bạn chỉ định biểu thức đơn giản có thể được dán lại với nhau. Trong mã ví dụ tôi đặt tên cho nó GetOptions. Đó là một trình bao bọc cho tất cả các tùy chọn có thể cho một truy vấn chọn. Nó chứa một tập hợp các mệnh đề nơi, một nhóm theo mệnh đề và mọi thứ khác. Các mệnh đề của chúng tôi khá phức tạp nhưng rõ ràng bạn có thể tạo một phiên bản đơn giản hơn một cách dễ dàng.

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

Một phiên bản đơn giản nhất của hệ thống này sẽ là chuyển phần WHERE của truy vấn dưới dạng một chuỗi trực tiếp đến mô hình.

Tôi xin lỗi vì phản ứng khá phức tạp này. Tôi đã cố gắng tóm tắt khuôn khổ của chúng tôi càng nhanh và rõ ràng càng tốt. Nếu bạn có bất kỳ câu hỏi nào, vui lòng hỏi họ và tôi sẽ cập nhật câu trả lời của tôi.

EDIT: Ngoài ra nếu bạn thực sự không muốn tải một số trường ngay lập tức, bạn có thể chỉ định tùy chọn tải lười biếng trong ánh xạ ORM của mình. Bởi vì tất cả các trường cuối cùng được tải thông qua getFieldphương thức, bạn có thể tải một số trường vào phút cuối khi phương thức đó được gọi. Đây không phải là một vấn đề lớn trong PHP, nhưng tôi không khuyến nghị cho các hệ thống khác.


3

Đây là một số giải pháp khác nhau mà tôi đã thấy. Có những ưu và nhược điểm đối với mỗi người trong số họ, nhưng đó là để bạn quyết định.

Vấn đề # 1: Quá nhiều lĩnh vực

Đây là một khía cạnh quan trọng đặc biệt là khi bạn đưa vào tài khoản Quét chỉ mục . Tôi thấy hai giải pháp để đối phó với vấn đề này. Bạn có thể cập nhật các hàm của mình để nhận tham số mảng tùy chọn có chứa danh sách các cột sẽ trả về. Nếu tham số này trống, bạn sẽ trả về tất cả các cột trong truy vấn. Điều này có thể hơi kỳ lạ; dựa trên tham số bạn có thể truy xuất một đối tượng hoặc một mảng. Bạn cũng có thể sao chép tất cả các hàm của mình để có hai hàm riêng biệt chạy cùng một truy vấn, nhưng một hàm trả về một mảng các cột và hàm kia trả về một đối tượng.

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

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

Vấn đề # 2: Quá nhiều phương pháp

Tôi đã làm việc ngắn gọn với Propel ORM một năm trước và điều này dựa trên những gì tôi có thể nhớ từ trải nghiệm đó. Propel có tùy chọn để tạo cấu trúc lớp dựa trên lược đồ cơ sở dữ liệu hiện có. Nó tạo ra hai đối tượng cho mỗi bảng. Đối tượng đầu tiên là một danh sách dài các chức năng truy cập tương tự như những gì bạn hiện đang liệt kê; findByAttribute($attribute_value). Đối tượng tiếp theo kế thừa từ đối tượng đầu tiên này. Bạn có thể cập nhật đối tượng con này để xây dựng các hàm getter phức tạp hơn.

Một giải pháp khác sẽ được sử dụng __call()để ánh xạ các hàm không xác định thành một cái gì đó có thể thực hiện được. __callPhương thức của bạn sẽ có thể phân tích findById và findByName thành các truy vấn khác nhau.

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]
    }
}

Tôi hy vọng điều này sẽ giúp ít nhất một số những gì.



0

Tôi đồng ý với @ ryan1234 rằng bạn nên chuyển xung quanh các đối tượng hoàn chỉnh trong mã và nên sử dụng các phương thức truy vấn chung để lấy các đối tượng đó.

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

Đối với việc sử dụng bên ngoài / điểm cuối, tôi thực sự thích phương pháp GraphQL.

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

0

Vấn đề # 3: Không thể phù hợp với giao diện

Tôi thấy lợi ích trong việc sử dụng các giao diện cho các kho lưu trữ, vì vậy tôi có thể trao đổi việc triển khai của mình (cho mục đích thử nghiệm hoặc khác). Sự hiểu biết của tôi về các giao diện là chúng xác định một hợp đồng mà việc thực hiện phải tuân theo. Điều này thật tuyệt vời cho đến khi bạn bắt đầu thêm các phương thức bổ sung vào kho lưu trữ của mình như findAllInCountry (). Bây giờ tôi cần cập nhật giao diện của mình để có phương thức này, nếu không, các triển khai khác có thể không có nó và điều đó có thể phá vỡ ứng dụng của tôi. Bởi điều này cảm thấy điên rồ ... một trường hợp đuôi vẫy con chó.

Ruột của tôi nói với tôi điều này có thể yêu cầu một giao diện thực hiện các phương thức tối ưu hóa truy vấn cùng với các phương thức chung. Các truy vấn nhạy cảm về hiệu năng nên có các phương thức được nhắm mục tiêu, trong khi các truy vấn không thường xuyên hoặc trọng lượng nhẹ được xử lý bởi một trình xử lý chung, có thể là chi phí của bộ điều khiển thực hiện việc tung hứng nhiều hơn một chút.

Các phương thức chung sẽ cho phép mọi truy vấn được thực hiện và do đó sẽ ngăn chặn các thay đổi trong giai đoạn chuyển tiếp. Các phương thức được nhắm mục tiêu cho phép bạn tối ưu hóa cuộc gọi khi có ý nghĩa và nó có thể được áp dụng cho nhiều nhà cung cấp dịch vụ.

Cách tiếp cận này sẽ giống với việc triển khai phần cứng thực hiện các nhiệm vụ được tối ưu hóa cụ thể, trong khi việc triển khai phần mềm thực hiện công việc nhẹ hoặc thực hiện linh hoạt.


0

Tôi nghĩ rằng graphQL là một ứng cử viên tốt trong trường hợp như vậy để cung cấp một ngôn ngữ truy vấn quy mô lớn mà không làm tăng sự phức tạp của kho lưu trữ dữ liệu.

Tuy nhiên, có một giải pháp khác nếu bạn không muốn sử dụng graphQL ngay bây giờ. Bằng cách sử dụng DTO trong đó một đối tượng được sử dụng để khắc dữ liệu giữa các quy trình, trong trường hợp này là giữa dịch vụ / bộ điều khiển và kho lưu trữ.

Một câu trả lời tao nhã đã được cung cấp ở trên, tuy nhiên tôi sẽ cố gắng đưa ra một ví dụ khác mà tôi nghĩ nó đơn giản hơn và có thể đóng vai trò là điểm khởi đầu cho một dự án mới.

Như được hiển thị trong mã, chúng ta sẽ chỉ cần 4 phương thức cho các hoạt động CRUD. cácfind phương pháp sẽ được sử dụng cho danh sách và đọc bằng cách truyền tham số đối tượng. Các dịch vụ cuối cùng có thể xây dựng đối tượng truy vấn được xác định dựa trên chuỗi truy vấn URL hoặc dựa trên các tham số cụ thể.

Đối tượng truy vấn ( SomeQueryDto) cũng có thể thực hiện giao diện cụ thể nếu cần. và dễ dàng được mở rộng sau này mà không cần thêm sự phức tạp.

<?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;
    }
}

Ví dụ sử dụng:

$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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.