Vor langer Zeit - manche sagen, damals streiften noch die Dinosaurier über die Erde - gab es in relationalen Datenbanken nur einfache, grundlegende Datentypen. Man konnte Integer ablegen, Strings, vielleicht noch Fließkommawerte. Inzwischen gibt es einen ganzen Zoo von komplexen Datentypen. Einige Typen können Geometrien aufnehmen, zum Beispiel eine Linie oder ein Polygon. Andere sind speziell auf UUIDs oder IP-Adressen ausgelegt. Es gibt Datentypen, die ganze XML-Dokumente aufnehmen können. Und es gibt spezielle Datentypen für das vielleicht wichtigste Format zum Datenaustausch, JSON. In PostgreSQL gibt es json und jsonb, in MySQL / MariaDB gibt es JSON, in Oracle ebenfalls JSON und so weiter.

Auch Ruby on Rails unterstützt seit langem JSON-basierte Felder in Modellen. Es kann aber vorkommen, dass man sich damit in die Ecke programmiert, und es zunächst gar nicht auffällt.

ActiveRecord bietet nämlich die Möglichkeit, die einzelnen “Unterfelder” in einem JSON-Feld direkt anzusprechen. Nehmen wir beispielsweise ein Message-Modell, mit einem title als String und einem content als JSON. Der Content enthält die folgenden Schlüsel innerhalb des JSON: sender, recipient, status_code. Will man nicht von Hand die Werte innerhalb von content heraus- und wieder hineinfummeln, scheint ein Store. Damit bietet ActiveRecord nämlich direkte Accessors für die Felder innerhalb des JSON:

class Message < ActiveRecord::Base
  CONTENT_FIELDS = %[sender recipient status_code]

  store :content, accessors: CONTENT_FIELDS, coder: JSON
end

Damit kann man direkt beispielsweise my_message.sender ansprechen. Das funktioniert auch wunderbar, auf den ersten Blick. Ein Test mit RSpec läuft problemlos durch:

let(:message) { create(:message) }
let(:new_sender) { 'changed_sender'}

it 'stores the sender correctly' do
  message.sender = new_sender
  message.save!

  message.reload
  expect(message.sender).to eq(new_sender)
end

Das liegt daran, dass ActiveStore freundlicherweise die Werte serializiert und wieder de-serialisiert. In der Datenbank landet allerdings nicht das JSON-Objekt, obwohl wir einen passenden Datentyp haben, sondern ein serialisierter String. Ein solcher String is valides JSON, also beschwert sich keines der beteiligten Systeme. Zum Problem wird es, sobald man direkt auf die Werte in der Datenbank zugreifen will, beispielsweise um eine Statistik zu erstellen. Dann findet man nämlich nicht wie erwartet etwas vor wie

{
  "sender": "example_sender",
  "recipient": "example_recipient",
  "status_code": 200
}

sondern eben etwas wie

"{\"sender\":\"example_sender\",\"recipient\":\"example_recipient\",\"status_code\":200}"

Der Grund ist recht einfach: store ist für einfache Datentypen gedacht, wenn man beispielsweise JSON-Daten in einem String-Feld in der Datenbank ablegen will. Für komplexe Datentypen wie JSON ist es aber die falsche Wahl. Hier passt store_accessor besser:

class Message < ActiveRecord::Base
  CONTENT_FIELDS = %[sender recipient status_code]

  store_accessor :content, *CONTENT_FIELDS
end

Jetzt landet auch ein “richtiges” JSON in der Datenbank, und kein serialisierter String.