Playing around mit Java und Hibernate
PostgreSQL unter Windows 10
Windows 10 mit Docker Desktop und WSL2 funktioniert einfach super. Man hat die Einfachheit von Windows gepaart mit der Power von Linux. Auf dem Entwicklerrechner habe ich daher:
version: "3.1"
services:
db:
image: postgres
command: ["postgres", "-c", "logging_collector=on", "-c", "log_directory=/var/log/postgresql", "-c", "log_filename=postgresql.log", "-c", "log_statement=all"]
restart: always
environment:
POSTGRES_PASSWORD: postgres
volumes:
- postgres-volume:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- 5432:5432
volumes:
postgres-volume:
Gerichteter Graph
Aufgabenstellung war die Speicherung eines gerichteten Graphen inklusive Informationen pro Kante. In SQL schaut es wie folgt aus:
CREATE SCHEMA IF NOT EXISTS playground;
DROP TABLE IF EXISTS playground.edges;
DROP TABLE IF EXISTS playground.nodes;
CREATE TABLE playground.nodes
(
id SERIAL PRIMARY KEY,
node_name VARCHAR(10) NOT NULL
);
CREATE TABLE playground.edges
(
id SERIAL PRIMARY KEY,
from_node_id INTEGER NOT NULL REFERENCES playground.nodes(id),
to_node_id INTEGER NOT NULL REFERENCES playground.nodes(id),
type VARCHAR(20) NOT NULL
);
In Hibernate war es aber eine Tüftelei, bis alles funktionierte. Ziel war es, einen Graph in einem „Aufwischer“ zu speichern.
@Entity
@Table(name = "nodes", schema = "playground", catalog = "postgres")
public class NodesEntity {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Basic
@Column(name = "node_name", nullable = false, length = 10)
private String nodeName;
@OneToMany(mappedBy = "fromNode", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
public Set<EdgesEntity> edges = new LinkedHashSet<>();
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getNodeName() { return nodeName; }
public void setNodeName(String nodeName) { this.nodeName = nodeName; }
}
@Entity
@Table(name = "edges", schema = "playground", catalog = "postgres")
public class EdgesEntity {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@ManyToOne
@JoinColumn(name = "from_node_id", nullable = false)
private NodesEntity fromNode;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "to_node_id", nullable = false)
private NodesEntity toNode;
@Basic
@Column(name = "type", nullable = false, length = 20)
private String type;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public NodesEntity getFromNode() { return fromNode; }
public void setFromNode(NodesEntity fromNode) { this.fromNode = fromNode; }
public NodesEntity getToNode() { return toNode; }
public void setToNode(NodesEntity toNode) { this.toNode = toNode; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
}
public static void main(final String[] args) throws Exception {
final Session session = getSession();
try {
NodesEntity nodeA = new NodesEntity();
nodeA.setNodeName("A");
NodesEntity nodeB = new NodesEntity();
nodeB.setNodeName("B");
EdgesEntity entityAToB = new EdgesEntity();
entityAToB.setFromNode(nodeA);
entityAToB.setToNode(nodeB);
entityAToB.setType("parent");
nodeA.edges.add(entityAToB);
EdgesEntity entityBToA = new EdgesEntity();
entityBToA.setFromNode(nodeB);
entityBToA.setToNode(nodeA);
entityBToA.setType("child");
nodeB.edges.add(entityBToA);
session.save(nodeA);
} finally {
session.close();
}
}
Effizientes Abfragen des Graphen
In der Datenbank wurden viele Graphen gespeichert. Würde man Hibernate die Arbeit machen lassen, würde Hibernate pro Knoten einen Round-Trip machen. Daher wurde zuerst per Common-Table-Expression (CTE) festgestellt, welche Knoten in Frage kommen und anschließend geladen.
CREATE INDEX edges_from_node_id_to_node_id_idx ON playground.edges(from_node_id,to_node_id);
INSERT INTO playground.nodes (node_name) VALUES(chr(generate_series(65,75))); -- A..K
INSERT INTO playground.edges (from_node_id, to_node_id, type)
VALUES
(1,2,'parent'),(2,3,'parent'),(3,1,'parent'),(3,10,'parent'),(10,3,'child'),
(7,8,'parent'),(8,7,'child');
CREATE OR REPLACE FUNCTION get_graph(node_id INTEGER)
RETURNS TABLE (node_id INTEGER)
LANGUAGE SQL
AS $$
WITH RECURSIVE traverse(id, path, cycle) AS (
SELECT
playground.edges.from_node_id,
ARRAY[playground.edges.from_node_id],
false
FROM playground.edges
WHERE playground.edges.from_node_id = node_id
UNION ALL
SELECT
playground.edges.to_node_id,
traverse.path || playground.edges.to_node_id,
playground.edges.to_node_id = ANY(traverse.path)
FROM traverse
INNER JOIN playground.edges
ON playground.edges.from_node_id = traverse.id
WHERE NOT cycle
)
SELECT id FROM traverse GROUP BY id ORDER BY MIN(CARDINALITY(path))
$$;
Der Aufruf in Java geht relative einfach:
List<Integer> nodeIds = session.createSQLQuery("SELECT get_graph(3)").list();
MultiIdentifierLoadAccess<NodesEntity> multiLoadAccess = session.byMultipleIds(NodesEntity.class);
List<NodesEntity> nodes = multiLoadAccess.multiLoad(nodeIds);
for (NodesEntity node:nodes) {
System.out.println(node.getNodeName());
for(EdgesEntity edge : node.edges) {
System.out.println(String.format(" %s (%d) -> %s (%d) %s",
edge.getFromNode().getNodeName(),
edge.getFromNode().getId(),
edge.getToNode().getNodeName(),
edge.getToNode().getId(),
edge.getType()));
}
}
/*
Hibernate:
select
nodesentit0_.id as id1_1_0_,
nodesentit0_.node_name as node_nam2_1_0_,
edges1_.from_node_id as from_nod3_0_1_,
edges1_.id as id1_0_1_,
edges1_.id as id1_0_2_,
edges1_.from_node_id as from_nod3_0_2_,
edges1_.to_node_id as to_node_4_0_2_,
edges1_.type as type2_0_2_,
nodesentit2_.id as id1_1_3_,
nodesentit2_.node_name as node_nam2_1_3_
from
playground.nodes nodesentit0_
left outer join
playground.edges edges1_
on nodesentit0_.id=edges1_.from_node_id
left outer join
playground.nodes nodesentit2_
on edges1_.to_node_id=nodesentit2_.id
where
nodesentit0_.id in (
?, ?, ?, ?
)
C
C (3) -> J (10) parent
C (3) -> A (1) parent
J
J (10) -> C (3) child
A
A (1) -> B (2) parent
B
B (2) -> C (3) parent
*/
Functions in Spring Data aufrufen
CREATE OR REPLACE FUNCTION some_function(some_ids UUID[])
RETURNS TABLE (id INTEGER)
LANGUAGE SQL
AS $$
-- ERROR: function some_function(record[]) does not exist
CREATE OR REPLACE FUNCTION some_function(VARIADIC some_ids UUID[])
RETURNS TABLE (id INTEGER)
LANGUAGE SQL
AS $$
// Doesnt work:
@Query(nativeQuery = true, value = "SELECT some_function(ARRAY[:uuids])")
LinkedHashSet<Integer> getSomeFunction(Collection<UUID> uuids);
// Works with VARIADIC
@Query(nativeQuery = true, value = "SELECT some_function(:uuids)")
LinkedHashSet<Integer> getSomeFunction(Collection<UUID> uuids);
Leider konnte ich - und laut Stackoverflow auch andere User - kein Function mit Array Parameter aufrufen. Spring Data fügt immer eine Klamma ums Array.
Duplikate ohne Primärschlüssel entfernen
PostgreSQL hat selber keine Temporal Tables. Diese historisieren Daten (siehe https://wiki.postgresql.org/wiki/SQL2011Temporal). Nun hat man eine Tabelle ohne Primärschlüssel und idente Rows - was kann man tun? Window-Functions fallen aus. Daher:
BEGIN;
LOCK TABLE new_table IN ACCESS EXCLUSIVE MODE;
CREATE TABLE new_table_clean AS
(SELECT DISTINCT ON(column1, column2) * FROM new_table);
TRUNCATE TABLE new_table;
INSERT INTO new_table SELECT * FROM new_table_clean;
END;
Weitere wichtige PostrgreSQL Findings
PostgreSQL hat keinen Ressource Manager wie e.g. in SQL Server den Resource Governor. Allerdings ist das relativ egal, weil man mehrere Instanzen ohne weitere Kosten installieren kann.
Das Suchen nach fehlenden Indizes ist nicht einfach. Man kann durch Statistiken Table Scans sehen. Auch gibt es Tools, welche Log-Files analysieren und Hints geben (pgbadger). Aber so einfach wie in SQL Server geht es leider eben nicht.
-- Source: https://www.cybertec-postgresql.com/
SELECT
relname, seq_scan, seq_tup_read,
idx_scan, idx_tup_fetch,
seq_tup_read / seq_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0 AND schemaname='myschema'
ORDER BY seq_tup_read DESC;
-- Richtige Einfügereihenfolge: https://www.cybertec-postgresql.com/en/postgresql-foreign-keys-and-insertion-order-in-sql/
WITH RECURSIVE fkeys AS (
/* source and target tables for all foreign keys */
SELECT conrelid AS source,
confrelid AS target
FROM pg_constraint
WHERE contype = 'f'
),
tables AS (
( /* all tables ... */
SELECT oid AS table_name,
1 AS level,
ARRAY[oid] AS trail,
FALSE AS circular
FROM pg_class
WHERE relkind = 'r'
AND NOT relnamespace::regnamespace::text LIKE ANY
(ARRAY['pg_catalog', 'information_schema', 'pg_temp_%'])
EXCEPT
/* ... except the ones that have a foreign key */
SELECT source,
1,
ARRAY[ source ],
FALSE
FROM fkeys
)
UNION ALL
/* all tables with a foreign key pointing a table in the working set */
SELECT fkeys.source,
tables.level + 1,
tables.trail || fkeys.source,
tables.trail @> ARRAY[fkeys.source]
FROM fkeys
JOIN tables ON tables.table_name = fkeys.target
/*
* Stop when a table appears in the trail the third time.
* This way, we get the table once with "circular = TRUE".
*/
WHERE cardinality(array_positions(tables.trail, fkeys.source)) < 2
),
ordered_tables AS (
/* get the highest level per table */
SELECT DISTINCT ON (table_name)
table_name,
level,
circular
FROM tables
ORDER BY table_name, level DESC
)
SELECT table_name::regclass,
level
FROM ordered_tables
WHERE NOT circular
ORDER BY level, table_name;
Ebenfalls interessant fand ich das Script für die richtige Insert-Reihenfolge.
RDBMS ist tot - lange lebe RDBMS
Interessant ist auch die Entscheidung: NoSQL oder RDBMS. NoSQL ist in aller Munde – daher wird man recht schnell dazu verleitet, sich gegen ein RDBMS zu entscheiden. Doch meine Meinung zu dem Thema ist klar: diese beiden Denkweisen können koexistieren. Beide haben Vor- als auch Nachteile. Oft ist es schwer, das Aggregate – welches man bei NoSQL im Regelfall für die meisten Typen – vor allem Key-Value oder Document – braucht – zu finden. Auch sind Queries, welche die Aggregategrenze überschreiten oft auch nicht trivial. Ja – es gibt dort auch Indizes. Aber referentielle Integrität über Aggregategrenzen hinaus lassen sich in RDBMS leichter abbilden. Hat man aber viele geschlossene Aggregate (die Bestellung, der Kunde, usw.) und beschränken sich die Abfragen auf das Aggregat, ist NoSQL sicher eine super Wahl.