如果不存在则创建UDF(用户定义函数),如果存在则跳过它

时间:2012-01-16 21:49:47

标签: sql sql-server tsql

嗨,谢谢你阅读本文。

我正在尝试使用IF EXISTS / IF NOT EXISTS语句来检查对象是否存在。基本上我想跳过它,如果它在那里或创建它,如果它不存在。

我用两种不同的方式编写代码但是出现错误:创建函数必须是批处理中唯一的函数。如果我把GO放在下面的插图语句之间,我会收到另一个警告:GO附近的语法错误。

我在哪里错了?

IF NOT EXISTS
(select * from Information_schema.Routines where SPECIFIC_SCHEMA='dbo' 
AND SPECIFIC_NAME = 'FMT_PHONE_NBR' AND Routine_Type='FUNCTION')

/*CREATE FUNCTION TO FORMAT PHONE NUMBERS*/
CREATE FUNCTION [dbo].[FMT_PHONE_NBR](@phoneNumber VARCHAR(12))
RETURNS VARCHAR(12)
AS
BEGIN
    RETURN SUBSTRING(@phoneNumber, 1, 3) + '-' + 
           SUBSTRING(@phoneNumber, 4, 3) + '-' + 
           SUBSTRING(@phoneNumber, 7, 4)
END

GO

或者这个:

IF NOT EXISTS
(SELECT name FROM sys.objects WHERE name = 'dbo.FMT_PHONE_NBR')

GO

/*CREATE FUNCTION TO FORMAT PHONE NUMBERS*/
CREATE FUNCTION [dbo].[FMT_PHONE_NBR](@phoneNumber VARCHAR(12))
RETURNS VARCHAR(12)
AS
BEGIN
    RETURN SUBSTRING(@phoneNumber, 1, 3) + '-' + 
           SUBSTRING(@phoneNumber, 4, 3) + '-' + 
           SUBSTRING(@phoneNumber, 7, 4)
END

GO

感谢您查看此内容!

7 个答案:

答案 0 :(得分:37)

解决此问题的最简单方法是删除已存在的函数,然后重新创建它:

/* If we already exist, get rid of us, and fix our spelling */
IF OBJECT_ID('dbo.FMT_PHONE_NBR') IS NOT NULL
  DROP FUNCTION FMT_PHONE_NBR
GO

/*CREATE FUNCTION TO FORMAT PHONE NUMBERS*/
CREATE FUNCTION [dbo].[FMT_PHONE_NBR](@phoneNumber VARCHAR(12))
RETURNS VARCHAR(12)
AS
BEGIN
    RETURN SUBSTRING(@phoneNumber, 1, 3) + '-' + 
           SUBSTRING(@phoneNumber, 4, 3) + '-' + 
           SUBSTRING(@phoneNumber, 7, 4)
END

GO

注意上面'object_id'功能的用法。这实际上是一种检查对象是否存在的常用方法,尽管它受到某些约束。

您可以在此处详细了解:OBJECT_ID

答案 1 :(得分:24)

当我长时间在这堵砖墙上打我的头时,我会再多投两分钱。

正如所指出的那样,只有当它不存在时添加它才会很好,但是如果不使用动态SQL就不可能在T-SQL中添加...并且包装你的函数,过程,触发器,视图,甚至可能是更加模糊不清的对象,因为动态语句太不切实际了。 (不要让我支持连续可能包含4个以上单个撇号的源代码!)

删除(如果存在)和(重新)创建是一个可行的解决方案。据推测,如果您要推出新代码,您可能希望创建该对象(如果该对象尚未存在),否则将删除现有/旧代码并将其替换为新代码。 (如果您可能不小心将“新”代码替换为“旧”代码,则会出现版本控制问题,这是一个不同且难度较大的主题。)

真正的问题是丢弃旧代码时丢失信息。什么信息?我经常遇到的是访问权限:谁拥有EXECUTE,或者对于某些功能,SELECT对象的权限?丢弃和更换,他们走了。当然,答案是将访问权限编写为部署脚本的一部分。但是,如果您遇到不同数据库托管环境具有不同配置(登录,域,组等)的情况,您可能会遇到以下情况:无法知道现有访问权限是什么在给定的实例上,如果您只是删除并重新创建它,现有用户可能无法再访问它。 (扩展属性和esoterica的其他位同样会受到影响。

第一个也是最好的解决方案是实现强大的安全性。设置数据库角色,为角色分配/关联适当的权限,然后您不必知道角色中的谁 - 这将是环境管理员的工作。 (你的脚本末尾仍然需要GRANT EXECUTE on ThisProc to dbo.xxx之类的内容,但这并不是那么难。

如果像我一样,你(a)没有被授权推出一个好的和强大的安全模型,并且(b)是懒惰的,可能不会检查数百行存储的结尾访问权限代码的过程文件,您可以执行以下操作。 (这是为存储过程设置的,但适用于函数和其他对象。)

-- isProcedure
-- IsScalarFunction    (Returns single value)
-- IsTableFunction     (Declared return table structure, multiple statements)
-- IsInlineFunction    (Based on single select statement)
-- IsView

IF objectproperty(object_id('dbo.xxx'), 'isProcedure') is null
 BEGIN
    --  Procedure (or function) does not exist, create a dummy placeholder
    DECLARE @Placeholder varchar(100)
    SET @Placeholder = 'CREATE PROCEDURE dbo.xxx AS RETURN 0'
    EXEC(@PlaceHolder)

    --  Configure access rights
    GRANT EXECUTE on dbo.xxx TO StoredProcedureUser
 END
GO

ALTER PROCEDURE dbo.xxx
(etc.)
GO

这将:

  • 首先检查程序是否存在。如果没有,创建一个“placholder”,并设置适当的访问权限
  • 然后,在脚本运行之前是否存在ALTER,并使用所需的代码进行设置。

还存在在模式中管理基于代码的对象(主要是存储过程)的问题,其中模式可能不存在。我还没想到那个,如果你很幸运,你将永远不会陷入同样的​​怪异局面。

答案 2 :(得分:4)

错误消息是完全正确的,CREATE FUNCTION语句必须是批处理中的第一个,这意味着不幸的是你不能这样做:

IF [condition]
BEGIN
    CREATE FUNCTION
    ...
END
GO

在这种情况下我通常做的是:

IF object_id('dbo.myFunction') IS NOT NULL
BEGIN
    DROP FUNCTION dbo.myFunction
END
GO

CREATE FUNCTION dbo.myFunction (
    ...
)
GO

请注意,我通常使用object_id()函数,因为它比EXISTS(SELECT * FROM sys.whatever)更简单,更易读,更健壮。

当然,如果你总是可以覆盖任何以前的函数定义,那么这个解决方案只对你​​有用。如果你的情况不好,请告诉我。

答案 3 :(得分:3)

实际上这适用于2008年

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[fn_GetTZDate]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
BEGIN
    execute dbo.sp_executesql @statement = N'
        CREATE FUNCTION [dbo].[fn_GetTZDate] ()

        RETURNS datetime
        AS -- WITH ENCRYPTION AS
        BEGIN
            -- Declare the return variable here
            DECLARE @tzadj int, @sysdate datetime
            SET @sysdate = getdate()
            SET @tzadj = 0
            SELECT @tzadj = [tzAdjustment] FROM USysSecurity WHERE [WindowsUserName] = SYSTEM_USER
            if @tzadj <> 0
            BEGIN
                SET @sysdate = dateadd(hh, @tzadj, @sysdate)
            END

            -- Return the result of the function
            RETURN @sysdate

        END    ' 
END

GO

答案 4 :(得分:2)

创新。
删除不是一个好主意,因为可能在对象上设置了权限。

因此,正确的做法实际上是
A)创建一个不存在的函数(虚拟)
B)更改该功能(如果已存在)。 (可能不是最新的)

示例:

-- DROP FUNCTION IF EXISTS [dbo].[TestFunction]

-- Do not drop the function if it exists - there might be privileges granted on it... 
-- You cannot alter function from table-valued function to scalar function or vice-versa 
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[TestFunction]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT')) 
BEGIN
    -- CREATE FUNCTION dbo.[TestFunction]() RETURNS int AS BEGIN RETURN 123 END 
    -- CREATE FUNCTION dbo.[TestFunction]() RETURNS table AS RETURN (SELECT * FROM information_schema.tables)  
    EXECUTE('
        CREATE FUNCTION dbo.[TestFunction]() RETURNS int AS BEGIN RETURN 123 END 
    ')
END 
GO



-- ALTER FUNCTION dbo.[TestFunction](@abc int) RETURNS table AS RETURN (SELECT * FROM information_schema.tables)  
ALTER FUNCTION dbo.[TestFunction]() RETURNS int AS BEGIN RETURN 'test' END 

请注意,您不能将表值函数更改为标量函数,反之亦然。
但是,您可以随意更改参数类型和参数数量以及返回模式。

答案 5 :(得分:1)

TL; DR

这是op的用例({Before dbo.sp_insertoralter过程在下面给出为工厂):

EXEC dbo.sp_insertoralter '[dbo].[FMT_PHONE_NBR]', '(@phoneNumber VARCHAR(12))
RETURNS VARCHAR(12)
AS
BEGIN
    RETURN SUBSTRING(@phoneNumber, 1, 3) + ''-'' + 
           SUBSTRING(@phoneNumber, 4, 3) + ''-'' + 
           SUBSTRING(@phoneNumber, 7, 4)
END', 0, 'IsScalarFunction'

print [dbo].[FMT_PHONE_NBR] ('987654132456') --  987-654-1324

警告:请记住,它并非旨在成为通用工厂,因为用户权限可能并非在所有地方都适用,并且存在大量可能的sql注入。


原始答案

从@PhilipKelley的answer开始,如果您希望使用'dbo'以外的模式,请参见下文。不需要温度变量来使EXEC('sql code')指令自己运行。

为了@obj_lookup重用,您可以取消注释最后EXEC('PRINT...')部分。

重要说明:字符串化的所有内容都不会启用拼写检查。您需要确保名称(架构和功能)正确无误,并且任何数据库重做都可能在不进行拼写检查通知的情况下破坏名称/方案。

我强烈建议阅读完整的@PhilipKelley的answer,尤其是对于“不要让我支持可能连续包含四个以上单撇号的源代码!” 部分。

提醒:这不是一个并发安全的操作,因为并行执行可能无法创建同一对象(同时在创建过程中),并且/或者将导致未知对象的主体更改(可能是最后一个对象)成功)。和所有其他“先创建后再创建”答案一样,但警告可能不会造成危害。

以下:

-- IsProcedure
-- IsScalarFunction    (Returns single value)
-- IsTableFunction     (Declared return table structure, multiple statements)
-- IsInlineFunction    (Based on single select statement)
-- IsView
-- (Full list available at https://docs.microsoft.com/fr-fr/sql/t-sql/functions/objectproperty-transact-sql?view=sql-server-ver15 )

DECLARE @obj_lookup nvarchar (max) = 'abc.xxx'
DECLARE @obj_alter nvarchar (max) = '() --  Object parameters come here
    RETURNS INT AS BEGIN --  Actual body goes here
        RETURN 0
    END'
DECLARE @obj_oralter bit = 0
DECLARE @obj_type nvarchar(100) = 'IsScalarFunction'
DECLARE @obj_application nvarchar(100) = 'FUNCTION'

IF objectproperty(object_id(@obj_lookup), @obj_type) is null
 --  Here is the "create if not exists" behaviour
 BEGIN
    EXEC('CREATE FUNCTION ' + @obj_lookup + @obj_alter)
    --  Configure access rights
    EXEC('GRANT EXECUTE on ' + @obj_lookup + ' TO StoredProcedureUser')
 END
ELSE IF @obj_oralter = 1 --  Here is the "or alter" behaviour
    EXEC('ALTER ' + @obj_application + ' ' + @obj_lookup + @obj_alter) --  Untouched access rights
/* --  Alternatively, you may prefer this 'CREATE OR ALTER' instruction to avoid the above objectproperty [if/else if] block:
EXEC('CREATE OR ALTER ' + @obj_application + ' ' + @obj_lookup + @obj_alter)
*/
GO

-- Actual code (considering the object now exists or was altered)
PRINT abc.xxx()
(..etc)
/* -- For the "@obj_lookup" reuse:
EXEC('PRINT ' + @obj_lookup + '()
(..etc)')
*/
GO

工厂

可以包装在一个过程中(是):

CREATE PROCEDURE dbo.sp_insertoralter(@obj_lookup as nvarchar(max), @obj_alter as nvarchar(max), @obj_oralter as bit = 0, @obj_type as nvarchar(100))
 AS BEGIN
    --  Type preparation
    declare @obj_application nvarchar(100) = case
            when @obj_type = 'IsProcedure' then 'PROCEDURE'
            when @obj_type = 'IsScalarFunction' or @obj_type = 'IsTableFunction' or @obj_type = 'IsInlineFunction' then 'FUNCTION'
            when @obj_type = 'IsView' then 'VIEW'
            when @obj_type = 'IsTable' or @obj_type = 'IsUserTable' then 'TABLE'
            when @obj_type = 'IsTrigger' then 'TRIGGER'
            else null --  Restriction to known (usefull) cases
        end
    if @obj_application is null
    begin
        raiserror ('An invalid @obj_type was specified for procedure', 10, 1); --throw 51000, 'An invalid @obj_type was specified for procedure', 1;
        return
    end

    IF objectproperty(object_id(@obj_lookup), @obj_type) is null
     BEGIN --  Here is the "create if not exists" behaviour
        EXEC('CREATE ' + @obj_application + ' ' + @obj_lookup + @obj_alter)
        --  Configure access rights
        EXEC('GRANT EXECUTE on ' + @obj_lookup + ' TO StoredProcedureUser')
     END
    ELSE IF @obj_oralter = 1 --  Here is the "or alter" behaviour
        EXEC('ALTER ' + @obj_application + ' ' + @obj_lookup + @obj_alter) --  Untouched access rights
    /* --  Alternatively, you may prefer the 'CREATE OR ALTER' instruction to avoid the above objectproperty [if/else if] block:
    EXEC('CREATE OR ALTER ' + @obj_application + ' ' + @obj_lookup + @obj_alter)
    */
 END
GO

您可能已经注意到,然后可以自动调用该过程(但必须存在该过程才能被调用),所以这是一个自动用例(重复两次):

--  The "dbo.sp_insertoralter" procedure has to exist (or whatever name you gave it), let's pretend someone manually executed the `Below:` part with these parameters:
EXEC dbo.sp_insertoralter 'dbo.sp_insertoralter', '(@obj_lookup as nvarchar(max), @obj_alter as nvarchar(max), @obj_oralter as bit = 0, @obj_type as nvarchar(100))
 AS BEGIN
    --  Type preparation
    declare @obj_application nvarchar(100) = case
            when @obj_type = ''IsProcedure'' then ''PROCEDURE''
            when @obj_type = ''IsScalarFunction'' or @obj_type = ''IsTableFunction'' or @obj_type = ''IsInlineFunction'' then ''FUNCTION''
            when @obj_type = ''IsView'' then ''VIEW''
            when @obj_type = ''IsTable'' or @obj_type = ''IsUserTable'' then ''TABLE''
            when @obj_type = ''IsTrigger'' then ''TRIGGER''
            else null --  Restriction to known (usefull) cases
        end
    if @obj_application is null
    begin
        raiserror (''An invalid @obj_type was specified for procedure'', 10, 1); --throw 51000, ''An invalid @obj_type was specified for procedure'', 1;
        return
    end

    IF objectproperty(object_id(@obj_lookup), @obj_type) is null
     BEGIN --  Here is the "create if not exists" behaviour
        EXEC(''CREATE '' + @obj_application + '' '' + @obj_lookup + @obj_alter)
        --  Configure access rights
        EXEC(''GRANT EXECUTE on '' + @obj_lookup + '' TO StoredProcedureUser'')
     END
    ELSE IF @obj_oralter = 1 --  Here is the "or alter" behaviour
        EXEC(''ALTER '' + @obj_application + '' '' + @obj_lookup + @obj_alter) --  Untouched access rights
    /* --  Alternatively, you may prefer the ''CREATE OR ALTER'' instruction to avoid the above objectproperty [if/else if] block:
    EXEC(''CREATE OR ALTER '' + @obj_application + '' '' + @obj_lookup + @obj_alter)
    */
 END', 1, 'IsProcedure'

现在,这是操作程序的用例(三倍):

EXEC dbo.sp_insertoralter '[dbo].[FMT_PHONE_NBR]', '(@phoneNumber VARCHAR(12))
RETURNS VARCHAR(12)
AS
BEGIN
    RETURN SUBSTRING(@phoneNumber, 1, 3) + ''-'' + 
           SUBSTRING(@phoneNumber, 4, 3) + ''-'' + 
           SUBSTRING(@phoneNumber, 7, 4)
END', 0, 'IsScalarFunction'

print [dbo].[FMT_PHONE_NBR] ('987654132456') --  987-654-1324

警告:请记住,它并非旨在成为通用工厂,因为用户权限可能并非在所有地方都适用,并且存在大量可能的sql注入。

答案 6 :(得分:1)

SQL Server 2016 Service Pack 1开始,可以直接CREATE OR ALTER功能:

CREATE OR ALTER FUNCTION [dbo].[FMT_PHONE_NBR](@phoneNumber VARCHAR(12))
RETURNS VARCHAR(12)
AS
BEGIN
    RETURN SUBSTRING(@phoneNumber, 1, 3) + '-' + 
           SUBSTRING(@phoneNumber, 4, 3) + '-' + 
           SUBSTRING(@phoneNumber, 7, 4)
END
GO

没有删除,没有动态SQL,如果SQL Server 2016 SP1可用,则应使用它。