ORM

April 13, 2020

Was sind ORMs? Wo sind sie sinnvoll und wo nicht? Welches ORM wird hier eingesetzt?

ORM steht für Object-Relational-Mapping, das hilft zunächst gar nicht weiter, also betrachten wir die einzelnen Teile:

"Object" steht für ein Objekt in einem Programm. Einige Programmiersprachen sind objektorientiert und packen die Daten in Objekte, dadurch kann man diese Daten sehr schön und zuverlässig bearbeiten. Zum längeren Aufbewahren dieser Daten muss man sie allerdings anschließend irgendwo speichern und da bieten sich Datenbanken ja gerade zu an. Daher kommt das Wort "Relational", nämlich von den Relationalen Datenbanken, wie z.B. PostgreSQL, MySQL, SQLServer, ... In allen Namen steckt "SQL" drin und da findet sich auch schon ein Problem, SQL ist die Sprache und die Art und Weise, wie man mit Datenbanken kommuniziert. Die Daten aus dem Objekt müssen also in Form von SQL-Anweisungen an die Datenbank übertragen werden. Dort werden die Daten als Zeilen in den Datentabellen der Datenbank gespeichert. Beim Laden der Daten geschieht genau das Umgekehrte: Eine SQL-Abfrage liefert die Daten aus der Datenbank und für jeden gelieferten Datensatz wird dann ein passendes Programm-Objekt erzeugt und mit den Daten gefüllt. Sobald die Daten in Form dieser Objekte vorliegen, können sie bearbeitet und z.B. an den Webbrowser übertragen werden.

Diese Kommunikation zwischen dem Programm und der Datenbank zu programmieren ist aufwendig, fehleranfällig und ziemlich nervig.

Ein ORM bietet eine Unterstützung bei der Kommunikation der Applikation mit der Datenbank. Sie bilden (mappen) die Objekte der Programmiersprache auf die Datensätze in der Datenbank ab und erzeugen den für die Kommunikation mit der Datenbank erforderlichen SQL-Code dynamisch selbst.

Durch den Einsatz von ORMs erleichtert man sich diese Programmierung enorm, allerdings kommt man dann nicht drum herum sich auf die Eigenarten des gewählten ORMs einzulassen und man büßt auch ein wenig oder ggf. einiges an Performanz gegenüber der selbstgestrickten Version ein. Es ist eine Abwägung von Entwicklerproduktivität zu Ausführungsgeschwindigkeit der Applikation.

Ein weiterer, wichtiger Vorteil von ORMs: Sie können diverse Datenbanken unterstützen, das bedeutet sie bilden eine Abstraktionsschicht und ermöglichen so die unter der Applikation liegende Datenbank mit geringem Aufwand auszutauschen. Da der SQL-Dialekt von Datenbank zu Datenbank unterschiedlich ist, wäre ein Wechsel der Datenbank, von z.B. MySQL nach PostgreSQL, bei der "handgestrickten" Version vermutlich deutlich aufwändiger!

Da die hier gewählte Programmiersprache Go ist, muss es natürlich auch ein in und für Go entwickeltes ORM sein. Unter den Go-Entwicklern scheint zum Beispiel GORM.io sehr beliebt zu sein. Bei codeM fiel die Wahl allerdings auf Upper.io/db.

Upper.io/db hat genau die Features und Funktionen die hier gewollt sind. Upper.io ist schlank und flexibel und hat sich als in vielen Projekten bewährt.

Wir finden, dass bei Upper.io die Kombination von Go- und SQL-Code richtig gut gelungen ist. Dieses zeigt folgendes Beispiel:

func GetSales(sess sqlbuilder.Database) ([]byte, error) {

	q := sess.Select(
		"Employee.LastName",
		"Employee.FirstName",
		"Employee.Region",
		"Orders.CustomerId",
		"Orders.OrderDate",
		"Orders.Freight",
		"Orders.ShipName",
		"Orders.ShipCity",
		"Orders.ShipCountry",
		"Customer.CompanyName",
		"Customer.City",
		"Customer.Country",
	).From("Orders").
		Join("Employee").On("Orders.EmployeeId = Employee.Id").
		Join("Customer").On("Orders.CustomerId = Customer.Id")

	var data types.DataResultSlice

	err := q.All(&data)
	if err != nil {
		return nil, err
	}

	result, err := json.Marshal(&data)
	return result, err
}

Hier ist eine Funktion definiert, die Verkaufsdaten aus der Datenbank liest und zur Lieferung an den Webbrowser zurückgibt. Die Funktion bekommt ein Datenbank-Session-Objekt (sess sqlbuilder.Database) und führt darauf eine SQL-Abfrage aus. Diese SQL-Abfrage kann man sehr schön im Source-Code wiedererkennen, wer SQL kann, wird sofort sehen was hier passiert und wird auch sofort mit den Code zurechtkommen.

Es wird also zuerst eine SQL-Abfrage (q) und dann ein generisches Datenobjekt (data) deklariert. Die Abfrage wird ausgeführt mit q.All(&data) - die Methode .All(...) liest alle Datensätze auf einmal - und die Ergebnisse in das data-Objekt übertragen. Am Schluss wird das Ergebnis in einen JSON-Datenstrom konvertiert, der dann direkt an den Webbrowser ausgeliefert werden kann.

Falls in Zukunft ein Feld hinzukommt, oder eine weitere Tabelle mit in die Abfrage einbezogen werden soll, muss lediglich die "Select"-Deklaration angepasst werden und fertig. Maximale Entwicklerproduktivität.

Fazit: ORMs können eine echte Erleichterung sein, wenn die zu entwickelnde Applikation eher eine Individual-Software ist, oder sich das Schema z.B. in frühen Phasen der Entwicklung häufiger mal ändert. Falls die Applikation etabliert ist, sehr viele Nutzer hat und es auf maximale Geschwindigkeit ankommt, dann könnte es sinnvoll werden die Daten-Kommunikationsschicht zu überarbeiten und dabei die SQL-Abfragen von Hand zu optimieren - die dafür erforderlichen Entwickler-Ressourcen vorrausgesetzt.