Erstens geht es in dem Artikel nicht darum, wie ich Schienen mag, und zweitens geht es in dem Artikel nicht darum, wie ich sie hasse. Sie können auf völlig unterschiedliche Weise behandelt werden und werden nur dann besser, wenn Sie sie ändern. Und sie können nur schlimmer werden, wenn sie sich zu verändern beginnen. Im Allgemeinen habe ich Sie gewarnt, aber Sie haben mich verstanden.
Eines der Hauptkonzepte von ActiveRecord ist, dass die Datenbank sehr nützlich ist und geändert werden kann. Nun, Sie sitzen da und schreiben Ihre Modelle mit MySQL. Plötzlich lesen Sie irgendwo, dass Sie stattdessen MySQL nehmen und durch MongoDB ersetzen können. Nun, nicht so radikal, aber unter PostgreSQL könnten Sie Gründe haben, MySQL zu ersetzen. Oder umgekehrt, ich habe nichts gegen MySQL. Hier behauptet ActiveRecord, es für Sie einfacher zu machen, angeblich Bereiche, bevor / nachdem Filter und Zuordnungen abstrakt genug sind, um sich nicht um das Generieren von Datenbankabfragen und das Kümmern um die Anwendungslogik zu kümmern. Anstelle von WHERE is_archived = TRUE
schreiben Sie gerne, where(is_archived: true)
und where(is_archived: true)
alles für Sie tun. Alle Beispiele werden für PostgreSQL übersetzt, nicht für MySQL, sodass MySQL-Benutzer ihr eigenes Fahrrad erfinden müssen.

Aber egal wie! In der Praxis stellt sich heraus, dass diese Abstraktionsebene vollständig mit Löchern gefüllt ist, wie ein Trog aus der Geschichte des Goldenen Fisches. Und dass viele grundlegende Funktionen nicht verwendet werden können, z. B. das Vergleichen von Daten oder das Arbeiten mit Arrays. Und Sie erhalten Bereiche mit erzwungenen where("#{quoted_table_name}.finished_at >= ?", Date.current)
oder where("#{quoted_table_name}.other_ids <@ ARRAY[?]", ids)
. Auf die ActiveRecord eine völlig bewusste und logische Antwort gibt: Verwenden Sie sie nicht. Verwenden Sie anstelle von Arrays die habtm-Verbindung. Wenn Sie Daten vergleichen müssen, leben Sie damit. Ja, und Gott verbietet Ihnen, in einem solchen Bereich den quoted_table_name
zu verpassen - die ersten includes
oder joins
setzen alles an seine Stelle. Es ist überall und immer einfacher zu schreiben, um die Hand nicht niederzuschlagen.
Sobald Sie sich entscheiden, ActiveRecord hier zu stören, gibt es natürlich kein Zurück mehr. Nicht, dass es keine Chance geben würde, auch nicht die gespenstische Hoffnung auf einen schmerzlosen Übergang zu einer anderen Datenbank. Es wird viel einfacher sein, diesen Quellcode zu drucken und zu brennen. Und natürlich gibt es keinen anderen Grund, die zusätzlichen Datenbankfunktionen in Ihrer Anwendung nicht zu verwenden. Verwenden Sie für die Gesundheit und zwingen Sie andere!
Und wenn sich herausstellt, dass Ihre Bereiche im Ordner "Modelle" aus mehr als der Hälfte dieser zusätzlichen Funktionen bestehen, ist es ziemlich offensichtlich, dass ActiveRecord nur eine praktische Shell ist, um einen Code mit Beschriftungen in einen anderen Code zu integrieren. Und Bereiche wie where(is_archived: true).joins(:sprint).merge(Sprint.archived)
funktionieren gut und das Kombinieren ist nicht schwieriger als Rührei zu machen, oder?

Die nächste Stufe wird die Denormalisierung sein. Nein, die Denormalisierung ist sozusagen nicht immer verschwunden, aber die Pflege lag bei den mächtigen Schultern von Rails und ActiveRecord, und Sie wissen, dass sich diese beiden Typen in Bezug auf Schnelligkeit und Askese in Bezug auf den Ressourcenbedarf nicht unterschieden. counter_cache: true
, counter_cache: true
ist der erste Schritt zur Denormalisierung, da Sie mit COUNT(*) AS sprints_count
nicht COUNT(*) AS sprints_count
können (Sie möchten select()
nicht ändern, oder?). Und counter_cache
nicht weit davon entfernt, perfekt zu sein, und in einigen Fällen kann es zu einer Desynchronisation des tatsächlichen Betrags von dem zwischengespeicherten kommen. Natürlich unkritisch, aber unangenehm. Und dies ist nur der erste Kandidat, der sich in der Datenbank niederlässt und nicht den bereits geladenen Kopf der Rubinmaschine lädt. Nur ein paar Auslöser und fertig! Erstens müssen Sie beim Löschen und Hinzufügen eines neuen Datensatzes zu Platte A die Anzahl der Datensätze in Platte B zählen, und das war's, richtig? Nun, beim Bearbeiten natürlich, wenn sich der foreign_key
geändert hat, weil die UPDATE B SET a_id = $1 WHERE id = $2
den counter_cache sowohl für das alte A als auch für das neue bricht.
CREATE OR REPLACE FUNCTION update_#{parent_table}_#{child_table}_counter_on_insert() RETURNS TRIGGER AS $$ BEGIN UPDATE #{parent_table} SET #{counter_column} = COALESCE((SELECT COUNT(id) FROM #{child_table} GROUP BY #{foreign_column} HAVING #{foreign_column} = NEW.#{foreign_column}), 0) WHERE (#{parent_table}.id = NEW.#{foreign_column}); RETURN NULL; END; $$ LANGUAGE plpgsql;
Der nächste Datenbankpfad bezieht sich auf Datum und Uhrzeit. Lassen Sie uns zunächst nur die Felder created_at
und updated_at
in der Datenbank updated_at
Glücklicherweise ist dies viel einfacher. Erste Standardeinstellungen:
change_column_default :table_name, :created_at, -> { 'CURRENT_TIMESTAMP' } change_column_default :table_name, :updated_at, -> { 'CURRENT_TIMESTAMP' }
Und um es sofort überall zu tun, können Sie einen Zyklus für alle Platten organisieren, auf denen sich diese Felder befinden. Neben den ar_internal_metadata
schema_migrations
und ar_internal_metadata
natürlich:
(tables -
Das war's, jetzt ist der Standardwert für diese Tabellen genau das, was wir brauchen. Und jetzt ist es an der Zeit sicherzustellen, dass die Schienen diese Felder nicht berühren. Dies geschieht mit zwei Schrauben an den richtigen Stellen. Und ja, es gibt eine Option beim Einrichten des Frameworks, das dafür verantwortlich ist:
Rails.application.config.active_record.record_timestamps = false
Der nächste Schritt besteht also darin, das Feld updated_at
zum Zeitpunkt der Aktualisierung des updated_at
zu aktualisieren. Das ist einfach:
CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_update() RETURNS TRIGGER AS $$ BEGIN SELECT CURRENT_TIMESTAMP INTO NEW.updated_at; RETURN NEW; END; $$ LANGUAGE plpgsql;
Jetzt müssen Sie die touch: true
vollständig loswerden touch: true
in Modellen. Dieses Ding ist dem Ziel im Armaturenbrett sehr ähnlich - auch völlig lochig. Und ich werde nicht einmal erklären, warum, weil Sie alle diese Fälle bereits kennen. Dies ist nicht viel komplizierter. Sie müssen update_at nur aktualisieren, nicht nur für sich selbst:
CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_update() RETURNS TRIGGER AS $$ BEGIN UPDATE foreign_table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.foreign_column_name; SELECT CURRENT_TIMESTAMP INTO NEW.updated_at; RETURN NEW; END; $$ LANGUAGE plpgsql;
Natürlich führt die Aufrufkette solcher Trigger eine unnötige Aktion aus, aber in den Postgres des vernünftigen Mechanismus werden Trigger aufgerufen, ohne den Datensatz selbst zu ändern. Sie können versuchen, SET title = title
auszuführen, aber es ist nicht besser als SET updated_at = CURRENT_TIMESTAMP
.
Genau derselbe Trigger befindet sich auf der Einfügung, nur eine Aktualisierung von updated_at
nicht erforderlich:
CREATE OR REPLACE FUNCTION touch_for_#{table_name}_on_insert() RETURNS TRIGGER AS $$ BEGIN UPDATE foreign_table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.foreign_column_name; RETURN NEW; END; $$ LANGUAGE plpgsql;
Natürlich können Sie versuchen, dies mit einer Funktion zu schreiben, indem Sie eine Überprüfung für das aktuelle Ereignis direkt im Trigger hinzufügen, ähnlich wie bei IF TG_OP = 'UPDATE' THEN
. Es ist jedoch vorzuziehen, alle Trigger so einfach wie möglich zu gestalten, um die Wahrscheinlichkeit eines Fehlers zu verringern.
Möglicherweise möchten Sie die Generierung solcher Trigger irgendwie automatisieren, und dann müssen Sie wahrscheinlich alle Außenbeziehungen zwischen der aktuellen Tabelle und dem Rest finden. Mit dieser Abfrage können Sie dies ganz einfach tun:
SELECT ccu.table_name AS foreign_table_name, kcu.column_name AS column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '#{table_name}' ORDER BY ccu.table_name;
Ein weiterer sehr hilfreicher Tipp. Rufen Sie Trigger auf die gleiche Weise auf, um zu überprüfen, ob eine Anforderung vorhanden ist oder nicht. Bei dieser Anforderung werden beispielsweise alle Trigger zum Einfügen von Berührungen gefunden:
SELECT routine_name AS name FROM information_schema.routines WHERE routine_name LIKE 'touch_for_%_on_insert' AND routine_type ='FUNCTION' AND specific_schema='public';
Und schließlich bleibt das Schlimmste übrig. Tatsache ist, dass die Schienen nicht für mindestens eine Art intelligente Datenbank ausgelegt sind und es ihnen wirklich egal war, dass sich zumindest etwas anderes als das id
Feld in der Datenbank ändern kann, und das nur beim Einfügen. Daher gibt es keinen vernünftigen Mechanismus, um RETURNING updated_at
hinzuzufügen, RETURNING updated_at
zu aktualisieren.

Mankipatch erwies sich als nicht sehr ordentlich, aber das Ziel war zunächst, die aktuelle Arbeit des Frameworks so wenig wie möglich zu beschädigen.
Ich werde ihn komplett bringen module ActiveRecord module Persistence # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L729-L741 def _create_record(attribute_names = self.attribute_names) attribute_names &= self.class.column_names attributes_values = attributes_with_values_for_create(attribute_names) an_id, *affected_rows = self.class._insert_record(attributes_values).dup self.id ||= an_id if self.class.primary_key Hash[ApplicationRecord.custom_returning_columns(self.class.quoted_table_name, :create).take(affected_rows.size).zip(affected_rows)].each do |column_name, value| public_send("#{column_name}=", self.class.attribute_types[column_name.to_s].deserialize(value)) if value end @new_record = false yield(self) if block_given? id end private :_create_record # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L710-L725 def _update_record(attribute_names = self.attribute_names) attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? affected_rows = [] @_trigger_update_callback = true else affected_rows = _update_row(attribute_names) @_trigger_update_callback = affected_rows.any? end Hash[ApplicationRecord.custom_returning_columns(self.class.quoted_table_name, :update).take(affected_rows.size).zip(affected_rows)].each do |column_name, value| public_send("#{column_name}=", self.class.attribute_types[column_name.to_s].deserialize(value)) end yield(self) if block_given? affected_rows.none? ? 0 : 1 end private :_update_record end module ConnectionAdapters module PostgreSQL module DatabaseStatements # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L93-L96 def exec_update(sql, name = nil, binds = []) execute_and_clear(sql_with_returning(sql), name, binds) { |result| Array.wrap(result.values.first) } end # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L147-L152 def insert(arel, name = nil, pk = nil, _id_value = nil, sequence_name = nil, binds = []) sql, binds = to_sql_and_binds(arel, binds) exec_insert(sql, name, binds, pk, sequence_name).rows.first end alias create insert # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L98-L111 def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: table_ref = extract_table_ref_from_insert_sql(sql) if pk.nil? # Extract the table from the insert sql. Yuck. pk = primary_key(table_ref) if table_ref end returning_columns = quote_returning_column_names(table_ref, pk, :create) if returning_columns.any? sql = "#{sql} RETURNING #{returning_columns.join(', ')}" end super end # No source in original repo def quote_returning_column_names(table_ref, pk, action) returning_columns = [] returning_columns << pk if suppress_composite_primary_key(pk) returning_columns += ApplicationRecord.custom_returning_columns(table_ref, action) returning_columns.map { |column| quote_column_name(column) } end # No source in original repo def sql_with_returning(sql) table_ref = extract_table_ref_from_update_sql(sql) returning_columns = quote_returning_column_names(table_ref, nil, :update) return sql if returning_columns.blank? "#{sql} RETURNING #{returning_columns.join(', ')}" end # No source in original repo def extract_table_ref_from_update_sql(sql) sql[/update\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*set/im] Regexp.last_match(1)&.strip end end end end end
Das Wichtigste ist, dass wir uns hier ApplicationRecord.custom_returning_columns
zuwenden, um herauszufinden, welche Spalten neben id für uns interessant sind. Und diese Methode sieht ungefähr so aus:
class << self def custom_returning_columns(table_ref, action) return [] if ['"schema_migrations"', '"ar_internal_metadata"'].include?(table_ref) res = [] res << :created_at if action == :create res << :updated_at res += case table_ref when '"user_applications"' [:api_token] when '"users"' [:session_salt, :password_changed_at] # ... else [] end res end end
Anstelle von Schlussfolgerungen können wir sagen, dass Rails Kopfschmerzen etwas weniger wund geworden sind. counter_cache
wie counter_cache
und touch
werden in Vergessenheit geraten, und im nächsten Artikel können wir uns etwas Globaleres counter_cache
, wie das Entfernen von hängenden Leerzeichen, die Datenvalidierung, das kaskadierende Löschen von Daten oder das paranoide Löschen. Wenn Ihnen dieser Artikel gefallen hat, natürlich.