-
Notifications
You must be signed in to change notification settings - Fork 21.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add prepared statements support for Mysql2Adapter
#23461
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What this does? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But how returning a hash instead of array in the result would be affected by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see now. Thanks for the explanation! |
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
require 'active_record/connection_adapters/abstract_mysql_adapter' | ||
require 'active_record/connection_adapters/mysql/database_statements' | ||
|
||
gem 'mysql2', '>= 0.3.18', '< 0.5' | ||
gem 'mysql2', '~> 0.4.4' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to bump mysql2 minimum version? 0.4.x is only required if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, mysql2 0.4.3 was broken without prepared statements. |
||
require 'mysql2' | ||
|
||
module ActiveRecord | ||
|
@@ -35,9 +36,11 @@ module ConnectionAdapters | |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use the value of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for prepared_statements disabled by default (keep previous default). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should respect it. If users ask to enable it, it should be enabled, to disable it, it should be disabled. This implementation while keeping the default is enabling if you do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation is: if config.key?(:prepared_statements)
# @prepared_statements is configured in `super`
super
else
# keep the previous default (disabled) when `:prepared_statements` is not specified
@prepared_statements = false
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh. I missed that change that moved AbstractAdapter to configure prepared_statements 👍 |
||
configure_connection | ||
end | ||
|
||
|
@@ -103,55 +106,6 @@ def disconnect! | |
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we call
super
here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PG and SQLite3 adapters does not call
super
.https://github.com/rails/rails/blob/v5.0.0.beta3/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L230-L232
https://github.com/rails/rails/blob/v5.0.0.beta3/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L149-L151
https://github.com/rails/rails/blob/v5.0.0.beta3/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L356-L358
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍