dev.net.ua

Українська Спільнота Розробникiв
 
Ласкаво просимо до dev.net.ua Увійти | Приєднатися | Допомога | Увійти Live ID
в Пошук

usarskyy

Маленька препарація SqlDataReader

Якось давніше пощастило прочитати короткий мануал по правильному поводженню з SqlDataReader. Лінк зараз не згадаю, тому прийдеться кортко переказати зміст мануалу.

1. Старайтесь якнайменше використовувати ось такий вид запису:
        using (IDataReader reader = SPs.TestSelect().GetReader())
    {
        for (int j = 0; reader.Read(); j++)
        {
            toReturn[j] = new TestItem();


            toReturn[j].Age      = (int)reader["Age"];
            toReturn[j].Gender   = reader["Gender"] as string;
            toReturn[j].ID       = (int)reader["ID"];
            toReturn[j].Login    = reader["LoginName"] as string;
            toReturn[j].Name     = reader["Name"] as string;
        }
    }


2. Якщо настільки "приперло" написати код схожий на поданий вище, то модифікуйте його таким чином:
    using (IDataReader reader = SPs.TestSelect().GetReader())
    {
        int ageIndex      = reader.GetOrdinal("Age");
        int genderIndex   = reader.GetOrdinal("Gender");
        int idIndex       = reader.GetOrdinal("ID");
        int loginIndex    = reader.GetOrdinal("LoginName");
        int nameIndex     = reader.GetOrdinal("Name");

        for (int j = 0; reader.Read(); j++)
        {
            toReturn[j] = new TestItem();

            toReturn[j].Age      = (int)reader[ageIndex];
            toReturn[j].Gender   = reader[genderIndex] as string;
            toReturn[j].ID       = (int)reader[idIndex];
            toReturn[j].Login    = reader[loginIndex] as string;
            toReturn[j].Name     = reader[nameIndex] as string;
        }
    }

3. А в ідеалі код має бути схожим на:
    using (IDataReader reader = SPs.TestSelect().GetReader())
    {
        int ageIndex      = reader.GetOrdinal("Age");
        int genderIndex   = reader.GetOrdinal("Gender");
        int idIndex       = reader.GetOrdinal("ID");
        int loginIndex    = reader.GetOrdinal("LoginName");
        int nameIndex     = reader.GetOrdinal("Name");

        for (int j = 0; reader.Read(); j++)
        {
            toReturn[j] = new TestItem();

            toReturn[j].Age      = reader.GetInt32(ageIndex);
            toReturn[j].Gender   = reader.GetString(genderIndex);
            toReturn[j].ID       = reader.GetInt32(idIndex);
            toReturn[j].Login    = reader.GetString(loginIndex);
            toReturn[j].Name     = reader.GetString(nameIndex);
        }
    }

В прикладах toReturn – це «TestItem[] toReturn» + ви якимось чином дізнались правильний розмір масиву :) Результатом роботи методу є цей самий «TestItem[] toReturn»

 

Перше що кидається в очі, чому 3-й варіант кращий за перші два – це unboxing при присвоєнні значень елементам масиву toReturn.
А чим другий кращий за перший? Як саме третій варіант відрізняється від перших двох? Про це в мануалі не написали, тому прийшлось розбиратись самотужки…

Звичайно, можна було б написати в саппорт майкрософта і запитати у них, але я вирішив, що з Reflector-ом я справлюсь швидше :)

Хто до читав до цього місця і вирішив, що дана тема йому малоцікава, може далі не читати. Так як далі буде тільки дизасемблювання стандартних ліб…


Отож, пройдемось по питаннях по порядку…
1. Чим другий варіант кращий за перший?

Відкриваємо у рефлекторі клас SqlDataReader і дивимось на тексти перегруженого оператора «[]».

public override object this[string name]
{

    get

    {

        return this.GetValue(this.GetOrdinal(name));

    }
}

та

	public override object this[int i]
{
get
{
return this.GetValue(i);
}

}

 

В першому лістінгу  бачимо, що перед викликом методу «GetValue» (який очевидно повертає потрібне нам значення :) ), йде виклик «GetOrdinal»… Відмінність ніби і не значна, але здоровий глузд підказує, що виконувати «GetOrdinal» лишні 200-300 тис разів не варто :) Тому від цього моменту конструкція типу reader["SomeKeyName"] перестає для нас існувати.


2. Як саме третій варіант відрізняється від перших двох?

Те що там відсутнє явне приведення типів – це ми бачимо, але що заховано всередині..? Для прикладу препаруємо метод «GetInt32»

public override int GetInt32(int i)
{

    this.ReadColumn(i);

    return this._dataIdea.Int32;
}

Звідси ясно, що нічого не ясно :) Треба копати глибше! Виклик методу «ReadColumn» я опущу і перейду до «return this._dataIdea.Int32». Цікаво, а що ж за поле  «_data»?

    private SqlBuffer[] _data;

Хм… Все одно нічого не ясно… Дивимось що таке «SqlBuffer» (весь код постити не буду, просто покажу те що нам потрібне):

    internal sealed class SqlBuffer
    {

        // Fields

        private bool _isNull;
private object _object;
private StorageType _type;
private
Storage _value;
        // Methods
        // Properties
        internal object Value
{
get
{
if (this.IsNull)
{
return DBNull.Value;
}
                       	switch (this._type)
{
case StorageType.Empty:
return DBNull.Value;

case StorageType.Boolean:
return this.Boolean;

case
StorageType.Byte:
return this.Byte;

case StorageType.DateTime:
return this.DateTime;

case StorageType.Decimal:
return this.Decimal;

case StorageType.Double:
return this.Double;

case StorageType.Int16:
return this.Int16;

case StorageType.Int32:
return this.Int32;

case StorageType.Int64:
return this.Int64;

case StorageType.Money:
return this.Decimal;

case StorageType.Single:
return this.Single;

case
StorageType.String:
return this.String;

case StorageType.SqlBinary:
return this.ByteArray;

case
StorageType.SqlCachedBuffer:
return ((SqlCachedBuffer) this._object).ToString();
 				case StorageType.SqlGuid:
return this.Guid;

case StorageType.SqlXml:
{
SqlXml xml = (SqlXml) this._object;
return xml.Value;
}
}
return null;
}
}
	internal int Int32
{
get
{
this.ThrowIfNull();

if (StorageType.Int32 == this._type)
{
return this._value._int32;
}
			return (int) this.Value;
}

set
{
this._value._int32 = value;
this._type = StorageType.Int32;
this._isNull = false;
}
}

 //Other properties
           

	[StructLayout(LayoutKind.Explicit)]
internal struct Storage
{
// Fields
[FieldOffset(0)]
internal bool _boolean;

[FieldOffset(0)]
internal byte _byte;
        	[FieldOffset(0)]
internal SqlBuffer.DateTimeInfo _dateTimeInfo;
        	[FieldOffset(0)]
internal double _double;
        	[FieldOffset(0)]
internal short _int16;
        	[FieldOffset(0)]
internal int _int32;
        	[FieldOffset(0)]
internal long _int64;
        	[FieldOffset(0)]
internal SqlBuffer.NumericInfo _numericInfo;
        	[FieldOffset(0)]
internal float _single;
}
    	internal enum StorageType
{
Empty,
Boolean,
Byte,
DateTime,
Decimal,
Double,
Int16,
Int32,
Int64,
Money,
Single,
String,
SqlBinary,
SqlCachedBuffer,
SqlGuid,
SqlXml
}

}

З приведеного вище коду видно, що значення потрібної нам табличної ячейки зберігається в одному елементі SqlBuffer. Для кожного типу даних SqlBuffer має окреме поле (див клас Storage). Отже, якщо кожен тип має своє поле, то ніякого приведення типів з object не відбувається!

Натомість при виклику методу «GetValue» (див текст пергруженого оператора «[]») ми витягуємо значення з проперті «Value», її значення проходить boxing в object, а потім unboxing з object в потрібний нам тип. Про перфоменс в такому випадку говорити не приходиться…

Варто зауважити, що клас Storage зберігає не усі типи в окремих змінних... Для прикладу, Storage не має поля для зберігання String-ових значень. Отримання потрібного значення там виглядає ось так:

internal string String
{
get
{
this.ThrowIfNull();
if (StorageType.String == this._type)
{
return (string) this._object;
}
if (StorageType.SqlCachedBuffer == this._type)
{
return ((SqlCachedBuffer) this._object).ToString();
}
return (string) this.Value;
}
}

Тому, при витягуванні string-значення через GetValue()-метод ми отримуємо звязку з_object_в_string -> з_string_в_object -> знову_з_object_в_string! Повний ахтунг! І хоча реально платформа передає референси на обєкт (String - це ж все-таки референс тип), я вважаю, що 2 надлишкові операції - це 100% зайве!

Законним є питання, а як SqlBuffer дізнається який тип даних він зберігає (і якому полю присвоїти прочитані з БД дані)? Це все відбувається на початку передачі даних з БД, коли клас отримує метадані у вигляді потоку байтів. Розписувати тут весь процес я не буду, так як це вимагає місця та часу.

 

P.S.: Ніякого відкриття я звичайно ж не зробив… просто хотілось прояснити для себе які механізми ховаються за звичайними методами. Надіюсь, що комусь це також згодиться :)

P.P.S.: прошу вибачення за форматування коду... у мене ніяк не виходить подружитись з вбудованим редактором :(

Опубліковані Sunday, December 02, 2007 4:55 PM від usarskyy
Помічено як:

Коментарі

 

Brand сказав:

Якщо вже так прискіпливо придивлятися, то зчитувати значення стовпчиків потрібно у тому порядку у якому вони ідуть у ResultSet'і.

Але останнім часом мене вистачає лише на

For Each User In From U In Users _

                               Order By U.Name

...

Next

:)

December 2, 2007 12:48 PM
 

demon_xxi сказав:

Сразу скажу спасибо за проделанную работу - она не лишняя, но и покритикую.

Если говорить о целесообразности - следует цифры приводить!

Если проигрыш есть по перформанс - то следует заморачиваться точными вызовами, если нет - то и не стоит умничать.

Хороший пример - Generic. про проверку типов не говорю, только о производительности. Так вот generiс проигрывает прямому кастингу, но все же меня ни за что не заставить в повседневной жизни писать больше кода ...

Думаю интеесне будет продолжение этого анализа над библиотеками LINQ!

Вот там уж точно все стоит отменить и вернутся к ADO :)

Ибо оно ж быстрее наверное ;)

December 4, 2007 3:46 AM
Анонімні коментарі деактивовані. Увійдіть або Зареєструйтесь щоб мати доступ до ресурсів Спільноти.