0

I've created a stored procedure that uses a temp table to take the massive results of a Bing maps XML and convert it to varchar so that I can use it to retrieve several fields. The reason for converting it to varchar was because the XML was too large for the sp_OAMethod and my variable was always blank.

set @serviceUrl = 'https://dev.virtualearth.net/REST/v1/Routes/Truck?wp.0=' + @ToAddress  + '&wp.1=' + @FromAddress + '&vehicleHazardousMaterials=Flammable&output=xml&key=XXX-000-XXX'

Exec sp_OACreate 'MSXML2.XMLHTTP', @Object OUT;
Exec sp_OAMethod @Object, 'open', NULL, 'get',@serviceUrl, 'false'
Exec sp_OAMethod @Object, 'send'
Exec sp_OAMethod @Object, 'responseText', @ResponseText OUTPUT

exec @returnCode = sp_oamethod @Object, 'read', @ResponseText out, -1

Create table #tmp(dt xml)

insert into #tmp
    exec @hr = sp_OAGetProperty @Object, 'responseXML.XML'

Set @ResponseText = Convert(varchar(max), (SELECT dt from #tmp))
Drop Table #tmp 

select REPLACE(SUBSTRING(@ResponseText,PATINDEX('%<TravelDistance>%',@ResponseText),21),'<TravelDistance>','') Miles

  ,REPLACE(REPLACE(SUBSTRING(@ResponseText,PATINDEX('%<TravelDuration>%',@ResponseText),21),'<TravelDuration>',''),'<','')/60 TravelMinutes

Above is not the complete code; I've edited it severely to make it easier to read.

I'm sure there is a better way to do it but this works and it's very exciting.

However, the plan was to create a function that could be used along with other ad-hoc queries to retrieve the same data. Not possible as a function because of the temp table or populating a table from within the function.

So does anyone have a way to make this possible?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
user974061
  • 343
  • 1
  • 5
  • 17
  • What part specifically are you looking to change to a function? You can't use `EXEC`, `CREATE`, `INSERT` (or any other DDL operator) within a function; which is the majority of the SQL you have. – Thom A Nov 01 '18 at 20:14
  • The only way I know of is to store the results in a table as a temporary cache and use the function to query that table. But then you need to schedule updates to the cache table using a different mechanism. – Dale K Nov 01 '18 at 20:15
  • The proc I've created will take two addresses and retrieve from Bing the travel time and the mileage. I realize with a function you can only have one return so I would essentially create two functions but I want to be able to use the function with a SELECT. So the piece from the proc I need to be able to change for use in the function is where the XML is changed to varchar to enable me to use the XML response. Unless someone has another way of achieving the result. – user974061 Nov 01 '18 at 20:17
  • Dave Burrell: so I need to have the function store the results in a table? Yeah, updating the table periodically wouldn't help if I was to use the function in a SELECT of 100 rows. – user974061 Nov 01 '18 at 20:18
  • How about a way to limit the XML that's returned? Does someone know how to do that so the sp_OAMethod is not overwhelmed? – user974061 Nov 01 '18 at 20:19
  • You can use a table valued function to return multiple results. You can't run an SP from within a function, so you're going to have to call the SP at some point. You can of course return results from an SP - maybe thats the way to go? Make your last statement in the SP a `select` and you can access the results in your code. – Dale K Nov 01 '18 at 20:22
  • 1
    Consider putting this logic in a CLR function. This avoids potential resource problems with `sp_OAMethod` (or at least makes them more manageable) and allows you to actually use a function. Furthermore, it allows for some more sophisticated ways of caching data than would be possible from T-SQL. Even so, it pays off to check if you can't do the whole data retrieval logic client side and store the results in tables -- this allows SQL Server to focus on what it does best, which is querying relational data. It's poorly equipped to process data from web services. – Jeroen Mostert Nov 01 '18 at 21:05
  • I'm assuming you are only using a database so you can perform your queries over the data quickly and don't plan on storing the results long term. The Bing Maps terms of use has restrictions on storage. You might also want to take a look at Azure Maps which provides more functionality, allows storage of results up to 30 days in Azure and is cheaper. https://learn.microsoft.com/en-us/rest/api/maps/route/getroutedirections and http://azure.com/maps – rbrundritt Nov 03 '18 at 03:02

1 Answers1

3

You really should use CLR for this. You're a long way from a correct stored procedure here, and the sp_OAxxx procedures are best avoided. You're not handling the return code correctly, you're using the wrong HTTP component, and not parsing the results using the SQL Server's XML parser. It may seem initially like a CLR implementation is more work, but maintaining code that uses sp_OAxxx stored procedures and COM interop will always be painful.

There are very few people who understand how this code works, how to change it, and whether it's safe and reliable. The next person to own your codebase is probably not one of those people. It's not good to have code like that in your project.

If you do use CLR, you still shouldn't use a function (although you technically can). You shouldn't perform any external access in the middle of a query.

Anyway here's some fixes to your existing approach:

declare @serviceURL varchar(max)
declare @returnCode int
declare @errorMessage nvarchar(max)
declare @Object int
declare @hr int
declare @key varchar(2000) = 'Aoj...nFS'
declare @FromAddress nvarchar(max) = '7000 N SH 161, Irving TX 75039'

declare @ToAddress nvarchar(max) = '8617 Garland Rd, Dallas, TX 75218'

set @serviceUrl = 'https://dev.virtualearth.net/REST/v1/Routes/Truck?wp.0=' + @ToAddress  + '&wp.1=' + @FromAddress + '&vehicleHazardousMaterials=Flammable&output=xml&key=' + @key


exec @hr = sp_OACreate 'MSXML2.ServerXMLHTTP', @Object OUT;
IF @hr <> 0
begin
    set @errorMessage = concat('sp_OACreate failed ', convert(varchar(20),cast(@hr as varbinary(4)),1));
    throw 60000, @errorMessage, 1;
end;
begin try
    Exec @hr = sp_OAMethod @Object, 'open', NULL, 'get',@serviceUrl, 'false'
    IF @hr <> 0
    begin
        set @errorMessage = concat('open failed ', convert(varchar(20),cast(@hr as varbinary(4)),1));
        throw 60000, @errorMessage, 1;
    end;

    Exec @hr = sp_OAMethod @Object, 'send'
    IF @hr <> 0
    begin
        set @errorMessage = concat('send failed ', convert(varchar(20),cast(@hr as varbinary(4)),1));
        throw 60000, @errorMessage, 1;
    end;

    declare @responseCode int;
    Exec @hr = sp_OAGetProperty @Object, 'status', @responseCode out
    IF @hr <> 0 or @responseCode <> 200
    begin
        set @errorMessage = concat('send failed hr:', convert(varchar(20),cast(@hr as varbinary(4)),1),' http response code: ', @responseCode);
        throw 60000, @errorMessage, 1;
    end;

    declare @tmp table(doc xml)
    insert into @tmp
    exec @hr = sp_OAGetProperty @Object, 'ResponseXML.XML'
    IF @hr <> 0
    begin
        set @errorMessage = concat('ResponseXML.XML failed ', convert(varchar(20),cast(@hr as varbinary(4)),1));
        throw 60000, @errorMessage, 1;
    end;



    WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/search/local/ws/rest/v1')
    select doc.value('(//TravelDistance)[1]', 'float') TravelDistance,
           doc.value('(//DistanceUnit)[1]', 'nvarchar(20)') DistanceUnit,
           doc.value('(//TravelDuration)[1]', 'float') TravelDuration,
           doc.value('(//DurationUnit)[1]', 'nvarchar(20)') DurationUnit
    from @tmp;
end try
begin catch
    exec @hr = sp_OADestroy  @Object ;
    throw;    
end catch
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • This works great and really fast. Thanks for your help. Can you also provide an example of how I would incorporate other fields from a sql table along with the doc.value results? I'm playing around and getting nowhere with making it happen. – user974061 Nov 02 '18 at 19:57
  • If you wrapped this into a stored procedure you could use INSERT … EXEC to load a temp table or table variable, and then join that in a subsequent query. – David Browne - Microsoft Nov 02 '18 at 21:48