[Любая версия] Связь базы данных SQLite и сервера

Настоятельно рекомендую почитать о запросах в SQLite, это очень упростит вам задачу! 

Итак, моя проблема была в том, чтобы хранить статистику игрока (к примеру деньги, уровень, ID дома и прочее) на сервере, чтобы можно было в любой момент изменить её (прямо с сайта или даже если игрок оффлайн и сервер выключен). 

Для решения данной проблемы я находил несколько решений:
  1. Capabilities - Оно основывается на хранении любых NBT значений в Entity, будь то игрок, корова или даже вагонетка, но данный способ мне не подходил, так как здесь возникает проблема изменения значений когда игрок оффлайн.
  2. WorldSavedData - Сохраняет значения в папке с миром. Не подходил данный метод из-за того, что изменение значений невозможно, когда сервер выключен, и к тому же для массивного хранения данных это так же не подходило.
  3. База данных SQLite - После размышлений над вторым вариантом, мне пришло в голову это. Идеальное решение для какого-либо проекта.
Как будет работать наша структура?
Очень просто.

Модель Клиент-Сервер мы уже знаем - они обмениваются пакетами данных, к примеру обновление статистики. О синхронизации мы можете почитать, в учебнике есть статья.

А вот Сервер выполняет запросы на Базу данных SQLite.
image.png



Если немного не понятно о чем идет речь, вот как устроена база данных:

image.png

В данном случае поле:
  1. id - уникальный ключ, используется почти в каждой базе данных
  2. name - имя игрока
  3. money - деньги
  4. Остальные поля, которые вы можете добавлять и изменять как вам угодно
Теперь то, что нам конкретно понадобится
  1. База данных SQLite (может быть размещена где угодно, хоть у Вас на компьютере, хоть возьмите сайт https://www.freemysqlhosting.net/. Там можно бесплатно поставить одну базу данных до 5 мб в размерах.
  2. Java IDE с установленным драйвером JDBC (ниже я рассказываю как это сделать).
Как установить JDBC драйвер в IDE:
Так как моды собираются на билдере Gradle, то изменить настройки его нам и понадобится.

В корне нашего проекта находим чудесный файл build.gradle.
Находим строку buildscript и в нем по иерархии будет dependencies.
Добавляем в dependencies такую строчку 
Код:
classpath 'mysql:mysql-connector-java:5.1.44' 
чтобы получилось примерно так:

Код:
buildscript {
    repositories {
        jcenter()
        maven { url = "http://files.minecraftforge.net/maven" }
    }
    dependencies {
        classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT'
        classpath 'mysql:mysql-connector-java:5.1.44'
    }
}
Обращаю внимание, что в зависимости от версий у всех может быть разный файл Gradle!

Теперь добавляем в корневой dependencies (который не в buildscript) такую строку:

Код:
compile 'mysql:mysql-connector-java:5.1.44'

И итоговый вариант build.gradle должен содержать вот такую штукенцию:

Код:
buildscript {
    repositories {
        jcenter()
        maven { url = "http://files.minecraftforge.net/maven" }
    }
    dependencies {
        classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT'
        classpath 'mysql:mysql-connector-java:5.1.44'
    }
}

dependencies {
    compile 'mysql:mysql-connector-java:5.1.44'
}


Дальше обновите файл сборки Gradle (в принципе можно и не обновлять, так как он это сделает сам при сборке мода).

Если что-то не получилось, погуглите ошибку.

Приступим к самой связи базы данных и сервера:

Создадим класс SqlHelper, который будет являться помощником работы с БД.
Код:
public class SqlHelper {

    private static final String user = "логин";
    private static final String password = "пароль";
    private static final String url = "jdbc:mysql://хост:3306/имя базы данных";

    public static Connection connection;
    public static Statement statement;
    public static ResultSet resSet;

    public static SqlHelper sqlHelperInstance = new SqlHelper(); //Создадим instance нашего класса для быстрого доступа

    public void setConnection() throws SQLException {
        try {
            Class.forName("com.mysql.jdbc.Driver"); //Настраиваем драйвер JDBC
            System.out.println("Driver was loaded");
        } catch (ClassNotFoundException e) {
            System.out.println("Class not Found!!!");
        }

        try {
            connection = DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            e.printStackTrace();
            System.err.println("Connection can't be estabilished!!!"); //В случае если не сможет подключиться
        }

        if (connection != null) {
            System.out.println("Connection with database was created!!!"); //Если все-таки соединение установлено, выведется это сообщение
        } else System.out.println("There's fail to create connection!");
    }

    public void registerPlayer(String name) throws SQLException { //Функция регистрация игрока в базе данных на основе запроса БД
        statement.executeUpdate("INSERT INTO players (name, money) VALUES ('" + name + "', '0',)");
        System.out.println("Register player");
    }

    public boolean playerRegistered(String name) throws SQLException { //Возвращает true, если игрок уже зарегистрирован в системе
        statement = connection.createStatement();
        resSet = statement.executeQuery("SELECT EXISTS(SELECT * FROM players WHERE name='" + name + "')");
        resSet.next();
        if (resSet.getInt(1) == 1) {
            System.out.println("Player registered");
            return true;
        } else {
            System.out.println("PLayer not registered");
            return false;
        }

    }
}



Теперь пропишем действия, при котором у нас должен вызываться эти методы

Код:
public class EventHandler { //Регистрируем игрока при входе если он не зарегистрирован в системе

    @SubscribeEvent
    public void onPlayerLogsIn(PlayerEvent.PlayerLoggedInEvent event) throws SQLException {
        EntityPlayer player = event.player;

        if (!player.worldObj.isRemote) {
            if (!SqlHelper.sqlHelperInstance.playerRegistered(player.getDisplayNameString())) {
                SqlHelper.sqlHelperInstance.registerPlayer(player.getDisplayNameString());

            }
        }
    }
И вот что у меня получилось - настоящая автоматическая регистрация в БД!
image.png


Дальше уже можно использовать БД как хотите, но опять же рекомендую настоятельно выучить язык SQL, это существенно облегчит вам задачу!
 
1,470
19
189
Очень годно и полезно

Но "впринципе"
 

Eifel

Модератор
1,624
79
609
А еще лучше будет юзать PreparedStatement вместо Statement
 
2,505
81
397
Надеюсь, ты не собираешься раздавать клиентам мод с паролем и прочим?
Еще операции, связанные с БД, лучше выполнять в отдельном потоке, если это допустимо.
Почему ты в проверке на зарегистрированность создаешь новый Statement, а в других запросах используешь уже созданный? Новый запрос - новый Statement.
После работы со Statement его лучше закрывать. Поэтому хорошо использовать try with resources.
 
Dahaka написал(а):
Надеюсь, ты не собираешься раздавать клиентам мод с паролем и прочим?
Еще операции, связанные с БД, лучше выполнять в отдельном потоке, если это допустимо.
Почему ты в проверке на зарегистрированность создаешь новый Statement, а в других запросах используешь уже созданный? Новый запрос - новый Statement.
После работы со Statement его лучше закрывать. Поэтому хорошо использовать try with resources.

Пароль и логин можно вынести в конфиг мода чтобы на сервере настроить. А вот насчёт Statement не разбирался, так как учил JDBC за один вечер)
 

deleted.user

Мошенник
321
43
Статья полезная, но лучше доработай касательно Statement'ов и вынесения в конфиг. Это уже будет заслуживать таки УВОЖЕНИЯ. Молодец!
 

Icosider

Kotliner
Администратор
3,603
99
664
"if (!player.worldObj.isRemote) {"
Сейчас бы на сервере проверять, что это серверная сторона... Или же ты планируешь раздавать доступ к бд налево и направо?
 

tox1cozZ

aka Agravaine
8,456
598
2,893
Все работает, да. Подключаюсь к БД в FMLServerStartingEvent, отключаюсь в FMLServerStoppingEvent.
Когда открываю гуи - отсылаю пакет на сервер, получаю денюжку из БД и отправляю обратно на клиент.
Проблема в том, что получается как-то медленно слишком и сервер начинает лагать.
Может можно это дело как-то оптимизировать? А то через Opis смотрю время и из-за получения денежки большие просадки(примерно 1500), в то время как без БД все отлично.
 
2,505
81
397
Серьезно лаг? У меня, конечно же, в отдельно потоке, но и в мэйн потоке не было лага от одного запроса. Мб запрос кривой?
 
Последнее редактирование:

deleted.user

Мошенник
321
43
По нормальному - бд должна быть на той же машине, где лежит сервер, ну и в таком случае уже не будет большой задержки, плюс да, запрос
 

tox1cozZ

aka Agravaine
8,456
598
2,893
Так вот и я удивляюсь, почему от одного простого запроса все тормозить начинает.
Java:
public int getPlayerMoney(String name){
    String sql = "SELECT money FROM " + table + " WHERE name = :name";
    return connection.createQuery(sql).addParameter("name", name).executeScalar(int.class);
}
А как вынести в отдельный поток-то?
 
2,505
81
397
А что за connection. у тебя? У меня метода createQuery( вообще нет. Может сама эта конструкция, да, красивая, но дорогая?

Создай себе ThreadExecutor и запускай таски по необходимости.
 
2,505
81
397
Попутал. Не ThreadExecutor, а ExecutorService.
 
2,505
81
397
Не проверял, но возможно заработает.
Kotlin:
val executor = Executors.newSingleThreadExecutor()

fun foo(task: Runnable) = executor.submit(task)
А вообще, это довольно удобная штука. Там submit возвращает Future. И основной поток сможет потом когда-нибудь обработать результат выполнения. И не создаются новые потоки (для любителей Thread { /* some work */ }.start()).
 

tox1cozZ

aka Agravaine
8,456
598
2,893
Блин, объясни смысл, я догнать никак не могу.
Зачем нужно отдельный поток, если мы все равно блокируем основной и ждем результат и нашего потока, чтобы уже его потом обработать в основном. Какая разница, обработаю я в основном потоке данные или заблочу его до того момента как данные обработаются в другом потоке?
 
2,505
81
397
Основной-то не блочится. И смотря что тебе нужно, после того, как прочитал данные с sql.
 
Сверху