2023-06-18

Reflexion à propos du modèle de donnée de PlanDive #dev

Entity Relation Diagram

Mermaid Live Editor on mermaid.live

Mermaid view with kroki

Représentation

erDiagram Auth ||--|| User : AUTHENTIFY User }o--|| Company : BELONGS_TO User ||--|| Profile : HAS Company ||--o{ Resource : HAS Resource ||--o{ CalendarSlot : HAS User ||--o{ Booking : "FRELANCE BOOK" Booking }o--|| Resource : FOR Booking }o--|| CalendarSlot : DURING User{ string name "Nicolas SAVON" string email "nicolas.savon@gmail.com" key auth_id "UGxhbkRpdmU=" string licence "YXozNGZkc2cyNA==" double rate "5" } Profile { string pseudo "Niko" enum type "[OWNER]/EMPLOYE/FREELANCE/..." } Company { string name "Continental Sea Inc" } Resource { string name "Tequila" enum type "[CRUISE BOAT]/SPEED BOAT/CATAMARAN/CAR/VAN/..." double quantity "20" } CalendarSlot { date from "2023/06/02 10:00.00 GMT" date to "2023/06/02 11:00.00 GMT" formula availableQuantity "= (resourceQuantity - bookingBooked) >= 0" } Booking { string name "Cool dive experience on new boat" array equipment "[food, drink, live music]" double booked "5" }

Intégration dans Firestore

Avec Document References et Denormalization

exports.initializeDb = onRequest(async (req, res) => {
  // Init user
  const user = {
    'name': 'Nicolas SAVON',
    'email': 'nicolas.savon@gmail.com',
    'licence': 'YXozNGZkc2cyNA==',
    'rate': 5.0,
    'profile': {
      'name': 'Niko',
      'type': 'OWNER',
    },
  };
  await db.collection(dbCollection.USER).doc('UGxhbkRpdmU=').set(user);

  // Init company
  const company1 = {
    'name': 'Continental Sea Inc',
  };
  const company1resource = [
      {
        'name': 'Tequila',
        'type': 'CRUISE BOAT',
        'quantity': 20.0,
      },
      {
        'name': 'Santa Cruz',
        'type': 'CRUISE BOAT',
        'quantity': 5,
      }
  ]
  const calendarSlot1 = {
    'from': new Date(2023, 5, 19, 10, 0, 0, 0),
    'to': new Date(2023, 5, 19, 18, 0, 0, 0),
    'availableQuantity': -1,
  };

  const company1Ref = db.collection(dbCollection.COMPANY).doc();
  await company1Ref.set(company1);
  company1resource.forEach( async (resource) => {
    const resourceRef = db.collection(dbCollection.COMPANY).doc(company1Ref.id).collection(dbCollection.RESOURCE).doc();
    await resourceRef.set(resource);
    await db.collection(dbCollection.COMPANY).doc(company1Ref.id).collection(dbCollection.RESOURCE).doc(resourceRef.id).collection(dbCollection.CALENDARSLOT).doc().set(calendarSlot1)
  })

  // Init booking
  const booking1 = {
    'name': 'Cool dive experience on new boat'
  };
  const booking1Ref = db.collection(dbCollection.BOOKING).doc();
  await booking1Ref.set(booking1);
  const booking2 = {
    'name': 'Cool dive experience on old boat'
  };
  const booking2Ref = db.collection(dbCollection.BOOKING).doc();
  await booking2Ref.set(booking2);
  
  // Adding company
  const company1QuerySnapshot = await db.collection(dbCollection.COMPANY).doc(company1Ref.id).get();
  const companyData1 = {
    id: company1QuerySnapshot.id, 
    ...company1QuerySnapshot.data()
  };
  await db.collection(dbCollection.BOOKING).doc(booking1Ref.id).update({company: companyData1});
  await db.collection(dbCollection.BOOKING).doc(booking2Ref.id).update({company: companyData1});
  
  // Adding resource  
  const resourcesFromCompany1QuerySnapshot = await db.collection(dbCollection.COMPANY).doc(company1Ref.id)
                                        .collection(dbCollection.RESOURCE).get();
  const resourceData0 = {
    id: resourcesFromCompany1QuerySnapshot.docs[0].id, 
    ...resourcesFromCompany1QuerySnapshot.docs[0].data()
  };
  const resourceData1 = {
    id: resourcesFromCompany1QuerySnapshot.docs[1].id, 
    ...resourcesFromCompany1QuerySnapshot.docs[1].data()
  };
  await db.collection(dbCollection.BOOKING).doc(booking1Ref.id).update({resource: resourceData0});
  await db.collection(dbCollection.BOOKING).doc(booking2Ref.id).update({resource: resourceData1});

  // Adding user
  const userQuerySnapshot = await db.collection('user').doc('UGxhbkRpdmU=').get();

  const userData = {
    id: userQuerySnapshot.id,
    ... userQuerySnapshot.data()
  }
  await db.collection(dbCollection.BOOKING).doc(booking1Ref.id).update({user: userData})
  await db.collection(dbCollection.BOOKING).doc(booking2Ref.id).update({user: userData})

  res.status(200).send('Initialization finish');
})

Représentation JSON

Représentation V1

Collections and subcollections
  • users
  • companies
  • companies.resources
  • calendarslots
  • booking
Schema
{
  "users": {
    "UGxhbkRpdmU=": {
      "name": "Nicolas SAVON",
      "email": "nicolas.savon@gmail.com",
      "licence": "YXozNGZkc2cyNA==",
      "rate": 5.0,
      "profile": {
        "name": "Niko",
        "type": "OWNER"
      }
    }
  },
  "companies": {
    "company1": {
      "name": "Continental Sea Inc",
      "resources": [
        {
          "name": "Tequila",
          "type": "CRUISE BOAT",
          "quantity": 20.0
        }
      ]
    }
  },
  "calendarslots": {
    "calendarslot1": {
      "from": "2023-06-02T10:00:00Z",
      "to": "2023-06-02T11:00:00Z"
    }
  },
  "bookings": {
    "booking1": {
      "name": "Cool dive experience on new boat",
      "equipment": ["food", "drink", "live music"],
      "company": {
        "id": "company1",
        "name": "Continental Sea Inc",
      },
      "resource": {
        "id": "resource1",
        "name": "Tequila",
        "type": "CRUISE BOAT",
      },
      "calendarslot": {
          "id": "calendarslot1",
          "from": "2023-06-02T10:00:00Z",
          "to": "2023-06-02T11:00:00Z",
      }
    }
  }
}

Représentation V2

Collections and subcollections
Schema
  • companies
  • companies.users
  • companies.resources
  • companies.resources.calendarslots
  • booking
{
  "companies": {
    "company1": {
      "name": "Continental Sea Inc",
      "users": {
        "UGxhbkRpdmU=": {
          "name": "Nicolas SAVON",
          "email": "nicolas.savon@gmail.com",
          "licence": "YXozNGZkc2cyNA==",
          "rate": 5.0,
          "profile": {
            "name": "Niko",
            "type": "OWNER"
          }
        }
      },
      "resources": [{
          "name": "Tequila",
          "type": "CRUISE BOAT",
          "quantity": 20.0,
          "calendarslots": {
            "calendarslot1": {
              "from": "2023-06-02T10:00:00Z",
              "to": "2023-06-02T11:00:00Z"
            },
            "calendarslot2": {
              "from": "2023-06-02T11:00:00Z",
              "to": "2023-06-02T12:00:00Z"
            },
            "calendarslot3": {
              "from": "2023-06-02T14:00:00Z",
              "to": "2023-06-02T18:00:00Z"
            },
          },
        }]
    }
  },

  "bookings": {
    "booking1": {
      "name": "Cool dive experience on new boat",
      "equipment": ["food", "drink", "live music"],
      "company": {
        "id": "company1",
        "name": "Continental Sea Inc",
      },
      "resource": {
        "id": "resource1",
        "name": "Tequila",
        "type": "CRUISE BOAT",
      },
      "calendarslot": {
          "id": "calendarslot1",
          "from": "2023-06-02T10:00:00Z",
          "to": "2023-06-02T11:00:00Z",
      }
    }
  }
}

Updating denormalized data

Updating denormalized data in Firestore can be tricky, as there are no built-in mechanisms for maintaining consistency between copies of the same data. However, there are a few strategies you can use:

  • Cloud Functions: You can use Firebase Cloud Functions to listen for changes in your resources subcollection in the companies collection. When a change occurs, the function can find all bookings that reference the changed resource and update the denormalized data accordingly.

Here's an example Cloud Function in JavaScript that listens for changes to a resource document and updates the corresponding bookings:

  // The Cloud Functions for Firebase SDK to create Cloud Functions and triggers.
  const {logger} = require("firebase-functions");
  const {onRequest} = require("firebase-functions/v2/https");
  const {onDocumentCreated, onDocumentUpdated, Change, FirestoreEvent} = require("firebase-functions/v2/firestore");

  // The Firebase Admin SDK to access Firestore.
  const {initializeApp} = require("firebase-admin/app");
  const {getFirestore} = require("firebase-admin/firestore");

  initializeApp();
  const db = getFirestore();

  const dbCollection = {
    USER: 'user',
    COMPANY: 'company',
    RESOURCE: 'resource',
    BOOKING: 'booking',
  };

  exports.updateResource = onDocumentUpdated("company/{companyId}/resource/{resourceId}", async (event) => {
      // Get an object representing the document
      const previousValue = event.data.before.data();
      const newValue = event.data.after.data();

      // Get all booking with a resource of a specific Id
      const bookingsSnapshot = await db.collection(dbCollection.BOOKING).where('resource.id', '==', event.params.resourceId).get();
      
      // Batch update all booking finded with the new resource value
      const updates = bookingsSnapshot.docs.map(doc => doc.ref.update({ 'resource': newValue }));

      await Promise.all(updates);
  });
  • Client-side code: You can also handle the updates from the client-side application code. Whenever you update a resource, you also fetch all the bookings that reference that resource and update the denormalized data. However, this approach can be less reliable than using Cloud Functions because it relies on the client to perform the updates, which can fail if the client loses its network connection or if the operation is interrupted for any other reason.

In both cases, you'll need to handle the potential for a large number of updates if a resource is referenced by many bookings. Firestore operations are charged per document read or written, so an operation that needs to update many documents can be costly. If you find that you're frequently needing to update a large number of documents due to changes in your resources, you may want to reconsider your data model or denormalization strategy.