Resumé

SQL2005 OPENXML and Custom Arrays

Cristian Merighi () 0.00

Use XML data type and relevant T-SQL statements, available since SQL 2005, to handle the insertion of an entire list of element with just one instruction.
This article is obsolete. Some functionalities might not work anymore. Comments are disabled.

In an enterprise application developed using object oriented design pattern, you have to care about custom entities and hierarchical relations among them. Your database schema is designed to match those relations and store the data kept by each of those entities.
ORM Tools take care to map these bindings between your database and the logic layer you designed, they also may introduce performance issues because of loops of (SQL) commands meant to save a set of entities' data into the underlying database.

I personally tend to avoid to blindly rely on autogenerated SQL stuff (let's say in a common Linq to SQL/Entities scenario), I prefer to point my commands to some personally written stored procedures trying to exploit the succulent and powerful features of the database technology.

I'm now designing a new blog engine as a pretext to learn some new aspects of the .Net technologies and also to tune Data Access aspects.
With this article I want to share the method I've used to insert - at once - an entire collection of business entities into the database with just one INSERT INTO... SELECT statement.
The key is the OPENXML T-SQL instruction, and a preventive xml serialization process.
The idea is to serialize into an xml object a list of custom entities and pass them to the SQL 2005 database.
In this specific case I'm sending to the database a new blog instance (which simply consists in a name and relevant author's data) and some extra data translated in different cultures (a collection of LocalizedData objects).

database extract

Here's an extract of my simple BlogLocalizedData class:

namespace Pacem.Providers.Blog.Logic

{

    [XmlRoot("LocalizedData")]

    public partial class BlogLocalizedData : ILocalizedData, IProvided<Pacem.Providers.Blog.BlogProvider>

    {

         

         // [...] Omitted code

 

        /// <summary>

        /// Gets or sets the title in the relevant culture.

        /// </summary>

        [XmlElement("Title")]

        public string Title

        {

            get

            {

                return _Title;

            }

            set

            {

                _Title = value;

            }

        }

 

        /// <summary>

        /// Gets or sets the description in the relevant culture.

        /// </summary>

        [XmlElement("Description")]

        public string Description

        {

            get

            {

                return _Description;

            }

            set

            {

                _Description = value;

            }

        }

 

        /// <summary>

        /// Gets or sets the culture of the data.

        /// </summary>

        [XmlElement("Culture")]

        public string Culture

        {

            get

            {

                return _Culture;

            }

            set

            {

                // check if it is a valid culture:

                // throws an exception if value isn't a valid culture.

                CultureInfo culture = new CultureInfo(value);

                _Culture = culture.Name;

            }

        }

 

         // [...] Omitted code

    }

}

Please note the custom attributes used to mark the public properties for serialization as XmlElements.
It follows the script of the stored procedure, as you can see (2nd highlighted line) and as you can read in the official documentation, I've been telling the SQL XML Parser to parse the xml variable in a element-centric perspective. That means the relevant data (see relevant WITH CLAUSE) must be retrieved using the content of the XmlElements having the provided name!
There are overloaded manners to introduce hybrid logic in this sense (using both elements and attributes), I point you to the official documentation for this.

-- =============================================

-- Author:        Cristian Merighi

-- =============================================

CREATE PROCEDURE [sproc_Pacem_Blog_Insert]

    (   

        @Key varchar(64),

        @AuthorFirstName nvarchar(32),

        @AuthorLastName nvarchar(32),

        @AuthorEmail varchar(128),   

        @LocalizedDataCollection xml

        /*

        -- sample xml:

        <LocalizedDataCollection>

            <LocalizedData>

                <Title>title</Title>

                <Description>description</Description>

                <Culture>en-US</Culture>

            </LocalizedData>

            <LocalizedData>

                <Title>titolo</Title>

                <Description>descrizione</Description>

                <Culture>it-IT</Culture>

            </LocalizedData>

            <LocalizedData>

                <Title>titre</Title>

                <Description>description</Description>

                <Culture>fr-FR</Culture>

            </LocalizedData>

        </LocalizedDataCollection>

        */

        , @NewID int output

    )

AS

BEGIN

    -- SET NOCOUNT ON added to prevent extra result sets from

    -- interfering with SELECT statements.

    SET NOCOUNT ON;

 

    BEGIN TRANSACTION

 

    BEGIN TRY

 

        -- inserting blog

        INSERT INTO [Pacem_Blogs]

           ([strBlogKey]

           ,[strAuthorFirstName]

           ,[strAuthorLastName]

           ,[strAuthorEmail])

        VALUES

           (@Key

           ,@AuthorFirstName

           ,@AuthorLastName

           ,@AuthorEmail)

 

        SELECT @NewID = SCOPE_IDENTITY()

 

        -- Insert statements for procedure here

        DECLARE @XmlHandle int

        EXEC sp_xml_preparedocument @XmlHandle OUTPUT, @LocalizedDataCollection

 

        INSERT INTO [Pacem_BlogsLocalized]

           ([IDBlog]

           ,[strCulture]

           ,[strTitle]

           ,[strDescription])

        SELECT @NewID, [Culture], [Title], [Description]

        FROM OPENXML (@XmlHandle, '//LocalizedData',    2 /* element centric */)

            WITH ([Culture]        varchar(8),

                  [Title]        nvarchar(128),

                  [Description]    nvarchar(512))

 

        COMMIT TRANSACTION

 

    END TRY

    BEGIN CATCH

        ROLLBACK TRANSACTION

        -- RAISE ERROR

        DECLARE @ErrorMessage NVARCHAR(4000);

        DECLARE @ErrorSeverity INT;

        DECLARE @ErrorState INT;

 

        SELECT

            @ErrorMessage = ERROR_MESSAGE(),

            @ErrorSeverity = ERROR_SEVERITY(),

            @ErrorState = ERROR_STATE();

 

        -- Use RAISERROR inside the CATCH block to return error

        -- information about the original error that caused

        -- execution to jump to the CATCH block.

        RAISERROR (@ErrorMessage, -- Message text.

                   @ErrorSeverity, -- Severity.

                   @ErrorState -- State.

                   );

 

    END CATCH

END

To put together all these pieces, you just have to pass your serialized list of BlogLocalizedData as the @LocalizedDataCollection parameter:

// serializing localizedData

// localizedData is an instance of List<BlogLocalizedData>

XmlSerializer serializer = new XmlSerializer(

    typeof(List<BlogLocalizedData>),

    new XmlRootAttribute("LocalizedDataCollection"));

 

StringBuilder sb = new StringBuilder();

using (StringWriter tw = new StringWriter(sb))

{

    XmlTextWriter xtw = new XmlTextWriter(tw);

    serializer.Serialize(xtw, localizedData);

}

string xmlFragment = sb.ToString();

Take care. Bye.

feedback
 

Syndicate

Author

Cristian Merighi facebook twitter google+ youtube

Latest articles

Top rated

Archive

Where am I?

Author

Cristian Merighi facebook twitter google+ youtube

I'm now reading

Feeds