• Czas czytania ~9 min
  • 03.10.2022

Laravel Eloquent to obecnie jedna z najpotężniejszych i najbardziej niesamowitych funkcji we współczesnym frameworku. Od rzutowania danych do obiektów wartości i klas, chroniła bazę danych za pomocą pól, które można wypełnić, transakcji, zakresów, zakresów globalnych i relacji. Eloquent pozwala odnieść sukces we wszystkim, co musisz zrobić z bazą danych.

Rozpoczęcie pracy z Eloquent może czasami wydawać się onieśmielające, ponieważ może zdziałać tak wiele, że nigdy nie wiesz, od czego zacząć. W tym samouczku skupię się na tym, co uważam za jeden z podstawowych aspektów każdej aplikacji - pisanie do bazy danych.

Możesz pisać do bazy danych w dowolnym obszarze aplikacji: kontroler, zadanie, oprogramowanie pośredniczące, polecenie rzemieślnika. Jaki jest najlepszy sposób obsługi zapisów w bazie danych?

Zacznijmy od prostego modelu Eloquent bez relacji.

final class Post extends Model
{
    protected $fillable = [
        'title',
        'slug',
        'content',
        'published',
    ];
 
    protected $casts = [
        'published' => 'boolean',
    ];
}

Mamy model Post który reprezentuje wpis na blogu; ma tytuł, ślimak, treść i flagę logiczną informującą, czy została opublikowana. W tym przykładzie wyobraźmy sobie, że opublikowana właściwość ma domyślną wartość true w bazie danych. Teraz, na początek, powiedzieliśmy Eloquent, że chcemy mieć możliwość wypełniania właściwości lub kolumn title, slug, content i published. Jeśli więc przekażemy coś, co nie jest zarejestrowane w tablicy fillable, zostanie zgłoszony wyjątek - chroniący naszą aplikację przed potencjalnymi problemami.

Teraz, gdy wiemy, jakie pola można wypełnić, możemy przyjrzeć się zapisywaniu danych w bazie danych, tworzeniu, aktualizowaniu lub usuwaniu. Jeśli Twój model dziedziczy cechę SoftDeletes, usunięcie rekordu jest czynnością zapisu — ale w tym przykładzie zachowam prostotę; usunięcie to usunięcie.

To, co najprawdopodobniej widziałeś, zwłaszcza w dokumentacji, jest mniej więcej takie:

Post::create($request->only('title', 'slug', 'content'));

To jest to, co mogę standardowo Eloquent, masz model i wywołujesz metodę statyczną, aby utworzyć nową instancję - przekazując określoną tablicę z żądania. Takie podejście ma swoje zalety; jest czysty i prosty, a wszyscy to rozumieją. Czasami jestem bardzo upartym programistą. Jednak nadal będę korzystać z tego podejścia, zwłaszcza jeśli jestem w trybie prototypowania, gdzie bardziej chodzi o testowanie pomysłu niż budowanie czegoś długoterminowego.

Możemy zrobić ten jeden krok dalej, uruchamiając nową instancję konstruktora zapytań Eloquent w modelu, zanim poprosisz o utworzenie nowej instancji. Wyglądałoby to tak:

Post::query()->create($request->only('title', 'slug', 'content'));

Jak widać, jest to nadal bardzo proste i staje się coraz bardziej ustandaryzowanym sposobem uruchamiania zapytań w Laravelu. Jedną z najważniejszych korzyści tego podejścia jest to, że wszystko po zapytanie jest zgodne z kontraktem konstruktora zapytań, który został niedawno wprowadzony. Z powodu tego, jak Laravel działa pod maską, twoje IDE nie zrozumie zbyt dobrze wywołań statycznych - ponieważ jest statycznym proxy do metody używającej __callStatic nad rzeczywistą metodą statyczną. Na szczęście nie dotyczy to metody zapytanie, która jest statyczną metodą rozszerzanego modelu elokwentnego.

Istnieje „starsza” metoda budowania modelu do zapisania baza danych. Rzadko jednak widzę, że jest używany bardzo często. Wspomnę o tym jednak dla jasności:

$post = new Post();
$post->title = $request->get('title');
$post->slug = $request->get('slug');
$post->content = $request->get('content');
$post->save();

Tutaj będziemy programowo budować model, przypisując wartości do właściwości, a następnie zapisując go w bazie danych. To było trochę rozwlekłe i zawsze wydawało się, że to zbyt duży wysiłek. Jest to jednak nadal akceptowalny sposób tworzenia nowego modelu, jeśli wolisz to robić w taki sposób.

Do tej pory przyjrzeliśmy się trzem różnym podejściom do tworzenia nowych danych w Baza danych. Możemy użyć podobnego podejścia do aktualizacji danych w bazie danych, statycznego wywołania update lub użycia kontraktu budowania zapytania query()->where('column', 'value')->update() lub w końcu programowo ustawiając właściwość, a następnie zapisz. Nie będę się tutaj powtarzał, ponieważ jest prawie tak samo, jak powyżej.

Co robimy, jeśli nie jesteśmy pewni, czy zapis już istnieje? Na przykład chcemy utworzyć lub zaktualizować istniejący post. Będziemy mieli kolumnę, którą chcemy sprawdzić pod względem unikalności - następnie przechodzimy przez tablicę wartości, które chcemy utworzyć lub zaktualizować w zależności od tego, czy istnieje.

Post::query()->updateOrCreate(
    attributes: ['slug' => $request->get('slug'),
    values: [
        'title' => $request->get('title'),
        'content' => $request->get('content'),
    ],
);

Ma to ogromne korzyści, jeśli nie jesteś pewien, czy rekord będzie istniał, a ostatnio sam to zaimplementowałem, gdy chciałem „upewnić się”, że rekord jest w bazie danych bez względu na wszystko. Na przykład w przypadku logowania społecznościowego OAuth 2.0 możesz zaakceptować informacje od dostawcy i zaktualizować lub utworzyć nowy rekord przed uwierzytelnieniem użytkownika.

Czy możemy zrobić krok dalej? Jakie byłyby korzyści? Możesz użyć wzorca takiego jak Wzorzec repozytorium, aby w zasadzie "zastępować" wywołania, które wysyłasz do elokwentnego przez inną klasę. Jest z tym kilka korzyści, a przynajmniej było, zanim Eloquent stał się tym, czym jest dzisiaj. Spójrzmy na przykład:

class PostRepository
{
    private Model $model;
 
    public function __construct()
    {
        $this->model = Post::query();
    }
 
    public function create(array $attributes): Model
    {
        return $this->model->create(
            attributes: $attributes,
        );
    }
}

Gdybyśmy używali fasady DB lub zwykłego PDO, być może wzorzec repozytorium dałby nam całkiem sporo korzyści w utrzymaniu spójności. Przejdźmy dalej.

W pewnym momencie ludzie zdecydowali, że dobrym pomysłem byłoby przejście z klasy Repository do klasy Service. Jednak to jest to samo... Nie zagłębiajmy się w to.

Chcemy więc mieć sposób na obsługę interakcji z Eloquent, który nie jest tak „inline” ani proceduralny. Kilka lat temu przyjąłem podejście, które obecnie określa się mianem „działań”. Jest podobny do wzorca repozytorium. Jednak każda interakcja z Eloquent jest osobną klasą, a nie metodą w ramach jednej klasy.

Spójrzmy na ten przykład, w którym mamy dedykowaną klasę dla każdej interakcji zwaną „akcją”:

final class CreateNewPostAction implements CreateNewPostContract
{
    public function handle(array $attributes): Model|Post
    {
        return Post::query()
            ->create(
                attributes: $attributes,
            );
    }
}

Nasza klasa implementuje kontrakt, aby ładnie powiązać go z kontenerem, co pozwala nam wstrzyknąć to do konstruktora i w razie potrzeby wywołać metodę uchwytu z naszymi danymi. Staje się to coraz bardziej popularne i wiele osób (a także pakietów) zaczęło stosować to podejście, ponieważ tworzysz klasy narzędziowe, które robią jedną rzecz dobrze - i mogą łatwo tworzyć dla nich dublety testowe. Inną korzyścią jest to, że używamy interfejsu; jeśli kiedykolwiek zdecydujemy się odejść od Eloquent (nie jestem pewien, dlaczego miałbyś chcieć), możemy szybko zmienić nasz kod, aby odzwierciedlić to bez konieczności szukania czegokolwiek.

Ponownie podejście, które jest całkiem dobre – i zasadniczo nie ma żadnych wad. Wspomniałem, że jestem dość wybrednym programistą, prawda? Cóż...

Mój największy problem z "działaniami" po ich używaniu przez tak długi czas polega na tym, że wszystkie integracje zapisu, aktualizacji i usuwania umieszczamy pod jednym dachem. Działania nie dzielą dla mnie wystarczająco rzeczy. Jeśli o tym pomyślę, to mamy dwie różne rzeczy, które chcemy osiągnąć – chcemy pisać i czytać. Odzwierciedla to częściowo inny wzorzec projektowy o nazwie CQRS (Command Query Responsibility Segregation), z którego trochę zapożyczyłem. W CQRS zwykle używa się magistrali poleceń i magistrali zapytań do odczytu i zapisu danych, zwykle emitując zdarzenia, które mają być przechowywane przy użyciu źródła zdarzeń. Czasami jednak jest to o wiele więcej pracy, niż potrzebujesz. Nie zrozum mnie źle, zdecydowanie jest czas i miejsce na takie podejście, ale powinieneś sięgać po nie tylko wtedy, gdy musisz - w przeciwnym razie przepracujesz swoje rozwiązanie od najmniejszej części.

Dlatego podzieliłem moje akcje pisania na „Polecenia”, a akcje czytania na „Zapytania”, aby moje interakcje były oddzielone i skoncentrowane. Rzućmy okiem na polecenie:

final class CreateNewPost implements CreateNewPostContract
{
    public function handle(array $attributes): Model|Post
    {
        return Post::query()
            ->create(
                attributes: $attributes,
            );
    }
}

Czy spojrzysz na to, poza nazewnictwem klas, to to samo, co akcja. To jest zgodne z projektem. Akcje to doskonały sposób zapisywania do bazy danych. Uważam, że mają tendencję do zbyt szybkiego robienia się zatłoczonych.

W jaki inny sposób możemy to poprawić? Wprowadzenie obiektu transferu domeny byłoby dobrym początkiem, ponieważ zapewnia on bezpieczeństwo typów, kontekst i spójność.

final class CreateNewPost implements CreateNewPostContract
{
    public function handle(CreatePostRequest $post): Model|Post
    {
        return Post::query()
            ->create(
                attributes: $post->toArray(),
            );
    }
}

Wprowadzamy więc teraz bezpieczeństwo typów w tablicy, w której wcześniej polegaliśmy na tablicach i mieliśmy nadzieję, że wszystko poszło we właściwy sposób. Tak, możemy sprawdzać prawdziwość naszych serc, ale obiekty mają lepszą spójność.

Czy jest jakiś sposób, w jaki moglibyśmy to poprawić? Zawsze jest miejsce na poprawę, ale czy musimy? To obecne podejście jest niezawodne, bezpieczne dla typu i łatwe do zapamiętania. Ale co zrobimy, jeśli tabela bazy danych zostanie zablokowana, zanim będziemy mogli pisać, lub jeśli wystąpi przerwa w łączności sieciowej, być może Cloudflare przestanie działać w niewłaściwym momencie. zachowaj nasze tyłki tutaj. Nie są one używane tak często, jak prawdopodobnie powinny, ale są potężnym narzędziem, które powinieneś rozważyć wkrótce.

W końcu do tego doszliśmy! Skakałbym z radości, gdybym zobaczył taki kod w PR lub przeglądzie kodu, który musiałem zrobić. Nie myśl jednak, że musisz pisać kod w ten sposób. Pamiętaj, że jest całkowicie w porządku, aby po prostu wbudować statyczne

create

, jeśli wykona to zadanie za Ciebie! Ważne jest, aby robić to, z czym czujesz się komfortowo, co sprawi, że będziesz skuteczny – a nie to, co inni mówią, że powinieneś robić w społeczności.

final class CreateNewPost implements CreateNewPostContract
{
    public function handle(CreatePostRequest $post): Model|Post
    {
        return DB::transaction(
            fn() => Post::query()->create(
                attributes: $post->toArray(),
            )
        );
    }
}
Przyjmując podejście, które właśnie przyjrzeliśmy, moglibyśmy w ten sam sposób podejść do czytania z bazy danych. Rozwiąż problem, określ kroki i miejsca, w których można wprowadzić ulepszenia, ale zawsze zastanawiaj się, czy nie posuwasz się o krok za daleko. Jeśli wydaje się to naturalne, to prawdopodobnie dobry znak.

Jak podchodzisz do pisania do bazy danych? Jak daleko byś posunął się w podróż, a kiedy jest za daleko? Podziel się z nami swoimi przemyśleniami na Twitterze!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297