Các vấn đề về mối quan hệ – Stitcher.io


Hay nói cách khác là xử lý các mối quan hệ cơ sở dữ liệu phức tạp và các mô hình Laravel.

Gần đây tôi phải giải quyết một vấn đề phức tạp về hiệu suất ở một trong những dự án Laravel lớn hơn của chúng tôi. Hãy để tôi nhanh chóng thiết lập hiện trường.

Chúng tôi muốn người dùng quản trị viên xem tổng quan về tất cả mọi người trong hệ thống trong một bảng và chúng tôi muốn một cột trong bảng đó liệt kê những hợp đồng nào đang hoạt động tại thời điểm đó cho mỗi người.

Mối quan hệ giữa ContractPerson là như sau:

Contract > HabitantContract > Habitant > Person

Tôi không muốn dành quá nhiều thời gian để đi sâu vào chi tiết về cách chúng tôi đi đến hệ thống phân cấp mối quan hệ này. Điều quan trọng là bạn phải biết rằng, vâng, hệ thống phân cấp này rất quan trọng đối với các trường hợp sử dụng của chúng tôi: a Contract có thể có một số Habitantsđược liên kết thông qua mô hình trục HabitantContract; và mỗi Habitant có mối quan hệ với một Person.

Vì chúng tôi đang hiển thị thông tin tổng quan về tất cả mọi người nên chúng tôi muốn thực hiện điều gì đó như thế này trong bộ điều khiển của mình:

class PeopleController
{
    public function index() 
    {
        $people = PersonResource::collection(Person::paginate());

        return view('people.index', compact('people'));
    }
}

Hãy làm rõ rằng đây là một ví dụ được đơn giản hóa quá mức, mặc dù tôi hy vọng bạn hiểu được ý chính. Lý tưởng nhất là chúng ta muốn lớp tài nguyên của mình trông giống như thế này:


class PersonResource extends JsonResource
{
    public function toArray($request): array
    {
        return (
            'name' => $this->name,

            'active_contracts' => $this->activeContracts
                ->map(function (Contract $contract) {
                    return $contract->contract_number;
                })
                ->implode(', '),

            
        );
    }
}

Đặc biệt chú ý đến Person::activeContracts mối quan hệ. Làm thế nào chúng ta có thể thực hiện công việc này?

Ý nghĩ đầu tiên có thể là sử dụng một HasManyThrough mối quan hệ, nhưng hãy nhớ rằng chúng ta có 4 cấp độ sâu trong hệ thống phân cấp mối quan hệ của mình. Ngoài ra, tôi thấy HasManyThrough nên rất bối rối.

Chúng tôi có thể truy vấn các hợp đồng một cách nhanh chóng, từng hợp đồng một cho mỗi người. Vấn đề ở đây là chúng tôi đang giới thiệu vấn đề n+1 vì sẽ có thêm một truy vấn mỗi người. Hãy tưởng tượng tác động về hiệu suất nếu bạn đang xử lý nhiều hơn một vài mô hình.

Một giải pháp cuối cùng mà tôi nghĩ đến là tải tất cả mọi người, tất cả các hợp đồng và ánh xạ chúng lại với nhau một cách thủ công. Cuối cùng thì đó chính xác là điều tôi đã làm, mặc dù tôi đã làm nó theo cách rõ ràng nhất có thể: sử dụng các mối quan hệ tùy chỉnh.

Hãy đi sâu vào.

# Cấu hình mô hình Person

Vì chúng tôi muốn $person->activeContracts để hoạt động chính xác như bất kỳ mối quan hệ nào khác, có rất ít việc phải làm ở đây: hãy thêm một phương thức quan hệ vào mô hình của chúng ta, giống như bất kỳ mối quan hệ nào khác.

class Person extends Model
{
    public function activeContracts(): ActiveContractsRelation
    {
        return new ActiveContractsRelation($this);
    }
}

Không còn gì để làm ở đây nữa. Tất nhiên chúng tôi chỉ mới bắt đầu vì chúng tôi chưa thực sự triển khai ActiveContractsRelation!

# Lớp quan hệ tùy chỉnh

Rất tiếc là không có tài liệu nào về việc tạo các lớp quan hệ của riêng bạn. May mắn thay, bạn không cần phải tìm hiểu nhiều về chúng: một số kỹ năng tìm hiểu mã và một chút thời gian sẽ giúp bạn tiến khá xa. Ồ, IDE cũng có ích.

Nhìn vào các lớp quan hệ hiện có do Laravel cung cấp, chúng ta biết rằng có một mối quan hệ cơ sở chi phối tất cả chúng: Illuminate\Database\Eloquent\Relations\Relation. Mở rộng nó có nghĩa là bạn cần triển khai một số phương thức trừu tượng.

class ActiveContractsRelation extends Relation
{
    
    public function addConstraints() {  }

    
    public function addEagerConstraints(array $models) {  }

    
    public function initRelation(array $models, $relation) {  }

    
    public function match(array $models, Collection $results, $relation) {  }

    
    public function getResults() {  }
}

Các khối tài liệu giúp chúng tôi tiếp tục thực hiện, mặc dù không phải lúc nào cũng hoàn toàn rõ ràng điều gì cần phải xảy ra. Một lần nữa, chúng ta thật may mắn, Laravel vẫn có một số lớp quan hệ hiện có mà chúng ta có thể xem xét.

Chúng ta hãy từng bước xây dựng lớp quan hệ tùy chỉnh của chúng ta. Chúng ta sẽ bắt đầu bằng cách ghi đè hàm tạo và thêm một số gợi ý kiểu vào các thuộc tính hiện có. Chỉ để đảm bảo, hệ thống loại sẽ ngăn chúng ta mắc những sai lầm ngu ngốc.

Trừu tượng Relation hàm tạo yêu cầu cả hai đặc biệt cho một tài hùng biện Builder lớp, cũng như mô hình cha mà mối quan hệ thuộc về. Các Builder được coi là đối tượng truy vấn cơ sở cho mô hình liên quan của chúng tôi, Contracttrong trường hợp của chúng ta.

Vì chúng ta đang xây dựng một lớp quan hệ dành riêng cho trường hợp sử dụng của mình nên không cần phải cấu hình trình xây dựng. Đây là giao diện của hàm tạo:

class ActiveContractsRelation extends Relation
{
    
    protected $query;

    
    protected $parent;

    public function __construct(Person $parent)
    {
        parent::__construct(Contract::query(), $parent);
    }

    
}

Lưu ý rằng chúng tôi gõ gợi ý $query cả với Contract mô hình cũng như Builder lớp học. Điều này cho phép IDE cung cấp khả năng tự động hoàn thành tốt hơn, chẳng hạn như phạm vi tùy chỉnh được xác định trên lớp mô hình.

Chúng ta đã xây dựng xong mối quan hệ: nó sẽ truy vấn Contract mô hình và sử dụng một Person mô hình như cha mẹ của nó. Chuyển sang xây dựng truy vấn của chúng tôi.

Đây là nơi addConstraints phương thức đi vào. Nó sẽ được sử dụng để định cấu hình truy vấn cơ sở. Nó sẽ thiết lập truy vấn quan hệ cụ thể theo nhu cầu của chúng tôi. Đây là nơi chứa hầu hết các quy tắc kinh doanh:

  • Chúng tôi chỉ muốn các hợp đồng đang hoạt động xuất hiện
  • Chúng tôi chỉ muốn tải các hợp đồng đang hoạt động thuộc về một người được chỉ định ( $parent về mối quan hệ của chúng ta)
  • Chúng ta có thể muốn tải một số quan hệ khác một cách háo hức, nhưng sẽ nói nhiều hơn về điều đó sau.

Đây là những gì addConstraints có vẻ như bây giờ:

class ActiveContractsRelation extends Relation
{
    

    public function addConstraints()
    {
        $this->query
            ->whereActive() 
            ->join(
                'contract_habitants', 
                'contract_habitants.contract_id', 
                '=', 
                'contracts.id'
            )
            ->join(
                'habitants', 
                'habitants.id', 
                '=', 
                'contract_habitants.habitant_id'
            );
    }
}

Bây giờ tôi giả định rằng bạn biết các phép nối cơ bản hoạt động như thế nào. Mặc dù tôi sẽ tóm tắt những gì đang xảy ra ở đây: chúng tôi đang xây dựng một truy vấn sẽ tải tất cả contracts và của họ habitantsthông qua contract_habitants bảng trụ, do đó cả hai tham gia.

Một hạn chế khác là chúng tôi chỉ muốn các hợp đồng đang hoạt động xuất hiện; để làm điều này, chúng ta chỉ cần sử dụng phạm vi truy vấn hiện có được cung cấp bởi Contract người mẫu.

Với truy vấn cơ sở của chúng tôi đã sẵn sàng, đã đến lúc bổ sung điều kỳ diệu thực sự: hỗ ​​trợ tải háo hức. Đây là lúc hiệu suất đạt được: thay vì thực hiện một truy vấn cho mỗi người để tải hợp đồng, chúng tôi đang thực hiện một truy vấn để tải tất cả các hợp đồng và liên kết các hợp đồng này với đúng người sau đó.

Đây là cái gì addEagerConstraints, initRelationmatch được sử dụng cho. Chúng ta hãy nhìn vào chúng từng cái một.

Đầu tiên addEagerConstraints phương pháp. Điều này cho phép chúng tôi sửa đổi truy vấn để tải vào tất cả các hợp đồng liên quan đến một nhóm người. Hãy nhớ rằng chúng tôi chỉ muốn hai truy vấn và liên kết các kết quả với nhau sau đó.

class ActiveContractsRelation extends Relation
{
    

    public function addEagerConstraints(array $people)
    {
        $this->query->whereIn(
            'habitants.contact_id', 
            collect($people)->pluck('id')
        );
    }
}

Kể từ khi chúng tôi tham gia habitants như bảng trước, phương pháp này khá dễ dàng: chúng tôi sẽ chỉ tải các hợp đồng thuộc nhóm người được cung cấp.

Tiếp theo initRelation. Một lần nữa, điều này khá dễ dàng: mục tiêu của nó là khởi tạo khoảng trống activeContract mối quan hệ trên mọi Person mô hình, để nó có thể được lấp đầy sau đó.

class ActiveContractsRelation extends Relation
{
    

    public function initRelation(array $people, $relation)
    {
        foreach ($people as $person) {
            $person->setRelation(
                $relation, 
                $this->related->newCollection()
            );
        }

        return $people;
    }
}

Lưu ý rằng $this->related tài sản được thiết lập bởi cha mẹ Relation lớp và đó là một phiên bản mô hình rõ ràng của truy vấn cơ sở của chúng tôi, nói cách khác, một lớp trống Contract người mẫu:

abstract class Relation
{
    public function __construct(Builder $query, Model $parent)
    {
        $this->related = $query->getModel();
    
        
    }
    
    
}

Cuối cùng, chúng ta đi đến chức năng cốt lõi sẽ giải quyết vấn đề của mình: liên kết tất cả mọi người và hợp đồng với nhau.

class ActiveContractsRelation extends Relation
{
    

    public function match(array $people, Collection $contracts, $relation)
    {
        if ($contracts->isEmpty()) {
            return $people;
        }

        foreach ($people as $person) {
            $person->setRelation(
                $relation, 
                $contracts->filter(function (Contract $contract) use ($person) {
                    return $contract->habitants->pluck('person_id')->contains($person->id);
                })
            );    
        }

        return $people;
    }
}

Chúng ta hãy xem những gì đang xảy ra ở đây: một mặt chúng ta có một loạt các mô hình cha mẹ, con người; mặt khác, chúng ta có một tập hợp các hợp đồng, kết quả của truy vấn được thực hiện bởi lớp quan hệ của chúng ta. Mục tiêu của match chức năng là liên kết chúng lại với nhau.

làm như thế nào? Điều đó không khó lắm: lặp lại tất cả mọi người và tìm kiếm tất cả các hợp đồng thuộc về từng người trong số họ, dựa trên những cư dân được liên kết với hợp đồng đó.

Sắp xong? À… còn một vấn đề nữa. Vì chúng tôi đang sử dụng $contract->habitants quan hệ, chúng ta cần đảm bảo rằng nó cũng được tải một cách háo hức, nếu không chúng ta chỉ chuyển vấn đề n+1 thay vì giải quyết nó. Thế là nó quay trở lại addEagerConstraints phương pháp này trong giây lát.

class ActiveContractsRelation extends Relation
{
    

    public function addEagerConstraints(array $people)
    {
        $this->query
            ->whereIn(
                'habitants.contact_id', 
                collect($people)->pluck('id')
            )
            ->with('habitants')
            ->select('contracts.*');
    }
}

Chúng tôi đang thêm with kêu gọi háo hức tải tất cả cư dân, nhưng cũng lưu ý cụ thể select tuyên bố. Chúng ta cần yêu cầu trình tạo truy vấn của Laravel chỉ chọn dữ liệu từ contracts bảng, vì nếu không thì dữ liệu về cư dân có liên quan sẽ được hợp nhất trên Contract mô hình, khiến nó có id sai và những gì không.

Cuối cùng chúng ta cần triển khai getResults phương thức, chỉ đơn giản là thực hiện truy vấn:

class ActiveContractsRelation extends Relation
{
    

    public function getResults()
    {
        return $this->query->get();
    }
}

Và thế là xong! Mối quan hệ tùy chỉnh của chúng ta bây giờ có thể được sử dụng giống như bất kỳ mối quan hệ Laravel nào khác. Đó là một giải pháp tinh tế để giải quyết một vấn đề phức tạp theo cách của Laravel.



Leave a Comment

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Scroll to Top