Data Mapper

Паттерн проектирования Data Mapper

Паттерн проектирования Data Mapper

Описание Data Mapper

Объектные и реляционные БД используют разные способы структурирования данных. Множество составляющих объектов, например коллекции и наследование, не представлены в реляционных БД. Когда проектируется объектная модель с большим количеством бизнес-логики, полезно применять такие механизмы для улучшения организации хранения данных и логики, которая работает c ними. Это приводит к различиям в организации. Так что объектная и реляционная схемы не идентичны.

Тем не менее, необходимость в обмене данными между двумя схемами не отпадает, и этот обмен становится, в свою очередь, сложным. Если же объект знает о реляционной структуре — изменения в одной из структур приведёт к проблемам в другой.

Data Mapper — это программная прослойка, разделяющая объект и БД. Его обязанность — пересылать данные между ними и изолировать их друг от друга. При использовании Data Mapper'а объекты не нуждаются в знании о существовании БД. Они не нуждаются в SQL-коде, и (естественно) в информации о структуре БД. Так как Data Mapper - это разновидность паттерна Mapper, сам объект-Mapper неизвестен объекту.

Примеры реализации

// Data Mapper Pattern in JavaScript
class User {
    constructor(id, name, email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

class UserDataMapper {
    constructor(database) {
        this.db = database;
    }
    
    findById(id) {
        const row = this.db.query('SELECT * FROM users WHERE id = ?', [id]);
        return row ? this.mapRowToUser(row) : null;
    }
    
    findAll() {
        const rows = this.db.query('SELECT * FROM users');
        return rows.map(row => this.mapRowToUser(row));
    }
    
    save(user) {
        if (user.id) {
            return this.update(user);
        } else {
            return this.insert(user);
        }
    }
    
    insert(user) {
        const id = this.db.insert('INSERT INTO users (name, email) VALUES (?, ?)', 
            [user.name, user.email]);
        return new User(id, user.name, user.email);
    }
    
    update(user) {
        this.db.update('UPDATE users SET name = ?, email = ? WHERE id = ?',
            [user.name, user.email, user.id]);
        return user;
    }
    
    delete(id) {
        return this.db.delete('DELETE FROM users WHERE id = ?', [id]);
    }
    
    mapRowToUser(row) {
        return new User(row.id, row.name, row.email);
    }
}

// Usage
const db = new Database();
const userMapper = new UserDataMapper(db);
const user = userMapper.findById(1);
// Data Mapper Pattern in C++
#include <iostream>
#include <vector>
#include <memory>
#include <string>

class User {
private:
    int id;
    std::string name;
    std::string email;
    
public:
    User(int id, const std::string& name, const std::string& email)
        : id(id), name(name), email(email) {}
    
    int getId() const { return id; }
    std::string getName() const { return name; }
    std::string getEmail() const { return email; }
};

class Database {
public:
    virtual ~Database() = default;
    virtual std::vector<std::map<std::string, std::string>> query(const std::string& sql) = 0;
    virtual int insert(const std::string& sql, const std::vector<std::string>& params) = 0;
    virtual void update(const std::string& sql, const std::vector<std::string>& params) = 0;
    virtual void remove(const std::string& sql, const std::vector<std::string>& params) = 0;
};

class UserDataMapper {
private:
    Database* db;
    
public:
    UserDataMapper(Database* database) : db(database) {}
    
    std::unique_ptr<User> findById(int id) {
        std::string sql = "SELECT * FROM users WHERE id = " + std::to_string(id);
        auto rows = db->query(sql);
        if (!rows.empty()) {
            return mapRowToUser(rows[0]);
        }
        return nullptr;
    }
    
    std::vector<std::unique_ptr<User>> findAll() {
        auto rows = db->query("SELECT * FROM users");
        std::vector<std::unique_ptr<User>> users;
        for (const auto& row : rows) {
            users.push_back(mapRowToUser(row));
        }
        return users;
    }
    
    std::unique_ptr<User> save(const User& user) {
        if (user.getId() > 0) {
            update(user);
            return std::make_unique<User>(user);
        } else {
            return insert(user);
        }
    }
    
private:
    std::unique_ptr<User> mapRowToUser(const std::map<std::string, std::string>& row) {
        int id = std::stoi(row.at("id"));
        return std::make_unique<User>(id, row.at("name"), row.at("email"));
    }
    
    std::unique_ptr<User> insert(const User& user) {
        // Implementation would insert and return new user with ID
        return std::make_unique<User>(1, user.getName(), user.getEmail());
    }
    
    void update(const User& user) {
        // Implementation would update user
    }
};
// Data Mapper Pattern in Go
package main

import (
    "database/sql"
    "fmt"
)

type User struct {
    ID    int
    Name  string
    Email string
}

type Database interface {
    Query(sql string, args ...interface{}) (*sql.Rows, error)
    Exec(sql string, args ...interface{}) (sql.Result, error)
}

type UserDataMapper struct {
    db Database
}

func NewUserDataMapper(db Database) *UserDataMapper {
    return &UserDataMapper{db: db}
}

func (m *UserDataMapper) FindByID(id int) (*User, error) {
    rows, err := m.db.Query("SELECT id, name, email FROM users WHERE id = ?", id)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    if rows.Next() {
        var user User
        err := rows.Scan(&user.ID, &user.Name, &user.Email)
        if err != nil {
            return nil, err
        }
        return &user, nil
    }
    return nil, nil
}

func (m *UserDataMapper) FindAll() ([]*User, error) {
    rows, err := m.db.Query("SELECT id, name, email FROM users")
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var users []*User
    for rows.Next() {
        var user User
        err := rows.Scan(&user.ID, &user.Name, &user.Email)
        if err != nil {
            return nil, err
        }
        users = append(users, &user)
    }
    return users, nil
}

func (m *UserDataMapper) Save(user *User) (*User, error) {
    if user.ID > 0 {
        return m.Update(user)
    } else {
        return m.Insert(user)
    }
}

func (m *UserDataMapper) Insert(user *User) (*User, error) {
    result, err := m.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", 
        user.Name, user.Email)
    if err != nil {
        return nil, err
    }
    
    id, err := result.LastInsertId()
    if err != nil {
        return nil, err
    }
    
    return &User{ID: int(id), Name: user.Name, Email: user.Email}, nil
}

func (m *UserDataMapper) Update(user *User) (*User, error) {
    _, err := m.db.Exec("UPDATE users SET name = ?, email = ? WHERE id = ?",
        user.Name, user.Email, user.ID)
    if err != nil {
        return nil, err
    }
    return user, nil
}
# Data Mapper Pattern in Python
from abc import ABC, abstractmethod
from typing import List, Optional

class User:
    def __init__(self, id: int, name: str, email: str):
        self.id = id
        self.name = name
        self.email = email

class Database(ABC):
    @abstractmethod
    def query(self, sql: str, params: tuple = None) -> List[dict]:
        pass
    
    @abstractmethod
    def insert(self, sql: str, params: tuple) -> int:
        pass
    
    @abstractmethod
    def update(self, sql: str, params: tuple) -> None:
        pass
    
    @abstractmethod
    def delete(self, sql: str, params: tuple) -> None:
        pass

class UserDataMapper:
    def __init__(self, database: Database):
        self.db = database
    
    def find_by_id(self, id: int) -> Optional[User]:
        rows = self.db.query("SELECT * FROM users WHERE id = %s", (id,))
        if rows:
            return self._map_row_to_user(rows[0])
        return None
    
    def find_all(self) -> List[User]:
        rows = self.db.query("SELECT * FROM users")
        return [self._map_row_to_user(row) for row in rows]
    
    def save(self, user: User) -> User:
        if user.id:
            return self._update(user)
        else:
            return self._insert(user)
    
    def _insert(self, user: User) -> User:
        user_id = self.db.insert(
            "INSERT INTO users (name, email) VALUES (%s, %s)",
            (user.name, user.email)
        )
        return User(user_id, user.name, user.email)
    
    def _update(self, user: User) -> User:
        self.db.update(
            "UPDATE users SET name = %s, email = %s WHERE id = %s",
            (user.name, user.email, user.id)
        )
        return user
    
    def _map_row_to_user(self, row: dict) -> User:
        return User(row['id'], row['name'], row['email'])

# Usage
if __name__ == "__main__":
    db = Database()
    mapper = UserDataMapper(db)
    user = mapper.find_by_id(1)
<?php
// Data Mapper Pattern in PHP
class User {
    private $id;
    private $name;
    private $email;
    
    public function __construct($id, $name, $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }
    
    public function getId() { return $this->id; }
    public function getName() { return $this->name; }
    public function getEmail() { return $this->email; }
}

interface Database {
    public function query($sql, $params = []);
    public function insert($sql, $params = []);
    public function update($sql, $params = []);
    public function delete($sql, $params = []);
}

class UserDataMapper {
    private $db;
    
    public function __construct(Database $database) {
        $this->db = $database;
    }
    
    public function findById($id) {
        $rows = $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
        if (!empty($rows)) {
            return $this->mapRowToUser($rows[0]);
        }
        return null;
    }
    
    public function findAll() {
        $rows = $this->db->query("SELECT * FROM users");
        return array_map([$this, 'mapRowToUser'], $rows);
    }
    
    public function save(User $user) {
        if ($user->getId()) {
            return $this->update($user);
        } else {
            return $this->insert($user);
        }
    }
    
    private function insert(User $user) {
        $id = $this->db->insert(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            [$user->getName(), $user->getEmail()]
        );
        return new User($id, $user->getName(), $user->getEmail());
    }
    
    private function update(User $user) {
        $this->db->update(
            "UPDATE users SET name = ?, email = ? WHERE id = ?",
            [$user->getName(), $user->getEmail(), $user->getId()]
        );
        return $user;
    }
    
    private function mapRowToUser($row) {
        return new User($row['id'], $row['name'], $row['email']);
    }
}

// Usage
$db = new Database();
$mapper = new UserDataMapper($db);
$user = $mapper->findById(1);
?>

Использована доработанная иллюстрация с сайта Мартина Фаулера.

Источник