Поделиться через


Тип денежной единицы и функция конвертации

В этом примере определяется пользовательский тип данных Currency на языке C#. Определяемый пользователем тип данных включает в себя количество и культуру, что помогает выбрать правильный способ подготовки к показу суммы в виде значения валюты в представлении конкретной культурной среды. В этом примере также реализуется функция конвертации валюты, возвращающая экземпляр пользовательского типа данных Currency. Если база данных AdventureWorks содержит коэффициент перевода из долларов США в валюту, связанную с заданной культурой, функция перевода возвращает определяемый пользователем тип данных Currency с коэффициентом перевода и культурой, соответствующей запрошенной. В противном случае определяемый пользователем тип данных Currency возвращается с исходной суммой, которая должна быть выражена в долларах США, и культурой en-us. В этом примере также показано, как регистрировать и отменять регистрацию методов и сборок среды CLR при помощи Transact-SQL.

ПредупреждениеВнимание!

Курсы обмена, используемые в этом образце, являются вымышленными и не должны использоваться при реальных финансовых транзакциях.

Предварительные требования

Для создания и запуска этого проекта должно быть установлено следующее программное обеспечение:

  • SQL Server или SQL Server Express. SQL Server Express можно получить бесплатно с веб-сайтадокументации и образцов SQL Server Express (возможно, на английском языке)

  • База данных AdventureWorks, которая доступна на веб-сайте разработки SQL Server (возможно, на английском языке)

  • Пакет SDK 2.0 для платформы .NET Framework или более поздняя версия либо среда Microsoft Visual Studio 2005 или более поздняя версия. Пакет SDK для платформы .NET Framework можно получить бесплатно.

  • Кроме того, должны выполняться следующие условия.

  • Для используемого экземпляра SQL Server должна быть включена интеграция со средой CLR.

  • Чтобы включить интеграцию со средой CLR, выполните следующие действия.

    Включение интеграции со средой CLR

    • Выполните следующие команды Transact-SQL:

    sp_configure 'clr enabled', 1

    GO

    RECONFIGURE

    GO

    ПримечаниеПримечание

    Чтобы включить CLR, необходимо иметь разрешение ALTER SETTINGS на уровне сервера, которое неявно назначается членам предопределенных ролей сервера sysadmin и serveradmin.

  • На используемом экземпляре SQL Server должна быть установлена база данных AdventureWorks.

  • Если вы не являетесь администратором используемого экземпляра SQL Server, то для завершения установки необходимо, чтобы администратор предоставил разрешение CreateAssembly .

Построение образца

Создайте и запустите образец в соответствии со следующими инструкциями.

  1. Откройте командную строку Visual Studio или .NET Framework.

  2. Если необходимо, создайте каталог для своего образца. В данном примере будет использоваться каталог C:\MySample.

  3. В каталоге c:\MySample создайте файл Currency.cs и скопируйте в него образец кода (C#), приведенный ниже.

  4. Скомпилируйте образец кода из командной строки, выполнив:

    • Csc /reference:C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Data.dll /reference:C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.dll /reference:C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Xml.dll /target:library Currency.cs
  5. Скопируйте код установки Transact-SQL в файл и сохраните его в файле Install.sql в том же каталоге.

  6. Если образец установлен в каталоге, отличном от C:\MySample\, внесите изменения в файл Install.sql, указав там этот каталог.

  7. Разверните сборку и хранимую процедуру, выполнив

    • sqlcmd -E -I -i install.sql
  8. Скопируйте скрипт проверки Transact-SQL в файл и сохраните его в файле test.sql в том же каталоге.

  9. Выполните скрипт проверки следующей командой

    • sqlcmd -E -I -i test.sql
  10. Скопируйте скрипт очистки Transact-SQL в файл и сохраните его в файле cleanup.sql в том же каталоге.

  11. Выполните скрипт следующей командой

    • sqlcmd -E -I -i cleanup.sql

Образец кода

Ниже приведены листинги кода для данного образца.

C#

using System;
using System.Globalization;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Data;
using System.Data.Sql;
using System.IO;
using System.Data.SqlClient;

    /// <summary>
    ///Defines a class for handing particular amounts of money in a 
    ///particular culture's monetary system.  This class is exposed as 
    ///a SQL Server UDT.
/// 
///Note that we are implementing IComparable to affect comparison behavior 
///only within the CLR.  This does not affect how SQL Server will compare the
///     the types.  How SQL Server will compare the type is determined by the Write 
///method on IBinarySerialize.
    /// </summary>
[Serializable]
    [SqlUserDefinedType(Format.UserDefined, IsByteOrdered = true, MaxByteSize = 32)]
    public struct Currency : INullable, IComparable, IBinarySerialize
    {
        const string nullMarker = "\0\0\0\0\0\0\0\0\0\0";
        const int cultureNameMaxSize = 10;

        private string cultureName;//Who issued the money (en-us, for example)

        private CultureInfo culture;//The object which represents cultureName

        private decimal currencyValue;//The amount of money

        // Public properties for private fields
        public CultureInfo Culture
        {
            get
            {
                //A culture name is required.  If not present the entire object is considered null.
                if (cultureName == null) return null;

                //If we've got a cached copy of the culture return it.
                if (culture != null) return culture;

                //Otherwise, set the cache and return the culture for the culture name specified.
                culture = CultureInfo.CreateSpecificCulture(cultureName);
                return culture;
            }
        }

        // Public property for the private field.
        public decimal CurrencyValue
        {
            get
            {
                return currencyValue;
            }
        }

        // Constructors for when we have the culture or the name of the culture

        public Currency(CultureInfo culture, decimal currencyValue)
        {
if (culture == null) throw new ArgumentNullException("culture");
            this.cultureName = culture.Name;
            this.culture = culture;
            this.currencyValue = currencyValue;
        }

        public Currency(string cultureName, decimal currencyValue)
        {
            this.cultureName = cultureName;
            this.culture = null;
            this.currencyValue = currencyValue;
        }

        //Return the string representation for the currency, including the currency symbol.
        [SqlMethod(IsDeterministic = true,
            IsPrecise = true, DataAccess = DataAccessKind.None,
            SystemDataAccess = SystemDataAccessKind.None)]
        public override string ToString()
        {
            if (this.Culture == null) return "null";

            return String.Format(this.Culture, "{0:c}", currencyValue);
        }

        //The entire value of the currency is considered null if the culture name is null
        public bool IsNull
        {
            get
            {
                return cultureName == null;
            }
        }

        //The no-argument constructor makes a null currency.
        public static Currency Null
        {
            get
            {
                Currency h = new Currency((String)null, 0);

                return h;
            }
        }

        //Be sure to set the current UI culture before using this method! Even better, provide the culture
        //specifically (for the method after this one).
        [SqlMethod(IsDeterministic = true, IsPrecise = true, DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None)]
        public static Currency Parse(SqlString sqlString)
        {
            return ParseWithCulture(sqlString, CultureInfo.CurrentUICulture);
        }

        public static Currency ParseWithCulture(SqlString sqlString, CultureInfo culture)
        {
            if (sqlString.IsNull 
|| (string.Compare(sqlString.Value, "null", true, CultureInfo.CurrentUICulture) == 0))
                return Currency.Null;

            int digitPos = -1;
            string stringValue = sqlString.Value;

            while (digitPos < stringValue.Length 
                && !Char.IsDigit(stringValue, ++digitPos))
            {
            }

            if (digitPos < stringValue.Length)
                return new Currency(culture, decimal.Parse(
                    stringValue.Substring(digitPos), culture));

            return Currency.Null;
        }

        public override int GetHashCode()
        {
            if (this.IsNull)
                return 0;

            return this.ToString().GetHashCode();
        }

//Note: This only affects the behavior of CLR, not SQL Server.  Comparisions
//for SQL Server will be determined by the Write method below.

public int CompareTo(object obj)
        {
            if (obj == null)
                return 1; //by definition

            if (obj == null || !(obj is Currency))
                throw new ArgumentException(
                    "the argument to compare is not a Currency");

            Currency c = (Currency)obj;

            if (this.IsNull)
            {
                if (c.IsNull)
                    return 0;

                return -1;
            }

            if (c.IsNull)
                return 1;

            string thisCultureName = this.Culture.Name;
            string otherCultureName = c.Culture.Name;
            if (!thisCultureName.Equals(otherCultureName))
                return thisCultureName.CompareTo(otherCultureName);
            return this.CurrencyValue.CompareTo(c.CurrencyValue);
        }
        // IBinarySerialize methods
        // The binary layout is as follow:
        //    Bytes 0 - 19:Culture name, padded to the right with null characters, UTF-16 encoded
        //    Bytes 20+:Decimal value of money
        // If the culture name is empty, the currency is null.
       
        public void Write(System.IO.BinaryWriter w)
        {
if (w == null) throw new ArgumentNullException("w");
            if (this.IsNull)
            {
                w.Write(nullMarker);
                w.Write((decimal)0);
                return;
            }

            if (cultureName.Length > cultureNameMaxSize)
            {
                throw new ApplicationException(string.Format(
                    CultureInfo.InvariantCulture, 
                    "{0} is an invalid culture name for currency as it is too long.", 
                    cultureNameMaxSize));
            }

            String paddedName = cultureName.PadRight(cultureNameMaxSize, '\0');
            for (int i = 0; i < cultureNameMaxSize; i++)
            {
                w.Write(paddedName[i]);
            }

            // Normalize decimal value to two places
            currencyValue = Decimal.Floor(currencyValue * 100) / 100;
            w.Write(currencyValue);
        }
        public void Read(System.IO.BinaryReader r)
        {
            char[] name = r.ReadChars(cultureNameMaxSize);
            int stringEnd = Array.IndexOf(name, '\0');

            if (stringEnd == 0)
            {
                cultureName = null;
                return;
            }

            cultureName = new String(name, 0, stringEnd);
            currencyValue = r.ReadDecimal();
        }
    }


    /// <summary>
    /// This class is used to compute the value of US money a given region.
    /// </summary>
    public sealed class CurrencyConverter
    {
        // Classes with only static members should not be instantiable
        private CurrencyConverter()
        {
        }

        private static readonly CultureInfo USCulture = CultureInfo.CreateSpecificCulture("en-us");

        /// <summary>
        ///Computes the value of a certain amount of money in the USA in a different region.
        /// </summary>
        /// <param name="fromAmount">The quantity of money</param>
        /// <param name="toCultureName">A culture which is a member of the region of interest</param>
        /// <returns></returns>
        [Microsoft.SqlServer.Server.SqlFunction(IsDeterministic = true, DataAccess = Microsoft.SqlServer.Server.DataAccessKind.Read)]
        public static Currency ConvertCurrency(SqlMoney fromAmount, SqlString toCultureName, SqlDateTime when)
        {
            CultureInfo toCulture = CultureInfo.CreateSpecificCulture(toCultureName.Value);

            if (toCulture.Equals(USCulture))
            {
                Currency c = new Currency(USCulture, (decimal)fromAmount);
                return c;
            }

            String toCurrencyCode = new RegionInfo(toCulture.LCID).ISOCurrencySymbol;

            // Find the rate closest to the specified date

            using (SqlConnection conn = new SqlConnection("context connection=true"))
            {

                SqlCommand command = conn.CreateCommand();
                command.CommandType = CommandType.StoredProcedure;
                command.CommandText = "usp_LookupConversionRate";

                SqlParameter onDateParameter
                    = new SqlParameter("@OnDate", SqlDbType.DateTime);
                onDateParameter.Value = when;
                command.Parameters.Add(onDateParameter);

                SqlParameter toCurrencyCodeParameter
                    = new SqlParameter("@ToCurrencyCode", SqlDbType.NChar, 3);
                toCurrencyCodeParameter.Value = toCurrencyCode;
                command.Parameters.Add(toCurrencyCodeParameter);

                SqlParameter resultParameter
                    = new SqlParameter("@Result", SqlDbType.Decimal);
                resultParameter.Precision = 10;
                resultParameter.Scale = 4;
                resultParameter.Direction = ParameterDirection.Output;
                command.Parameters.Add(resultParameter);

                conn.Open();
                command.ExecuteNonQuery();

                decimal conversionFactor;

                if (resultParameter.Value is decimal)
                {
                    conversionFactor = (decimal)(resultParameter.Value);
                }
                else
                {
                    conversionFactor = 1.0M;
                    toCulture = USCulture;
                }

                return new Currency(toCulture, ((decimal)fromAmount * conversionFactor));
            }
        }
    }

Это скрипт установки Transact-SQL (Install.sql), который выполняет развертывание сборки и создает в базе данных хранимую процедуру.

USE AdventureWorks
GO

IF EXISTS (SELECT * FROM sys.procedures WHERE [name] = N'usp_LookupConversionRate')
DROP PROCEDURE [dbo].[usp_LookupConversionRate]
GO

IF EXISTS (SELECT * FROM sys.types WHERE [name] = N'Currency') 
DROP TYPE Currency;
GO

IF EXISTS (SELECT [name] FROM sys.assemblies WHERE [name] = N'Currency')
DROP ASSEMBLY Currency;
GO

IF EXISTS (SELECT * FROM sys.objects WHERE ([name] = N'ConvertCurrency') AND ([type] = 'FS'))
DROP FUNCTION ConvertCurrency;
GO

-- You may need to modify the value of the this variable if you have installed the sample someplace other than the default location.
DECLARE @SamplesPath nvarchar(1024)
set @SamplesPath = 'C:\MySample\'
CREATE ASSEMBLY Currency 
FROM @SamplesPath + 'Currency.dll'
with permission_set = safe;

USE AdventureWorks
GO



CREATE TYPE Currency EXTERNAL NAME [Currency].[Currency];
GO

CREATE FUNCTION ConvertCurrency
(
@fromAmount AS money,
@toCultureName AS nvarchar(10),
@when as DateTime
)
RETURNS Currency
AS EXTERNAL NAME [Currency].[CurrencyConverter].ConvertCurrency;
GO

CREATE PROCEDURE usp_LookupConversionRate
(
@OnDate datetime,
@ToCurrencyCode nchar(3),
@Result decimal(10,4) OUTPUT
)
AS
BEGIN
--It is not permitted to perform certain side-effects in functions, and
--SET NOCOUNT is one of them.  Since this sproc is called from 
--the ConvertCurrency CLR UDF, we must not do that side-effect or
--there will be an error at runtime.
--SET NOCOUNT ON

SELECT @Result = (SELECT TOP 1 AverageRate FROM Sales.CurrencyRate 
WHERE CurrencyRateDate <= @OnDate AND FromCurrencyCode = N'USD' 
AND ToCurrencyCode = @ToCurrencyCode 
ORDER BY CurrencyRateDate DESC);

IF (@Result IS NULL)
SELECT @Result = (SELECT TOP 1 AverageRate FROM Sales.CurrencyRate 
WHERE CurrencyRateDate > @OnDate AND FromCurrencyCode = N'USD' 
AND ToCurrencyCode = @ToCurrencyCode
ORDER BY CurrencyRateDate ASC);
END;

Это файл test.sql, который проверяет образец, выполняя его функции.

use AdventureWorks
GO

DECLARE @TwoBitsEuro Currency;
SELECT @TwoBitsEuro = dbo.ConvertCurrency(CAST('.25' as money), 'FR-FR', GetDate());
PRINT '$0.25 in USD is equivalent to ' + @TwoBitsEuro.ToString();

Следующий скрипт Transact-SQL удаляет из базы данных сборку, тип и функции.

USE AdventureWorks
GO

IF EXISTS (SELECT * FROM sys.procedures WHERE [name] = N'usp_LookupConversionRate')
DROP PROCEDURE [dbo].[usp_LookupConversionRate]
GO

См. также

Основные понятия

Сценарии использования и примеры интеграции со средой CLR