1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge pull request #23461 from kamipo/prepared_statements_for_mysql2_adapter

Add prepared statements support for `Mysql2Adapter`
This commit is contained in:
Jeremy Daer 2016-04-23 22:27:47 -07:00
commit ab56c92f3c
No known key found for this signature in database
GPG key ID: AB8F6399D5C60664
6 changed files with 165 additions and 60 deletions

View file

@ -92,7 +92,7 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
group :db do
gem 'pg', '>= 0.18.0'
gem 'mysql2', '>= 0.4.0'
gem 'mysql2', '>= 0.4.4'
end
end

View file

@ -166,9 +166,9 @@ GEM
mono_logger (1.1.0)
multi_json (1.11.2)
mustache (1.0.2)
mysql2 (0.4.2)
mysql2 (0.4.2-x64-mingw32)
mysql2 (0.4.2-x86-mingw32)
mysql2 (0.4.4)
mysql2 (0.4.4-x64-mingw32)
mysql2 (0.4.4-x86-mingw32)
nio4r (1.2.1)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
@ -295,7 +295,7 @@ DEPENDENCIES
listen (~> 3.0.5)
minitest (< 5.3.4)
mocha (~> 0.14)
mysql2 (>= 0.4.0)
mysql2 (>= 0.4.4)
nokogiri (>= 1.6.7.1)
pg (>= 0.18.0)
psych (~> 2.0)

View file

@ -1,3 +1,10 @@
* MySQL: Prepared statements support.
To enable, set `prepared_statements: true` in config/database.yml.
Requires mysql2 0.4.4+.
*Ryuta Kamizono*
* Schema dumper: Indexes are now included in the `create_table` block
instead of listed afterward as separate `add_index` lines.

View file

@ -1,4 +1,5 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
require 'active_record/connection_adapters/mysql/column'
require 'active_record/connection_adapters/mysql/explain_pretty_printer'
require 'active_record/connection_adapters/mysql/quoting'
@ -56,9 +57,17 @@ module ActiveRecord
INDEX_TYPES = [:fulltext, :spatial]
INDEX_USINGS = [:btree, :hash]
class StatementPool < ConnectionAdapters::StatementPool
private def dealloc(stmt)
stmt[:stmt].close
end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
if version < '5.0.0'
raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0."
end
@ -93,6 +102,12 @@ module ActiveRecord
true
end
# Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
true
end
# Technically MySQL allows to create indexes with the sort order syntax
# but at the moment (5.5) it doesn't yet implement them
def supports_index_sort_order?
@ -178,6 +193,14 @@ module ActiveRecord
end
end
# CONNECTION MANAGEMENT ====================================
# Clears the prepared statements cache.
def clear_cache!
reload_type_map
@statements.clear
end
#--
# DATABASE STATEMENTS ======================================
#++
@ -191,11 +214,6 @@ module ActiveRecord
MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
end
def clear_cache!
super
reload_type_map
end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
log(sql, name) { @connection.query(sql) }

View file

@ -0,0 +1,125 @@
module ActiveRecord
module ConnectionAdapters
module MySQL
module DatabaseStatements
# Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [], preparable: nil)
result = if ExplainRegistry.collect? && prepared_statements
unprepared_statement { super }
else
super
end
@connection.next_result while @connection.more_results?
result
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
arel, binds = binds_from_relation(arel, binds)
@connection.query_options.merge!(as: :hash)
select_result(to_sql(arel, binds), name, binds) do |result|
@connection.next_result while @connection.more_results?
result.first
end
ensure
@connection.query_options.merge!(as: :array)
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
def select_rows(sql, name = nil, binds = [])
select_result(sql, name, binds) do |result|
@connection.next_result while @connection.more_results?
result.to_a
end
end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
if @connection
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
end
super
end
def exec_query(sql, name = 'SQL', binds = [], prepare: false)
if without_prepared_statement?(binds)
execute_and_free(sql, name) do |result|
ActiveRecord::Result.new(result.fields, result.to_a) if result
end
else
exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
ActiveRecord::Result.new(result.fields, result.to_a) if result
end
end
end
def exec_delete(sql, name, binds)
if without_prepared_statement?(binds)
execute_and_free(sql, name) { @connection.affected_rows }
else
exec_stmt_and_free(sql, name, binds) { |stmt| stmt.affected_rows }
end
end
alias :exec_update :exec_delete
protected
def last_inserted_id(result)
@connection.last_id
end
private
def select_result(sql, name = nil, binds = [])
if without_prepared_statement?(binds)
execute_and_free(sql, name) { |result| yield result }
else
exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result }
end
end
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
if @connection
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
end
type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
log(sql, name, binds) do
if cache_stmt
cache = @statements[sql] ||= {
stmt: @connection.prepare(sql)
}
stmt = cache[:stmt]
else
stmt = @connection.prepare(sql)
end
begin
result = stmt.execute(*type_casted_binds)
rescue Mysql2::Error => e
if cache_stmt
@statements.delete(sql)
else
stmt.close
end
raise e
end
ret = yield stmt, result
result.free if result
stmt.close unless cache_stmt
ret
end
end
end
end
end
end

View file

@ -1,7 +1,9 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
require 'active_record/connection_adapters/mysql/database_statements'
gem 'mysql2', '>= 0.3.18', '< 0.5'
require 'mysql2'
raise 'mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+' if Mysql2::VERSION == '0.4.3'
module ActiveRecord
module ConnectionHandling # :nodoc:
@ -35,9 +37,11 @@ module ActiveRecord
class Mysql2Adapter < AbstractMysqlAdapter
ADAPTER_NAME = 'Mysql2'.freeze
include MySQL::DatabaseStatements
def initialize(connection, logger, connection_options, config)
super
@prepared_statements = false
@prepared_statements = false unless config.key?(:prepared_statements)
configure_connection
end
@ -103,55 +107,6 @@ module ActiveRecord
end
end
#--
# DATABASE STATEMENTS ======================================
#++
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
arel, binds = binds_from_relation(arel, binds)
execute(to_sql(arel, binds), name).each(as: :hash) do |row|
@connection.next_result while @connection.more_results?
return row
end
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
def select_rows(sql, name = nil, binds = [])
result = execute(sql, name)
@connection.next_result while @connection.more_results?
result.to_a
end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
if @connection
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
end
super
end
def exec_query(sql, name = 'SQL', binds = [], prepare: false)
result = execute(sql, name)
@connection.next_result while @connection.more_results?
ActiveRecord::Result.new(result.fields, result.to_a) if result
end
def exec_delete(sql, name, binds)
execute to_sql(sql, binds), name
@connection.affected_rows
end
alias :exec_update :exec_delete
def last_inserted_id(result)
@connection.last_id
end
private
def connect