12.01.2018, 11:02
(
Last edited by RIDE2DAY; 13/01/2018 at 08:21 AM.
)
When to use the different MySQL queries
[*] Index- Introduction
- MySQL queries
- Threaded and non-threaded queries
- What function should I use to send a query?
- mysql_query
- mysql_tquery
- mysql_pquery
- MySQL transactions
- mysql_format or format?
- How to create a table correctly
- The end
Hello guys, this is the second part of my MySQL tutorial series I'm writing and at the same time it's the second one I write in English, so just like I say always: if you find any grammatical error let me know please. If you read the first part you should know that I'm using the R41-4 version, if you didn't I invite you to in order to know how to establish a connection correctly. In this tutorial we're going to see what type of queries we can use and in which cases we should use each one. Before anything I'd like to let you know that I'm not a MySQL expert, so if you know better code techniques you're welcome to share them with us.
[*] MySQL queries
So, what is a query? Well, a dictionary might help you with that! Alright, jokes apart, a query basically is a "question" we send to the database in order to receive an "answer", also known as "result". The result contains the answer to our query, so that's what we process when the MySQL server returns it. With the current MySQL plugin we can send different types of queries, by different types I mean how they will be executed/treated by the SA-MP server. Here's where threaded and non-threaded queries come in.
[*] Threaded and non-threaded queries
In SA-MP the script runs on a single thread, that means that if you execute two functions consecutively the second one will run after the first one finishes, but what would happen if the first function requires a lot of time? You can read more about it on this tutorial wrote by Slice.
In short, threaded queries have been created in order to avoid unnecessary waits. Multithreading is something very useful because we can't predict the time a query requires. If the server sleeps too much we can get into desynchronization troubles. Let's see a little graphic explanation, the black line represents the SA-MP script while the green one the MySQL query:
Non-threaded query: the SA-MP script sleeps while the MySQL query is being processed. | Threaded query: the SA-MP script runs while the MySQL query is being processed. |
[*] What function should I use to send a query?
As I said before, with the current plugin we can send different types of queries. Let's see what we got:
- mysql_query: sends a non-threaded query to the MySQL server.
- mysql_tquery: sends a query which will be executed in another thread and calls a callback (if there was one specified) when the execution is successfully finished.
- mysql_pquery: sends a query which will be executed in another thread concurrently and calls a callback (if there was one specified) when the execution is successfully finished.
- mysql_query
Code:Parameters: MySQL:handle -> The connection handle this will be processed on. const query[] -> The query you want to execute. bool:use_cache = true -> Set to true by default, if you don't want to use the cache set to false. Return values: A valid cache-id if use_cache was set to true, MYSQL_INVALID_CACHE otherwise.
In my opinion it's hard to tell when non-threaded queries are the way to go, but they might come in handy in some rare cases. Imagine a gmx case, if you use tquery/pquery in OnGameModeInit players might connect before your systems have loaded because the SA-MP script won't sleep while the queries are being processed. Neverthless, using non-threaded queries OnPlayerConnect won't be called until all queries have been processed and OnGameModeInit finished.
PHP Code:public OnGameModeInit()
{
new rows;
new Cache:result;
result = mysql_query(db_handle, "SELECT * FROM `houses`"); // Select all houses.
if(cache_get_row_count(rows)) // Make sure the function succeeds.
{
for(new r = 0; r < rows; r++) // Process the rows (if there are any), if rows = 0 the code inside the for loop won't run.
{
// Cache functions here.
}
}
cache_delete(result); // The previous result has been processed, delete it and free the memory.
result = mysql_query(db_handle, "SELECT * FROM `vehicles`"); // Select all vehicles.
// Repeat the process here.
return 1;
}
PHP Code:public OnGameModeInit()
{
LoadHouses();
LoadVehicles();
return 1;
}
LoadHouses()
{
new rows;
new Cache:result;
result = mysql_query(db_handle, "SELECT * FROM `houses`");
if(cache_get_row_count(rows))
{
for(new r = 0; r < rows; r++)
{
}
}
cache_delete(result);
return 1;
}
LoadVehicles()
{
new rows;
new Cache:result;
result = mysql_query(db_handle, "SELECT * FROM `vehicles`");
if(cache_get_row_count(rows))
{
for(new r = 0; r < rows; r++)
{
}
}
cache_delete(result);
return 1;
}
PHP Code:cache_delete(
mysql_query(db_handle, "UPDATE `houses` SET `price` = %d WHERE `id` = 50")
);
/* Notice the cache_delete function, we're not using SELECT there but the
UPDATE statement will return the amount of rows that were actually changed.
If you want to ignore the result, set the use_cache parameter to false: */
mysql_query(db_handle, "UPDATE `houses` SET `price` = %d WHERE `id` = 50", false);
- mysql_tquery
Code:Parameters: MySQL:handle -> The connection handle this will be processed on. const query[] -> The query you want to execute. const callback[] -> The result callback to call after the query successfully executed (optional). const format[] -> The format specifier string for the result callback (optional). {Float,_}:... -> Indefinite number of arguments (optional). Return values: 1 if the query was successfully queued for execution, 0 otherwise.
PHP Code:public OnPlayerConnect(playerid)
{
new query[73];
new p_name[MAX_PLAYER_NAME];
GetPlayerName(playerid, p_name, sizeof(p_name));
// Later we'll see why we're using mysql_format instead of format.
mysql_format(db_handle, query, sizeof(query), "SELECT COUNT(*) FROM `players` WHERE `name` = '%e'", p_name);
mysql_tquery(db_handle, query, CheckPlayerAccount, "d", playerid);
return 1;
}
forward CheckPlayerAccount(playerid);
public CheckPlayerAccount(playerid)
{
new value;
/* COUNT is a MySQL function, in this case it'll return a row even if the name
doesn't exist in our table. If it doesn't exist the value will be 0, otherwise
it'll be 1 or the amount of times the name is found.
In the next function we're using 0 as row index and column index because we're
working with a row which has one column only. */
cache_get_value_index_int(0, 0, value);
if(value > 0)
{
// That name is registered.
}
else
{
// That name isn't registered.
}
return 1; // This is very important, return 1 in order to delete the cache.
}
It's good to know that these queries are queued, if you send two consecutive queries the second one won't be processed until the first one finishes. So yes, they're good for those cases where a query depends on a previous one, for example:
PHP Code:public OnGameModeInit()
{
mysql_tquery(db_handle, "CREATE TABLE IF NOT EXISTS `houses` (...)");
mysql_tquery(db_handle, "SELECT * FROM `houses`", OnHousesLoad);
/* You can't select the houses if their table hasn't been created,
doing this the second query will be executed once the first one finishes.
Notice how the first query doesn't use any callback, we don't need it in this case. */
return 1;
}
forward OnHousesLoad();
public OnHousesLoad()
{
// Process the houses selected here.
return 1; // Delete the cache.
}
- mysql_pquery
Code:Parameters: MySQL:handle -> The connection handle this will be processed on. const query[] -> The query you want to execute. const callback[] -> The result callback to call after the query successfully executed (optional). const format[] -> The format specifier string for the result callback (optional). {Float,_}:... -> Indefinite number of arguments (optional). Return values: 1 if the query was successfully queued for execution, 0 otherwise.
PHP Code:public OnGameModeInit()
{
mysql_pquery(db_handle, "CREATE TABLE IF NOT EXISTS `houses` (...)");
mysql_pquery(db_handle, "SELECT * FROM `houses`", OnHousesLoad);
/* This code isn't good, avoid it. The SELECT statement might execute before the
CREATE TABLE one. That wouldn't make sense and it might generate errors. */
return 1;
}
forward OnHousesLoad();
public OnHousesLoad()
{
return 1;
}
PHP Code:public OnPlayerConnect(playerid)
{
new query[73];
new p_name[MAX_PLAYER_NAME];
GetPlayerName(playerid, p_name, sizeof(p_name));
mysql_format(db_handle, query, sizeof(query), "SELECT COUNT(*) FROM `players` WHERE `name` = '%e'", p_name);
// Notice we're using mysql_pquery.
mysql_pquery(db_handle, query, CheckPlayerAccount, "d", playerid);
return 1;
}
forward CheckPlayerAccount(playerid);
public CheckPlayerAccount(playerid)
{
new value;
cache_get_value_index_int(0, 0, value)
if(value > 0)
{
// That name is registered.
}
else
{
// That name isn't registered.
}
return 1; // Delete the cache.
}
It depends on the POOL_SIZE option, by default it's set to two but you can change its value with mysql_set_option or set it inside the .ini file used by mysql_connect_file. If you want to know more about these functions take a look to the first tutorial.
The current plugin supports MySQL transactions but they cannot be used with all the functions we've been talking about before, only mysql_tquery supports them. Let's analyze a hypothetical case: suppose you're leaving a mod garage with your tuned Elegy, how do you save its components? Well, before anything you must create a table to add them to. Table's structure should look something like this:
PHP Code:
CREATE TABLE IF NOT EXISTS `vehicle_mods` (
`vehicleid` mediumint(11) NOT NULL,
`slot` tinyint(4) NOT NULL,
`componentid` smallint(6) NOT NULL,
UNIQUE KEY `vehicleid` (`vehicleid`,`slot`),
FOREIGN KEY (`vehicleid`) REFERENCES `vehicle_info`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/* vehicleid is the SQL ID of the vehicle which we've saved inside the vehicle_info table. */
PHP Code:
public OnEnterExitModShop(playerid, enterexit, interiorid)
{
if(!enterexit)
{
new componentid;
for(new i = 0; i < 14; i++)
{
componentid = GetVehicleComponentInSlot(GetPlayerVehicleID(playerid), i);
if(componentid != 0)
{
// Send save query here.
}
}
}
return 1;
}
PHP Code:
mysql_format(db_handle, query, sizeof(query),
"INSERT INTO `vehicle_mods` VALUES(%d, %d, %d) ON DUPLICATE KEY UPDATE `componentid` = %d;",
v_Info[idx][SQLID],
i,
componentid,
componentid
);
mysql_pquery(db_handle, query); // Notice we're using mysql_pquery.
That technique isn't bad at all, it works, but we can do it better: MySQL transactions. This would be even faster:
PHP Code:
mysql_tquery(db_handle, "START TRANSACTION;"); // Start transaction.
for(new i = 0; i < 14; i++)
{
componentid = GetVehicleComponentInSlot(GetPlayerVehicleID(playerid), i);
if(componentid != 0)
{
mysql_format(db_handle, query, sizeof(query),
"INSERT INTO `vehicle_mods` VALUES(%d, %d, %d) ON DUPLICATE KEY UPDATE `componentid` = %d;",
v_Info[idx][SQLID],
i,
componentid,
componentid
);
mysql_tquery(db_handle, query); // Notice we're using mysql_tquery.
}
}
mysql_tquery(db_handle, "COMMIT;"); // Commit transaction.
PHP Code:
mysql_tquery(db_handle, "START TRANSACTION;");
mysql_tquery(db_handle, "SELECT * FROM ...", ...);
mysql_tquery(db_handle, "INSERT INTO ...", ...);
mysql_tquery(db_handle, "UPDATE ...", ...);
mysql_tquery(db_handle, "COMMIT;");
Many people asked me about this, it's really easy: mysql_format uses the charset you've set with mysql_set_charset, if I'm not wrong by default it's set to utf8. I'm not really sure about which charset uses SA-MP's format function but I think it's utf8 also.
So, supposing both use the same charset by default, why would I use mysql_format? It has special format specifiers, like %e, which escapes strings automatically instead of using mysql_escape_string. Check the documentation to see what other specifiers it supports.
[*] How to create a table correctly
As I told you before, we should make sure our tables have been created before using SELECT statements. Currently we can run SQL statements written in external files using:
- mysql_query_file: reads all queries from the specified file and executes them in a non-threaded manner.
- mysql_tquery_file: reads all queries from the specified file and executes them in a threaded manner.
PHP Code:
public OnGameModeInit()
{
mysql_query_file(db_handle, "my_tables.sql");
LoadGarages(); // SELECT statement inside.
return 1;
}
- my_tables.sql
PHP Code:CREATE TABLE IF NOT EXISTS `garage_info` (
`id` mediumint(9) NOT NULL AUTO_INCREMENT,
`typeid` tinyint(4) NOT NULL,
`price` mediumint(9) NOT NULL,
`level` tinyint(4) NOT NULL,
`x` float NOT NULL,
`y` float NOT NULL,
`z` float NOT NULL,
`angle` float NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `garage_owners` (
`garageid` mediumint(9) NOT NULL,
`ownerid` mediumint(9) NOT NULL,
UNIQUE KEY `garageid` (`garageid`,`ownerid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
// Notice we're using CREATE TABLE IF NOT EXISTS in both cases.
[*] The end
This is where the second tutorial finishes, on the next one we're going to see how to make a login/register system using Whirlpool hashes. You might follow the development of the MySQL gamemode I'm writing for these tutorials on GitHub, that way you could see and understand better what I do.
Thank you for reading my thread, if you want to support what I do you might buy me a coffe! Subscribe to my ******* channel if you wish to follow the development of my gamemode.
Please, if you find any error let me know so I can correct it.
Arrivederci!