30-18 之 DataSource Layer- DataMapper

这一篇文章我们将要谈谈常常听到的 DataMapper 这个东西,应该是有不少人在一些 ORM 的 libary 中有听到这东西。

什麽是 DataMapper 呢 ?

A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.

我个人的理解为 :

这个模式的理念在於,将『 domain 』从『 persistence layer ( 可以想成资料库存取 ) 』分离出来,也就是说你的 domain model 实体中没有所谓的资料库处理这一块。

然後他大部份的使用时机为 :

复杂的业务,且 DataMapper 都会与 Domain Layer 的 Domain Model 一起用

然後有一些重点要注意一下 :

  • 查询资料时,通常会在 app 层呼叫 mapper 来取得资料,然後在交给 domain model 来操作流程。
  • 有些情况 domain 会呼叫 mapper 来取得到资料,但作者建议使用 Lazy Loading 与 Separated Interface 来处理这件事,因为他想将 domain 与 mapper 的依赖关系降到最低。( 这两个名词之後会提到 )
  • application 层通常可以有多个 mapper,但如果你有使用 Metadata Mapping ( 有缘分会说 ) 则可以只有一个。

接下来我觉得直接看范例,会比较能理解做啥。

范例

https://ithelp.ithome.com.tw/upload/images/20211003/20089358683qGGQwZO.png

这个范例业务是这样的 :

有个用户注册,但是需要检查该员工的公司是否已满额,并且也要 18 岁才能 ( 不能是童工啊 ),但如果是 VIP 公司则没限制。

// Domain Layer  =======================================
class PersonDomain{
    id: string
    age: number
    name: string
    company: string
    constructor(id:string, age: number, name: string,company: string){
        this.id = id
        this.age = age
        this.name = name
        this.company = company
    }
    isAdult(): boolean{
        return this.age >= 18
    }
    isVIP(): boolean{
        return ['HAHOW','GOOGLE','KKBOX'].includes(this.company)
    }
}

interface IPersonMapper{
    findById(id: string): PersonDomain
    findByCompany(company: string): PersonDomain[]
    insert(person: PersonDomain): void
    update(person: PersonDomain): void
    delete(person: PersonDomain): void
}

// DataSource Layer =======================================

function _mockExecuteSelectSql(sql){
    return [
        {
            id: '1',
            name: 'mark',
            age: 18,
            company: 'HAHOW'
        }
    ] 
}

class PersonMapper implements IPersonMapper {
    findById(id: string): PersonDomain{
        const resultSet: any = _mockExecuteSelectSql(`SELECT * FROM person WHERE id=${id}`)
        const result = this.doLoad(resultSet)
        return result[0]
    }
    findByCompany(company: string): PersonDomain[]{
        console.log('Connect to db for find')
        const resultSet: any = _mockExecuteSelectSql(`SELECT * FROM person WHERE company=${company}`)
        const result = this.doLoad(resultSet)
        return result
    }
    insert(person: PersonDomain): void{

    }
    update(person: PersonDomain): void{

    }
    delete(person: PersonDomain): void{

    }
    private doLoad(resultSet: any): PersonDomain[]{
        const result = []
        for (const data of resultSet) {
            result.push(new PersonDomain(data.id, data.age, data.name, data.company))
        }
        return result
    }
}

// Application Layer  =======================================
const personMapper = new PersonMapper()
const newPerson: PersonDomain = new PersonDomain(null, 18, 'Mark', 'HAHOW')
if(!newPerson.isVIP()){
   const personsInCompany: PersonDomain[] = personMapper.findByCompany('HAHOW') 
	 if(personsInCompany.length >= 10) throw Error('公司人数已达限制')
}

if(!newPerson.isAdult()) throw Error('要成年才能注册喔')
personMapper.insert(newPerson)

小总结

这个知识点可以用来解释什麽现象

DataMapper 是为了要将『 资料储存 』与 『 Domain Model 』分开而诞生,那反过来思考,如果合在一起会变成怎麽样呢 ?

我觉得比较大的问题是,很多的资料库 query 不是为了该 domain 而产生的,例如在实务上我是有为了处理 N+1 的问题来抓某些资料,或是为了其它 domain 来抓资料,但那个 domain 需要的资料与原本的 domain 不相同,这几种情况。这样如果分离,就不用被绑死在 domain。

这个知识点可以和以前的什麽知识连结呢 ?

之前开发时有需要使用到 TypeORM,然後我看了现在就有个疑问 。

TypeORM - Active Record vs Data Mapper

你点进去,然後看这一段范例码,是不是很像我们上面范例的 application 层那的程序码,然後我的疑问是 :

Repository 就是指 DataMapper 吗 ?

这个问题我们下集待续。

const userRepository = connection.getRepository(User);

// example how to save DM entity
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
await userRepository.save(user);

// example how to remove DM entity
await userRepository.remove(user);

// example how to load DM entities
const users = await userRepository.find({ skip: 2, take: 5 });
const newUsers = await userRepository.find({ isActive: true });
const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" });

我要如何运用这个知识点 ?

事实上我还不确定要如何运用呢 ~ 因为有几个问题我还有点困惑 :

  • Domain Model 感觉就是只处理 domain 的运算逻辑,其它的 i/o 与资料库操作,感觉都是在 application 透过 mapper 来处理,我不确定我的理解这样对不对。
  • 那这样如果有 cache 要进行,是不是应该会写在 mapper 又或是 application 呢 ?
  • 而且我发现我好多逻辑写在 controller,还真有点不确定对不对呢,但我雄雄想到这个应该是之前有提到的 Service 部份。

参考资料


<<:  DAY18 - 将档案上传到 firebase storage

>>:  Day21 - 前处理: 语者正规化

让API 返回 Kafka- producer.send 事件 use Promise

这边使用的是nodejs(egg) 一张图简介一下 ELK+kafka做什麽用 (用於数据分析,lo...

[Day-11] R语言 - K - mode 实作 ( K - mode in R.Studio)

您的订阅是我制作影片的动力 订阅点这里~ 影片程序码 library(naniar) data(ir...

Day14 CSS基础设定_4

今天我们要来教一些常用到的基本设定、包括宽高、背景颜色、文字颜色,以及inline与block的区别...

30天零负担轻松学会制作APP介面及设计【DAY 06】

大家好,我是YIYI,今天要来聊聊我想制作的APP的规格表。 动机与目的 如同【DAY 02】所说,...

Day.10 进入 ARM 世界: ARM Cortex-M Exception 介绍

Exception 与 Interrupt Interrupt 是由内部 timer 或 I/O 装...