Gedanken Identifier helfen dabei, manche Entitäten – eindeutig – zu benennen. Diesmal starte ich unten im Code-Model und mit einem technischen Detail: Wie gestalte ich die Tabellen am besten?
CREATE TABLE schema.table ( id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY meistens findet man sowas vor. Die erste Frage, welche sich stellt: Sollte man diese id nach außen hin zur Verfügung stellen? E.g. in einer API wie /persons/{id}? Meine Meinung dazu: nein. Einige Gründe:
Requirement Traceability “We just learned that the new union contract is changing how overtime pay and shift bonuses are calculated,” Justin reported at the weekly team meeting. “It’s also changing how the seniority rules affect priority for vacation scheduling and shift preferences. We have to update the payroll and staff scheduling systems to handle all these changes right away. How long do you think it will take to get this done, Chris?”
Ich versuche meine Meinung mit folgender Definition zu beginnen:
Object-relational impedance mismatch: Bezeichnet die Herausforderung, Objekte aus einer objektorientierten Programmiersprache in einer relationalen Datenbank zu speichern. Dafür gibt es ein oft verwendetes Hilfsmittel: ORM (Objekt-Relationales Mapping): Beispiele dafür sind Hibernate oder Entity-Framework. Es handelt sich um Libraries, die das Arbeiten mit relationalen Datenbanken – fast – transparent erledigen. Der Benutzer kann in seiner objektorientieren Welt bleiben und das Framework kümmert sich um Joinen, Change-Tracking, uvm.
General goal Kafka is used as source of truth it lots of projects - especially when the project is modeled around events. In lots of situations other technologies are needed to make the post-processing / allowing for queries / … easier. PostgreSQL for example can be used to make analytical queries. PostgreSQL can also act as a time series database (using the BRIN index and for in some situation also a B-Tree - influxDB may use less memory - but anyhow).
Ein Test Der modulare Monolith ist ein gängiger Architectural Style. Die Vorteile sind – was mir auf die Schnelle einfällt:
Wiederverwendung von Funktionalität ist sehr einfach Änderungen, welche mehrere Komponenten betreffen, sind einfacher zu koordinieren Commincation Style ist sehr einfach und die Performance ist leichter vorhersehbar Das Deployment bzw. dessen Planung ist sehr einfach Das schöne bei diesem Stil ist: Architekturelle Unit-Tests sind möglich. Siehe dazu folgende Blog-Einträge:
https://travisgosselin.com/architectural-unit-testing/ https://www.baeldung.com/java-archunit-intro Vor einiger Zeit hab ich mir schon Roslyn angeschaut. Für mich hat sich die Frage gestellt: Was hat sich geändert? Wie leicht kann man das mit der Roslyn API realisieren? Das Demo Projekt findet man unter: https://github.com/mvodep/Playground/tree/main/Roslyn/Architecture
Die freie Wahl, Home-Office zu machen, finde ich genial. An manchen Tagen will man sich die Zeit von 1.5h, die man in Öffis verbringt, einfach sparen. Zeit, sich im Home-Office einzurichten. Da man als ITler rund 9h in einen Monitor schaut war mir eines wichtig: es muss etwas Hochwertiges sein. Ich habe bereits 2 DELL U2711D (11 Jahr alt – damals zu einem stolzen Stückpreis von 700 EUR) im Einsatz und bin sehr zufrieden. Das aktuelle Modell ist der DELL U2722D. Die beiden DELL U2711D werden per DisplayPort an einen Stand-PC angeschlossen. Durch die WQHD Auflösung war das vor 10 Jahren auch nicht trivial – ich habe mir damals eine Nvidia Quadro FX580 besorgen müssen. Heute sind Notebooks aber schon extrem leistungsstark. Mein Notebook: Lenovo Campus ThinkPad T14 20UES00L00:
Ein interessantes Thema – vorallem, weil die Schreibweisen von Adressen oft nicht trivial sind. Auf die Schnelle habe ich folgende zwei Strategien gefunden:
pg_trgm: Dieses Modul erlaubt es, die Ähnlichkeit von Wörtern zu messen. Durch das Anlegen eines Index, ist diese Methode extrem performant. Phonet: Erlaubt einen String auf einen „Aussprache“ String zu projizieren. Ähnlich klingende Strings mappen somit auf den gleichen String. Anschließend Abgleich mit pg_gtrm. Der Vorteil: Aussprachefehler werden „ausgebessert“ und führen zu einem besseren Match. Wann funktioniert das gut? Wenn man die Menge an Möglichkeiten voreinschränkt. Dazu bietet sich natürlich die Postleitzahl an. In Österreich 4-stellig – aber es reicht, wenn man auch nur 1-2 Stellen eingibt. Vorangestellt oder nicht – je nach dem. Ich war mit dem Ergebnis sehr zufrieden.
Health Endpoint eines Services Ich habe eine interessante Problemstellung bearbeitet: Wie könnte ein Health-Endpoint ausschauen? Nach kurzer Recherche im Internet:
the status of the connections to the infrastructure services used by the service instance the status of the host, e.g. disk space application specific logic (Quelle: https://microservices.io/patterns/observability/health-check-api.html)
Meine Meinung dazu:
Punkt 1: zu oberflächlich Punkt 2: aus meiner Sicht sind das Metriken – anderes Thema Punkt 3: ok (obwohl nicht genau gesagt wurde, was hiermit gemeint ist) .NET Core 2.2 hat es native enhalten: Health checks in ASP.NET Core. So wird z.B. bei SQL Server mit SELECT 1 überprüft, ob die Verbindung und die Authentifizierung funktioniert. Wie immer würde ich das gerne mit theoretischen Sichtweisen verschmelzen.
Dapper Ansich nichts besonderes - trotzdem ein interessantes Thema. Points of Interst:
Wenn die Tabelle leer ist, muss man sich einer Hilfstabelle bedienen Gherkin Tests sollten nicht zu technisch sein - ob man jetzt tasächlich Tabellen prüfen sollte - fraglich. Anderseits ist es ein netter Integrationstest - ob BDD das beste Tool dafür ist, sei dahingestellt. Ich kenne zumindest keinen Business Analysten, der sich solche Tests selber zusammen schraubt. Allerdings kann es beim Verfassen von Akzeptanzkriterien hilfreich sein. using Dapper; using Npgsql; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; namespace PgsqlToGherkinTable { class Program { private static Dictionary<string, string> TableTypes; static void Main(string[] args) { // Check if database has the same one - we assume its allways en-US CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); if (args.Length != 5) { Console.WriteLine($"<server> <database> <schema> <userid> <password>"); Environment.Exit(1); } var server = args[0]; var database = args[1]; var tableSchema = args[2]; var user = args[3]; var password = args[4]; using (var connection = new NpgsqlConnection($"Server={server};Database={database};User Id={user};Password={password};")) { Console.WriteLine($"Connecting to {server} ..."); connection.Open(); using (var streamWriter = new StreamWriter("result.txt")) { Console.WriteLine($"Write result to {((FileStream)streamWriter.BaseStream).Name} ..."); InitializeTableTypes(connection, tableSchema); WriteTableContentsToStream(tableSchema, connection, streamWriter); } } } private static void InitializeTableTypes(NpgsqlConnection connection, string schema) { TableTypes = connection.Query<KeyValuePair<string, string>>("SELECT CONCAT(table_schema, '.', table_name, '.', column_name) AS Key, data_type As Value FROM information_schema.columns WHERE table_schema = @schema", new { schema = schema }).ToDictionary(p => p.Key, p => p.Value); } private static void WriteTableContentsToStream(string tableSchema, NpgsqlConnection connection, StreamWriter writer) { string query = "SELECT table_name FROM information_schema.tables WHERE table_schema = @schema AND table_name <> 'pg_stat_statements' ORDER BY table_name"; foreach (var tableName in connection.Query<string>(query, new { schema = tableSchema })) { writer.WriteLine($"And table \"{tableSchema}\".\"{tableName}\" contains rows:"); var result = connection.Query($"SELECT * FROM {tableSchema}.\"{tableName}\""); var rowsAsList = result.Cast<IDictionary<string, object>>().Select(r => ByPassTypeHandling(r, tableSchema, tableName)); if (rowsAsList.Any()) { var length = new Dictionary<string, int>(); var headerBuilder = new StringBuilder("|"); foreach (var column in rowsAsList.First().Keys) { var maxContentLength = rowsAsList.SelectMany(r => r.Where(c => c.Key == column)).Max(r => r.Value.ToString().Length); var maxLength = Math.Max(maxContentLength, column.Length); length[column] = maxLength; headerBuilder.AppendFormat($" {{0,-{maxLength}}} |", column); } writer.WriteLine(headerBuilder.ToString()); foreach (var rows in rowsAsList) { var rowBuilder = new StringBuilder("|"); foreach (var value in rows) { rowBuilder.AppendFormat($" {{0,-{length[value.Key]}}} |", value.Value); } writer.WriteLine(rowBuilder.ToString()); } } else { var columns = connection.Query<string>("SELECT column_name FROM information_schema.columns WHERE table_schema = @schema AND table_name = @table ORDER BY ordinal_position", new { schema = tableSchema, table = tableName }); var headerBuilder = new StringBuilder("|"); foreach (var column in columns) { headerBuilder.Append($" {column} |"); } writer.WriteLine(headerBuilder.ToString()); } writer.WriteLine(); } } private static IDictionary<string, string> ByPassTypeHandling(IDictionary<string, object> rows, string tableSchema, string tableName) { var result = new Dictionary<string, string>(); foreach (var column in rows) { var type = TableTypes[$"{tableSchema}.{tableName}.{column.Key}"]; if (type == "timestamp without time zone" && column.Value != null) { // ISO 8601 result[column.Key] = DateTime.Parse(column.Value.ToString()).ToString("yyyy-MM-ddTHH:mm:ssZ"); } else if (type == "timestamp with time zone" && column.Value != null) { // ISO 8601 result[column.Key] = DateTime.Parse(column.Value.ToString()).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"); } else { result[column.Key] = column.Value == null ? string.Empty : column.Value.ToString(); } } return result; } } }
Wie kann man Daten am besten historisieren? Viele Wege führen nach Rom – ich möchte an dieser Stelle einen beschreiben.
Möglichkeit 1: Man nutzt Frameworks in der Zielsprache. Zu erwähnen wäre Hibernate Envers oder Entity Framework Auto-History bzw. Audit.NET.
Möglichkeit 2: Man lässt das die Persistenz erledigen. Datenbank wie SQL Server unterstützen Temporale Tabellen. PostgreSQL hat Plugins – aber man ist auch gut beraten, selber Hand anzulegen – warum, zeige ich gleich. Temporal Tables sind im SQL:2011 Standard zu finden.