logo

Βελτίωση των eloquent queries σε Laravel (+ ένα bug)

14/08/2020

Καταρχήν προτείνεται χωρίς δεύτερη σκέψη το debugbar, που είναι ένα φοβερό debug εργαλείο. Πέρα του ότι μπορείς να πετάς ότι μηνύματα ή data θες (χωρίς να παιδεύεσαι με dd και άλλα παρόμοια που διακόπτουν την ροή της εκτέλεσης του κώδικα), σου δίνει και μια λεπτομερή εικόνα για τι στο διάολο κάνεις με την database σου. Ποια ακριβώς sql queries τρέχουν, πόση μνήμη χρησιμοποιείται, πόσα μοντέλα φορτώνονται κτλ

Η εγκατάσταση του debugbar είναι πανεύκολη, όπως εγκαθιστάς κάθε πακέτο με τον composer. Άμεσα θα σου εμφανίσει (όταν είσαι σε dev περιβάλλον και τρέχεις την εφαρμογή σου), μία μπάρα με όλα τα ενδιαφέροντα που προσφέρει το εργαλείο.

Το debugbar, λοιπόν, θα μας δείξει τα προβλήματα που υπάρχουν στην εφαρμογή μας, με τα queries που κάνουμε. Πιο χαρακτηριστικό το N+1 πρόβλημα.

Ας πούμε ότι έχουμε ένα model με τα posts μας και αυτά έχουν relation με tags. Οπότε θέλοντας να εμφανίσουμε μία λίστα με όλα τα posts και τις ετικέτες που ανήκουν σε καθένα από αυτά, θα κάνουμε κάτι τέτοιο στον controller.

$posts = Post::all();

Στην συνέχεια μέσα στο view μας θα εμφανίσουμε τα tags του κάθε post, κάπως έτσι:

@foreach ($post->tags()->get() as $tag)
    {{ $tag->name }}
@endforeach

Όλα ωραία. Εμφανίζεται όπως θέλουμε το αποτέλεσμα, αλλά τι συμβαίνει από κάτω; Τι sql queries θα τρέξουν; Καταρχήν ένα select που θα πάρει όλα τα posts.

select * from posts;

Στην συνέχεια για κάθε post θα κάνει ένα ξεχωριστό select για να πάρει όλα τα tags που ανήκουν σε αυτό, κάπως έτσι.

select * from tags where id = ?
select * from tags where id = ?
select * from tags where id = ?
select * from tags where id = ?
select * from tags where id = ?
.
.
.
Ν φορές

Άρα το σύνολο των queries θα είναι Ν+1. Καταλαβαίνουμε ότι σε μεγάλες βάσεις, αυτό δεν είναι αποδεχτό. Τι κάνουμε λοιπόν;

Κάνουμε eager loading. Δηλαδή αντί να τρέχει ένα ξεχωριστό query κάθε φορά που ζητάμε τα tags ενός post, τα προσθέτει μέσα στο post όλα από την αρχή. Για να το πετύχουμε αυτό, κάνουμε κάτι τέτοιο:

$posts = Post::with('tags')->get();

Αντίστοιχα στο view:

@foreach ($post->tags as $tag)
    {{ $tag->name }}
@endforeach

Με τον τρόπο αυτό έχουν γίνει μόνο 2 queries!

select * from posts;
select * from tags where id in (1, 2, 3, 4, 5, ...);

Στο eordaia.info που δουλεύω τελευταία, διόρθωσα έτσι τα queries μου, αλλά μου έτυχε ένα περίεργο bug. Ξενύχτησα χθες για να καταλάβω τι παίζει και το βρήκα σήμερα. Ενώ γενικά τα περισσότερα posts εμφανίζονταν σωστά και γίνονταν load και όλα τα relations για κάθε ένα από αυτά, σε κάποια posts δεν δούλευε το eager loading. Δεν μπορούσε να τραβήξει τα data από τα relations.

Το δύσκολο ήταν να καταλάβω τι περίεργο έχουν κάποια posts σε σχέση με τα άλλα που δούλευαν. Τελικά βρήκα τι έφταιγε και αυτό ήταν το ότι χρησιμοποιώ uuid’s για τα id’s. Οπότε για να τραβήξει τα tags π.χ. έκανε κάτι τέτοιο:

select * from tags where id in (3535, 2, 33435, 2123, 54756, ...);

Δηλαδή μέσα στην παρένθεση αντί να ψάχνει για uuid’s τα μετέτρεπε σε integer. Έτσι, κάποιες μετατροπές ξέφευγαν από το όριο των integers και έψαχνε άλλα αντί άλλων.

Η λύση είναι τελικά να βάλεις στο model (π.χ. Post.php) την συγκεκριμένη γραμμή:

protected $keyType = 'string';

Με τον τρόπο αυτό τα uuid’s μετατρέπονται σε string κι έτσι δουλεύουν όλα σωστά.

Write your comment

rocean (at) error.gr
rocean
error.gr
feed