Drupal-Tipp: Feldlänge trotz bestehender Daten anpassen

Jeder Drupal-Entwickler steht früher oder später vor folgendem Problem: Ein Kunde oder Redakteur bittet darum, ein Standard-Textfeld (wie eine Headline oder Subheadline) von 255 auf 512 Zeichen zu erweitern. Doch egal, ob du die Änderung in die YAML-Konfiguration einträgst oder die Entity API bemühst: Beim Speichern oder Konfigurations-Import läufst du unweigerlich in einen fatalen Fehler.

Wenn ein Feld bereits Daten enthält, weigert sich die Entity API strikt, das zugrundeliegende Datenbank-Schema zu ändern, um versehentlichen Datenverlust zu verhindern. Dieser Beitrag zeigt dir, wie du diese Einschränkung sicher umgehst, die Datenbanktabellen direkt modifizierst und Drupals internen Schema-Status synchronisierst, ohne eine einzige Zeile deiner Produktionsdaten zu verlieren.

„Das Ändern einer Feldgröße bei vorhandenen Daten wirft eine FieldStorageDefinitionUpdateForbiddenException. Das System verhindert jegliche Schema-Updates über die Standard-API, um die Datenintegrität zu garantieren, was eine manuelle Schema-Manipulation erforderlich macht.“

Um dieses Problem sauber zu lösen, müssen wir die High-Level Entity API umgehen und direkt auf einer niedrigeren Ebene mit der State-Engine interagieren.

Die Probleme mit dem Standard-API-Ansatz:

  • Vorhandene Daten führen zu Exception: SqlContentEntityStorageSchema::onFieldStorageDefinitionUpdate() löst sofort eine Exception aus, sobald Daten in der Tabelle existieren.
  • Konfiguration ist out-of-sync: Das bloße Ändern der .yml-Feldkonfigurationsdatei und deren Import via Drush schlägt während der Konfigurationssynchronisation fehl, wenn das aktive Datenbank-Schema nicht übereinstimmt.
  • Fehler im Status Report: Wenn du Konfigurationswerte erzwingst, meldet Drupals Statusbericht (/admin/reports/status) eine Diskrepanz im Entity-Schema, was Hook-Implementierungen und automatisierte Tests zum Absturz bringen kann.

Der Database-First-Ansatz: Die Lösung des Problems

  • Sichere Anpassung: Das Ändern einer Spalte von varchar(255) auf varchar(512) ist eine nicht-destruktive Datenbankoperation. MySQL/MariaDB und PostgreSQL verarbeiten dies, ohne Daten abzuschneiden oder Tabellen dauerhaft zu sperren.
  • Manuelle Anpassung der Definitionen: Indem wir die Low-Level Key-Value-Collection entity.storage_schema.sql direkt aktualisieren, signalisieren wir dem Entity Definition Update Manager, dass der Zustand konsistent ist.
  • Direkte Konfigurationsmanipulation: Das direkte Ändern der Field-Storage-Konfiguration via Configuration Factory hebelt die API-Validierung aus. So greifen die neuen Zeichenlimits im System sofort, ohne eine Exception auszulösen.

Wie kann das Ganze nun umgesetzt werden?

Die Änderung wird mithilfe eines hook_post_update_NAME()-Hooks in der .post_update.php-Datei eines Moduls implementiert. Post-Update-Hooks laufen nach den regulären hook_update_N-Hooks ab. Das stellt sicher, dass der Service Container vollständig verfügbar und einsatzbereit ist. In unserem Beispiel soll die Feldlänge von field_subheadline_text innerhalb eines Paragraph-Feldtyps angepasst werden.

Folgende Logik muss dafür in einem Custom Module unter folgender Verzeichnisstruktur hinterlegt werden:

web/modules/custom/my_module/
├── my_module.info.yml
├── my_module.module
└── my_module.post_update.php

 1<?php
 2
 3/**
 4 * @file
 5 * Post update functions for my_module module.
 6 */
 7
 8declare(strict_types=1);
 9
10use Drupal\Core\Database\Database;
11use Drupal\field\FieldStorageConfigInterface;
12
13/**
14 * Increase the maximum length of paragraph subheadline field to 512 characters.
15 */
16function my_module_post_update_increase_max_subheadline_length(array &$sandbox): void {
17  $entity_type = 'paragraph';
18  $field_name = 'field_subheadline_text';
19  $new_length = 512;
20  $value_column = "{$field_name}_value";
21  $tables = [
22    "{$entity_type}__{$field_name}",
23    "{$entity_type}_revision__{$field_name}",
24  ];
25
26  // 1. Alter the DB columns directly. Drupal's entity API refuses to change a
27  // field's schema once it holds data
28  // (SqlContentEntityStorageSchema::onFieldStorageDefinitionUpdate() throws
29  // FieldStorageDefinitionUpdateForbiddenException), so we widen the columns
30  // ourselves and then sync the tracked definitions below. Widening a varchar
31  // never truncates existing values.
32  $db_schema = Database::getConnection()->schema();
33  $column_spec = ['type' => 'varchar', 'length' => $new_length, 'not null' => FALSE];
34  foreach ($tables as $table) {
35    if ($db_schema->tableExists($table)) {
36      $db_schema->changeField($table, $value_column, $value_column, $column_spec);
37    }
38  }
39
40  // 2. Sync the installed SQL storage schema so the entity definition update
41  // manager stops reporting a mismatch. This is stored in the
42  // "entity.storage_schema.sql" key-value collection under the key
43  // "{entity_type}.field_schema_data.{field_name}" (see
44  // SqlContentEntityStorageSchema::loadFieldSchemaData()).
45  $kv = \Drupal::keyValue('entity.storage_schema.sql');
46  $schema_key = "{$entity_type}.field_schema_data.{$field_name}";
47  $field_schema = $kv->get($schema_key);
48  if (is_array($field_schema)) {
49    foreach ($field_schema as &$table_schema) {
50      if (is_array($table_schema)
51        && is_array($table_schema['fields'] ?? NULL)
52        && is_array($table_schema['fields'][$value_column] ?? NULL)
53        && array_key_exists('length', $table_schema['fields'][$value_column])
54      ) {
55        $table_schema['fields'][$value_column]['length'] = $new_length;
56      }
57    }
58    unset($table_schema);
59    $kv->set($schema_key, $field_schema);
60  }
61
62  // 3. Update the active field storage config. Use the raw config factory to
63  // bypass the entity API (a FieldStorageConfig::save() would re-trigger the
64  // forbidden-schema-change check from step 1).
65  \Drupal::configFactory()
66    ->getEditable("field.storage.{$entity_type}.{$field_name}")
67    ->set('settings.max_length', $new_length)
68    ->save();
69
70  // 4. Update the last-installed field storage definition so it matches the
71  // active config.
72  $repository = \Drupal::service('entity.last_installed_schema.repository');
73  $definitions = $repository->getLastInstalledFieldStorageDefinitions($entity_type);
74  if (isset($definitions[$field_name]) && $definitions[$field_name] instanceof FieldStorageConfigInterface) {
75    $definitions[$field_name]->setSetting('max_length', $new_length);
76    $repository->setLastInstalledFieldStorageDefinitions($entity_type, $definitions);
77  }
78
79  // 5. Clear cached definitions so the new schema takes effect immediately.
80  \Drupal::service('entity_type.manager')->clearCachedDefinitions();
81  \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
82}
83

Testen und überprüfen

Sobald der Code implementiert ist, müssen folgende Kommandos im Terminal ausgeführt werden, um die Schema-Migration anzuwenden und die Container-Caches zu leeren:

# Post-Update-Hooks ausführen, um unsere manuelle Migration zu triggern
drush updatedb
# Caches neu aufbauen, damit der Entity Field Manager die Definitionen frisch einliest
drush cache:rebuild
# Konfiguration exportieren, damit deine field.storage.*.yml dem neuen Datenbankzustand entspricht
drush config:export

Alternativ können diese Schritte auch über die Drupal Benutzeroberfläche durchgeführt werden, was jedoch mit etwas mehr Zeitaufwand verbunden ist. Im Anschluss sollte Drupals Statusbericht (/admin/reports/status) noch einmal geprüft werden, um sicherzustellen, dass keine Fehler bezüglich der Feldkonfiguration mehr vorliegen.

Besonderheiten bei Update & Fallbacks

  • Datenbank-Treiber: Diese Strategie nutzt die Standard-Syntax der Drupal Schema API (changeField()). Sie funktioniert nahtlos auf MySQL, MariaDB und PostgreSQL. Falls du SQLite nutzt, verhält sich das Ändern von Spaltenlängen anders, da SQLite keine strikten String-Limits erzwingt – die Metadaten-Synchronisationen sind dennoch zwingend erforderlich.
  • Views-Integration: Wenn das Feld aktuell in Views als Argument oder Filter genutzt wird, muss der Views-Cache geleert werden (drush cache:clear views). Die View-Konfiguration selbst trackt die Zeichenlänge nicht direkt, weshalb sie ohne Modifikation weiter funktioniert.
  • Form Validierung: Die Anpassung der Feldspeicher-Einstellung (Field Storage) erhöht lediglich das Limit auf Datenbankebene. Falls die Konfiguration der Feldinstanz (Field Instance) die Eingaben an anderer Stelle einschränkt oder überschreibt, müssen diese ebenfalls überprüft werden.
Steffen Rühlmann
  • Senior-Drupal-Entwickler

Steffen macht nun schon seit zwanzig Jahren Dinge mit Webseiten. Er ist ein Drupal-Allrounder und Acquia Certified Developer seit 2014, der sich sowohl in der Modul-Programmierung als auch bei Theming oder Sitebuilding sicher bewegt. An Remote Work im eigenen Haus mag er den Ausblick auf die Felder. Wenn er nicht gerade arbeitet, wird er von seinen Kindern auf Trab gehalten und ist gerne in der Natur.