Praktische Methode find_each
in ActiveRecord
Ich werde über die Geschichte schreiben, weil die Implementierungsmethode nicht gut war und eine Endlosschleife aufwies und das Phänomen auftrat, dass sie vom OOM-Killer in der Produktionsumgebung gewaltsam gestoppt wurde.
Find_each ruft nicht eine große Datenmenge auf einmal ab und schleift sie, sondern in festen Einheiten (1.000 Standardeinstellungen) und schleift sie. Wenn Sie mit einer großen Datenmenge arbeiten und alle Daten gleichzeitig erfassen, verwenden Sie eine große Speichermenge. Sie können sie jedoch mit einer kleinen Speichermenge verarbeiten, indem Sie sie mit find_each teilen.
Es ist schwer zu verstehen, selbst wenn Sie es in Worten schreiben. Das Folgende ist ein Ausführungsbeispiel.
#Wenn es 10.000 Benutzer gibt
pry(main)> User.all.count
(1.1ms) SELECT COUNT(*) FROM `users`
=> 10000
#Wenn Sie jeweils verwenden, werden 10.000 Gegenstände gleichzeitig erworben.
pry(main)> User.all.each {|user| p user.id}
User Load (4.5ms) SELECT `users`.* FROM `users`
1
2
3
...
10000
# find_1 mit jedem,Holen Sie sich 000 Artikel gleichzeitig
[8] pry(main)> User.all.find_each {|user| p user.id}
User Load (3.9ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1000
1
2
3
...
1000
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000
1001
1002
1003
...
2000
User Load (0.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` > 2000 ORDER BY `users`.`id` ASC LIMIT 1000
2001
...
10000
Weitere Informationen finden Sie im Rails Guide. https://railsguides.jp/active_record_querying.html#find-each
Es ist eine bequeme Methode find_each, aber wie ich am Anfang schrieb, habe ich einen Fehler in der Implementierung gemacht und eine Endlosschleife durchgeführt. Bevor wir die Endlosschleifenimplementierung erläutern, wollen wir zunächst sehen, wie find_each funktioniert.
Lassen Sie uns anhand des am Anfang gezeigten Ausführungsbeispiels überprüfen, wie es funktioniert.
Werfen wir einen Blick auf die zuerst ausgegebene SQL.
SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1000
1000 Gegenstände werden durch Angabe des Limits 1000 erworben. Hierbei ist zu beachten, dass sie in aufsteigender Reihenfolge des PRIMARY KEY (id) angeordnet sind.
Wie bekommen Sie die nächsten 1000 Fälle?
SELECT `users`.* FROM `users` WHERE `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000
Beim Abrufen der nächsten 1000 Elemente mithilfe von SQL werden häufig LIMIT und OFFSET verwendet, OFFSET wird in diesem SQL jedoch nicht verwendet. Stattdessen können Sie sehen, dass die where-Klausel eine zusätzliche Anforderung von "users.id> 1000" enthält.
Für 1000 in users.id> 1000
wird die letzte ID der ersten erhaltenen 1000 angegeben.
Da die Daten dieses Mal in aufsteigender Reihenfolge der ID angeordnet sind, können die nächsten 1000 Elemente ohne Verwendung von OFFSET erfasst werden, indem "users.id> 1000" angegeben wird. Dies bedeutet, dass Daten erfasst werden, die größer als die letzte ID sind. tun.
Find_each, wo die Endlosschleife aufgetreten ist, wurde wie folgt implementiert. Was wird passieren?
# users.Ausweis und Bücher.Da nur der Titel verwendet wird, werden nur die erforderlichen Daten durch Auswahl erfasst.
Book.joins(:user).select('users.id, books.title').find_each do |book|
p "user_id: #{book.id}, title: #{book.title}"
end
Zunächst wird die folgende SQL ausgegeben.
SELECT users.id, books.title FROM `books` INNER JOIN `users` ON `users`.`id` = `books`.`user_id` ORDER BY `books`.`id` ASC LIMIT 1000
Es gibt kein besonderes Problem mit dem ersten SQL. Was ist also mit dem SQL, das die nächsten 1000 bekommt?
SELECT users.id, books.title FROM `books` INNER JOIN `users` ON `users`.`id` = `books`.`user_id` WHERE `books`.`id` > 1000 ORDER BY `books`.`id` ASC LIMIT 1000
Die Bedingung books.id> 1000
wurde hinzugefügt. Die Bedingung 1000 ist die ID der letzten 1000 zuerst erhaltenen Daten.
Es ist schwer zu bemerken, wenn Sie sich nur die SQL ansehen, aber die ID, die Sie mit dieser SQL erhalten, lautet "users.id" anstelle von "books.id".
Daher gibt 1000, das auf "books.id> 1000" eingestellt ist, die users.id der letzten Daten an.
In dieser SQL steigt die Reihenfolge von books.id an und die Reihenfolge von users.id wird nicht besonders gesteuert. Daher ist es möglich, dass die letzten Daten des nächsten 1000. Elements "books.id: 2000, users.id: 1" sind. In diesem Fall lautet die als nächstes auszugebende SQL wie folgt.
SELECT users.id, books.title FROM `books` INNER JOIN `users` ON `users`.`id` = `books`.`user_id` WHERE `books`.`id` > 1 ORDER BY `books`.`id` ASC LIMIT 1000
Die Bedingung ist "books.id> 1", und die Daten vor dem vorherigen SQL ("books.id> 1000") werden abgerufen. Durch die Einbeziehung von users.id, deren Reihenfolge im Zustand von books.id nicht auf diese Weise gesteuert wird, werden die zu erfassenden Daten verwechselt, und im schlimmsten Fall werden dieselben Daten viele Male erfasst, und es tritt eine Endlosschleife auf. Ich werde.
Der problematische Teil dieses Problems ist nicht immer eine Endlosschleife, und abhängig von den Daten kann "books.id> # {last users.id}" zufällig so schön und vollständig angegeben werden. Es gibt. In diesem Fall handelt es sich nicht um einen Fehler, sondern um einen Fehler, bei dem es schwer zu bemerken ist, dass die Daten etwas seltsam sind. Daher ist es möglicherweise besser, eine Endlosschleife zu haben.
Wenn Sie im obigen Beispiel die Erfassungsspalte nicht mit select eingrenzen, wird auch books.id erfasst, sodass es ordnungsgemäß funktioniert. Selbst wenn Sie die Erfassungsspalte mit select eingrenzen, funktioniert dies ordnungsgemäß, wenn Sie books.id wie unten gezeigt ordnungsgemäß erfassen.
Book.joins(:user).select('books.id AS id, users.id AS user_id, books.title').find_each do |book|
p "user_id: #{book.user_id}, title: #{book.title}"
end
Wenn Sie es wie oben beheben, ist das Update abgeschlossen, aber ich denke, das Problem war diesmal, dass es keinen automatisierten Test gab. Es gab einen Test, der den entsprechenden Prozess bestanden hat, aber ich habe keinen Test geschrieben, der find_each mehr als zweimal wiederholt. Wenn Sie einen Test haben, sind Sie sich des Fehlers wahrscheinlich bewusst, da er sich auf unbestimmte Zeit wiederholt oder seltsame Ergebnisse liefert. Mit diesem Auslöser habe ich auch einen Test hinzugefügt, bei dem find_each mehr als zweimal wiederholt wird.
Selbst wenn Sie den Mechanismus von find_each richtig verstehen, ist es schwierig, diesen Fehler zu bemerken, indem Sie ihn auf dem Schreibtisch überprüfen, z. B. die Codeüberprüfung. Darüber hinaus ist es ein seltener Prozess, dass die Anzahl der Fälle 1000 überschreitet und die Einheit 1000 nur eine Frage des Programms ist. Daher wurde sie für eine Weile als potenzieller Fehler versteckt, ohne dass dies selbst im Black-Box-Betriebstest bemerkt wurde.
Als ich darüber nachdachte, wie ich das im Voraus hätte bemerken sollen, dachte ich, dass es keine andere Wahl gab, als einen Test durchzuführen, bei dem find_each im White-Box-Test 2 schleift. Es ist eine Verschwendung, den White-Box-Test einmal manuell auszuführen. Daher ist es eine gute Idee, einen automatisierten Test zu schreiben, damit er kontinuierlich überprüft werden kann.