DEV Community

Marant7
Marant7

Posted on

Data Mapper Pattern in Enterprise Application Architecture

Introduction

In the book "Patterns of Enterprise Application Architecture", Martin Fowler introduces the Data Mapper pattern, a crucial technique for applications that require a clean separation between business logic and data persistence.

While other patterns (like Active Record) mix business logic and database operations into a single object, Data Mapper enforces a clear separation.


What is the Data Mapper Pattern?

The Data Mapper pattern:

  • Maps domain objects to database structures and vice versa.
  • Keeps business objects unaware of how their data is stored or retrieved.
  • Enables better scaling for large enterprise applications.

Benefits:

  • High separation of concerns.
  • Easier unit testing of business logic.
  • Simplifies switching database technologies (e.g., from SQLite to PostgreSQL).

Real-world Example in Python

Without DataMapper

import sqlite3

class Product:
    def __init__(self, id, name, price):
        self.id = id
        self.name = name
        self.price = price
        self.connection = sqlite3.connect("database.db") 
    def save(self):

        cursor = self.connection.cursor()
        cursor.execute('''INSERT INTO products (id, name, price) VALUES (?, ?, ?)''',
                       (self.id, self.name, self.price))
        self.connection.commit()

    @classmethod
    def find(cls, product_id):

        connection = sqlite3.connect("database.db")  
        cursor = connection.cursor()
        cursor.execute('''SELECT * FROM products WHERE id = ?''', (product_id,))
        row = cursor.fetchone()
        if row:
            return Product(row[0], row[1], row[2])
        return None

    @classmethod
    def list_all(cls):

        connection = sqlite3.connect("database.db")
        cursor = connection.cursor()
        cursor.execute('''SELECT * FROM products''')
        rows = cursor.fetchall()
        return [Product(row[0], row[1], row[2]) for row in rows] 
Enter fullscreen mode Exit fullscreen mode

Imagine we want to manage a collection of products for a store. We'll use:

  • A domain model Product.
  • A mapper class ProductMapper.
  • A simple SQLite database for persistence.

1. Domain Model (domain/product.py)

class Product:
    def __init__(self, product_id: int, name: str, price: float):
        self.product_id = product_id
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product(id={self.product_id}, name='{self.name}', price={self.price})"
Enter fullscreen mode Exit fullscreen mode

2. Data Mapper (mappers/product_mapper.py)

import sqlite3
from app.domain.product import Product

class ProductMapper:
    def __init__(self, connection: sqlite3.Connection):
        self.connection = connection
        self._create_table()

    def _create_table(self):
        cursor = self.connection.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS products (
                product_id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                price REAL NOT NULL
            )
        ''')
        self.connection.commit()

    def insert(self, product: Product):
        cursor = self.connection.cursor()
        cursor.execute('''
            INSERT INTO products (product_id, name, price) VALUES (?, ?, ?)
        ''', (product.product_id, product.name, product.price))
        self.connection.commit()

    def find(self, product_id: int) -> Product:
        cursor = self.connection.cursor()
        cursor.execute('SELECT product_id, name, price FROM products WHERE product_id = ?', (product_id,))
        row = cursor.fetchone()
        if row:
            return Product(*row)
        return None

    def list_all(self):
        cursor = self.connection.cursor()
        cursor.execute('SELECT product_id, name, price FROM products')
        rows = cursor.fetchall()
        return [Product(*row) for row in rows]
Enter fullscreen mode Exit fullscreen mode

3.Using the Mapper (main.py)

import sqlite3
from app.domain.product import Product
from app.mappers.product_mapper import ProductMapper

def main():
    connection = sqlite3.connect(":memory:")  # In-memory database for testing
    mapper = ProductMapper(connection)

    # Inserting products
    mapper.insert(Product(1, "Laptop", 999.99))
    mapper.insert(Product(2, "Smartphone", 499.50))

    # Listing products
    products = mapper.list_all()
    for product in products:
        print(product)

    # Finding a product
    product = mapper.find(1)
    print(f"Found product: {product}")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

4. Testing with Python (tests/test_product_mapper.py)

import unittest
import sqlite3
from app.domain.product import Product
from app.mappers.product_mapper import ProductMapper

class TestProductMapper(unittest.TestCase):
    def setUp(self):
        self.connection = sqlite3.connect(":memory:")
        self.mapper = ProductMapper(self.connection)

    def test_insert_and_find(self):
        product = Product(1, "Tablet", 299.99)
        self.mapper.insert(product)
        found = self.mapper.find(1)
        self.assertIsNotNone(found)
        self.assertEqual(found.name, "Tablet")

    def test_list_all(self):
        self.mapper.insert(Product(1, "Tablet", 299.99))
        self.mapper.insert(Product(2, "Monitor", 199.99))
        products = self.mapper.list_all()
        self.assertEqual(len(products), 2)

if __name__ == "__main__":
    unittest.main()

Enter fullscreen mode Exit fullscreen mode

Conclusion

The Data Mapper pattern is an excellent choice for applications that prioritize business logic independence from database concerns.
By separating responsibilities, it enables easier evolution of the system, integration of caching layers, database migrations, and safe refactoring without impacting business logic.

Adopting patterns like Data Mapper will help you build cleaner, more robust, and scalable enterprise software.

link repository: https://github.com/Marant7/datamapperpython.git

Top comments (5)

Collapse
 
andree_sebastianfloresm profile image
Andree Sebastian FLORES MELENDEZ

Excellent article! It clearly and practically explains the Data Mapper pattern, highlighting its usefulness in separating business logic from data persistence. The Python examples help you understand how to implement this architecture to improve the scalability and maintainability of enterprise applications. A recommended read for those looking to design more robust and decoupled systems.

Collapse
 
erick_yoelaymachoque_a1 profile image
ERICK YOEL AYMA CHOQUE

This article clearly explains the Data Mapper pattern and its value in enterprise-level applications. The comparison with the non-mapper approach makes it easy to understand the benefits of separating concerns. The Python example was especially helpful in showing how the domain model is decoupled from the persistence layer.

Collapse
 
elvis_ronaldleyvasardon profile image
ELVIS RONALD LEYVA SARDON

You can see a clear and practical explanation of the Data Mapper pattern. It's interesting to see how it compares implementations without and with the pattern, which helps understand its real benefits, such as separation of responsibilities and improved code testability.
An observation would be to include a brief section on disadvantages or practical considerations, such as the greater initial complexity compared to other simpler patterns like Active Record.

Collapse
 
jf2021070309 profile image
Jaime Elias Flores Quispe

I found the explanation of the Data Mapper pattern in this article quite helpful. The distinction between the domain model and the data mapping layer is clearly demonstrated through the Python examples, making it easy to understand how the pattern keeps business logic separate from database operations

Collapse
 
brian_danilochitequispe profile image
BRIAN DANILO CHITE QUISPE

This article provides a detailed and practical explanation of the Data Mapper pattern, a key technique for separating business logic from data persistence in applications. What's interesting is that it not only explains the concept but also guides the reader through its implementation in Python, making it more accessible and useful for developers looking to structure their applications more efficiently and scalably.