Wednesday, September 30, 2009

Майкл К. Физерс. Эффективная работа с унаследованным кодом



Останнім часом я зустрічав багато посилань на книгу "Эффективная работа с унаследованным кодом" із дуже позитивним відгуками. Тому сподівався, що книга мені сподобається. Але прочитавши її, мої очікування не виправдались. Навпаки, книга здалась мені дуже нудною. Не знаю чия це вина, автора, перекладачів, чи мого сприйняття. Багато сторінок, я просто переглядав по діагоналі. Часто ловив себе на тому, що прочитавши якийсь розділ нічого не зрозумів, бо думки гуляли деінде,  а перечитувати ніякого бажання немає. І взагалі не розумію, як можна стільки уваги приділити питанню - як розірвати залежність між кодом, і жодним словом не обмовитись про Dependecy Injection Containers (IOC). А, точно, це ж успадкований код. До того ж судячи з книги, більшість успадкованого коду - це код на C та C++. Але мене це мало цікавить.

Sunday, September 27, 2009

High/Low generator with SQL

In current project, we use High/Low generator strategy to generate identifiers for entities. (Fabio Maulo wrote a good explanation why using identity is a bad thing). The question I sometimes see in NHibernate forums is what to do if some other applications also use the same database. Usual answer is - implement some service which would generate id, and use it in that applications.
Yesterday I met similar issue. What I needed to do is to insert some fake data using SQL only. For that purpose I wrote such stored procedure:

IF OBJECT_ID('dbo.GenerateHiLoIdRange') IS NOT NULL
  DROP PROCEDURE dbo.GenerateHiLoIdRange
GO
CREATE PROCEDURE dbo.GenerateHiLoIdRange (
    @IdsCount INT
)
AS
/* =============================================================== */
/* == Generates a range of identifiers using High/Low strategy. == */
/* =============================================================== */
BEGIN
    SET NOCOUNT ON;
    
    IF @IdsCount <= 0
    BEGIN
        RAISERROR('@IdsCount should be positive. But was %d', 16, -1, @IdsCount)
        RETURN(-100);
    END

    DECLARE @NextHi INT
    DECLARE @MaxLo  INT
    SET @MaxLo = 100  -- Hardcoded value used over all tables in a database.
 
    BEGIN TRANSACTION UpdateHiLoTable
        
        SET @NextHi = (
            SELECT next_hi 
            FROM hibernate_unique_key WITH (UPDLOCK, ROWLOCK)
        )
        
        UPDATE hibernate_unique_key 
        SET next_hi = @NextHi + 1 + (@IdsCount - 1) / @MaxLo
        WHERE next_hi = @NextHi
        
    COMMIT TRANSACTION UpdateHiLoTable
    
    SELECT @NextHi * @MaxLo + IntValue
    FROM   dbo.Integers(@IdsCount - 1)
END
GO

Main points of procedure:
  • It uses hard coded MaxLo value (100) , which is used over all entities in our application.
  • Calculates new NextHi value based on requested identifiers count and updates hibernate_unique_key table in transaction.
  • To return a result set containing generated identifiers procedure needs some sort of "Integers" table. In my case i simulate such table with a table-valued function.
IF OBJECT_ID('dbo.Integers') IS NOT NULL
  DROP FUNCTION dbo.Integers
GO
CREATE FUNCTION dbo.Integers (
    @MaxValue INT
)
RETURNS @Integers TABLE (IntValue INT NOT NULL)
AS
/* =============================================================== */
/* ==   Returns the [0,  @MaxValue] range of integer values.    == */
/* =============================================================== */
BEGIN
    IF @MaxValue > 9999
    BEGIN
      RETURN;
    END;
    
    DECLARE @Digits TABLE
    (
        Digit INT NOT NULL PRIMARY KEY
    )

    INSERT INTO @Digits (Digit) VALUES (0)
    INSERT INTO @Digits (Digit) VALUES (1)
    INSERT INTO @Digits (Digit) VALUES (2)
    INSERT INTO @Digits (Digit) VALUES (3)
    INSERT INTO @Digits (Digit) VALUES (4)
    INSERT INTO @Digits (Digit) VALUES (5)
    INSERT INTO @Digits (Digit) VALUES (6)
    INSERT INTO @Digits (Digit) VALUES (7)
    INSERT INTO @Digits (Digit) VALUES (8)
    INSERT INTO @Digits (Digit) VALUES (9)
    
    INSERT INTO @Integers
    SELECT Number
    FROM  
        (SELECT 
            Thousands.Digit * 1000 + 
            Hundreds.Digit * 100 + 
            Tens.Digit * 10 + 
            Ones.Digit AS Number
         FROM   
            @Digits Thousands
            CROSS JOIN @Digits Hundreds
            CROSS JOIN @Digits Tens
            CROSS JOIN @Digits Ones
        ) Integers
    WHERE Number <= @MaxValue
    ORDER BY Number
        
    RETURN;
END
GO
Note that number of integers in a function is limited to 10000. But that is what I need.

Now I can generate identifiers in t-sql batches:

CREATE TABLE #TempId (Id INT NOT NULL)

SELECT * FROM hibernate_unique_key

INSERT INTO #TempId
  EXEC dbo.GenerateHiLoIdRange 250

SELECT Id AS Id, 
    'PersonName' +  CAST(ROW_NUMBER() OVER(ORDER BY Id) - 1 AS VARCHAR(255)) AS PersonName
FROM #TempId

DROP TABLE #TempId

In my sandbox database I see such results: