Caches entwerfen mit Java

Veröffentlicht am 16. December 2010

Ich habe bei meinen aktuellen Java Web-Applikationen oft mit der Situation zu tun, dass verschiedene Handler der Applikation auf einen gemeinsamen Pool von Daten lesend (und manchmal auch schreibend) zugreifen müssen.

Das einfache Vorgehen, dass ich auch zu meinen PHP-Zeiten gewohnt war, ist es, eine Datenverbindung (soll heißen: eine offene MySQL-Verbindung) zu errichten oder aufrechtzuerhalten, und diese an alle Handler weiterzugeben, so dass diese die benötigten Daten unmittelbar aus der Datenbank lesen und Änderungen direkt in die Datenbank zurückschreiben können.

Allgemeine Überlegungen

Dieses “Read-On-Demand”-Verfahren hat aber ein paar häßliche Nachteile, weshalb ich es heute nicht mehr gerne einsetze:

  • Die Daten aus der Datenbank müssen evtl. bei jedem Request neu eingelesen und verarbeitet werden. Das kann bei großen (oder komplizierten) Daten einiges an Verarbeitungszeit kosten
    (Beispielsweise dauert das Neueinlesen der Weblog-Daten aus diesem Blog schon ein paar Sekunden, da erst die enthaltenen Bilder nachgeprüft und verkleinert werden müssen, LaTeX-Formeln verarbeitet werden usw.)
  • Bei einer Änderung der Datenschnittstelle (z.B: Änderung des Tabellenformats in der Datenbank) müssen alle Handler angepasst werden. Dabei kann man leicht eine Abfrage vergessen, die dann zu einem Bug führt.
  • In jeden Handler muss das Lesen und Interpretieren der Daten (inklusive SQL-Abfragen) hineinprogrammiert werden, was den Aufwand zum Entwickeln eines Handlers stark erhöht. Oft ergibt sich dann doppelter Code, weil verschiedene Handler die selben Daten einlesen
  • Bei mehreren gleichzeitigen Abfragen (wobei mindestens eine davon die Daten verändert) können Datenhazards auftreten, also eine Verfälschung der Daten durch eine falsche Verarbeitungsreihenfolge. Dadurch kann auch permanenter Datenverlust auftreten

Um diese Probleme zu umgehen bin ich seit kurzem auf Caches umgestiegen. Unter einem Cache verstehe ich eine gemeinsame Instanz, die den Zugriff auf eine bestimmte Datenmenge der Applikation zur Verfügung stellt:

  • Ein Cache regelt alle wichtigen Datenbankabfragen (Lesen, Einfügen, Löschen, Verändern) und stellt dies als Funktionalität zur Verfügung, so dass die einzelnen Handler keinen direkten Datenbankzugriff merh benötigen.
  • Da in Java eine Instanz eines Servlets permanent im Speicher verbleibt, kann der Cache als permanente Instanz erzeugt werden. Er muss nicht bei jedem Aufruf neu erzeugt werden, sondern bleibt auch dauerhaft (inklusive der gelesenen Daten) im Speicher
  • Lesende Zugriffe auf den Cache verändern die Daten nicht, benötigen also keinen Zugriff auf die Datenbank
  • Der Cache muss die Daten nicht im gleichen Format bereithalten wie die Datenbank, sondern kann die Daten bereits verknüpfen, selektieren, sortieren, aggregieren und indexieren.
    Handler können so schnell und einfach auf einzelnde Datenblöcke zugreifen, die bestimmte Kriterien erfüllen (z.B. Auswahl nach einer ID)
  • Schreibende Zugriffe können in den Cache durchgeführt werden, so dass nur eine Aktualisierung der Daten im Cache und kein Neueinlesen erforderlich wird, während der Cache die Änderungen in die Datenbank zurückschreibt
  • Der Cache sollte manuell neu eingelesen werden können, falls von außen Änderungen an der Datenbank vorgenommen werden
  • Der Cache sollte durch geeignete Locking Methoden sicherstellen, dass durch mehrere Threads keine Datenhazards auftreten können und Daten immer in der korrekten Reihenfolge ausgewertet werden

Grundlegende Struktur

Nach diesen allgemeinen Überlegungen können wir uns Gedanken machen, wie ein solcher Cache beschaffen sein sollte.

Datenbank-Verbindung

Ein Cache, der Daten aus einer Datenbank verwaltet, sollte mit einer Datenbankverbindung konstruiert werden und diese (sofern die offen bleibt) als Membervariable speichern.

Ich verwende in diesem Tutorial einen Wrapper Database für eine MySQL-Datenbank, die eine Methode public PreparedStatement getAndPrepare(String key, String sql) throws SQLException besitzt, welche ein PreparedStatement mit dem SQL-Befehl sql erstellt und unter dem Schlüssel key abspeichert. Außerdem sorgt dieser Wrapper noch dafür, dass die Datenverbindung offenbleibt und jedes PreparedStatement nur das erste mal erstellt werden muss.
Die genaue Implementierung dieser Hilfsklasse ist hier nicht von Interesse, eine Implementierung mit einer normalen JDBC-Verbindung sollte nicht großartig anders aussehen.

Da das Neuladen des Caches von außen aufrufbar sein sollte und sich meist nicht sonderlich von erstmaligen Laden der Daten unterscheidet, lege ich normalerweise alle Leseoperationen in eine Methode public void reload() throws SQLException, die auch im Konstruktor aufgerufen wird:

import java.sql.SQLException;

public class MyCache {

	private final Database db;
	
	public MyCache(Database db) throws SQLException {
		this.db = db;
		reload();
	}
	
	public void reload() throws SQLException {
		// Hier: Laden der Daten
	}
	
}

Einlesen der Daten

Ich gehe in diesem Beispiel von einem sehr einfachen Datenbankformat aus:

Table data:

id|value
--+-----
 1|Foo
 2|Bar
 3|Baz

Diese Daten sollen in dem Beispiel einerseits als Hashtable gespeichert werden, und außerdem noch die Values in einem sortierten Set.

Das Einlesen geschieht über ein einfaches SQL-Statement:

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Hashtable;
import java.util.TreeSet;

public class MyCache {

	// Keys fuer Prepared Statements
	private static final String KEY_READDATA = "MyCache_Read";
	
	// SQL Queries
	private static final String QUERY_READDATA =
		"select id, value from data";
	
	private final Database db;
	
	// Zwischengespeicherte Daten
	private final Hashtable<Integer, String> data;
	private final TreeSet<String> values;
	
	/**
	 * Erstelle Cache.
	 * @param db die offene Datenbankverbindung
	 * @throws SQLException bei SQL Fehler
	 */
	public MyCache(Database db) throws SQLException {
		this.db = db;
		this.data = new Hashtable<Integer, String>();
		this.values = new TreeSet<String>();
		reload();
	}
	
	/**
	 * (Neues) Einlesen.
	 * @throws SQLException bei SQL Fehler
	 */
	public void reload() throws SQLException {
		PreparedStatement stmt = this.db.getAndPrepare(KEY_READDATA, QUERY_READDATA);
		stmt.execute();
		ResultSet res = stmt.getResultSet();
		
		// Leeren der aktuellen Daten
		this.data.clear();
		this.values.clear();
		
		// Neues Einlesen
		while (res.next()) {
			int id = res.getInt("id");
			String value = res.getString("value");
			this.values.add(value);
			this.data.put(id, value);
		}
		
	}
	
}

Verfügbarmachen der Daten

Um auf die gelesenen Daten von der Anwendung aus zugreifen zu können, sollte noch geeignete Methoden in den Cache implementiert werden.

Manchmal kann es sinnvoll sein, die internen Hashtables mit den Daten nach außen lesbar zu machen, aber meistens ist das unnötig.
Hier gehe ich davon aus, dass nur folgender Zugriff benötigt wird:

  • Lesen des Values zu einer bestimmten ID
  • Menge der IDs abrufen
  • Menge der Values abrufen

Es sollte darauf geachtet werden, dass die zurückgegebenen Werte entweder von sich aus immutable, also unveränderbar sind, oder unveränderbar gemacht werden. Außerdem sollten Strukturen von den internen Strukturen des Cache losgelöst sein, damit ein zwischenzeiliche Veränderung des Caches nicht die zurückgegebenen Daten verändert (sonst drohen wiederum Daten-Hazards).
Hier sollte die Regel gelten: greift die Applikation einmal Daten von Cache ab, so werden diese durch den Cache nicht mehr beeinflusst. Die abgerufenen Daten bilden also einen Schnappschuss der Daten zum Abrufzeitpunkt.
(Falls mehrere Datenblöcke nacheinander abgerufen werden müssen, könnten aus Konsistenzgründen noch geeignete Locking-Mechanismen an dieser Stelle implementiert werden)

public String getValue(int id) {
	return this.data.get(id);
}

public Set<Integer> getIDs() {
	Set<Integer> result = new TreeSet<Integer>(this.data.keySet());
	return Collections.unmodifiableSet(result); 
}

public Set<String> getValues() {
	Set<String> result = new TreeSet<String>(this.values);
	return Collections.unmodifiableSet(result);
}

Zum jetzigen Zeitpunkt kann der Cache zum reinen Lesen schon voll benutzt werden.

Datenmanipulationen über den Cache

Der nächte Schritt wird es sein, den Cache nicht nur zum Lesen, sondern auch zum Manipulieren der Datenbank zu verwenden.

Dazu muss man sich zunächst überlegen, welche Datenmanipulationen man für seine Daten benötigt.
Im Normalfall werden die Funktionen “Hinzufügen”, “Löschen” und “Verändern” von Datenzeilen benötigt.

Bei der Implementierung der Manipulationenfunktionen kann man sich auch überlegen, wie man die Veränderungen in den lokalen Strukturen abbilden kann, ohne den ganzen Cache neu einlesen zu müssen. Je mehr die Daten lokal aufgearbeitet werden, desto komplizierter kann ein lokales Update des Caches werden.
Das Neueinlesen funktioniert immer und ist sicher, aber das Neueinlesen kann bei größeren Datenmengen deutlich langsamer sein.

In unserem Beispiel ist das lokale Ändern der Cachedaten sehr leicht, da die intere Datenstruktur sehr einfach aufgebaut ist.

Auf die Korrektheit der übergebenen Daten (gültige Werte für alle Felder, keine doppelten IDs auf Primary Keys, etc.) sollte von außerhalb des Cache geprüft werden und im besten Fall die Manipulationefunktionen nur aufgerufen werden, wenn die Daten alle Plausibilitätstest bestanden haben.
Die Implementierung der Manipulationsstrukturen sollte hier nicht so schwer sein, ich werde also direkt das Ergebnis präsentieren:

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Set;
import java.util.TreeSet;

public class MyCache {

	// Keys fuer Prepared Statements
	private static final String KEY_READDATA = "MyCache_Read";
	private static final String KEY_INSERTDATA = "MyCache_Insert";
	private static final String KEY_UPDATEDATA = "MyCache_Update";
	private static final String KEY_DELETEDATA = "MyCache_Delete";
	
	// SQL Queries
	private static final String QUERY_READDATA =
		"select id, value from data";
	private static final String QUERY_INSERTDATA =
		"insert into data (id, value) values (?, ?)";
	private static final String QUERY_UPDATEDATA =
		"update data set value = ? where id = ? limit 1";
	private static final String QUERY_DELETEDATA =
		"delete from data where id = ? limit 1";
	
	private final Database db;
	
	// Zwischengespeicherte Daten
	private final Hashtable<Integer, String> data;
	private final TreeSet<String> values;
	
	/**
	 * Erstelle Cache.
	 * @param db die offene Datenbankverbindung
	 * @throws SQLException bei SQL Fehler
	 */
	public MyCache(Database db) throws SQLException {
		this.db = db;
		this.data = new Hashtable<Integer, String>();
		this.values = new TreeSet<String>();
		reload();
	}
	
	/**
	 * (Neues) Einlesen.
	 * @throws SQLException bei SQL Fehler
	 */
	public void reload() throws SQLException {
		PreparedStatement stmt = this.db.getAndPrepare(KEY_READDATA, QUERY_READDATA);
		stmt.execute();
		ResultSet res = stmt.getResultSet();
		
		// Leeren der aktuellen Daten
		this.data.clear();
		this.values.clear();
		
		// Neues Einlesen
		while (res.next()) {
			int id = res.getInt("id");
			String value = res.getString("value");
			this.values.add(value);
			this.data.put(id, value);
		}
		
	}
	
	/**
	 * Wert zu einer ID zurückliefern.
	 */
	public String getValue(int id) {
		return this.data.get(id);
	}
	
	/**
	 * Liste alle verfügbarer IDs zurückliefern.
	 */
	public Set<Integer> getIDs() {
		Set<Integer> result = new TreeSet<Integer>(this.data.keySet());
		return Collections.unmodifiableSet(result); 
	}
	
	/**
	 * Liste alle verfügbarer Werte zurückliefern.
	 */
	public Set<String> getValues() {
		Set<String> result = new TreeSet<String>(this.values);
		return Collections.unmodifiableSet(result);
	}
	
	/**
	 * Wertepaar einfügen.
	 */
	public void insert(int id, String value) throws SQLException {
		PreparedStatement stmt = this.db.getAndPrepare(KEY_INSERTDATA, QUERY_INSERTDATA);
		stmt.setInt(1, id); // Parameter 1
		stmt.setString(2, value); // Parameter 2
		stmt.execute();
		
		// lokale Änderung
		this.data.put(id, value);
		this.values.add(value);
	}
	
	/**
	 * Wert neu zuordnen.
	 */
	public void update(int id, String value) throws SQLException {
		PreparedStatement stmt = this.db.getAndPrepare(KEY_UPDATEDATA, QUERY_UPDATEDATA);
		stmt.setString(1, value); // Parameter 1
		stmt.setInt(2, id); // Parameter 2
		stmt.execute();
		
		// lokale Änderung
		this.data.put(id, value);
		this.values.clear(); // Values neu aufarbeiten
		for (String val : this.data.values()) {
			this.values.add(val);
		}
	}
	
	/**
	 * Wertepaar löschen.
	 */
	public void delete(int id) throws SQLException {
		PreparedStatement stmt = this.db.getAndPrepare(KEY_DELETEDATA, QUERY_DELETEDATA);
		stmt.setInt(1, id); // Parameter 1
		stmt.execute();
		
		// lokale Änderung
		this.data.remove(id);
		this.values.clear(); // Values neu aufarbeiten
		for (String val : this.data.values()) {
			this.values.add(val);
		}
	}
	
}

Threadsicherheit und Locking

Wenn man eine Applikation mit mehreren Threads entwickelt (z.B. mit Swing-Fenstern) oder eine Cache-Instanz von mehreren gleichzeitig laufenden Applikationen benutzt wird, muss man sich noch über Threadsicherheit gedanken machen.

Ein Problem tritt immer dann auf, wenn ein Thread gerade eine Datenmanipulation (oder ein Reload) durchführt und ein anderer Thread gleichzeitig auch auf den Cache zugreift.
Im besten Fall passiert gar nichts, im schlechten Fall liefert der Cache einem Prozess eine erst halb gefüllte Datenstruktur zurück, im schlechtesten Fall gibt es Datenkorruption oder das Programm stürzt gar ab.

Um das zu verhindern, benutzt man Locking-Verfahren, die den Zugriff eines Threads solange Verzögern, bis der andere Thread mit den Änderungen fertig ist.

In Java gibt es mehrere Möglichkeiten, Locks zu verwenden; ich verwende hier die Lock-Klasse [var]ReentrantReadWriteLock[/var], weil diese eine Unterscheidung zwischen Lese- und Schreibzugriffen ermöglicht und einem Thread einen Lese-Zugriff ermöglicht, wenn dieser bereits einen Schreibzugriff hat.

// Locks
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();

Als nächstes muss nun in allen Methoden, die Daten an die Applikation nach draußen geben, ein Readlock installiert werden; und an alle Methoden, die die Datenstrukturen auch verändern (also die Manipulationsfunktionen), ein Writelock. Dadurch wird sicher gestellt, dass es zu den oben beschriebenen Datenhazards nicht kommen kann.
Es sollte aber darauf geachtet werden, dass das Freigeben eines Locks in einem finally-Block geschieht; sonst kann es ggf. passieren, dass der Cache durch eine Exception ode ein verfrühtes return-Statement dauerhaft gesperrt bleibt und das Programm hängenbleibt.

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyCache {

	// Locks
	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	private final Lock read = lock.readLock();
	private final Lock write = lock.writeLock();
	
	// Keys fuer Prepared Statements
	private static final String KEY_READDATA = "MyCache_Read";
	private static final String KEY_INSERTDATA = "MyCache_Insert";
	private static final String KEY_UPDATEDATA = "MyCache_Update";
	private static final String KEY_DELETEDATA = "MyCache_Delete";
	
	// SQL Queries
	private static final String QUERY_READDATA =
		"select id, value from data";
	private static final String QUERY_INSERTDATA =
		"insert into data (id, value) values (?, ?)";
	private static final String QUERY_UPDATEDATA =
		"update data set value = ? where id = ? limit 1";
	private static final String QUERY_DELETEDATA =
		"delete from data where id = ? limit 1";
	
	private final Database db;
	
	// Zwischengespeicherte Daten
	private final Hashtable<Integer, String> data;
	private final TreeSet<String> values;
	
	/**
	 * Erstelle Cache.
	 * @param db die offene Datenbankverbindung
	 * @throws SQLException bei SQL Fehler
	 */
	public MyCache(Database db) throws SQLException {
		this.db = db;
		this.data = new Hashtable<Integer, String>();
		this.values = new TreeSet<String>();
		reload();
	}
	
	/**
	 * (Neues) Einlesen.
	 * @throws SQLException bei SQL Fehler
	 */
	public void reload() throws SQLException {
		write.lock();
		try {
			PreparedStatement stmt = this.db.getAndPrepare(KEY_READDATA, QUERY_READDATA);
			stmt.execute();
			ResultSet res = stmt.getResultSet();
			
			// Leeren der aktuellen Daten
			this.data.clear();
			this.values.clear();
			
			// Neues Einlesen
			while (res.next()) {
				int id = res.getInt("id");
				String value = res.getString("value");
				this.values.add(value);
				this.data.put(id, value);
			}
		} finally {
			write.unlock();
		}
	}
	
	/**
	 * Wert zu einer ID zurückliefern.
	 */
	public String getValue(int id) {
		read.lock();
		try {
			return this.data.get(id);
		} finally {
			read.unlock();
		}
	}
	
	/**
	 * Liste alle verfügbarer IDs zurückliefern.
	 */
	public Set<Integer> getIDs() {
		read.lock();
		try {
			Set<Integer> result = new TreeSet<Integer>(this.data.keySet());
			return Collections.unmodifiableSet(result); 
		} finally {
			read.unlock();
		}
	}
	
	/**
	 * Liste alle verfügbarer Werte zurückliefern.
	 */
	public Set<String> getValues() {
		read.lock();
		try {
			Set<String> result = new TreeSet<String>(this.values);
			return Collections.unmodifiableSet(result);
		} finally {
			read.unlock();
		}
	}
	
	/**
	 * Wertepaar einfügen.
	 */
	public void insert(int id, String value) throws SQLException {
		write.lock();
		try {
			PreparedStatement stmt = this.db.getAndPrepare(KEY_INSERTDATA, QUERY_INSERTDATA);
			stmt.setInt(1, id); // Parameter 1
			stmt.setString(2, value); // Parameter 2
			stmt.execute();
			
			// lokale Änderung
			this.data.put(id, value);
			this.values.add(value);
		} finally {
			write.unlock();
		}
	}
	
	/**
	 * Wert neu zuordnen.
	 */
	public void update(int id, String value) throws SQLException {
		write.lock();
		try {
			PreparedStatement stmt = this.db.getAndPrepare(KEY_UPDATEDATA, QUERY_UPDATEDATA);
			stmt.setString(1, value); // Parameter 1
			stmt.setInt(2, id); // Parameter 2
			stmt.execute();
			
			// lokale Änderung
			this.data.put(id, value);
			this.values.clear(); // Values neu aufarbeiten
			for (String val : this.data.values()) {
				this.values.add(val);
			}
		} finally {
			write.unlock();
		}
	}
	
	/**
	 * Wertepaar löschen.
	 */
	public void delete(int id) throws SQLException {
		write.lock();
		try {
			PreparedStatement stmt = this.db.getAndPrepare(KEY_DELETEDATA, QUERY_DELETEDATA);
			stmt.setInt(1, id); // Parameter 1
			stmt.execute();
			
			// lokale Änderung
			this.data.remove(id);
			this.values.clear(); // Values neu aufarbeiten
			for (String val : this.data.values()) {
				this.values.add(val);
			}
		} finally {
			write.unlock();
		}
	}
	
}

Abstraktion

Um die Entwicklung von mehreren Cache zu vereinfachen, kann es ab einem gewissen Level sinnvoll sein, die generischen Elemente (Locking, Schnittstelle zum Reload, etc.) ein eine gemeinsame abstrakte Oberklasse auszulagern.

Die einzelnen Caches müssen sich dann nur um ihre Daten, und nicht mehr um elementare, gleich bleibende Elemente kümmern.

Abstrakte Oberklasse
import java.sql.SQLException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public abstract class AbstractCache {

	// Locks
	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	private final Lock read = lock.readLock();
	private final Lock write = lock.writeLock();
	
	// Datenbankverbindung
	private final Database db;
	
	/**
	 * Erstelle Cache.
	 * @param db die offene Datenbankverbindung
	 * @throws SQLException bei SQL Fehler
	 */
	public AbstractCache(Database db) {
		this.db = db;
		
	}
	
	/**
	 * (Neues) Einlesen.
	 * @throws SQLException bei SQL Fehler
	 */
	public abstract void reload() throws SQLException;
	
	/**
	 * Datenbank zurückliefern.
	 */
	protected Database db() {
		return this.db;
	}
	
	/**
	 * Zum Schreiben sperren.
	 */
	protected void writeLock() {
		write.lock();
	}
	
	/**
	 * Zum Schreiben freigeben.
	 */
	protected void writeUnlock() {
		write.unlock();
	}
	
	/**
	 * Zum Lesen sperren.
	 */
	protected void readLock() {
		read.lock();
	}
	
	/**
	 * Zum Lesen freigeben.
	 */
	protected void readUnlock() {
		read.unlock();
	}
	
}
Konkrete Implementierung
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Set;
import java.util.TreeSet;

public class MyCache extends AbstractCache {

	// Keys fuer Prepared Statements
	private static final String KEY_READDATA = "MyCache_Read";
	private static final String KEY_INSERTDATA = "MyCache_Insert";
	private static final String KEY_UPDATEDATA = "MyCache_Update";
	private static final String KEY_DELETEDATA = "MyCache_Delete";
	
	// SQL Queries
	private static final String QUERY_READDATA =
		"select id, value from data";
	private static final String QUERY_INSERTDATA =
		"insert into data (id, value) values (?, ?)";
	private static final String QUERY_UPDATEDATA =
		"update data set value = ? where id = ? limit 1";
	private static final String QUERY_DELETEDATA =
		"delete from data where id = ? limit 1";
	
	// Zwischengespeicherte Daten
	private final Hashtable<Integer, String> data;
	private final TreeSet<String> values;
	
	public MyCache(Database db) throws SQLException {
		super(db);
		this.data = new Hashtable<Integer, String>();
		this.values = new TreeSet<String>();
		reload();
	}
	
	/**
	 * (Neues) Einlesen.
	 * @throws SQLException bei SQL Fehler
	 */
	@Override
	public void reload() throws SQLException {
		writeLock();
		try {
			PreparedStatement stmt = db().getAndPrepare(KEY_READDATA, QUERY_READDATA);
			stmt.execute();
			ResultSet res = stmt.getResultSet();
			
			// Leeren der aktuellen Daten
			this.data.clear();
			this.values.clear();
			
			// Neues Einlesen
			while (res.next()) {
				int id = res.getInt("id");
				String value = res.getString("value");
				this.values.add(value);
				this.data.put(id, value);
			}
		} finally {
			writeUnlock();
		}
	}
	
	/**
	 * Wert zu einer ID zurückliefern.
	 */
	public String getValue(int id) {
		readLock();
		try {
			return this.data.get(id);
		} finally {
			readUnlock();
		}
	}
	
	/**
	 * Liste alle verfügbarer IDs zurückliefern.
	 */
	public Set<Integer> getIDs() {
		readLock();
		try {
			Set<Integer> result = new TreeSet<Integer>(this.data.keySet());
			return Collections.unmodifiableSet(result); 
		} finally {
			readUnlock();
		}
	}
	
	/**
	 * Liste alle verfügbarer Werte zurückliefern.
	 */
	public Set<String> getValues() {
		readLock();
		try {
			Set<String> result = new TreeSet<String>(this.values);
			return Collections.unmodifiableSet(result);
		} finally {
			readUnlock();
		}
	}
	
	/**
	 * Wertepaar einfügen.
	 */
	public void insert(int id, String value) throws SQLException {
		writeLock();
		try {
			PreparedStatement stmt = db().getAndPrepare(KEY_INSERTDATA, QUERY_INSERTDATA);
			stmt.setInt(1, id); // Parameter 1
			stmt.setString(2, value); // Parameter 2
			stmt.execute();
			
			// lokale Änderung
			this.data.put(id, value);
			this.values.add(value);
		} finally {
			writeUnlock();
		}
	}
	
	/**
	 * Wert neu zuordnen.
	 */
	public void update(int id, String value) throws SQLException {
		writeLock();
		try {
			PreparedStatement stmt = db().getAndPrepare(KEY_UPDATEDATA, QUERY_UPDATEDATA);
			stmt.setString(1, value); // Parameter 1
			stmt.setInt(2, id); // Parameter 2
			stmt.execute();
			
			// lokale Änderung
			this.data.put(id, value);
			this.values.clear(); // Values neu aufarbeiten
			for (String val : this.data.values()) {
				this.values.add(val);
			}
		} finally {
			writeUnlock();
		}
	}
	
	/**
	 * Wertepaar löschen.
	 */
	public void delete(int id) throws SQLException {
		writeLock();
		try {
			PreparedStatement stmt = db().getAndPrepare(KEY_DELETEDATA, QUERY_DELETEDATA);
			stmt.setInt(1, id); // Parameter 1
			stmt.execute();
			
			// lokale Änderung
			this.data.remove(id);
			this.values.clear(); // Values neu aufarbeiten
			for (String val : this.data.values()) {
				this.values.add(val);
			}
		} finally {
			writeUnlock();
		}
	}
	
}
Beispiel für eine Main-Methode
import java.sql.SQLException;

public class Main {

	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		
		// Datenbankverbindung
		Database db = new MySQLDB("localhost", "user", "password", "mydatabase");
		MyCache cache = new MyCache(db);
		
		// Daten ausgeben
		for (int id : cache.getIDs()) {
			System.out.println("ID:" + id + " -> Value:" + cache.getValue(id));
		}
		
		// Daten einfügen
		cache.insert(5, "Foo");
		cache.insert(7, "Bar");
		
		// Daten verändern
		cache.update(7, "FooBar");
		System.out.println("ID:5 -> Value:" + cache.getValue(5)); // Foo
		System.out.println("ID:7 -> Value:" + cache.getValue(7)); // FooBar
		
		// (Veränderte) Daten ausgeben
		for (int id : cache.getIDs()) {
			System.out.println("ID:" + id + " -> Value:" + cache.getValue(id));
		}
		
		// Daten löschen
		cache.delete(7);
		
	}

}

Beispiele zum Download

Download als ZIP

Die hier aufgeführten Java-Dateien wurden als Beispiele zu diesem Artikel erstellt.
Diese Java-Klassen dürfen unter den Bedingungen der Lesser GPL weiterverwendet und verbreitet werden.

Kategorie: Java