add cache_rows option to enable/disable internal row caching for results

This commit is contained in:
Brian Lopez 2010-08-27 12:08:48 -07:00
parent 10222fb455
commit ae6c33a13f
4 changed files with 39 additions and 16 deletions

View File

@ -100,7 +100,7 @@ The default result type is set to :hash, but you can override a previous setting
I may add support for :as => :csv or even :as => :json to allow for *much* more efficient generation of those data types from result sets.
If you'd like to see either of these (or others), open an issue and start bugging me about it ;)
== Timezones
=== Timezones
Mysql2 now supports two timezone options:
@ -112,14 +112,14 @@ Then, if :application_timezone is set to say - :local - Mysql2 will then convert
Both options only allow two values - :local or :utc - with the exception that :application_timezone can be [and defaults to] nil
== Casting "boolean" columns
=== Casting "boolean" columns
You can now tell Mysql2 to cast tinyint(1) fields to boolean values in Ruby with the :cast_booleans option.
client = Mysql2::Client.new
result = client.query("SELECT * FROM table_with_boolean_field", :cast_booleans => true)
== Async
=== Async
Mysql2::Client takes advantage of the MySQL C API's (undocumented) non-blocking function mysql_send_query for *all* queries.
But, in order to take full advantage of it in your Ruby code, you can do:
@ -136,6 +136,14 @@ NOTE: Because of the way MySQL's query API works, this method will block until t
So if you really need things to stay async, it's best to just monitor the socket with something like EventMachine.
If you need multiple query concurrency take a look at using a connection pool.
=== Row Caching
By default, Mysql2 will cache rows that have been created in Ruby (since this happens lazily).
This is especially helpful since it saves the cost of creating the row in Ruby if you were to iterate over the collection again.
If you only plan on using each row once, then it's much more efficient to disable this behavior by setting the :cache_rows option to false.
This would be helpful if you wanted to iterate over the results in a streaming manner. Meaning the GC would cleanup rows you don't need anymore as you're iterating over the result set.
== ActiveRecord
To use the ActiveRecord driver, all you should need to do is have this gem installed and set the adapter in your database.yml to "mysql2".
@ -182,6 +190,8 @@ For example, if you were to yield 4 rows from a 100 row dataset, only 4 hashes w
Now say you were to iterate over that same collection again, this time yielding 15 rows - the 4 previous rows that had already been turned into ruby hashes would be pulled from an internal cache, then 11 more would be created and stored in that cache.
Once the entire dataset has been converted into ruby objects, Mysql2::Result will free the Mysql C result object as it's no longer needed.
This caching behavior can be disabled by setting the :cache_rows option to false.
As for field values themselves, I'm workin on it - but expect that soon.
== Compatibility

View File

@ -12,7 +12,7 @@ static VALUE intern_encoding_from_charset;
static ID intern_new, intern_utc, intern_local, intern_encoding_from_charset_code,
intern_localtime, intern_local_offset, intern_civil, intern_new_offset;
static ID sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_application_timezone,
sym_local, sym_utc, sym_cast_booleans;
sym_local, sym_utc, sym_cast_booleans, sym_cache_rows;
static ID intern_merge;
static void rb_mysql_result_mark(void * wrapper) {
@ -316,7 +316,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
ID db_timezone, app_timezone, dbTz, appTz;
mysql2_result_wrapper * wrapper;
unsigned long i;
int symbolizeKeys = 0, asArray = 0, castBool = 0;
int symbolizeKeys = 0, asArray = 0, castBool = 0, cacheRows = 1;
GetMysql2Result(self, wrapper);
@ -339,6 +339,10 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
castBool = 1;
}
if (rb_hash_aref(opts, sym_cache_rows) == Qfalse) {
cacheRows = 0;
}
dbTz = rb_hash_aref(opts, sym_database_timezone);
if (dbTz == sym_local) {
db_timezone = intern_local;
@ -369,7 +373,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
}
if (wrapper->lastRowProcessed == wrapper->numberOfRows) {
if (cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) {
// we've already read the entire dataset from the C result into our
// internal array. Lets hand that over to the user since it's ready to go
for (i = 0; i < wrapper->numberOfRows; i++) {
@ -380,11 +384,13 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
rowsProcessed = RARRAY_LEN(wrapper->rows);
for (i = 0; i < wrapper->numberOfRows; i++) {
VALUE row;
if (i < rowsProcessed) {
if (cacheRows && i < rowsProcessed) {
row = rb_ary_entry(wrapper->rows, i);
} else {
row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool);
rb_ary_store(wrapper->rows, i, row);
if (cacheRows) {
rb_ary_store(wrapper->rows, i, row);
}
wrapper->lastRowProcessed++;
}
@ -453,6 +459,7 @@ void init_mysql2_result() {
sym_cast_booleans = ID2SYM(rb_intern("cast_booleans"));
sym_database_timezone = ID2SYM(rb_intern("database_timezone"));
sym_application_timezone = ID2SYM(rb_intern("application_timezone"));
sym_cache_rows = ID2SYM(rb_intern("cache_rows"));
rb_global_variable(&opt_decimal_zero); //never GC
opt_decimal_zero = rb_str_new2("0.0");

View File

@ -2,12 +2,13 @@ module Mysql2
class Client
attr_reader :query_options
@@default_query_options = {
:as => :hash,
:async => false,
:cast_booleans => false,
:symbolize_keys => false,
:database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
:application_timezone => nil # timezone Mysql2 will convert to before handing the object back to the caller
:as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
:async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
:cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
:symbolize_keys => false, # return field names as symbols instead of strings
:database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
:application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
:cache_rows => true # tells Mysql2 to use it's internal row cache for results
}
def initialize(opts = {})

View File

@ -47,8 +47,13 @@ describe Mysql2::Result do
end
end
it "should cache previously yielded results" do
@result.first.should eql(@result.first)
it "should cache previously yielded results by default" do
@result.first.object_id.should eql(@result.first.object_id)
end
it "should not cache previously yielded results if cache_rows is disabled" do
result = @client.query "SELECT 1", :cache_rows => false
result.first.object_id.should_not eql(result.first.object_id)
end
end