1

I am having such a hard time with OOP and databases. Both in C++ with SQL and even more so with Node.js and MongoDB. For this question I will use Node/Mongo.

Let's say we have a user object. Over time user objects can get pretty big..

class User {
   constructor(){
      this.name = '';
      this.id = '';
      this.country = '';
      this.email = '';
      this.password = '';
      this.level = 1;
      // ... and on
   }

   isAdmin(){
      return this.level === 0;
   }
}

In my app the User object can be constructed to check for authorization among other things. But 90% of its purpose is to just hold data. So when a user is entered into the database, all properties in the class are entered as well. That is where the simplicity ends. Once I need to retrieve a user, I end up with a plain old object that looks exactly like a User object but without any functions. So what do I do? I can

  1. Pass the user object retrieved into a User constructor that has all 30ish parameters via and object
  2. Call 30 setters such as User.setName(doc.name)
  3. Write some intermediary function in the main app like App.setUserFromDatabase(user, doc)
  4. Just use the object directly, ie validatePassword(password, doc.password)

Converting my plain MongoDB object to a User class object is such a waste of time, yet I feel so dirty about using the MongoDB object directly since it has no definition like the class object. But using the class object seems so pointless just to call one or two functions.

So I am just stuck here trying to understand the right way to user database objects and actual application objects when they are basically the same, but only one (the application object) has a very clearly defined structure.

8
  • 4
    "X is such a waste of time" types of arguments tend to be solved by implementing more reusability or automation, rather than not doing X altogether. I'm no C++ dev, but other languages have ORMs which specifically do the mapping for you, to cut out the boilerplating as much as possible.
    – Flater
    Commented Apr 8, 2021 at 1:57
  • 1
    Looking forward to an answer. AFAIK Java people think this situation is just normal. I don't get it myself, which is why I'd like to know what should be happening. (Though I will say, even knowing this is just an example, a user object that includes all this stuff directly isn't really a good object model.)
    – davidbak
    Commented Apr 8, 2021 at 2:30
  • 2
    Real encapsulated objects and data structures are not the same thing. With or without a database being involved. Ask yourself what those one or two functions really depend on and build an object around that. Commented Apr 8, 2021 at 3:00
  • 1
    Hide non-OOP concepts with Repository pattern.
    – Basilevs
    Commented Apr 8, 2021 at 7:27
  • 2
    If your database tables and application objects are the same, then you've likely fallen into a trap of conflating database persistence with your application model. You mention that you want the User class to check for 'authorization among other things' - this sounds to me like User is going to end up being a God object, which is really the antithesis of OO. A more 'OO' approach would involve clean separation of persisted data away from behaviour, then define classes based on their behaviour and not the data they work with - For example, an 'Authenticator' for user identity. Commented Apr 8, 2021 at 10:08

5 Answers 5

1

OOP and databases use data quite differently.

You are modeling your OO data structure on the database's row model. The problem is that a row model has no purpose in your application, a User model does.

So, create a LoginUser, that is accessed from the table, but only contains the elements needed for logging in.

class LoginUser {
   private String name;
   private long id;
   private String password; // I hope that's a hash!
   private int level;
}

Now, even though your database has an additional 40 columns, you don't care.

What about when the user wants to update their address? Well, that's a UserAddress.

class UserAddress {
   private String name;
   private long id;
   private String street1;
   private String street2;
   private String city;
   private String stateOrDistrict;
   private String country;
}

and their email? The same thing.

There is no need to ruin your domain model by making it identical to the database model. Doing so (which is the core of the Active Record Pattern and the Row Data Gateway pattern) means that your model will start looking like database rows and operations on database rows.

If you prefer a descriptive Domain Model (where the objects represent the real world ideas and not the way they are stored or structured), selecting a model that represents a database row is a bad idea.

Note that by avoiding the row based model, I can now separate my OOP model from storage.

class Storage {
   public LoginUser getLoginUserById(long id) {
      ... by looking it up in the user table ...
      -or-
      ... by looking it up in a new password table ...
   }

   public boolean updateUserAddress(UserAddress address) throws NotFoundException {
      ... by storing it all in the user row ...
      -or-
      ... by storing it a joined table ...
   }

}

1-to-many patterns can be serialized into legacy comma separated values in a field, or they can become joined selects, etc.

There are tools that manage a lot of the boilerplate for moving objects into and out of tables, where the objects aren't required to match the table's rows exactly. These tools are called Object - Relational (Database) Managers (ORMs) These are popular in the Java space, and have developed to a point that you can query the database using Object-Oriented like languages instead of SQL. While using a system like this might be a good choice for you; often it can be overkill as configuring the mappings between objects and relational database tables can be complex.

1

Assumed your generic MongoDB "User" object has exactly the same properties as your custom User object, you can use Object.assign for the implementation of a User constructor in one line of code, without the "30 setter calls":

class User {
   constructor(mongoDbUser){
      mongoDbUser && Object.assign(this, mongoDbUser);
   }
   // ... User methods follow here
   // ...
}

(Credits to this SO answer from 2014, where you find more information.)

And before you start complaining about "performance": I would expect that retrieving an object from a database first would usually be so much slower than this kind of type conversion that you can ignore the performance hit in most cases.

0

The problem with using the object or structure returned by the database is, as you point out, That the rest of your code doesn't know what properties it has.

If later you call user.name and the user doesn't have a name, you will get a run time error. Populating a Class or struct allows you to guarantee that this error wont happen.

Obviously if you then go and use javascript or some other play language without 'strong' typing the benefits are limited.

0

How to solve it?

A very popular is a variant from 3: in the repository pattern a repository object acts like a container of objects, and interacts with the database for getting or changing objects.

This works great, is consistent with OO style, and you could easily replace a database with another, by using a repository with the same interface but a completely different implementation.

Is this the best approach?

It’s a proven and effective approach. However its suitability completely depends on your architecture and the complexity of the domain.

For very simple systems, the repository could be an overkill. And other patterns, such as the active record or a row data gateway could provide a simpler solution.

In any case, approach 1 and approach 2 are complementary to implement the loading of objects based on the database:

  • Typically the constructor wound require only core arguments absolutely necessary to create an object; the rest are set with setters.
  • A builder pattern can be used to hide such details. This can be especially relevant if the structure is dynamic and cannot be completely known at coding time.
  • a repository would use a builder or a combination of constructor and setters to return objects.

The option 4 is not to be recommended, since it leads to procedural code in an OO disguise.

-2

It just a simple concept of "Separation of Concern". The DB object holds the data for an instance of the User class object. The distinction is in the usage. For example, your application has 2 features

  1. calculate/Determine the age from the age of the user from its date of birth.
  2. borrow/lend money to another user.

Note: The user has "date_of_birth", "amount_in_wallet", and "parent_id" fields and properties in the DB and the class definition, with reference to your example.

Before you read ahead, take a moment to deliberate where each function could be implemented.

Feature 1 should be implemented inside your DB class(or more commonly known as a schema in the case of MongoDB ). It would be something like this:

// User.schema.js
   const calculateAge = (dateObjectThatRepresentTheDateOfBirth)=>{
     return whateverTheAgeShouldBeForSomeoneBornOnThatDate
}   

Feature 2 should be implemented inside your user class. It would be something like this:

//User.js
const borrowMoneyFromParent = (amount) => {
           let parentDBObject =  UserShema.find(this.parentID)
            this.walletAmount += amount
             parentDBObject.update({ amount: parentDBObject.walletAmount -  amount})
            /**
            * either force call the save method for this user here
            * or call in your app where the  user borrows money
          */
        
    } 

Now In your app where you need to manipulate the user's data, the DB object is used as a reference to access all the orm functionalities to save you the overhead of creating a class for database manipulation(which is common to every app). Further, you would use the user object to carry out the borrow or lending operation because it's not the concern of the data to send or receive money but to store the result of the transaction.

I hope this helps you wrap your head around the idea.

Not the answer you're looking for? Browse other questions tagged or ask your own question.