Design DAL

Design DAL

HOWTOUSEDATA

DAL

A data access layer (DAL) in computer software is a layer of a computer program which provides simplified access to data stored in rational \ not-rational \ else storage.

  • Simplified interface of the data for the system logic.

  • Models definitions, objects attributes and structure, dbs to handle models storage.

Sub objects \ patterns

  • DB handlers

    Db connection handlers, implment the technology language specific use.

    Example: Using python sqlite library for connection and execution.

Code Example

Abstract db handler:

from abc import abstractmethod, ABC


class DBHandler(ABC):

    def __init__(self):
        pass

    @abstractmethod
    def connect(self):
        raise NotImplementedError

    @abstractmethod
    def disconnect(self):
        raise NotImplementedError

    @property
    @abstractmethod
    def connection(self):
        raise NotImplementedError

    @property
    @abstractmethod
    def session(self):
        raise NotImplementedError

Concrete db technology handler:

from sqlalchemy import create_engine, Engine
from sqlalchemy.orm import sessionmaker, Session

from dal_example.dal.db.db_handler import DBHandler


class SqlAlchemyConsts:
    URL = "url"


class SqlAlchemyHandler(DBHandler):

    def __init__(self, **kwargs):
        super().__init__()
        self.__connection_kwargs = kwargs
        self.__connection_string = self.__connection_kwargs.pop(SqlAlchemyConsts.URL, None)
        self._connection: Engine | None = None
        self._session_maker: sessionmaker | None = None

    def connect(self):
        self._connection = create_engine(self.__connection_string, **self.__connection_kwargs)
        self._session_maker = sessionmaker(bind=self.connection)

    def disconnect(self):
        self._connection.dispose()

    @property
    def connection(self) -> Engine:
        return self._connection

    @property
    def session(self) -> Session:
        return self._session_maker()
  • Models

    The classes to represent the objects structure in the db.
    With orm \ odm dessign the models also defines the tables \ collections structure in th DB.

Example code

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Notification(Base):
    __tablename__ = 'notifications'

    id = Column(Integer, primary_key=True)
    message = Column(String, nullable=False)
    user_id = Column(Integer, nullable=False)

  • DAO

    Data access object, for each model there is a dao object that his responsibility to hold the connection between the db and the real life objects in the project.
    Contains query logic and query building process, working with the database relevant handler.

Code Example

Abstract dao:

import uuid
from abc import ABC, abstractmethod
from typing import Generic, TypeVar

from pydantic import BaseModel

from dal_example.dal.db.db_handler import DBHandler

Object = TypeVar('Object')
DbHandler = TypeVar('DbHandler', bound=DBHandler)


class DataAccessObject(ABC, Generic[Object, DbHandler]):

    def __init__(self, db_handler: DbHandler):
        self._db_handler = db_handler
        self.connect()

    @property
    def db_handler(self) -> DbHandler:
        return self._db_handler

    def connect(self):
        self.db_handler.connect()

    def disconnect(self):
        self.db_handler.disconnect()

    @abstractmethod
    def create(self, new_object: Object) -> Object:
        raise NotImplementedError

    @abstractmethod
    def read(self, object_id: uuid) -> Object:
        raise NotImplementedError

    @abstractmethod
    def update(self, object_id: uuid, updated_object: Object) -> Object:
        raise NotImplementedError

    @abstractmethod
    def delete(self, object_id: uuid) -> None:
        raise NotImplementedError

Concrete tech oriented dao:

import uuid
from typing import TypeVar, Type

from dal_example.dal.dao.dao import DataAccessObject, DbHandler
from dal_example.dal.db.db_handlers.sql_alchemy_handler import SqlAlchemyHandler

DeclarativeBase = TypeVar('DeclarativeBase')


class SqlAlchemyDAO(DataAccessObject[DeclarativeBase, SqlAlchemyHandler]):

    def __init__(self, db_handler: DbHandler, model_class: Type[DeclarativeBase]):
        super().__init__(db_handler)
        self.model_class = model_class

    def create(self, new_object: DeclarativeBase) -> DeclarativeBase:
        with self.db_handler.session as session:
            session.add(new_object)
            session.commit()
            session.refresh(new_object)
        return new_object

    def read(self, object_id: uuid) -> DeclarativeBase:
        with self.db_handler.session as session:
            return session.query(self.model_class).filter_by(id=object_id).first()

    def update(self, object_id: uuid, updated_object: DeclarativeBase) -> DeclarativeBase:
        with self.db_handler.session as session:
            db_object = session.query(self.model_class).filter_by(id=object_id).first()
            if db_object:
                for key, value in updated_object.__dict__.items():
                    setattr(db_object, key, value)
                session.commit()
                session.refresh(db_object)
            return db_object

    def delete(self, object_id: uuid) -> None:
        with self.db_handler.session as session:
            db_object = session.query(self.model_class).filter_by(id=object_id).first()
            if db_object:
                session.delete(db_object)
                session.commit()

Concrete object dao:

from typing import Type
from dal_example.dal.dao.generic_daos.sql_alchemy_dao import SqlAlchemyDAO
from dal_example.dal.db.db_handler import DBHandler
from dal_example.dal.db.db_handlers.sql_alchemy_handler import SqlAlchemyHandler
from dal_example.dal.models.notification import Notification


class NotificationDAO(SqlAlchemyDAO[Notification]):
    def __init__(self, db_handler: DBHandler):
        # Pass both db_handler and Notification model to the parent constructor
        super().__init__(db_handler, Notification)

    def find_by_user_id(self, user_id: int) -> list[Notification]:
        """Find all notifications for a specific user."""
        with self.db_handler.session as session:
            notifications = session.query(Notification).filter_by(user_id=user_id).all()
            return notifications

    def get_all_notifications(self) -> list[Notification]:
        with self.db_handler.session as session:
            notifications = session.query(Notification).all()
            return notifications

  • REPOSITORY

    Highest part in the dal pattern, implment data related logic and advanced analyze of the data in order to externalize facade for the business logic layer.

Code Example

Abstract repository:

import uuid
from abc import ABC, abstractmethod
from typing import TypeVar, Generic

from dal_example.dal.dao.dao import DataAccessObject

Object = TypeVar("Object")
Dao = TypeVar('Dao', bound=DataAccessObject)


class Repository(ABC, Generic[Object, Dao]):
    def __init__(self, dao: Dao) -> None:
        self._dao = dao

    @abstractmethod
    def create(self, new_object: Object) -> Object:
        raise NotImplementedError

    @abstractmethod
    def read(self, object_id: uuid) -> Object:
        raise NotImplementedError

    @abstractmethod
    def update(self, object_id: uuid, updated_object: Object) -> Object:
        raise NotImplementedError

    @abstractmethod
    def delete(self, object_id: uuid) -> None:
        raise NotImplementedError

    @property
    def dao(self) -> Dao:
        return self._dao

Concerete object repository:

import uuid

from dal_example.dal.dao.daos.notification_dao import NotificationDAO
from dal_example.dal.db.db_handlers.sql_alchemy_handler import SqlAlchemyHandler
from dal_example.dal.models.notification import Notification
from dal_example.dal.repository.repository import Repository


class NotificationRepository(Repository[Notification, NotificationDAO]):

    def create(self, new_object: Notification) -> Notification:
        return self.dao.create(new_object)

    def read(self, object_id: uuid) -> Notification:
        return self.dao.read(object_id)

    def update(self, object_id: uuid, updated_object: Notification) -> Notification:
        return self.dao.update(object_id, updated_object)

    def delete(self, object_id: uuid) -> None:
        self.dao.delete(object_id)

    def get_longest_notification(self) -> Notification:
        notifications = self.dao.get_all_notifications()
        return max(notifications, key=lambda notification: len(notification.message))

    def user_has_notifications(self, user_id: int) -> bool:
        notifications = self.dao.find_by_user_id(user_id)
        return len(notifications) > 0

    def get_all_user_notifications(self, user_id: int) -> list[Notification]:
        return self.dao.find_by_user_id(user_id)

Full code example

Advanced subjects

ORM

Schemas

Comming soon….