Praca z bazą danych w Laravel - 8 przydatnych porad
Laravel słynie z dużej intuicyjności oraz łatwego pisania kodu. Jego twórcy wiele rzeczy zaprojektowali tak, aby możliwie najmocniej uprościć korzystanie z tego frameworka. Nie inaczej jest w przypadku pracy z bazą danych. Przydatne informacje na ten temat znajdziesz w dokumentacji. Natomiast w tym tekście skupię się na aspektach, które nie są tam tak dobrze opisane, a mogą przenieść Twoją pracę z bazą danych na wyższy poziom.
Połączenie z bazą danych w Laravelu
Przed rozpoczęciem pracy nad nowym projektem, należy skonfigurować połączenie z bazą danych w Laravelu. Jak zapewne się domyślacie, jest to bardzo proste. W katalogu głównym projektu w pliku .env wystarczy wpisać dane dostępowe do bazy:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=”your_db_name”
DB_USERNAME=”your_db_user”
DB_PASSWORD=”your_db_pass”
Oczywiście musimy mieć uruchomiony serwer MySQL oraz stworzoną odpowiednią bazę. Czasami jednak, szczególnie jeśli zaczynamy nowy nieduży projekt, warto zamiast MySQL użyć bazy SQLite. W tym celu należy utworzyć plik database/database.sqlite, a następnie w pliku .env powyższy zapis zastąpić tym:
DB_CONNECTION=sqlite
Gdy wszystko jest już skonfigurowane, możemy skupić się na usprawnieniu naszej pracy z bazą.
1. DB::transaction()
Załóżmy, że mamy aplikację, w której użytkownik może za pomocą bramki płatności wykupić punkty, dzięki którym status jego konta zmieni się z “limited” na “premium”. Na potrzeby tego przykładu przyjmijmy, że dane o rejestracji płatności, punkty oraz status konta trzymane są w trzech oddzielnych tabelach. Nasz kontroler będzie wyglądał mniej więcej tak:
public function store()
{
Payment::where(['user_id' => auth()->user()->id])->update(['payment_success' => 1]);
UserPoint::create([
'user_id' => auth()->user()->id,
'points' => 1000,
]);
UserStatus::where(['user_id' => auth()->user()->id])->update(['status' => 'premium']);
}
Jednak co się stanie, jeśli z jakiejś przyczyny druga lub trzecia operacja, w której dodajemy punkty, a następnie zmieniamy status, zakończy się niepowodzeniem? Nasza baza danych w Laravelu stanie się niespójna. Z pomocą przychodzi metoda DB::transaction(), dzięki której w przypadku błędu, nasze dane zostaną przywrócone do stanu poprzedniego:
{
DB::beginTransaction();
try {
Payment::where(['user_id' => auth()->user()->id])->update(['payment_success' => 1]);
UserPoint::create([
'user_id' => auth()->user()->id,
'points' => 1000,
]);
UserStatus::where(['user_id' => auth()->user()->id])->update(['status' => 'premium']);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
}
2. Zapytanie do bazy danych - zastąp warunek if warunkiem when
Bardzo często budując zapytanie do bazy danych za pomocą Eloquent, chcemy kondycyjnie dodać jakiś warunek. Dobrym przykładem będzie pobranie wszystkich użytkowników z bazy lub wszystkich użytkowników ze statusem aktywny/nieaktywny, jeśli URL zawiera parametr “active”.
public function index()
{
$users = User::orderBy('name', 'ASC');
if (request()->has('active')) {
$users->where('active', \request('active'));
}
$users->get();
}
Jednak nie wszyscy wiedzą, że ten sam warunek można zapisać beż użycia bloku if, korzystając tylko i wyłącznie z Eloquent:
public function index()
{
$users = User::orderBy('name', 'ASC')
->when(request()->has('active'), function ($query) {
$query->where('active', request('active'));
})
->get();
}
3. Rozszerzanie modelu o dodatkowe dane
Załóżmy, że w tabeli products przechowujemy dane o produktach w sklepie. W kolumnie price zapisana jest cena produktu w postaci liczby naturalnej w groszach. Zatem produkt, który kosztuje 2EUR, zapisany jest w postaci 200 (200 centów). Jest to bardzo popularna, ogólnie przyjęta praktyka. Oczywiście użytkownikowi nie będziemy wyświetlać ceny w centach, musimy ją najpierw przekonwertować. Tę operację możemy wykonać za pomocą accessora w Eloquent. Całość jest bardzo dobrze opisana w dokumentacji Laravela.
Jednak co jeśli będziemy chcieli cenę podaną w euro przekonwertować też na inne waluty i dołączyć do kolekcji danych o produkcie, którą zwraca Eloquent? Do tego celu również możemy użyć accessora. Zatem jeśli chcemy, aby informacja o cenie w innych walutach była zawarta w atrybucie prices, musimy stworzyć publiczną metodę getPricesAttribute, a następnie w modelu stworzyć tablicę $appends, zawierającą nazwę atrybutu (w tym przypadku będzie to prices). Oto jak powinna wyglądać całość:
class Product extends Model
{
protected $appends = ['prices'];
public function getPricesAttribute()
{
return [
'EUR' => $this->price,
'USD' => $this->price, // Use some kind of currency converter here.
'GBP' => $this->price, // Use some kind of currency converter here.
];
}
}
Ważne: jeśli nazwa atrybutu składa się z dwóch lub więcej słów, np. “converted prices”, nazwę metody zapisujemy zgodnie ze standardem w formacie camel case (getConvertedPricesAttribute), natomiast nazwę w tablicy $appends zapisujemy w formacie snake case (converted_prices).
4. Automatyczne usuwanie relacji w bazie danych Laravel
Najpowszechniejsza relacja w bazie danych to relacja jeden do wielu. Załóżmy, że w tabeli users mamy informacje o użytkowniku, a w tabeli contacts przechowujemy informacje o kontaktach, które użytkownik utworzył na swoim profilu. Tworząc aplikację, zawsze trzeba mieć na uwadze to, co będzie się z nią działo za kilka lat. Zatem powinniśmy zadbać o to, aby w sytuacji gdy użytkownik usunie swoje konto, wszelkie dane z nim powiązane w innych tabelach, również zostały usunięte. W przeciwnym wypadku, po kilku latach baza danych może osiągnąć rozmiary nawet kilkunastu gigabajtów, a to będzie stanowić duży problem.
Automatyczne usuwanie relacji możemy osiągnąć na kilka sposobów, jednak moim ulubionym jest zdefiniowanie tego już na poziomie migracji.
Tak wygląda migracja tabeli contacts:
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('fist_name');
$table->string('last_name');
$table->string('address');
$table->string('email');
$table->timestamps();
});
}
Kolumna user_id odnosi się oczywiście do id użytkownika, który utworzył kontakt w tabeli contacts. Musimy więc zdefiniować tą relację w migracji, wraz z odpowiednią informacją o tym co zrobić z danymi w przypadku usunięcia rekordu.
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('first_name');
$table->string('last_name');
$table->string('address');
$table->string('email');
$table->timestamps();
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('cascade');
});
}
5. Używanie map() zamiast foreach
Powyżej wspomniałem o accessorach, dzięki którym możemy modyfikować dane pobrane z bazy. Ta metoda sprawdza się, jeśli za każdym razem chcemy otrzymać dane w zmienionej formie. Co natomiast, jeśli chcemy dokonać modyfikacji tylko w jednym przypadku? Wróćmy do przykładu z produktami, gdzie ceny są zapisane w postaci liczby naturalnej. Załóżmy, że tym razem chcemy zamienić centy na euro oraz dodać atrybut ‘currency’, zawierający informację o walucie. Oczywiście możemy do tego użyć pętli foreach:
class ProductController extends Controller
{
public function index()
{
$products = Product::all();
foreach ($products as $product) {
$product->price = $product->price / 100;
$product->currency = 'EUR';
}
}
}
Jednak, jak to bywa w Laravelu, twórcy tego frameworka przygotowali bardziej eleganckie rozwiązanie. Możemy zamiast pętli foreach, użyć metody map():
class ProductController extends Controller
{
public function index()
{
$products = Product::query()->get()->map(function (Product $product) {
$product->price = $product->price / 100;
$product->currency = 'EUR';
return $product;
});
}
}
6. Wygodniejsze wyszukiwanie według daty
Zgodnie z konwencją, daty w Laravelu zapisuje się w formacie Y-m-d H:i:s. To pozwala na wygodne wyszukiwanie rekordów w określonym przedziale czasowym, przy pomocy metod whereDate, whereMonth, whereDay, whereYear lub whereTime. Zobaczcie poniższe przykłady:
$products = Product::whereDate('created_at', '2018-01-31')->get();
$products = Product::whereMonth('created_at', '12')->get();
$products = Product::whereDay('created_at', '31')->get();
$products = Product::whereYear('created_at', date('Y'))->get();
$products = Product::whereTime('created_at', '=', ’14:13:58')->get();
7. N+1 problem
Tworzenie relacji między tabelami jest w Laravelu niezwykle proste. Rozwiązania zastosowane w tym frameworku mają wiele zalet, jednak nie są pozbawione wad. Wróćmy do przykładu przedstawionego wcześniej z relacją jeden do wielu, gdzie do jednego użytkownika jest przypisane wiele kontaktów. Zdefiniowanie relacji odbywa się w ten sposób:
class User extends Authenticatable
{
public function contacts()
{
return $this->hasMany(Contact::class);
}
}
Następnie załóżmy, że chcemy pobrać wszystkich użytkowników oraz wyświetlić kontakty powiązane za pomocą relacji:
public function index()
{
$users = User::all();
foreach ($users as $user) {
echo $user->name;
foreach ($user->contacts as $contact) {
echo $contact->last_name;
}
}
}
Niestety, to co ułatwia nam pracę z bazą danych dzięki Eloquent, powoduje też, że nie do końca wiemy, co tak naprawdę dzieje się “pod przykryciem”. Całe szczęście z pomocą przychodzi kolejne znakomite narzędzie Laravela czyli Telescope. Dzięki niemu możemy zobaczyć dokładnie, jak wygląda w tym przypadku zapytanie do bazy danych:
Jak widać, Laravel najpierw pobiera wszystkich użytkowników z tabeli users, a następnie dla każdego użytkownika z osobna wykonuje zapytanie do tabeli contacts. Jest to poważny problem pod kątem wydajności. W przypadku większej liczby użytkowników, będziemy mieli całą masę niepotrzebnych zapytań do bazy. Na szczęście rozwiązanie jest bardzo proste. Musimy zamienić metodę all() na with(), w celu zadeklarowania, że chcemy, aby nasze wyniki zostały pobrane od razu z danymi znajdującymi się w kolumnie ‘contacts’.
public function index()
{
$users = User::with('contacts')->get();
foreach ($users as $user) {
echo $user->name;
foreach ($user->contacts as $contact) {
echo $contact->last_name;
}
}
}
8. Sortowanie danych w relacji
Dla ostatniego przykładu, wróćmy jeszcze raz do użytkownika oraz jego kontaktów. Warto byłoby wyświetlać kontakty posortowane alfabetycznie. Oczywiście możemy to zrobić w ten sposób:
$users = User::with(['contacts' => function ($query) {
$query->orderBy('last_name', 'ASC');
}])->get();
Jednak śmiało możemy założyć, że właściwie za każdym razem kiedy będziemy pobierać te dane, będzie potrzeba posortowania ich właśnie w ten sposób. W takim razie, co zrobić, aby nie trzeba było powtarzać tej czynności? Spójrzmy jeszcze raz, jak wygląda definicja relacji w modelu:
public function contacts()
{
return $this->hasMany(Contact::class);
}
Tutaj właśnie należy dodać metodę orderBy, aby nasze wyniki za każdym razem były posortowane w odpowiedni sposób:
public function contacts()
{
return $this->hasMany(Contact::class)
->orderBy('last_name' ,'ASC');
}
Bądź na bieżąco z Laravelem
Laravel słynie z wielu wygodnych rozwiązań. Dlatego warto nieustannie poszerzać swoją wiedzę i poznawać coraz to nowe techniki, które ułatwią pracę z tym systemem. Kolejne wersje tego frameworka wydawane są systematycznie, często przynosząc całą masę nowych funkcjonalności. Jesteśmy z tym w Droptica na bieżąco, realizując usługi PHP.