From e88fcfb1c71cb03691b38ff681b409d8501ec3fd Mon Sep 17 00:00:00 2001 From: Hippolyte Thomas Date: Sun, 28 Jan 2024 21:01:25 +1300 Subject: [PATCH] fix(db): issue with results consistency --- src/Service/MongoDB.php | 106 ++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/src/Service/MongoDB.php b/src/Service/MongoDB.php index 941f33c..ff478c9 100644 --- a/src/Service/MongoDB.php +++ b/src/Service/MongoDB.php @@ -7,6 +7,7 @@ use MongoDB\BSON\Regex; use MongoDB\Collection; use MongoDB\Client as Mongo; +use MongoDB\Database; use MongoDB\Driver\CursorInterface; use Psr\Log\LoggerInterface; @@ -19,24 +20,33 @@ public function __construct( ) { } /** - * Retrieves a MongoDB collection or returns false on failure - * @return Collection|false The MongoDB collection or false if the connection fails. + * Retrieves a MongoDB database or returns false on failure + * @return Database|false The MongoDB database or false if the connection fails. */ - private function getCollection(): Collection|false + private function getDb(): Database|false { // Create a new MongoDB client instance $mongo = new Mongo($this->mongoDbUrl); - // Test the connection to the MongoDB server and return the collection + // Test the connection to the MongoDB server and return the database try { $mongo->{$this->mongoDbName}->command(['ping' => 1]); - return $mongo->{$this->mongoDbName}->journal; + return $mongo->{$this->mongoDbName}; } catch (Exception $e) { $this->logger->error('[MongoDB] Exception: {exception}', [ 'exception' => $e->getMessage(), ]); } return false; + } + + /** + * Retrieves a MongoDB collection or returns false on failure + * @return Collection|false The MongoDB collection or false if the connection fails. + */ + private function getCollection(): Collection|false + { + return $this->getDb()?->journal ?? false; } /** @@ -45,7 +55,7 @@ private function getCollection(): Collection|false */ public function isConnected(): bool { - return $this->getCollection() !== false; + return $this->getDb() !== false; } /** @@ -120,15 +130,21 @@ public function findOne(int $user_id, DateTime $date): array|false */ public function retrieveUserNutritionData(int $user_id, array $date_list, array $fields_group, int $sort = -1, int $limit = 100, int $offset = 0, array $filters_group = []): CursorInterface|false { - $collection = $this->getCollection(); - if (!$collection) return false; + // Build query for the View + $aggregate_query = $this->getNutritionAggregateQuery(); + + // Check if the view exist otherwise create it + $view = $this->getView("nutrition", "journal", $aggregate_query); + if (!$view) return false; + + // Prepare the query $match = ['user_id' => $user_id]; // Prepare the date list for the request if (empty($date_list)) return false; $dates = []; foreach ($date_list as $date) { - $dates[] = [ 'entry.dateTime' => new Regex("{$date}.*") ]; + $dates[] = [ 'dateTime' => new Regex("{$date}.*") ]; } $match['$or'] = $dates; @@ -143,18 +159,15 @@ public function retrieveUserNutritionData(int $user_id, array $date_list, array // Add filters for the request $match = array_merge($match, $filters_group); - // Search into the collection and return results as an array, if successfully found + // Search into the view and return results as an array, if successfully found try { - return $collection->aggregate([ + return $view->aggregate([ [ '$match' => $match ], - [ - '$sort' => [ 'timestamp' => -1 ] - ], [ '$group' => [ - '_id' => '$entry.dateTime', + '_id' => '$dateTime', 'data' => [ '$first' => !empty($fields) ? $fields : '$$ROOT.nutrition' ] @@ -182,4 +195,67 @@ public function retrieveUserNutritionData(int $user_id, array $date_list, array } return false; } + + /** + * Get or create a MongoDB view + * @param string $view_name The name of the MongoDB view + * @param string $collection_name The name of the collection on which the view is based + * @param array $aggregate_query An array representing the aggregation pipeline to create the view + * @return Collection|false Returns the MongoDB view (Collection) if it exists or was successfully created. Returns false on failure + */ + private function getView(string $view_name, string $collection_name = "", array $aggregate_query = []): Collection|false + { + $db = $this->getDb(); + if (!$db) return false; + + // Check if the view exist + $collections = $db->listCollections(); + foreach ($collections as $collection) { + if ($collection->getType() === "view" && $collection->getName() === $view_name) { + return $db->{$view_name}; + } + } + if (empty($aggregate_query) || empty($collection_name)) return false; + + // Create the view if it doesn't exist + try { + $db->command([ + 'create' => $view_name, + 'viewOn' => $collection_name, + 'pipeline' => $aggregate_query + ]); + } catch (Exception $e) { + $this->logger->error('[MongoDB] Exception: {exception}', [ + 'exception' => $e->getMessage(), + ]); + return false; + } + return $this->getView($view_name); + } + + private function getNutritionAggregateQuery(): array + { + return [ + [ + '$sort' => [ 'timestamp' => -1 ] + ], + [ + '$group' => [ + '_id' => [ + 'user_id' => '$user_id', + 'dateTime' => '$entry.dateTime' + ], + 'nutrition' => [ + '$first' => '$$ROOT.nutrition' + ], + 'dateTime' => [ + '$first' => '$$ROOT.entry.dateTime' + ], + 'user_id' => [ + '$first' => '$$ROOT.user_id' + ] + ] + ] + ]; + } }