diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultElements.csv b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultElements.csv
new file mode 100644
index 0000000000000000000000000000000000000000..77811d69fb314648dbd6a4c53d7c90a2ca4b3775
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultElements.csv
@@ -0,0 +1,25 @@
+"sys_language"
+,"uid","pid","hidden","title","flag"
+,1,0,0,"Dansk","dk"
+,2,0,0,"Deutsch","de"
+"sys_category"
+,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parent","items","l10n_diffsource","description"
+,28,0,256,0,0,0,0,0,0,0,0,0,"Category A",0,0,,
+,29,0,512,0,0,0,0,0,0,0,0,0,"Category B",0,0,,
+,30,0,768,0,0,0,0,0,0,0,0,0,"Category C",0,0,,
+,31,0,1024,0,0,0,0,0,0,0,0,0,"Category A.A",28,0,,
+"sys_category_record_mm"
+,"uid_local","uid_foreign","tablenames","sorting","sorting_foreign","fieldname"
+,28,297,"tt_content",0,1,"categories"
+,29,297,"tt_content",0,2,"categories"
+,29,298,"tt_content",0,1,"categories"
+,30,298,"tt_content",0,2,"categories"
+"tt_content"
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","image","categories","tx_irretutorial_1nff_hotels"
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",0,2,0
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",0,2,0
+"tx_irretutorial_1nff_hotel"
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l18n_diffsource","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","offers"
+,3,89,1,0,0,0,,0,0,0,0,0,0,"Hotel #1",297,"tt_content",,0
+,4,89,2,0,0,0,,0,0,0,0,0,0,"Hotel #2",297,"tt_content",,0
+,5,89,1,0,0,0,,0,0,0,0,0,0,"Hotel #1",298,"tt_content",,0
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv
new file mode 100644
index 0000000000000000000000000000000000000000..1d91e2c6c795ef8ffeda15852c9692b8001e772d
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv
@@ -0,0 +1,6 @@
+"pages"
+,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title"
+,1,0,256,0,0,0,0,0,0,0,"FunctionalTest"
+,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest"
+,89,88,256,0,0,0,0,0,0,0,"Relations"
+,90,88,512,0,0,0,0,0,0,0,"Target"
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/Fixtures/HookFixture.php b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/Fixtures/HookFixture.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1ed327dc1953e6918238e3fe617e022b6db4a22
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/Fixtures/HookFixture.php
@@ -0,0 +1,189 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Tests\Functional\DataHandling\DataHandler\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\DataHandling\DataHandler;
+use TYPO3\CMS\Core\SingletonInterface;
+
+/**
+ * Class for testing execution of DataHandler hook invocations.
+ */
+class HookFixture implements SingletonInterface
+{
+    /**
+     * @var array[]
+     */
+    protected $invocations = [];
+
+    /**
+     * Purges the state of this singleton instance
+     */
+    public function purge()
+    {
+        $this->invocations = [];
+    }
+
+    /**
+     * @param string $methodName
+     * @return array|null
+     */
+    public function findInvocationsByMethodName(string $methodName)
+    {
+        return $this->invocations[$methodName] ?? null;
+    }
+
+    /**
+     * @param DataHandler $dataHandler
+     */
+    public function processDatamap_beforeStart(DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = true;
+    }
+
+    /**
+     * @param array $fieldArray
+     * @param string $table
+     * @param string|int $id
+     * @param DataHandler $dataHandler
+     */
+    public function processDatamap_preProcessFieldArray(array $fieldArray, string $table, $id, DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = [
+            'fieldArray' => $fieldArray,
+            'table' => $table,
+            'id' => $id,
+        ];
+    }
+
+    /**
+     * @param string $status
+     * @param string $table
+     * @param string|int $id
+     * @param array $fieldArray
+     * @param DataHandler $dataHandler
+     */
+    public function processDatamap_postProcessFieldArray(string $status, string $table, $id, array $fieldArray, DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = [
+            'status' => $status,
+            'table' => $table,
+            'id' => $id,
+            'fieldArray' => $fieldArray,
+        ];
+    }
+
+    /**
+     * @param string $status
+     * @param string $table
+     * @param string|int $id
+     * @param array $fieldArray
+     * @param DataHandler $dataHandler
+     */
+    public function processDatamap_afterDatabaseOperations(string $status, string $table, $id, array $fieldArray, DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = [
+            'status' => $status,
+            'table' => $table,
+            'id' => $id,
+            'fieldArray' => $fieldArray,
+        ];
+    }
+
+    /**
+     * @param DataHandler $dataHandler
+     */
+    public function processDatamap_afterAllOperations(DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = true;
+    }
+
+    /**
+     * @param DataHandler $dataHandler
+     */
+    public function processCmdmap_beforeStart(DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = true;
+    }
+
+    /**
+     * @param string $command
+     * @param string $table
+     * @param string|int $id
+     * @param mixed $value
+     * @param DataHandler $dataHandler
+     * @param bool|string $pasteUpdate
+     */
+    public function processCmdmap_preProcess(string $command, string $table, $id, $value, DataHandler $dataHandler, $pasteUpdate)
+    {
+        $this->invocations[__FUNCTION__][] = [
+            'command' => $command,
+            'table' => $table,
+            'id' => $id,
+            'value' => $value,
+            'pasteUpdate' => $pasteUpdate,
+        ];
+    }
+
+    /**
+     * @param string $command
+     * @param string $table
+     * @param string|int $id
+     * @param mixed $value
+     * @param bool $commandIsProcessed
+     * @param DataHandler $dataHandler
+     * @param bool|string $pasteUpdate
+     */
+    public function processCmdmap(string $command, string $table, $id, $value, bool $commandIsProcessed, DataHandler $dataHandler, $pasteUpdate)
+    {
+        $this->invocations[__FUNCTION__][] = [
+            'command' => $command,
+            'table' => $table,
+            'id' => $id,
+            'value' => $value,
+            'commandIsProcessed' => $commandIsProcessed,
+            'pasteUpdate' => $pasteUpdate,
+        ];
+    }
+
+    /**
+     * @param string $command
+     * @param string $table
+     * @param string|int $id
+     * @param mixed $value
+     * @param DataHandler $dataHandler
+     * @param bool|string $pasteUpdate
+     * @param bool|string $pasteDatamap
+     */
+    public function processCmdmap_postProcess(string $command, string $table, $id, $value, DataHandler $dataHandler, $pasteUpdate, $pasteDatamap)
+    {
+        $this->invocations[__FUNCTION__][] = [
+            'command' => $command,
+            'table' => $table,
+            'id' => $id,
+            'value' => $value,
+            'pasteUpdate' => $pasteUpdate,
+            'pasteDatamap' => $pasteDatamap,
+        ];
+    }
+
+    /**
+     * @param DataHandler $dataHandler
+     */
+    public function processCmdmap_afterFinish(DataHandler $dataHandler)
+    {
+        $this->invocations[__FUNCTION__][] = true;
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/HookTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/HookTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c72ea72ea4995a9abfe3b1687664244b4212721
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/HookTest.php
@@ -0,0 +1,363 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Tests\Functional\DataHandling\DataHandler;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Tests\Functional\DataHandling\AbstractDataHandlerActionTestCase;
+use TYPO3\CMS\Core\Tests\Functional\DataHandling\DataHandler\Fixtures\HookFixture;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+
+/**
+ * Tests triggering hook execution in DataHandler.
+ */
+class HookTest extends AbstractDataHandlerActionTestCase
+{
+    const VALUE_PageId = 89;
+    const VALUE_ContentId = 297;
+    const TABLE_Content = 'tt_content';
+    const TABLE_Hotel = 'tx_irretutorial_1nff_hotel';
+    const TABLE_Category = 'sys_category';
+    const FIELD_ContentHotel = 'tx_irretutorial_1nff_hotels';
+    const FIELD_Categories = 'categories';
+
+    /**
+     * @var HookFixture
+     */
+    protected $hookFixture;
+
+    /**
+     * @var string
+     */
+    protected $scenarioDataSetDirectory = 'typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/';
+
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->importScenarioDataSet('LiveDefaultPages');
+        $this->importScenarioDataSet('LiveDefaultElements');
+        $this->backendUser->workspace = 0;
+
+        $this->hookFixture = GeneralUtility::makeInstance(HookFixture::class);
+        $this->hookFixture->purge();
+        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][__CLASS__] = HookFixture::class;
+        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][__CLASS__] = HookFixture::class;
+    }
+
+    protected function tearDown()
+    {
+        parent::tearDown();
+
+        unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][__CLASS__]);
+        unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][__CLASS__]);
+        unset($this->hookFixture);
+    }
+
+    /**
+     * @test
+     */
+    public function hooksAreExecutedForNewRecords()
+    {
+        $newTableIds = $this->actionService->createNewRecord(
+            self::TABLE_Content,
+            self::VALUE_PageId,
+            ['header' => 'Testing #1']
+        );
+        $this->recordIds['newContentId'] = $newTableIds[self::TABLE_Content][0];
+
+        $this->assertHookInvocationsCount([
+                'processDatamap_beforeStart',
+                'processDatamap_afterAllOperations'
+        ], 1);
+
+        $this->assertHookInvocationsPayload([
+            'processDatamap_preProcessFieldArray',
+            'processDatamap_postProcessFieldArray',
+            'processDatamap_afterDatabaseOperations',
+        ], [
+            [
+                'table' => self::TABLE_Content,
+                'fieldArray' => [ 'header' => 'Testing #1', 'pid' => self::VALUE_PageId ]
+            ]
+        ]);
+    }
+
+    /**
+     * @test
+     */
+    public function hooksAreExecutedForExistingRecords()
+    {
+        $this->actionService->modifyRecord(
+            self::TABLE_Content,
+            self::VALUE_ContentId,
+            ['header' => 'Testing #1']
+        );
+
+        $this->assertHookInvocationsCount([
+            'processDatamap_beforeStart',
+            'processDatamap_afterAllOperations'
+        ], 1);
+
+        $this->assertHookInvocationsPayload([
+            'processDatamap_preProcessFieldArray',
+            'processDatamap_postProcessFieldArray',
+            'processDatamap_afterDatabaseOperations',
+        ], [
+            [
+                'table' => self::TABLE_Content,
+                'fieldArray' => [ 'header' => 'Testing #1' ]
+            ]
+        ]);
+    }
+
+    /**
+     * @test
+     */
+    public function hooksAreExecutedForNewRelations()
+    {
+        $contentNewId = StringUtility::getUniqueId('NEW');
+        $hotelNewId = StringUtility::getUniqueId('NEW');
+        $categoryNewId = StringUtility::getUniqueId('NEW');
+
+        $this->actionService->modifyRecords(
+            self::VALUE_PageId,
+            [
+                self::TABLE_Content => [
+                    'uid' => $contentNewId,
+                    'header' => 'Testing #1',
+                    self::FIELD_ContentHotel => $hotelNewId,
+                    self::FIELD_Categories => $categoryNewId,
+                ],
+                self::TABLE_Hotel => [
+                    'uid' => $hotelNewId,
+                    'title' => 'Hotel #1',
+                ],
+                self::TABLE_Category => [
+                    'uid' => $categoryNewId,
+                    'title' => 'Category #1',
+                ],
+            ]
+        );
+
+        $this->assertHookInvocationsCount([
+            'processDatamap_beforeStart',
+            'processDatamap_afterAllOperations'
+        ], 1);
+
+        $this->assertHookInvocationPayload(
+            'processDatamap_preProcessFieldArray',
+            [
+                [
+                    'table' => self::TABLE_Content,
+                    'fieldArray' => [
+                        'header' => 'Testing #1',
+                        self::FIELD_ContentHotel => $hotelNewId,
+                        self::FIELD_Categories => $categoryNewId,
+                    ],
+                ],
+                [
+                    'table' => self::TABLE_Hotel,
+                    'fieldArray' => [ 'title' => 'Hotel #1' ],
+                ],
+                [
+                    'table' => self::TABLE_Category,
+                    'fieldArray' => [ 'title' => 'Category #1' ],
+                ],
+            ]
+        );
+
+        $this->assertHookInvocationPayload(
+            'processDatamap_postProcessFieldArray',
+            [
+                [
+                    'table' => self::TABLE_Content,
+                    'fieldArray' => [ 'header' => 'Testing #1' ],
+                ],
+                [
+                    'table' => self::TABLE_Hotel,
+                    'fieldArray' => [ 'title' => 'Hotel #1' ],
+                ],
+                [
+                    'table' => self::TABLE_Category,
+                    'fieldArray' => [ 'title' => 'Category #1' ],
+                ],
+            ]
+        );
+
+        $this->assertHookInvocationPayload(
+            'processDatamap_afterDatabaseOperations',
+            [
+                [
+                    'table' => self::TABLE_Content,
+                    'fieldArray' => [
+                        'header' => 'Testing #1',
+                        self::FIELD_ContentHotel => 1,
+                    ],
+                ],
+                // @todo Fix the double invocation for this tt_content record
+                [
+                    'table' => self::TABLE_Content,
+                    'fieldArray' => [
+                        'header' => 'Testing #1',
+                        self::FIELD_Categories => 1,
+                    ],
+                ],
+                [
+                    'table' => self::TABLE_Hotel,
+                    'fieldArray' => [ 'title' => 'Hotel #1' ],
+                ],
+                [
+                    'table' => self::TABLE_Category,
+                    'fieldArray' => [ 'title' => 'Category #1' ],
+                ],
+            ]
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function hooksAreExecutedForExistingRelations()
+    {
+        $this->actionService->modifyRecord(
+            self::TABLE_Content,
+            self::VALUE_ContentId,
+            [
+                'header' => 'Testing #1',
+                self::FIELD_ContentHotel => '3,4,5',
+                self::FIELD_Categories => '28,29,30',
+            ]
+        );
+
+        $this->assertHookInvocationsCount([
+            'processDatamap_beforeStart',
+            'processDatamap_afterAllOperations'
+        ], 1);
+
+        $this->assertHookInvocationPayload(
+            'processDatamap_preProcessFieldArray',
+            [
+                [
+                    'table' => self::TABLE_Content,
+                    'fieldArray' => [
+                        'header' => 'Testing #1',
+                        self::FIELD_ContentHotel => '3,4,5',
+                        self::FIELD_Categories => '28,29,30',
+                    ]
+                ]
+            ]
+        );
+
+        $this->assertHookInvocationsPayload([
+            'processDatamap_postProcessFieldArray',
+            'processDatamap_afterDatabaseOperations',
+        ], [
+            [
+                'table' => self::TABLE_Content,
+                'fieldArray' => [
+                    'header' => 'Testing #1',
+                    self::FIELD_ContentHotel => 3,
+                    self::FIELD_Categories => 3,
+                ]
+            ]
+        ]);
+    }
+
+    /**
+     * @param string[] $methodNames
+     * @param int $count
+     */
+    protected function assertHookInvocationsCount(array $methodNames, int $count)
+    {
+        $message = 'Unexpected invocations of method "%s"';
+        foreach ($methodNames as $methodName) {
+            $invocations = $this->hookFixture->findInvocationsByMethodName($methodName);
+            $this->assertCount(
+                $count,
+                $invocations,
+                sprintf($message, $methodName)
+            );
+        }
+    }
+
+    /**
+     * @param string[] $methodNames
+     * @param array $assertions
+     */
+    protected function assertHookInvocationsPayload(array $methodNames, array $assertions)
+    {
+        foreach ($methodNames as $methodName) {
+            $this->assertHookInvocationPayload($methodName, $assertions);
+        }
+    }
+
+    /**
+     * @param string $methodName
+     * @param array $assertions
+     */
+    protected function assertHookInvocationPayload(string $methodName, array $assertions)
+    {
+        $message = 'Unexpected hook payload amount found for method "%s"';
+        $invocations = $this->hookFixture->findInvocationsByMethodName($methodName);
+        $this->assertNotNull($invocations);
+
+        foreach ($assertions as $assertion) {
+            $indexes = $this->findAllArrayValuesInHaystack($invocations, $assertion);
+            $this->assertCount(
+                1,
+                $indexes,
+                sprintf($message, $methodName)
+            );
+            $index = $indexes[0];
+            unset($invocations[$index]);
+        }
+    }
+
+    /**
+     * @param array $haystack
+     * @param array $assertion
+     * @return int[]
+     */
+    protected function findAllArrayValuesInHaystack(array $haystack, array $assertion)
+    {
+        $found = [];
+        foreach ($haystack as $index => $item) {
+            if ($this->equals($assertion, $item)) {
+                $found[] = $index;
+            }
+        }
+        return $found;
+    }
+
+    /**
+     * @param array $left
+     * @param array $right
+     * @return bool
+     */
+    protected function equals(array $left, array $right)
+    {
+        foreach ($left as $key => $leftValue) {
+            $rightValue = $right[$key] ?? null;
+            if (!is_array($leftValue) && (string)$leftValue !== (string)$rightValue) {
+                return false;
+            } elseif (is_array($leftValue)) {
+                if (!$this->equals($leftValue, $rightValue)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Framework/ActionService.php b/typo3/sysext/core/Tests/Functional/DataHandling/Framework/ActionService.php
index 8e634b11c89337a807f4791ef016e0c695d79cec..e5cf2baa0bde53519b9b7ecf1e078189a791d0a3 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/Framework/ActionService.php
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/Framework/ActionService.php
@@ -138,9 +138,11 @@ class ActionService
             $recordData = $this->resolvePreviousUid($recordData, $currentUid);
             $currentUid = $recordData['uid'];
             if ($recordData['uid'] === '__NEW') {
-                $recordData['pid'] = $pageId;
                 $currentUid = StringUtility::getUniqueId('NEW');
             }
+            if (strpos($currentUid, 'NEW') === 0) {
+                $recordData['pid'] = $pageId;
+            }
             unset($recordData['uid']);
             $dataMap[$tableName][$currentUid] = $recordData;
             if ($previousTableName !== null && $previousUid !== null) {