javamysqljdbcperconaconnector-j

Empty resultset on BINARY comparison though java.sql.PreparedStatement.setBytes


I have Percona Mysql server and Java client with custom ORM. In DB I have table:

CREATE TABLE `PlayerSecret` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `created` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  `secret` binary(16) NOT NULL,
  `player_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `PlayerSecret_secret_unique` (`secret`),
  KEY `PlayerSecret_player_id` (`player_id`)
) ENGINE=InnoDB AUTO_INCREMENT=141 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 

I found that query SELECT PlayerSecret.player_id FROM PlayerSecret WHERE PlayerSecret.secret = ? returns an empty resultset when parameter is provided by java.sql.PreparedStatement#setBytes method, and works as expected though java.sql.PreparedStatement#setBinaryStream. I've enabled mysql general log and found that in this log both queries are the same, I've checked this in hex mode.

In general log it looks like:

SELECT PlayerSecret.player_id FROM PlayerSecret WHERE PlayerSecret.secret = '<96>R\Ø8üõA\í¤Z´^E\Ô\ÊÁ\Ö'

Query parameter from general log in hex mode: 2796 525c d838 fcf5 415c eda4 5ab4 055c d45c cac1 5cd6 27

Value in database:

mysql> select hex(secret) from PlayerSecret where id=109;
+----------------------------------+
| hex(secret)                      |
+----------------------------------+
| 9652D838FCF541EDA45AB405D4CAC1D6 |
+----------------------------------+
1 row in set (0.00 sec)

The problem is that my ORM doing this query though setBytes method, I think it is right way for BINARY data type, but it doesn't work.

Part of my.cnf with encoding settings(maybe it's matters):

[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
general_log = on
general_log_file=/var/log/mysql/mysqld_general.log
require_secure_transport = ON
init-connect = SET collation_connection = utf8mb4_unicode_ci
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

Java code:

var uuid = UUID.fromString("9652d838-fcf5-41ed-a45a-b405d4cac1d6");
var array = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array();
// works
stmt.setBinaryStream(index, new ByteArrayInputStream(array));
// don't works
stmt.setBytes(index, array);

I can't understand what is the difference between both cases, and how to fix this for setBytes variant. Maybe someone can clarify this or point me to important parts/places?

My env:


Solution

  • Finally I've figured it out. The problem was in character_set_client=utf8 instead of utf8mb4.
    This query shows the difference between expected values and real thread values(think it's very handy query):

    SELECT VARIABLE_NAME, gv.VARIABLE_VALUE 'Global', tv.VARIABLE_VALUE 'Thread value', THREAD_ID, PROCESSLIST_ID 
    FROM performance_schema.global_variables gv   
      JOIN performance_schema.variables_by_thread tv USING (VARIABLE_NAME) 
      JOIN performance_schema.threads USING(THREAD_ID) 
    WHERE gv.VARIABLE_VALUE <> tv.VARIABLE_VALUE ;
    
    +-----------------------+--------------------+--------------------+-----------+----------------+
    | VARIABLE_NAME         | Global             | Thread value       | THREAD_ID | PROCESSLIST_ID |
    +-----------------------+--------------------+--------------------+-----------+----------------+
    | autocommit            | ON                 | OFF                |        82 |             56 |
    | character_set_client  | utf8mb4            | utf8               |        82 |             56 |
    | character_set_results | utf8mb4            |                    |        82 |             56 |
    

    When I replaced init-connect = SET collation_connection = utf8mb4_unicode_ci with init_connect='SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci' in my.cnf the problem disappeared and queries though setBytes began to work as expected.

    Why is it works for setBinaryStream and doesn't work for setBytes - because in first case works this code com.mysql.cj.ServerPreparedQueryBindings#setBinaryStream(int, java.io.InputStream, int):

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, int length) {
        if (x == null) {
            setNull(parameterIndex);
        } else {
            ServerPreparedQueryBindValue binding = getBinding(parameterIndex, true);
            this.sendTypesToServer.compareAndSet(false, binding.resetToType(MysqlType.FIELD_TYPE_BLOB, this.numberOfExecutions));
            binding.value = x;
            binding.isLongData = true;
            binding.bindLength = this.useStreamLengthsInPrepStmts.getValue() ? length : -1;
        }
    }
    

    Important part here is binding.resetToType(MysqlType.FIELD_TYPE_BLOB - driver notes mysql that this data is BLOB

    And in second case com.mysql.cj.ServerPreparedQueryBindings#setBytes(int, byte[]) contains:

    @Override
    public void setBytes(int parameterIndex, byte[] x) {
        if (x == null) {
            setNull(parameterIndex);
        } else {
            ServerPreparedQueryBindValue binding = getBinding(parameterIndex, false);
            this.sendTypesToServer.compareAndSet(false, binding.resetToType(MysqlType.FIELD_TYPE_VAR_STRING, this.numberOfExecutions));
            binding.value = x;
        }
    }
    

    MysqlType.FIELD_TYPE_VAR_STRING means that this is not simple bytes, but string in some encoding(with some collation). I really don't know why driver sets this type of data for bytes - this question stays opened for me.