Thursday, March 13, 2014

How to recover SharePoint 2013 access apps record



Access 2013 is using Microsoft SharePoint 2013 to host the front end of the app and Microsoft SQL Server 2012 as its data storage technology. As a result, each item inside access app table is NOT list item but a database entry. If you delete an entry or massed up the content of the table, there is no easy way to restore from UI. In this blog, I will explain how to restore the Access app based on the Access 2013 architecture.

As we described in previous blog, each access app will have a real database table created on the database side. All the records are stored inside the database. We will use precious Access Apps named HarryDevApp1” with two tables “Employee” and “Rating” as in the screen shot. You could find the database name like “db_2105d1af_e924_460f_a749_59376ae2a9fd” with two tables named “Access. Employee” and “Access.Rating” are created. At meantime, there are two individual logins named “db_2105d1af_e924_460f_a749_59376ae2a9fd_custom” and “db_2105d1af_e924_460f_a749_59376ae2a9fd_dbo” are also created as owners for these two databases. 




Now you might be able to identify the recover strategy already. If user request to backup or restore Access tables, we could follow the below procedure.


  • Identify the Access App database by click “File” from Access client as described from previous blog
  • Identify the tables inside the database like Access. Employee
  • Restore the table form previous desired time

Please note, there might be macro created by end users that will be translated into database trigger as described in previous blog. You might need to recover the trigger also if users messed up the macro. If you need to restore entire Access App, you might need to restore the entire Access database.

Workaround for SharePoint 2013 access app no audit history issue – use data micro to track field change to an audit list



SharePoint 2013 Access App features a new application model. By using Microsoft SharePoint 2013 to host the front end of the app and Microsoft SQL Server 2012 as its data storage technology, SharePoint 2013 Access App has been adopted quickly by power users who has database development background. Jeff Conrad from Microsoft mentioned Access 2013 is to change the world one app at a time."]

Since Access App is a database application as we discussed in previous blog, it does not support some of the OoB functions. One of the key functions not supported is the audit history. If users modified or delete an entry from Access App list (table), there is no audit history. In this blog, I would like to provide a workaround learned from Microsoft Access App expertise Jeff Conrad to track the audit history.

I’ve using the similar Access Apps HarryDevApp2” with two tables “Employee” and “Rating” as in previous blog. You could find the database name like “db_af65a2a2_8e70_47d6_994b_207ff0351650” and the two tables named “Access. Employee” and “Access.Rating” are created inside that database. Since employee rating is extremely import and we like to track the change history. Here is the procedure learned from Jeff Conrad.


  1. Create a new table named AuditHistory with old rating, new rating, and modified time
  2. Create a “On Update” event and add the following data macro





After the macro saved, when you change the rating of one record, the change will be recorded into the AuditHistory table. Of cause, you can capture other information to make it more usable.



The design behind “On Update” event is that it creates a trigger on the database side to capture the changed data and insert to AuditHistory table. You can see the trigger in the database table.


The trigger created by the macro on the database side is listed below.



USE [db_af65a2a2_8e70_47d6_994b_207ff0351650]

GO

/****** Object:  Trigger [Access].[_dm_Employee_AFTER_UPDATE]    Script Date: 3/13/2014 1:49:17 PM ******/

SET ANSI_NULLS ON

GO

SET QUOTED_IDENTIFIER ON

GO

ALTER TRIGGER [Access].[_dm_Employee_AFTER_UPDATE]

ON [Access].[Employee]

WITH EXECUTE AS 'AccessWriter'

AFTER UPDATE

AS

BEGIN

       SET NOCOUNT ON

       SET XACT_ABORT OFF

      

       DECLARE @_dm_actionRunning NVARCHAR(128)

       DECLARE @_dm_macroRunning NVARCHAR(128)

      

       DECLARE @_dm_traceOn BIT = 0

       IF AccessSystem.ApplicationPropertiesSelect('DataMacroTracing') IS NOT NULL

              SET @_dm_traceOn = 1

      

       DECLARE @_dm_lastLoggingID INT

       IF @_dm_traceOn > 0

              SELECT

                     @_dm_lastLoggingID = COALESCE(Max(ID), 0)

              FROM

                     [Access].[Trace]

      

       DECLARE @_dm_savePoint as VARCHAR(36)

       SET @_dm_savePoint = CONVERT(VARCHAR(36), NEWID())

      

       DECLARE @_dm_initialTranCount INT

       SET @_dm_initialTranCount = @@TRANCOUNT

       IF @_dm_initialTranCount > 0

              SAVE TRAN @_dm_savePoint

       ELSE

              BEGIN TRAN

       IF OBJECT_ID('tempdb..#Context') IS NOT NULL

       BEGIN

              DECLARE @_dm_isTopLevel BIT

              SELECT @_dm_isTopLevel = isTopLevel

              FROM #Context

              WHERE ID = 1

      

              IF @_dm_isTopLevel = 1

              BEGIN

                     UPDATE #Context

                     SET isTopLevel = 0

                     WHERE ID = 1

                     SET @_dm_initialTranCount = 0

              END

       END

       SET @_dm_macroRunning = N'Employee:On Update'

       BEGIN TRY

              DECLARE @varRatingOld Int

              DECLARE @varRatingNew Int

              DECLARE @_dm_cancelRecordChange Bit

             

              DECLARE @_dm_temp1 Int

              DECLARE _dm_cur1 CURSOR LOCAL STATIC FOR

                     SELECT

                           [Rating]

                     FROM

                           [DELETED]

                    

              OPEN _dm_cur1

              DECLARE @_dm_temp2 Int

              DECLARE _dm_cur2 CURSOR LOCAL STATIC FOR

                     SELECT

                           [Rating]

                     FROM

                           [INSERTED]

                    

              OPEN _dm_cur2

             

              FETCH NEXT FROM

                     _dm_cur1

              INTO

                     @_dm_temp1

              FETCH NEXT FROM

                     _dm_cur2

              INTO

                     @_dm_temp2

              WHILE @@FETCH_STATUS = 0

              BEGIN

                     /* Get the old and new values of the rating and place them in variables. */

                     SET @_dm_actionRunning = N'SetLocalVar'

                     SET @varRatingOld = @_dm_temp1

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'SetLocalVar', N'varRatingOld', @varRatingOld, N'', N''

                    

                     SET @_dm_actionRunning = N'SetLocalVar'

                     SET @varRatingNew = @_dm_temp2

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'SetLocalVar', N'varRatingNew', @varRatingNew, N'', N''

                    

                     /* Create a record in the AuditHistory table and pass in the local variables to the appropriate fields. */

                     DECLARE @_dm_temp3 NVarChar(220)

                     DECLARE @_dm_temp4 Decimal(28,6)

                     DECLARE @_dm_temp5 Decimal(28,6)

                     DECLARE @_dm_temp6 Date

                     SET @_dm_temp4 = 0.0

                     SET @_dm_temp5 = 0.0

                    

                     SET @_dm_cancelRecordChange = 0

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'CreateRecord', N'', N'', N'', N''

                     SET @_dm_actionRunning = N'SetField'

                     SET @_dm_temp3 = @_dm_temp3

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'SetField', N'AuditHistory.Employee', @_dm_temp3, N'', N''

                     SET @_dm_actionRunning = N'SetField'

                     SET @_dm_temp4 = @varRatingOld

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'SetField', N'AuditHistory.OldRating', @_dm_temp4, N'', N''

                     SET @_dm_actionRunning = N'SetField'

                     SET @_dm_temp5 = @varRatingNew

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'SetField', N'AuditHistory.NewRating', @_dm_temp5, N'', N''

                     SET @_dm_actionRunning = N'SetField'

                     SET @_dm_temp6 = AccessSystem.NOW()

                     IF @_dm_traceOn > 0

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, N'SetField', N'AuditHistory.LastEditedTime', @_dm_temp6, N'', N''

                     SET @_dm_actionRunning = N'CreateRecord'

                     IF @_dm_cancelRecordChange = 0

                     BEGIN

                           INSERT INTO [AuditHistory] (

                                  [Employee],

                                  [OldRating],

                                  [NewRating],

                                  [LastEditedTime])

                           VALUES (

                                  @_dm_temp3,

                                  @_dm_temp4,

                                  @_dm_temp5,

                                  @_dm_temp6)

                     END

                     FETCH NEXT FROM

                           _dm_cur1

                     INTO

                           @_dm_temp1

                     FETCH NEXT FROM

                           _dm_cur2

                     INTO

                           @_dm_temp2

              END

              CLOSE _dm_cur1

              DEALLOCATE _dm_cur1

              CLOSE _dm_cur2

              DEALLOCATE _dm_cur2

       END TRY

       BEGIN CATCH

              DECLARE @_dm_sqlException NVARCHAR(128) = N'SQL exception'

              DECLARE @_dm_errorNumber INT = ERROR_NUMBER()

              DECLARE @_dm_errorMessage NVARCHAR(4000) = ERROR_MESSAGE()

      

              IF @_dm_TraceOn > 0

              BEGIN

                     /* Before rollback happens, cache the tracing info so that we can refill later */

                     DECLARE @tracingCache TABLE(

                           [ID] INT IDENTITY PRIMARY KEY,

                           [MacroName] nvarchar(128),

                           [ActionName] nvarchar(128),

                           [Operand] nvarchar(4000),

                           [Output] nvarchar(max),

                           [TargetRow] nvarchar(4000),

                           [Timestamp] datetime2(3),

                           [RuntimeErrorMessage] nvarchar(4000))

      

                     INSERT INTO @tracingCache

                           SELECT

                                  [MacroName],

                                  [ActionName],

                                  [Operand],

                                  [Output],

                                  [TargetRow],

                                  [Timestamp],

                                  [RuntimeErrorMessage]

                           FROM

                                  [Access].[Trace]

                           WHERE

                                  [ID] > @_dm_lastLoggingID

              END;

      

              IF @_dm_initialTranCount > 0

              BEGIN

                     IF XACT_STATE() <> -1

                           ROLLBACK TRAN @_dm_savepoint

              END

              ELSE

                     ROLLBACK TRAN

      

              IF @_dm_TraceOn > 0 AND XACT_STATE() <> -1

              BEGIN

                     /* After rollback happens, refill the Tracing table with the cached info */

                     INSERT INTO [Access].[Trace](

                           [MacroName],

                           [ActionName],

                           [Operand],

                           [Output],

                           [TargetRow],

                           [Timestamp],

                           [RuntimeErrorMessage])

                           SELECT

                                  [MacroName],

                                  [ActionName],

                                  [Operand],

                                  [Output],

                                  [TargetRow],

                                  [Timestamp],

                                  [RuntimeErrorMessage]

                           FROM

                                  @tracingCache

      

                     /* By default, RAISERROR raises an error message with an error number of 50000.

                     If the caught error's ID is 50000, then it is a user raised error

                     Else the caught error is a SQL exception (the Error ID won't be 50000 for SQL exception) */

                     IF @_dm_errorNumber <> 50000

                           /* Log the current running macro/action with the SQL exception error */

                           EXEC [AccessSystem].[LogActionTrace] @_dm_macroRunning, @_dm_actionRunning, '', '', '', @_dm_sqlException;

              END;

      

              THROW;

      

       END CATCH

      

END


Now you understand the Access App is just a database UI and you can use all your database development skills to empower your access apps.