From b7c521c8e7856fb8e1ce08c8f006c461bdfb014e Mon Sep 17 00:00:00 2001 From: Linden <65407488+thelindat@users.noreply.github.com> Date: Sat, 12 Feb 2022 06:03:23 +1100 Subject: [PATCH] feat(server): database optimisations Limited testing, but works in a fairly clean ESX environment. Sets up foreign keys in the database to allow for easy and optimised character deletion. Foreign keys will only be set where owner and identifier columns exist, the field cannot be NULL and uses VARCHAR. --- server/main.lua | 119 +++++++++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/server/main.lua b/server/main.lua index a0c13e15..5b672bc7 100644 --- a/server/main.lua +++ b/server/main.lua @@ -19,7 +19,7 @@ elseif ESX.GetConfig().Multichar == true then end end - local DB_TABLES = {} + local DB_TABLES = {users = 'identifier'} local FETCH = nil local SLOTS = Config.Slots or 4 local PREFIX = Config.Prefix or 'char' @@ -106,13 +106,13 @@ elseif ESX.GetConfig().Multichar == true then local function DeleteCharacter(source, charid) local identifier = ('%s%s:%s'):format(PREFIX, charid, GetIdentifier(source)) - local query = "DELETE FROM ?? WHERE ?? = ?" - local tableCount = #DB_TABLES - local queries = table.create(tableCount, 0) + local query = 'DELETE FROM %s WHERE %s = ?' + local queries = {} + local count = 0 - for i=1, tableCount do - local v = DB_TABLES[i] - queries[i] = {query = query, values = {v.table, v.column, identifier}} + for table, column in pairs(DB_TABLES) do + count += 1 + queries[count] = {query = query:format(table, column), values = {identifier}} end MySQL.transaction(queries, function(result) @@ -127,47 +127,82 @@ elseif ESX.GetConfig().Multichar == true then end MySQL.ready(function() - MySQL.Async.fetchAll('SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND (COLUMN_NAME = ? OR COLUMN_NAME = ?) ', { - DATABASE, 'identifier', 'owner' - }, function(result) - if result then - local varchar, varsize = {}, 0 - for k, v in pairs(result) do - if v.CHARACTER_MAXIMUM_LENGTH and v.CHARACTER_MAXIMUM_LENGTH >= 40 and v.CHARACTER_MAXIMUM_LENGTH < 60 then varchar[v.TABLE_NAME] = v.COLUMN_NAME varsize = varsize+1 end - table.insert(DB_TABLES, {table = v.TABLE_NAME, column = v.COLUMN_NAME}) - end - if next(varchar) then - local query = "ALTER TABLE ?? MODIFY COLUMN ?? VARCHAR(60)" - local queries = table.create(varsize, 0) + local length = 42 + #PREFIX + local DB_COLUMNS = MySQL.query.await(('SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "%s" AND DATA_TYPE = "varchar" AND COLUMN_NAME IN (?)'):format(DATABASE, length), { + {'identifier', 'owner'} + }) - for k, v in pairs(varchar) do - queries[#queries+1] = {query = query, values = {k, v}} - end + if DB_COLUMNS then + local FK = MySQL.query.await(('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = "FOREIGN KEY" AND TABLE_SCHEMA = "%s"'):format(DATABASE)) + local FOREIGN_KEY = {} - MySQL.Async.transaction(queries, function(result) - if result then - print(('[^2INFO^7] Updated ^5%s^7 columns to use VARCHAR(60)'):format(varsize)) - else - print(('[^2INFO^7] Unable to update ^5%s^7 columns to use VARCHAR(60)'):format(varsize)) - end - end) - end - ESX.Jobs = {} - local function GetJobs() - if ESX.GetJobs then return ESX.GetJobs() - else return exports['es_extended']:getSharedObject().Jobs end - end - repeat - ESX.Jobs = GetJobs() - Citizen.Wait(50) - until next(ESX.Jobs) - FETCH = MySQL.Sync.store("SELECT identifier, accounts, job, job_grade, firstname, lastname, dateofbirth, sex, skin, disabled FROM users WHERE identifier LIKE ? LIMIT ?") + for i = 1, #FK do + FOREIGN_KEY[FK[i].TABLE_NAME] = true end - end) + + local columns = {} + local count = 0 + + for i = 1, #DB_COLUMNS do + local v = DB_COLUMNS[i] + + if v?.CHARACTER_MAXIMUM_LENGTH ~= length then + count += 1 + columns[v.TABLE_NAME] = v.COLUMN_NAME + end + end + + if next(columns) then + local query = 'ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(%s)' + local queries = table.create(count, 0) + + for k, v in pairs(columns) do + queries[#queries+1] = {query = query:format(k, v, length)} + end + + if MySQL.transaction.await(queries) then + print(('[^2INFO^7] Updated ^5%s^7 columns to use VARCHAR(%s)'):format(count, length)) + else + print(('[^2INFO^7] Unable to update ^5%s^7 columns to use VARCHAR(%s)'):format(count, length)) + end + end + + for i = 1, #DB_COLUMNS do + local v = DB_COLUMNS[i] + + if not FOREIGN_KEY[v.TABLE_NAME] then + DB_TABLES[v.TABLE_NAME] = v.COLUMN_NAME + + if v.TABLE_NAME ~= 'users' and v.IS_NULLABLE == 'NO' then + MySQL.update(([[ALTER TABLE `%s` + ADD CONSTRAINT `FK_%s_users` FOREIGN KEY (`%s`) REFERENCES `users` (`identifier`) ON UPDATE CASCADE ON DELETE CASCADE]]):format(v.TABLE_NAME, v.TABLE_NAME, v.COLUMN_NAME), function(result) + if result then + print(('[^2INFO^7] Updated ^5"%s.%s"^7 to use FOREIGN_KEY referencing ^5"users.identifier"^7'):format(v.TABLE_NAME, v.COLUMN_NAME)) + end + end) + end + end + end + + ESX.Jobs = {} + + local function GetJobs() + if ESX.GetJobs then + return ESX.GetJobs() + end + return exports.es_extended:getSharedObject().Jobs + end + + repeat + ESX.Jobs = GetJobs() + Citizen.Wait(50) + until next(ESX.Jobs) + + FETCH = 'SELECT identifier, accounts, job, job_grade, firstname, lastname, dateofbirth, sex, skin, disabled FROM users WHERE identifier LIKE ? LIMIT ?' + end end) RegisterNetEvent('esx_multicharacter:SetupCharacters', function() - AddEventHandler('esx_multicharacter:SetupCharacters', function() SetupCharacters(source) end)