Az előadással egybekötött gyakorlat célja, hogy bemutassuk a NoSQL
(Not Only SQL) adatbázisok telepítését, használatát és különféle alkalmazásokba való integrációs lehetőségeit. A gyakorlat elvégzésével a hallgató képes lesz egy NoSQL
alapú webes és asztali alkalmazás összeállítására, mely adatfogadásra és adat kiszolgálásra is alkalmas lehet adattárház és BI rendszerek irányába.
forrás: http://www.monitis.com/blog/2011/05/22/picking-the-right-nosql-database-tool
- Kulcs-érték alapú
- REDIS, Voldemort, Riak
- Oszlopos (vagy tabulált) adatbázis
- Cassandra, HBase
- Dokumentum alapú
- MongoDb
- Gráf alapú
- Neo4J, InfoGrid, Infinite Graph
- Objektum adatbázisok
- OrientDB
A gyakorlat elvégzéséhez nem szükséges külön Redis telepítése, azt a 91.134.196.66 címen biztosítunk. A Redis kipróbálására pedig lehetőség van az https://try.redis.io oldalon.
A REDIS egy rendkívül gyors, egyszerű kulcs-érték pár tároló, amely nagyrészt a memóriában dolgozva nagyon alacsony válaszidővel is képes rendelkezni. Ugyanakkor a perzisztencia is biztosított megkötésekkel, létezik snapshot szerű állapot mentés, illetve folyamatos írási napló alapú perzisztencia is. Ezekről bővebben itt lehet olvasni:
https://redis.io/topics/persistence
A REDIS cluster üzemmódot is támogat az új verzióban, így a skálázhatóság is biztosított.
Próbáljuk ki a következő alap REDIS parancsokat néhány példa adaton.
REDIS-ben több adatbázisunk is lehet, melyek egy számmal vannak reprezentálva, az alapértelmezett a 0-s adatbázis.
Válasszuk ki az alapértelmezett adatbázist (nem kötelező):
SELECT 0
Illesszünk be egy értéket:
SET department AUT
Kérdezzük le a beillesztett értéket:
GET department
REDIS-ben az adatok elsőként a memóriába kerülnek tárolásra, de a REDIS gondoskodik a perzisztálásról/lemezre mentésről is (alapesetben másodpercenként), így a gyors és hosszú távú adattárolás is megoldott.
Lehetőség van azonban megadott ideig tárolni csak egy adatot. A következő paranccsal létrehozunk egy értéket, és beállítjuk, hogy csak 10 másodpercig legyen érvényes. A TTL parancs a hátralevő érvényességi idő kiírására szolgál:
SET department AUT
EXPIRE department 10
TTL department
Amennyiben mégis szeretnénk perzisztálni, akkor a PERSIST parancsot kell használni:
PERSIST department
Ebben az esetben a TTL már -1 értéket ad vissza.
SCUBSCRIBE mychannel
PSUBSCRIBE my\*
PUBLUSH mychannel “test message”
LPUSH mylist a # (integer) 1
LPUSH mylist b # (integer) 2
LPUSH mylist c # (integer) 3
LRANGE mylist 0 -1 # ”a”, “b”, “c”
LLEN mylist # (integer) 3
DEL mylist # (integer) 1
Következő feladatként készítsünk egy JavaFX alapú chat alkalmazást. Az alkalmazás használatához a felhasználóknak meg kell adni a nevüket, majd ezt követően különböző szobákat tud létrehozni, melyekbe üzeneteket írhatnak. Az üzenetek, az új szobák létrehozása, valamint az online felhasználók listája az alkalmazásban automatikusan megjelennek és frissülnek.
Első lépésként hozzunk létre WebStorm-ban egy Java projektet.
Ezután töltsük le a Redis eléréséhez a Jedis
nevezetű Java osztálykönyvtárat az alábbi URL-ről:
http://central.maven.org/maven2/redis/clients/jedis/2.9.0/jedis-2.9.0.jar
A letöltött jar fájlt másoljuk be a projektünk alá, majd importáljuk be. Ezt a File/Project Structure/Libraries menüpont alatt tehetjük meg.
Hozzunk létre egy chat
nevű package-t, abban pedig egy ChatDBManager
Java osztályt, amely a Redis műveletekért lesz felelős. Az osztályt a Singleton tervezési minta alapján készítsük el és vegyük fel a csatlakozási paramétereket tartalmazó statikus property-ket:
public class ChatDBManager {
private static String REDIS_HOST = "91.134.196.66";
private static Integer REDIS_DB = 1;
private static final ChatDBManager INSTANCE = new ChatDBManager();
private ChatDBManager() {
}
public static ChatDBManager getInstance() {
return INSTANCE;
}
}
Egészítsük ki az osztályt a Redis csatlakozást kezelő függvényekkel:
private Jedis jedis = null;
public void connect() {
jedis = new Jedis(REDIS_HOST);
jedis.connect();
jedis.select(REDIS_DB);
}
public void disconnect() {
if (jedis != null) {
jedis.disconnect();
}
}
Hozzunk létre egy új metódust, ami a bejelentkezés után regisztrálja, kilépés után pedig törli a felhasználót a Redis adatbázisban. A jelenlévő felhasználók regisztrációja az online:FELHASZNÁLÓ kulcs létrehozásával történik.
private String currentUser;
public void setCurrentUser(String currentUser) {
this.currentUser = currentUser;
}
public String getCurrentUser() {
return currentUser;
}
public void registerUser(String username) {
jedis.set("online:" + username, "true");
setCurrentUser(username);
}
public void unregisterCurrentUser() {
jedis.del("online:" + getCurrentUser());
}
Hozzunk létre egy metódust a jelenlévő felhasználók lekéréséhez:
public List<String> getOnlineUsers() {
ScanParams params = new ScanParams();
params.match("online:*");
//Scan creates a REDIS cursor based iterator, which starts at 0 and
//terminates when the result is 0, see: https://redis.io/commands/scan
ScanResult<String> scanResult = jedis.scan("0", params);
String nextCursor = scanResult.getStringCursor();
boolean scanEnd = false;
List<String> users = new ArrayList<>();
while (!scanEnd) {
for (String key : scanResult.getResult()) {
users.add(key.replaceFirst("online:", ""));
}
scanResult = jedis.scan(nextCursor, params);
nextCursor = scanResult.getStringCursor();
if (nextCursor.equals("0")) {
scanEnd = true;
}
}
return users;
}
A metódus az online:\*
minta alapján kigyűjti az adatbázisból a felhasználókat, majd visszatér azok listájával (levágva a kulcsokról az online:
előtagot).
Ezek után készítsük el az üzenetek adatbázisba mentéséhez és lekéréséhez szükséges metódusokat:
public void sendMessage(String message) {
String msg = (new Date()).getTime() + ":" + getCurrentUser() + ":" + message;
jedis.rpush(getCurrentRoom() + ":messages", msg);
}
public List<String> getMessages() {
return jedis.lrange(getCurrentRoom() + ":messages", 0, -1);
}
Az üzenetek Redis listákban tárolódnak, melyekbe rpush (right push) segítségével ”jobbról” szúrjuk be az új üzeneteket. A listák kulcsa tartalmazza a szoba nevét, amely így épül fel: SZOBA:messages
. A szobákhoz tartozó listákban az üzenetek az alábbi módon tárolódnak:
IDŐBÉLYEG:FELHASZNÁLÓ:ÜZENET
Az üzeneteket lrange-el tudjuk kiolvasni a listákból, amit 0, -1 paraméterrel hívunk meg. Ez azt jelenti, hogy az összes listaelemet lekérdezzük.
Következő lépésben hozzuk létre a szobákat kezelő metódusokat:
public void addRoom(String roomName) {
jedis.rpush("rooms", roomName);
}
public List<String> getRooms() {
return jedis.lrange("rooms", 0, -1);
}
private String currentRoom = "main";
public void setCurrentRoom(String currentRoom) {
this.currentRoom = currentRoom;
}
public String getCurrentRoom() {
return currentRoom;
}
public Map<String,Long> getRoomsLength(){
//TODO: implement
return Collections.emptyMap();
}
Az üzenetekhez hasonlóan a szobák neveit is listában (rooms
) tároljuk. Az aktuálisan megnyitott szoba nyilvántartását a currentRoom
property-ben tároljuk.
Készítsük el JavaFX alkalmazásunk belépési pontjaként szolgáló Main
osztályt a main
package-en belül:
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("login.fxml"));
primaryStage.setTitle("NoSQL labor");
primaryStage.setScene(new Scene(root, 900, 600));
primaryStage.show();
}
@Override
public void stop(){
ChatDBManager.getInstance().unregisterCurrentUser();
ChatDBManager.getInstance().disconnect();
}
public static void main(String[] args) {
launch(args);
}
}
Ez az osztály két dologért felelős. Létrehozza a Scene-ünket a login.fxml
alapján, valamint az ablak bezárása esetén a kilépteti a felhasználót és lezárja a kapcsolatot a Redis-el.
Készítsük el FXML-ben a bejelentkező képernyő felületét. Ehhez a main
package-en belül hozzuk létre a login.fxml
nevű fájlt:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.GridPane?>
<GridPane alignment="center" hgap="10" vgap="10" xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/8" fx:controller="main.MainController">
<children>
<Label text="Felhasználói név:"/>
<TextField fx:id="username" GridPane.columnIndex="1"/>
<Button fx:id="loginBtn" text="Belépés" onAction="#btnSubmit" GridPane.columnIndex="2"/>
</children>
</GridPane>
Hozzuk létre az FXML-ben hivatkozott MainController
Java osztályt. Ez
az osztály felelős a felhasználói bejelentkezésért.
public class MainController implements Initializable {
@FXML
private TextField username;
@Override
public void initialize(URL location, ResourceBundle resources) {
ChatDBManager.getInstance().connect();
}
public void btnSubmit(ActionEvent actionEvent) throws IOException {
if (!"".equals(username.getText())) {
//Register the user within the database
ChatDBManager.getInstance().registerUser(username.getText().replaceAll("[^A-Za-z0-9]", ""));
//Load the chat interface and attach it to a new scene
Parent parent = FXMLLoader.load(getClass().getResource("/chat/chat.fxml"));
Scene scene = new Scene(parent);
//Set the new scene as our current scene
Stage stage = (Stage) ((Node) actionEvent.getSource()).getScene().getWindow();
stage.setScene(scene);
stage.show();
}
}
}
A bejelentkezés gombra kattintáskor az FXML fájlban megadott btnSubmit
metódus fog lefutni, melyben beregisztráljuk a felhasználót Redis-ben, a
fentebb létrehozott registerUser metódus meghívásával. Ezután átváltunk
egy új Scene
-re, melynek kinézetét a chat.fxml
-ben definiáljuk.
Hozzuk létre a chat.fxml
fájlt a chat
package-en belül:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="665.0"
prefWidth="900.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="chat.ChatController">
<children>
<Label layoutX="715.0" layoutY="25.0" text="Online Felhasználók">
<font>
<Font size="17.0"/>
</font>
</Label>
<VBox fx:id="online" layoutX="719.0" layoutY="49.0" prefHeight="481.0" prefWidth="163.0"/>
<ScrollPane fx:id="messageScroll" layoutX="9.0" layoutY="43.0" prefHeight="470.0" prefWidth="693.0">
<content>
<VBox fx:id="messages" prefHeight="468.0" prefWidth="668.0"/>
</content>
</ScrollPane>
<TabPane fx:id="roomsTabPane" layoutX="9.0" layoutY="10.0" prefHeight="481.0" prefWidth="673.0">
<tabs>
<Tab id="main" closable="false" text="Fő szoba"/>
<Tab id="_newroom" closable="false" style="-fx-background-color: #85C899;" text="+ Új szoba">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="503.0" prefWidth="485.0">
<children>
<TextField fx:id="roomName" layoutX="261.0" layoutY="242.0"/>
<Label layoutX="170.0" layoutY="247.0" text="Szoba neve:"/>
<Button layoutX="449.0" layoutY="242.0"
onAction="#addRoomAction" text="Létrehoz"/>
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
<TextArea fx:id="message" layoutX="9.0" layoutY="534.0" prefHeight="66.0" prefWidth="578.0"/>
<Button fx:id="sendBtn" layoutX="596.0" layoutY="534.0" onAction="#sendAction"
prefHeight="66.0" prefWidth="105.0" text="Küldés"/>
</children>
</AnchorPane>
Hozzuk létre az fxml fájlhoz tartozó kontroller osztályt ChatController
néven és @FXML
annotációval hozzunk létre hivatkozást az egyes nézet elemekre.
public class ChatController implements Initializable {
@FXML
private VBox online;
@FXML
private TextArea message;
@FXML
private ScrollPane messageScroll;
@FXML
private TextField roomName;
@FXML
private TabPane roomsTabPane;
@FXML
private VBox messages;
@Override
public void initialize(URL location, ResourceBundle resources) {
loadOnlineUsers();
loadRooms();
loadMessages();
roomsTabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
@Override
public void changed(ObservableValue<? extends Tab> observableValue, Tab tab, Tab changedTab) {
ChatDBManager.getInstance().setCurrentRoom(changedTab.getId());
loadMessages();
}
});
}
}
A nézet betöltődése után az initialize
metódus fog automatikusan meghívódni. Ebben inicializáljuk az egyes felületi elemek tartalmát (jelenlévő felhasználók listája, szobák listája, üzenetek). A szobák listáját Tab-ok formájában jelenítjük meg. Ha a felhasználó rákattint az egyik tab-ra, akkor átállítjuk az aktuális szobát, majd újratöltjük az üzenetek listáját, hogy az aktuális szobához tartozóak jelenjenek meg. Ehhez beregisztrálunk egy listener
függvényt, ahol ezeket a műveleteket lekezeljük.
Készítsük el az initialize metódusban hivatkozott további inicializáló függvényeket:
private void loadOnlineUsers() {
//Clear the current list
online.getChildren().clear();
//Fetch the currently onlnie users from the database and append them as labels to the online tag
List<String> onlineUsers = ChatDBManager.getInstance().getOnlineUsers();
for (String onlineUser : onlineUsers) {
Label user = new Label(onlineUser);
//If the user is the current user make their name bold
if (onlineUser.equals(ChatDBManager.getInstance().getCurrentUser())) {
user.setFont(Font.font("Verdana", FontWeight.BOLD, 12));
}
online.getChildren().add(user);
}
}
private void addMessage(String msg) {
//Messages are stored in the follwign format timestamp:user:message text
String[] msgParts = msg.split(":", 3);
//Timestamp
Label date = new Label(new Timestamp(Long.parseLong(msgParts[0])).toString());
date.setId("date");
date.setPrefWidth(200);
//User name
Label user = new Label("@" + msgParts[1]);
user.setPrefWidth(100);
//Message body
Text messageText = new Text(msgParts[2]);
//Pack them enatly into a horizontal box
HBox hBox = new HBox();
hBox.getChildren().add(date);
hBox.getChildren().add(user);
hBox.getChildren().add(messageText);
hBox.setPrefHeight(20);
hBox.setSpacing(10);
hBox.setPadding(new Insets(5, 5, 5, 5));
hBox.setStyle("-fx-background-color: #DFDFDF; -fx-border-color: transparent transparent gray transparent");
messages.getChildren().add(hBox);
messageScroll.setVvalue(1.0);
}
private void addRoom(String room) {
Tab tab = new Tab(room);
tab.setId(room);
VBox messagesVBox = new VBox();
messagesVBox.setId("messages");
tab.setContent(messagesVBox);
roomsTabPane.getTabs().add(tab);
}
private void loadRooms() {
for (String room : ChatDBManager.getInstance().getRooms()) {
addRoom(room);
}
}
private void loadMessages() {
messages.getChildren().clear();
for (String msg : ChatDBManager.getInstance().getMessages()) {
addMessage(msg);
}
}
Következő lépésben hozzuk létre az üzenet küldés és a szoba létrehozás gombhoz tartozó akció metódusokat:
public void sendAction(ActionEvent actionEvent) {
if (!"".equals(message.getText())) {
ChatDBManager.getInstance().sendMessage(message.getText());
message.setText("");
}
}
public void addRoomAction(ActionEvent actionEvent) {
if (!"".equals(roomName.getText())) {
ChatDBManager.getInstance().addRoom(roomName.getText());
ChatDBManager.getInstance().setCurrentRoom(roomName.getText());
roomName.setText("");
}
}
Alkalmazásunk ezek után már működőképes, csak nem szinkronizálja más felhasználók tevékenységeit. Nem jelennek meg automatikusan az alkalmazás indítása óta küldött új üzenetek, bejelentkezett új felhasználók és a létrehozott új szobák (csak újraindítás után). Következő lépésekben ezt fogjuk implementálni a Redis Publish/Subscribe módszer alkalmazásával. Ennek lényege, hogy különböző csatornákra üzenetet küldünk, melyeket minden feliratkozott kliens megkap.
Először is egészítsük ki ChatDBManager
osztályunkat, hogy kezelni tudja a csatornákra való feliratkozást és az azokon érkező üzenetek fogadását:
public void subscribe(String channel, JedisPubSub listener) {
Thread thread = new Thread(new JedisTask(listener, channel));
thread.setDaemon(true);
thread.start();
}
private static class JedisTask implements Runnable {
Jedis jedis = null;
JedisPubSub channelListener;
String channel;
JedisTask(JedisPubSub listener, String channel) {
this.channelListener = listener;
this.channel = channel;
this.jedis = new Jedis(REDIS_HOST);
jedis.connect();
jedis.select(REDIS_DB);
}
@Override
public void run() {
try {
jedis.psubscribe(channelListener, channel);
while (true) {
Thread.sleep(200);
}
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
Csatornákra feliratkozni későbbiekben a létrehozott subscribe
metóduson keresztül tudunk, ami egy külön szálon felcsatlakozik a Redis-re a JedisTask subclass segítségével, majd a csatornákon érkező üzeneteket egy átadott listener segítségével tudjuk lekezelni.
Ezek után egészítsük ki az említett műveleteket, hogy egy csatornán jelezzék a feliratozott klienseknek.
Először a felhasználók regisztrációiról küldjünk a users:reload
csatornán egy üzenetet, annak érdekében, hogy jelezzük, hogy a kliensalkalmazásokban újra kell tölteni az online felhasználók listáját. Ehhez a registerUser
és az uregisterCurrentUser
metódosunkba vegyünk fel egy publish metódushívást (a változtatás vastagon van jelölve):
public void registerUser(String username) {
jedis.set("online:" + username, "true");
setCurrentUser(username);
* jedis.publish("users:reload", "");
}
public void unregisterCurrentUser() {
jedis.del("online:" + getCurrentUser());
* jedis.publish("users:reload", "");
}
A csatornán küldött üzenet tartalma üres string, mivel azt jelen esetben nem kezeljük le (bármi lehet).
Ezután egészítsük ki publish hívással a sendMessage
és addRoom
metódusokat is.
public void sendMessage(String message) {
String msg = (new Date()).getTime() + ":" + getCurrentUser() + ":" + message;
jedis.rpush(getCurrentRoom() + ":messages", msg);
jedis.publish("room:" + getCurrentRoom() + ":message", msg);
}
public void addRoom(String roomName) {
jedis.rpush("rooms", roomName);
jedis.publish("rooms:new", roomName);
}
Ebben a két esetben a publish üzenet tartalma már nem üres string, hanem a küldött chat üzenet, valamint a szoba neve.
Végül nem maradt más hátra, mint ezekre a csatornákra való feliratkozás. Térjünk vissza a ChatController
osztályunkhoz, majd egészítsük ki az initialize() függvényünket egy subscribeToRedisChannels() hívással:
@Override
public void initialize(URL location, ResourceBundle resources) {
loadOnlineUsers();
loadRooms();
loadMessages();
//Subscribe to tab change events, changing rooms accordingly
roomsTabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
@Override
public void changed(ObservableValue<? extends Tab> observableValue, Tab tab, Tab changedTab) {
ChatDBManager.getInstance().setCurrentRoom(changedTab.getId());
loadMessages();
}
});
subscribeToRedisChannels();
}
A subscribeToRedisChannels
tartalma pedig a következő lesz:
private void subscribeToRedisChannels() {
ChatDBManager.getInstance().subscribe("users:reload", new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String room) {
Platform.runLater(new Runnable() {
@Override
public void run() {
loadOnlineUsers();
}
});
}
});
ChatDBManager.getInstance().subscribe("room:*:message", new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String message) {
Platform.runLater(new Runnable() {
@Override
public void run() {
String roomName = channel.split(":")[1];
if (roomName.equals(ChatDBManager.getInstance().getCurrentRoom())) {
addMessage(message);
messageScroll.setVvalue(1.0);
}
}
});
}
});
ChatDBManager.getInstance().subscribe("rooms:new", new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String room) {
Platform.runLater(new Runnable() {
@Override
public void run() {
addRoom(room);
if (room.equals(ChatDBManager.getInstance().getCurrentRoom())) {
roomsTabPane.getSelectionModel().selectLast();
}
}
});
}
});
}
Első esetben a users:reload csatornára iratkozunk fel, ahol ha üzenet érkezik akkor a loadOnlineUsers metódusunk meghívásával újratöltjük az online felhasználók listáját. A Platform.runLater hívásra azért van szükség, mert a csatornákon az üzenetek külön szálon érkeznek és ennek segítségével tudunk a JavaFX szálunkon módosítani a megjelenített elemeken.
A következő feliratkozás a room:*:message
mintára illeszkedő csatornákra történik. Itt a *
segítségével illesztünk a különböző szoba nevekre, amit később visszanyerünk a csatorna nevéből a :
elválasztó mentén felbontva azt. Ezután ellenőrizzük, hogy az aktuálisan érkezett chat üzenet ugyanabban a szobában jött-e, mint ahol jelenleg vagyunk. Ha
igen, akkor hozzáadjuk azt az üzenetet listához (a teljes újratöltés nélkül).
Végül feliratkozunk a rooms:new
csatornára is, ahol lekezeljük az
újonnan keletkezett szobák felvételét is.
Valósítsa meg, hogy ha egy olyan szobába jön üzenet, ami jelenleg nem aktív, akkor a szoba neve vastagon legyen jelölve.
Figyelem: a fő szobára is működjön megfelelően.
Tipp: ne szövegre, hanem id-ra vizsgáld a szobanevet.
Egészítse ki az előző feladatot úgy, hogy ha rákattintunk egy vastagon jelölt szoba nevére, akkor az váltson vissza nem vastagra
Figyelem: az új szoba tab háttere maradjon zöld!
Valósítson meg egy új képernyőt, amely egy kördiagrammon ábrázolja az egyes szobákba érkezett üzeneteket számát. Képernyőn jelenjen meg egy jelmagyarázat és egy vissza gomb is, mellyel az előző nézetre lehet visszanavigálni. Valamint a szobanevek után zárójelben jelenjen meg az üzenetek száma.
Lépések:
- Új gomb felvétel a képernyőn
ChatDBManager
getRoomsLength()
függvény implementálása- Grafikon kirajzoló felület (fxml) és kontroller létrehozása
- PieChart példa: http://docs.oracle.com/javafx/2/charts/pie-chart.htm#CIHFDADD
- Vissza gomb létrehozása
Grafikon folyamatosan frissüljön új üzenet érkezésekor.