CodeNewbie Community 🌱

Tim Rohrer
Tim Rohrer

Posted on

The Nirvana of Database DAOs

I do like databases, and nearly all of my projects have them at their core.

I hear you. I'm not special or unique in this regard. But for me this has become my focal point of learning recently.

The Problem

Despite the popularity of web apps that make use of databases, I feel the ideal solution, or approach, eludes me. In my last project, I see two significant problems with the approach I took:

  1. The service layer is tightly coupled to the database.
  2. It doesn't scale well.

With that in mind, I will describe my latest experiences in my quest for the best approach to manage database operations. I'm hoping you, the reader, will share your experiences and constructive criticism about my approach. This will make be a better developer.

The two most frequently used databases for me are MongoDB and Sqlite3, and these are both being incorporated in my latest project. And I want to use each with multiple features. After much research and poking around the web, I discovered that an Abstract Factory pattern is a reasonable choice, and Refactoring Guru was used for my templates because of the clean examples in Typescript.

The Data Access Object (DAO) originated somewhere in the Object Oriented programming world and it seems to be commonly found in Java and Microsoft references. The approach makes sense to me because I like how the methods are grouped and calls are quickly identifiable.

In previous projects, I've developed a DAO (for example, UsersDAO) using a Singleton pattern so that there is one (and only one) instance and static methods are used by the client code. I recognize a lot of developers aren't fans of this pattern, but they make sense to me for this type of use case.

Unfortunately, my experience so far in Typescript is that an Abstract Factory pattern doesn't mesh well with creation of a true Singleton. I'm not sure if this break down is due to something I'm approaching wrong in my implementation or if the issues are inherent to Typescript. More details are below.

Some Code

Let's first take a look at the setup. Here is a draft UML diagram showing the interfaces and classes to be implemented.

classes and interfaces UML

The gist is that each concrete factory (MongoDB, Sqlite3, etc) implements the DAOFactory interface:

interface DAOFactory {
  createQuickenDAO(): QuickenDAO | Promise<QuickenDAO>
  createPortfolioDAO(): PortfolioDAO | Promise<PortfolioDAO>
}
Enter fullscreen mode Exit fullscreen mode

In this example, QuickenDAO is the interface defining methods that will need to be implemented to support this Quicken feature set. In this case:

export interface QuickenDAO {
  addImport(arg0: Quicken): DAOActionResult<Date, Error>
  removeImport(arg0: Date): DAOActionResult<boolean, Error>
  getImport(arg0: Date): DAOActionResult<Quicken | null, Error>
}
Enter fullscreen mode Exit fullscreen mode

Note: Here is where the rub is (well, one of them) between Abstract Factory and Singleton patterns in Typescript. Typescript doesn't allow modifiers like static to be added to elements in an interface. And if you try to add them in the actual implementation, Typescript will complain. If someone knows a way to work around this, please let me know.

The concrete factory creates the actual DAO compatible with the feature set.

class MongoDAOFactory implements DAOFactory {
  dbConnection: MongoDB.MongoClient

  async createConnection(
    dbURI: string,
    dbName: string,
    mongoClientOptions: MongoDB.MongoClientOptions,
  ) {
    try {
      this.dbConnection = await MongoDB.MongoClient.connect(
        dbURI,
        mongoClientOptions,
      )
      this.dbConnection.db(dbName)
    } catch (error) {
      console.error(error)
    }
  }

  createQuickenDAO(): QuickenDAO {
    const mongoQuickenDAO = new MongoQuickenDAO()
    mongoQuickenDAO.injectDB(this.dbConnection)
    return mongoQuickenDAO
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

And finally, in order to set up the DAO for use, I have a file setupStore.ts which exports a function of the same name and the DAO itself:

let quickenDAO: QuickenDAO

 export default async function setupDataStore(isDevelopment: boolean) {
  let mongoClientOptions: MongoDB.MongoClientOptions

  const dbURI = process.env.DB_URI
  const dbName = process.env.DB_NAME

  const mongoFactory = new MongoDAOFactory()
  await mongoFactory.createConnection(dbURI, dbName, mongoClientOptions)
  quickenDAO = mongoFactory.createQuickenDAO()
}

export { quickenDAO }
Enter fullscreen mode Exit fullscreen mode

Because of ES6's top-level await, I can run await setupStore() from my index.ts to generate my DAOs for the app.

Concerns

I am working on a demo of this approach, but it remains largely untested.

The concerns I have (for discussion) are:

  1. Is there anything wrong with how I'm actually creating the DAO and exporting it as an object? Will this create problems in production.
  2. The DAO is not a true Singleton and I'm unsure if that will present problems in the future. Technically, I do not believe there is anything stopping a future contributor to the code from creating a new instance of a particular DAO. But, Typescript won't allow me to add a private constructor when implementing interfaces.
  3. Unit testing of the service layer is tricky because the DAO is undefined until setupStore is run, which means setting up a database connection (even if to something like MongoDB Memory Server). I'm also then tied to a particular database for testing.

I'll make a few more changes to the demo in the coming days and I welcome discussion about this approach.

Thanks!

Top comments (1)