Intoduction (What is DAO)
DAO (Data Access Object) is a nifty design pattern that separates the presentation, service, and data persistence layers in an application. In any data-driven application, the following layers are implemented:
Presentation Layer: This layer is responsible for presenting data in an application.
Service Layer: This layer acts as an intermediate between the presentation layer and the data persistence layer. The application uses this layer to fetch and push data to and from a given data store.
Persistence Layer: This layer handles how data in an application is stored in a given data store.
DAO ensures that the application achieves less coupling among the above layers, i.e., the presentation layer only needs to format the available data to cater to the application's needs. The presentation layer logic can be achieved by defining schemas that can be used to serialize and deserialize complex data structures to cater to the application's needs. The service layer calls defined interfaces to fetch and push data to a given data store, while the persistence layer provides store-specific implementations to achieve data storage. Let's DAO this!.
DAO By Example
Let's dive into a practical example of DAO by implementing a simple API that stores and fetches user account information.
For the sake of simplicity, let's assume our user account needs the following information:
username -> string
user email -> string
creation date -> datetime
bio -> text
active -> boolean
Presentaion Layer
In our simple application, we want to be able to create User instance objects from simple JSON sent to our endpoints. Similarly, we need to convert User instances to JSON objects, which provides a common data type that most of our API consumers would be able to handle. For such a capability, our application needs a serializer/deserializer service.
Let's make this fun and user-friendly! Our API is like a digital playground where you can create, fetch, and play with user accounts. Get ready to swing into action!
from marshmallow import Schema, fields, post_load, validate
from datetime import datetime as dt
from models import User
class UserSchema(Schema):
username = fields.Str(
required=True,
validate=validate.Length(min=1)
)
email = fields.Email(dump_only=True)
created_at = fields.DateTime()
"""
for simplicity we will use a string field but one can use Text
fields and add complex validation schema
"""
bio = fields.Str(required=True)
active = fields.Boolean(dump_only=True)
@post_load
def create_user(self, data, **kwargs):
return User(**data)
Great! It looks like we are making some good progress. Here's an update on our serializer/deserializer code:
The code above represents our Serializer/Deserializer code. The Schema can convert simple Python dictionaries and JSON objects to User objects by simply calling schema.load(dict_object) or schema.loads(json_objec).
The load(s) method of our schema validates the data that is passed to it before casting the passed objects to our defined User object. This ensures that we only store and retrieve valid user account information.
Let's get this blueprint off the ground and start building some amazing user accounts! Let's go!
import datetime
from dataclasses import dataclass
@dataclass(frozen=True, eq=True)
class User:
username: str
email: str
created_at: datetime.datetime = datetime.datetime.now()
bio: str
active: bool = False
The two classes you see above provide the Presentation Layer for our application.
Now, suppose we wanted to persist our user data and store it into some data store. Using DAO as a design pattern, we can provide an interface to our application that would facilitate data storage. Our design needs to hide any storage implementation logic from any other layer in our application. With such a design, we can support any data store without breaking any application code.
To achieve this, our service and persistence layer need to agree on a common interface that would be used to handle data storage. The application only needs to know of the interface's signature. Any data store that our application ends up supporting only needs to implement the interface.
With such a design, we could have multiple data persistence logic. Imagine having a playground with multiple swings, and each swing represents a different data store! Now that's what we call fun with DAO! Let's swing into action and start building our data storage interface.
THE CONTRACT
Alright, let's define our contract (interface) for the data storage layer. This will enable easy data store integration to our application. Each data store provider we support needs to agree on this common interface.
For our application, we need to store one or multiple users by calling a method called insert provided by a service layer, and passing all the required parameters. To retrieve a user, we need to call get_user method provided by the service layer, and pass all the required parameters.
Our contract (interface) can be defined as follows:
from abc import ABC
from typing import List Interable
class StorageContract(abc.ABC):
@abstract_method
def insert(self,users: List[User], **kwargs) -> None:
pass
@abstract_method
def get_user(self,username:str, **kwargs) -> User:
pass
@abstract_method
def fetch_all(self, **kwargs)->Interable[User]:
pass
Great, let's continue building on our example with a specific data store implementation that satisfies the StorageContact interface.
Suppose we need to persist our data to a JSON file on our file system. We can define the JSONStore class as follows:
import os
from somewhere.models import User
from somewhere.schema import UserSchema
from somewhere.exceptions import UserNotFound
from somewhere.contracts import StorageContract
from typing import List, Iterable
import json
class JSONSTORE(StorageContract):
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
def __init__(self):
self.schema = UserSchema()
def insert(self,users: List[User], **kwargs)->None:
users_obj = self.schema.dump(users, many=True)
with open(os.path.join(JSONSTORE.BASE_DIR, 'store.json'), 'r') as file:
store = json.loads(file)
for user in users_obj:
if user not in store:
store.append(user)
with open(os.path.join(JSONSTORE.BASE_DIR, 'store.json'), 'w') as file:
file.write(json.dumps(store))
def get_user(self,username:str, **kwargs)-> User:
with open(os.path.join(JSONSTORE.BASE_DIR, 'store.json'), 'r') as file:
store = json.loads(file)
user = filter(lambda user: user['username'] == username, store)
if not user:
raise UserNotFound(f'user with {username} not found')
return self.schema.load(list(user), many=True)[-1]
def fetch_all(self, **kwargs)->Iterable[User]:
with open(os.path.join(JSONSTORE.BASE_DIR, 'store.json'), 'r') as file:
store = json.loads(file)
return self.schema.load(store, many=True)
This JSONStore class implements the StorageContract interface by providing its own implementation of the insert, get_user, and fetch_all methods. In this case, we're reading and writing data from/to a JSON file on the file system.
With this implementation, we can now store and retrieve user data to/from a JSON file with ease!
e
How about we add support for in-memory data persistence? We can use our 'MemoryStorage' class which provides us with a convenient way to store data in memory. Just like the 'JSONSTORE,' our 'Memory Store' lets us use 'insert,' 'get_user,' and 'fetch_all' methods to read and write data to memory. And the best part? With 'Dao,' we can support any data store imaginable. Let's make this happen!"
from models import User
from schema import UserSchema
from somewhere.exceptions import UserNotFound
from contracts import StorageContract
from typing import List, Iterable
MEMORY_STORE = []
class MemoryStorage(StorageContact):
def __init__(self):
self.schema = UserSchema()
def insert(self,users:List[User], **kwargs)->None:
_users = self.schema.dump(users, many=True)
for user in _users:
if user not in MEMORY_STORE:
MEMODY_STORE.append(user)
def get_user(self, username:str, **kwargs) -> User:
user = filter(lambda user: user['username'] == username, MEMORY_STORE)
return self.schema.load(list(user), many=True)[-1]
def fetch_all(self, **kwargs)->Iterable[User]:
return self.schema.load(MEMORY_STORE, many=True)
The DAO pattern helps to achieve low coupling between different components of an application, making it easier to change or update specific parts of the application without affecting the rest of the system.
By working with interfaces, the DAO pattern promotes excellent object-oriented programming practices that help to write modular, flexible, and maintainable code. And by defining a common contract (interface), the DAO pattern provides a layer of abstraction between the application and the data store, making it easier to support multiple data storage mechanisms without changing the application's core logic.
Overall, the DAO pattern is an excellent design pattern that can help to simplify data persistence in complex applications.
Top comments (0)