Skocz do zawartości
MAGNET

[Poradnik] Co nieco o SQL

Rekomendowane odpowiedzi

Większość z osób, która czyta ten tekst wie co to bazy danych i żeby zainstalować u siebie na serwerze RankME trzeba stworzyć bazę MySQL...jednak o co w tym tak naprawdę chodzi? Postaram się o tym opowiedzieć w możliwie jak najkrótszej i najprostszej do zrozumienia formie, a także pokażę, jak używać SQL'a w naszych pluginach

 

Zadaniem baz danych jest przechowywanie ważnych dla nas informacji, aby móc korzystać z nich w dowolnym momencie. Istnieje ich wiele rodzajów - obiektowe, strumieniowe, temporalne...a także relacyjne. SQL jest właśnie przedstawicielem relacyjnych baz danych.

 

1. Teoria

Oczywiście nie jest ona nieuporządkowanym zbiorem walających się po pamięci danych - bazy SQL są ułożone w ustrukturyzowany sposób, który teraz omówię. Gdybym miał posłużyć się tutaj analogią, powiedziałbym, że przypominają one...matrioszkę

a38bd6504bcba6594686914049c4.jpg.f285ac3067dabdbf1f5ac2f975716569.jpg

 

Mamy bazę, w środku są tabele, w tabelach kolumny i wiersze...ale po kolei:

 

Baza danych

Wyskoczę teraz trochę w przyszłość. Kiedy konfigurujemy połączenie SQL, wpis w databases.cfg wygląda mniej więcej tak:

"rankME"
	{
		"host"				"sql.twoja-strona.pl"
		"database"			"7213_rankme"
		"user"				"root"
		"pass"				"asQ%c^d"
		//"timeout"			"0"
		//"port"			"0"
	}

(jeśli nie wiesz jeszcze co tu się wydarzyło - spokojnie, wyjaśnienie pojawi się na końcu poradnika)

Jako jeden z argumentów podane zostało "database" - i jest to właśnie nasza baza! W niej znajdują się tzw. tabele. Narysujmy sobie taką bazę:

0.thumb.png.fbae5a513fe35bb39c24723edacf7e99.png

Na pewno nie mówi nam to jednak zbyt wiele - musimy zagłębić się jeszcze bardziej...

 

Tabela

W każdej bazie danych, która przechowuje jakieś informacje, muszą znaleźć się tabele. Rysunek poglądowy:

1.thumb.png.f00e1b79ad41f244594473e4f68e3f52.png

Nie jesteśmy ograniczeni do jednej tabeli - możemy stworzyć sobie dowolną ich ilość. Właśnie w nich znajdują się dane, które używane są np. do przechowywania informacji o CodModzie, rankME, sourceBansie i wszystkiego, gdzie wykorzystuje się SQL. Dane w tabelach ułożone są w specyficzny sposób - jak tabelki w excelu...

 

Kolumny

Definiowane są one podczas tworzenia tabeli i mówią jakie dane, i o jakim typie, będą tam przechowywane. Układ kolumn w tabeli:

2.thumb.png.d0cc63cf3051e6a5e4c125eaa3ed3de3.png

Na tym etapie możemy zobrazować sobie jak będzie wyglądała struktura jakiejś prostej bazy danych. Załóżmy, że chcemy przechowywać informacje o ilości zabójstw każdego gracza. W najprostszym wydaniu tabela będzie posiadała dwie kolumny: identyfikator gracza oraz liczba zabójstw:

    steamID | zabójstwa
-------------------------
            |
            |
            |

Same kolumny jednak nie wystarczą...trzeba je jakoś zapełnić danymi. Tym zajmą się oczywiście...

 

Wiersze

Każdy wiersz to pewna porcja danych i zarazem najmniejsza cząstka naszej bazy danych. Wiedząc o ich istnieniu możemy wypełnić tabelę z zabójstwami przykładowymi danymi:

 

    steamID | zabójstwa
-------------------------
    3515151 |    34
    3774639 |    728
    7352395 |    31

W pierwszym wierszu widnieje identyfikator steamID(3515151) oraz zabójstwa(34). W drugim wierszu - steamID 3774639 i zabójstwa 728. Kiedy pobieramy dane z bazy danych, mają one właśnie formę wierszy, lecz o tym będziemy mówić dopiero później.

 

W tym miejscu zakończyliśmy omawianie teorii. Czas opowiedzieć jak odbywa się komunikacja pomiędzy bazą a hostem (np. serwerem CS:GO)

 

2. Grunt, żeby się dogadać

Komunikacja z bazą danych odbywa się na zasadzie tzw. zapytań - mówimy jakie polecenie mamy wykonać, a SQL wykonuje nasze polecenie. Do najpopularniejszych zapytań należą:

CREATE TABLE - tworzy nową tabelę w bazie

SELECT - używamy go, gdy chcemy pobrać dane z określonej tabeli,

INSERT - służy do wprowadzenia nowych wierszy,

UPDATE - aktualizuje wiersze już znajdujące się w tabeli,

DELETE - usuwa wiersze

 

Tworząc pluginy w CS:GO będziemy opierali się praktycznie tylko na wymienionych wyżej poleceniach - więcej nam nie będzie zwyczajnie potrzebne.

 

 

Niewiele osób o tym wie, jednak zapytania SQL dzielą się na dwa rodzaje - synchroniczne oraz asynchroniczne. Co to znaczy?

 

zapytania synchroniczne - kiedy w którymś miejscu w kodzie wykonywane jest takie zapytanie, program staje w miejscu i oczekuje na rezultat zwrócony przez bazę. Dopiero po trzymaniu wyniku plugin rusza dalej

zapytania asynchroniczne - po odpaleniu zapytania program kontynuuje wykonywanie poleceń, natomiast rezultat zapytania jest przekazywany do specjalnej funkcji. Plugin nie staje się bezczynnie i kontynuuje swoje działanie

 

Którą metodę warto wybrać? Z całą pewnością zapytania synchroniczne są dużo prostsze do zaimplementowania, gdyż kod po prostu wykonuje się linijka po linijce. Z drugiej strony, nie mamy pojęcia kiedy SQL zwróci rezultat naszego działania. Co jeśli łączymy się z sql w Chinach?

W zapytaniu asynchronicznym ten problem nie występuje. Plugin kontynuuje swoje działanie i reaguje dopiero kiedy dotrze do niego rezultat query(zapytania). Z drugiej strony, może to powodować problemy w opracowaniu pluginu, który będzie reagował na brak odpowiedzi w mądry sposób, np. poradzenie sobie w sytuacji, gdy wczytujemy dane gracza, który już znalazł się na serwerze (wówczas trzeba blokować niektóre funkcjonalności do czasu, aż jego dane nie zostaną wczytane).

 

 

Wiemy już jak działa baza danych, jaką ma strukturę i jak komunikuje się z serwerem. Teraz możemy taką bazę zaprogramować

 

3. Implementacja

Na samym początku należy połączyć się z bazą danych. Możemy wykonywać to w OnPluginStart. Wpierw jednak stwórzmy sobie w zmiennych globalnych uchwyt do bazy:

Database DB;

a następnie w OnPluginStart:

char sError[512];
DB = SQL_Connect("NazwaNaszejKonfiguracji",true, sError, sizeof(sError));

if (DB == null) {
  PrintToServer("Błąd połączenia z bazą danych! Error: %s", sError);
}

 

Na samym początku musimy oczywiście wiedzieć z jakim serwerem chcemy sie połączyć. Określa to pierwszy argument funkcji SQL_Connect
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami! - pobiera informacje z wpisu z pliku databases.cfg, znajdującego się w folderze sourcemod/configs. Musimy uzupełnić go o dodatkowy wpis:

"NazwaNaszejKonfiguracji"
	{
		"host"				"link do naszej bazy"
		"database"			"nazwa bazy"
		"user"				"nazwa użytkownika"
		"pass"				"hasło użytkownika"
		//"timeout"			"0"
		//"port"			"0"
	}

Baza danych nie stworzy się sama - musimy ją założyć wraz z mającym do niej dostęp użytkownikiem. Najczęściej wykorzystuje się do tego celu tzw phpMyAdmin, jednak poradnik nie obejmuje jego konfiguracji. Hosting, na którym baza się znajduje zawsze pokazuje w jaki sposób taką bazę skonfigurować. Oglądowy film pokazujący jak to środowisko wygląda można zobaczyć tutaj
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami!

Gdyby okazało się, że coś poszło nie tak, uchwyt do bazy będzie nullem, a do podanego przez nas stringa zostanie przekazany błąd. Należy dostosować się do jego zaleceń i skorygować pomyłkę.

 

a) Tworzenie tabeli

Po nazwiązaniu połączenia z bazą nie jesteśmy jeszcze gotowi na umieszczanie w niej danych, ponieważ nie ma w niej żadnych tabeli! Musimy je utworzyć za pomocą polecenia CREATE TABLE. Zapytanie te możemy wysłać w momencie, kiedy połączymy się poprawnie z SQL. Poprawmy więc nasz kod powyżej:

char sError[512];
DB = SQL_Connect("NazwaNaszejKonfiguracji",true, sError, sizeof(sError));

if (DB == null) {
  PrintToServer("Błąd połączenia z bazą danych! Error: %s", sError);
} else {
  if (!SQL_FastQuery(DB, "CREATE TABLE IF NOT EXISTS Zabojstwa(steamID INT NOT NULL UNIQUE, kille INT NOT NULL)")) {
      char error[255];
      SQL_GetError(db, error, sizeof(error));
      PrintToServer("Failed to query (error: %s)", error);
  }
}

W tym przypadku nie oczekujemy zwrócenia żadnego rezultatu przez bazę danych - można wówczas wykorzystać SQL_FastQuery
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami!, co można przetłumaczyć jako "szybkie zapytanie". 

 

Przeanalizujmy sobie krótko jego treść:

CREATE TABLE IF NOT EXISTS - tworzy tabelę tylko w sytuacji, gdy nie istnieje jeszcze ona w bazie. Jeśli już tam jest, zapytanie jest ignorowane

Zabojstwa - nazwa naszej tabeli. Podczas późniejszych zapytań będziemy odwoływali się właśnie do niej

steamID INT NOT NULL UNIQUE - dodaje kolumnę do tabeli. Ponadto, określamy jej typ - jest to INT, czyli integer, z którym mieliśmy już do czynienia chociażby tworząc zwykłe zmienne. NOT NULL oznacza, że podczas dodawania kolejnych wierszy pole to nie może być puste - nie chcemy sytuacji, w której pobierzemy puste pole, gdyż mogły by wyniknąć z tego jakieś komplikacje. NOT NULL nie jest wymagany, jednak w tym przypadku jego zastosowanie jest zasadne. UNIQUE, jak nazwa wskazuje, nie pozwala na stworzenie wiersza z drugim takim samym steamID - chcemy, aby nasze identyfikatory były unikatowe, a duble powodowałby błędy w działaniu programu.

kille INT NOT NULL - dodanie wiersza, w którym przechowywane będą zabójstwa graczy

 

b) Dodawanie rekordów

W celu dodania nowego elementu do naszej bazy danych używamy polecenia INSERT. Przykładowy wygląd takiego zapytania mógłby wyglądać w następujący sposób:

INSERT INTO Zabojstwa VALUES(1234123, 0)

Powyższe polecenie spowoduje dodanie do bazy danych nowego rekordu, w którym SteamID będzie wynosiło 1234123, natomiast ilość killi 0 - to logiczne, gdyż najprawdopodobniej jest to nowy gracz, który nie zdążył jeszcze nikogo zabić.

 

Kolejność wpisywania wartości odbywa się zgodnie z kolejnością, z jaką została stworzona nasza tabela i należące do niej rekordy w CREATE TABLE (kolumny 'steamid' i 'kille'). Gdybyśmy chcieli dodać wartości tylko do konkretnych kolumn, wówczas można zrobić to w następujący sposób:

INSERT INTO Zabojstwa(steamid,kille) VALUES(1234123, 0)

 

 

Jak takie zapytanie wysłać? Robi się to w taki sam sposób, jak podczas tworzenia tabeli - używając Query (np. SQL_FastQuery).

Co ważne, gdybyśmy chcieli przykładowo do bazy dodać nowego gracza, to musimy podać jego dane, takie jak steamid. Te informacje muszą zostać przekazane przez zapytanie, więc aby tego dokonać należy stworzyć buffer, na którym użyjemy reguł formatujących. Może to wyglądać w następujący sposób:

void DodajGraczaDoBazy(int steamID, int kille) {
  
	char buffer[1024];
 	Format(buffer, sizeof(buffer), "INSERT INTO Zabojstwa VALUES(%d,%d);", steamID, kille);
	if (!SQL_FastQuery(DB, buffer)) {
		char error[255];
		SQL_GetError(db, error, sizeof(error));
		PrintToServer("Failed to query (error: %s)", error);
	}
}

Średnik w miejscu VALUES(%d,%d); został dodany nieprzypadkowo, ponieważ dotyczy się on stricte naszego zapytania, a nie składni SourceModa.

 

Gdybyśmy dodatkowo założyli, że w naszej bazie chcemy przechowywać nick gracza, operowanie na stringu w zapytaniach wiąże się z bardzo ważnym obowiązkiem opatrywania go w apostrofy (' '). Dodatkowo, nazwy naszych kolumn i tabel możemy opatrywać apostrofem, który znajdziemy pod klawiszem ESC (` `).

 

Prawidłowy zapis dodawania nowego gracza do bazy:

Format(buffer, sizeof(buffer), "INSERT INTO Zabojstwa(`steamid`,`nick`,`kille`) VALUES(%d,'%s'%d);", steamID, nick, kille);

(Wartości kolumn są nieobowiązkowe)

 

c) Pobieranie informacji z bazy

 

Równie często będziemy chcieli otrzymać jakieś informacje z naszej bazy. Tutaj posługujemy się poleceniem SELECT.

Polecenie, które pobierze DOSŁOWNIE CAŁĄ TABELĘ wygląda w następujący sposób:

SELECT * FROM `Zabojstwa`;

Jednak praktycznie nigdy nie chcemy tego robić w ten sposób - przeważnie zależy nam na konkretnych informacjach. Dzisiaj pokażę dwie najprostsze metody selekcji pobieranych informacji:

 

1. Selekcja kolumn

Nic prostszego! W poprzednim zapytaniu * odpowiadała za pobranie wszystkich wierszy. Jeśli chcemy tylko konkretne - po prostu je wypiszmy:

SELECT kille FROM `Zabojstwa`;

Teraz zamiast obydwu kolumn wyciągniemy jedynie wszystkie kille.

 

2. Selekcja wierszy

Tutaj pomoże nam klauzula WHERE. Wybiera ona wierszy na podstawie wyrażenia logicznego. Używaliście już kiedyś IFów w SourceModzie? Tutaj reguła jest bardzo podobna 🙂\

 

Nasze podstawowe operatory logiczne to:

= (równość)

> (większe niż)

< (mniejsze niż)

>= (większe bądź równe)

<= (mniejsze bądź równe)

<> (nie równa się, coś jak negacja '!' w SM)

 

W 80% przypadków używa się jednak po prostu operatora przyrównania '='.

Powiedzmy, że chcemy pobrać z bazy dane dotyczące gracza o konkretnym steamid. Zapytanie będzie wyglądało następująco:

SELECT * FROM `Zabojstwa` WHERE `steamid`=1234123;

Prawda, że proste? 🙂

 

Gdybyśmy natomiast chcieli znaleźć wszystkie osoby, których ilość killi jest większa od 100:

SELECT * FROM `Zabojstwa` WHERE kille > 100;

 

Ponadto selekcję po kolumnach i wierszach można łączyć:

SELECT steamid FROM `Zabojstwa` WHERE kille > 100;

To zapytanie pobierze jedynie identyfikatory osób, którzy nabili ponad 100 killi.

 

 

Teraz, kiedy wiemy jak przygotować zapytanie pobierające informacje z bazy danych, pojawia się pytanie - jak sprawić, aby teraz te informacje pojawiły się w naszym pluginie?

Tak samo, jak dane trzymane są w bazie danych w postaci wierszy, tak samo wiersze są pobierane przez SourceModa. Proces ściągania danych sprowadza się do "odwiedzania" wiersza po wierszu i zczytywania informacji tak długo, aż wiersze nam się skończą.

 

Wróćmy do przykładu, w którym pobieramy graczy, którzy mają więcej niż 100 killi. Powiedzmy, że chcemy wypisać na czacie wszystkich szczęśliwców - załóżmy też, że nasza baza posiada pole (czyli kolumnę) z nickiem gracza. Może to wyglądać tak:

 

DBResultSet query = SQL_Query(db, "SELECT `nick`,`kille` FROM `Zabojstwa` WHERE `kille` > 100;");
if (query == null) {
	char error[255];
	SQL_GetError(db, error, sizeof(error));
	PrintToServer("Failed to query (error: %s)", error);
} 
else {
    char nickBuffer[MAX_NAME_LENGTH];
    int kille;
    while (SQL_FetchRow(query)) {
        SQL_FetchString(query, 0, nickBuffer, sizeof(nickBuffer));
        kille = SQL_FetchInt(query, 1);
        PrintToChatAll("Gracz: %s | Kille: %d", nickBuffer, kille);
    }
	delete query;
}

Zasadniczą różnicą jest to, że nie używamy już SQL_FastQuery, a SQL_Query - różnica jest taka, że w przypadku SQL_FastQuery nie interesuje nas zwracany rezultat. My chcemy coś z bazy uzyskać, dlatego stosujemy SQL_Query.

Wszystkie wiersze pobrane z MySQL trafiają do "query", które jest dość niecodziennego typu "DBResultSet", który jest właśnie zdolny do trzymania wierszy z SQL.

Kolejnym etapem jest przygotowanie buffera i zmiennej na nadchodzące dane.

 

Później wchodzimy do pętli, która opatrzona jest warunkiem while(SQL_FetchRow(query)). O co tu chodzi? Najprościej wytłumaczyć to w taki sposób, że w naszym "query" istnieje nieznana nam ilość wierszy i swoisty "wskaźnik", mówiący który wiersz aktualnie przeglądamy. Funkcja SQL_FetchRow sprawia, że wskaźnik przesuwa się do kolejnego wiersza. Ponadto, zwraca nam true, jeśli takowy wiersz w ogóle istnieje, a jeśli dane się skończyły - zwraca false. Dzięki temu możemy przerobić dosłownie wszystkie kolumny z naszego zapytania.

 

W kolejnym etapie wchodzimy do ciała pętli i tutaj następuje pobranie wartości z wiersza, który aktualnie "mielimy". Tutaj najczęściej będziemy używać SQL_FetchInt(pobierz integera), SQL_FetchString(pobierz stringa) oraz SQL_FetchFloat(pobierz floata). Wszystkie trzy funkcje mają zaczynają się tak samo - najpierw ustalamy skąd pobieramy informacje - czyli z naszego query. Następnie mówimy które pole chcemy pobrać, numerując od zera. I tak, w naszym przypadku 0 to będzie nick, a 1 to zabójstwa (zgodnie z kolejnością kolumn podawanej w zapytaniu SELECT). Różnica pomiędzy SQL_FetchInt a SQL_FetchString jest taka, że SQL_FetchInt zwraca bezpośrednio wartość przez returna (stąd mamy kille = SQL_FetchInt(... ) ), natomiast SQL_FetchString robi to przez modyfikowanie stringa, który podamy jako trzeci argumeny (nie zapominając oczywiście o czwartym argumencie - jego długości).

 

I to wszystko 🙂

A co, gdybyśmy chcieli, aby wyniki przyszły posortowane? Fajnie by było, aby w pierwszym wierszu znajdowała się osoba z największą ilością killi, w drugim druga najlepsza osoba etc.

Okazuje się, że nie musimy to robić ręcznie - SQL posiada wbudowaną opcję sortowania wyników!

 

Jedyne co musimy zrobić, to delikatnie zmodyfikować koniec naszego zapytania:

SELECT `nick`,`kille` FROM `Zabojstwa` WHERE `kille` > 100 ORDER BY `kille` DESC;

"ORDER BY", czyli po angielsku "sortuj po". Dosłownie: sortuj po wartości kolumny `kille`. A co to DESC? to skrót od słowa "descending", czyli malejący. Dodana fraza powoduje sortowanie wierszy według malejącej wartości kolumny `kille`. Gdybyśmy natomiast chcieli posortować rosnąco (od najmniejszej do największej wartości), zamiast DESC wpisalibyśmy po prostu ASC (ascending, czyli rosnący).

 

 

d) Aktualizowanie wierszy

 

Prosta sprawa - polecenie UPDATE. Mając na uwadze Wasze wcześniejsze doświadczenie z UPDATE i INSERT ta część nie powinna stanowić większego problemu:

UPDATE `Zabojstwa` SET `kille`=200,`nick`='MAGNET' WHERE `steamid`=1234123

Krótka piłka - mówicie którą tabelę chcecie zaktualizować, a później wypisujecie po kolei wartości interesujących Was kolumn. Dodatkowo, potrzebujemy tutaj naszego WHERE, gdyż bez niego WSZYSTKIE wiersze w tabeli otrzymałyby wartości z zapytania - WHERE działa tutaj dokładnie tak samo, jak przy SELECT.

 

e) Usuwanie wierszy

 

Rozdział jeszcze prostszy, niż UPDATE. Tutaj używamy DELETE w banalny sposób:

DELETE FROM `Zabojstwa` WHERE `kille` < 20;

Czyli dosłownie: usuń z tabeli `Zabojstwa` te wiersze, gdzie ilość killi jest mniejsza od 20. Podobnie jak przy UPDATE, nie spodziewamy się tutaj raczej żadnego rezultatu ,więc możemy użyć SQL_FastQuery, jak przy zakładaniu nowej tabeli.

 

 

 

 

 

 

No i to na tyle 🙂

 

Mam nadzieję, że pomogłem niektórym osobom zagłębić się w świat baz danych i pokazać, że nie są one takie straszne, na jakie wyglądają. Jeśli macie jakieś uwagi, pytania bądź sugestie co można dodać następnym razem, to oczywiście komentarze nie gryzą

 

Dzięki za dotrwanie do końca ^^

 

Udostępnij tę odpowiedź


Odnośnik do odpowiedzi
Udostępnij na innych stronach

Odnalazłem ten poradnik, który zacząłem pisać prawie rok temu 😮

 

Dziś go dokończyłem i odsłoniłem

Przydał się?

Udostępnij tę odpowiedź


Odnośnik do odpowiedzi
Udostępnij na innych stronach

Dołącz do dyskusji

Możesz dodać zawartość już teraz a zarejestrować się później. Jeśli posiadasz już konto, zaloguj się aby dodać zawartość za jego pomocą.

Gość
Dodaj odpowiedź do tematu...

×   Wklejono zawartość z formatowaniem.   Usuń formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Odnośnik został automatycznie osadzony.   Przywróć wyświetlanie jako odnośnik

×   Przywrócono poprzednią zawartość.   Wyczyść edytor

×   Nie możesz bezpośrednio wkleić obrazków. Dodaj lub załącz obrazki z adresu URL.


×
×
  • Dodaj nową pozycję...