DTO、儲存庫和資料對映器在DDD中的作用 | Khalil Stemmler
在領域驅動設計中,對於在物件建模系統的開發中需要發生的每一件事情都有一個正確的工具。
負責處理驗證邏輯的是什麼?值物件。
你在哪裡處理領域邏輯?儘可能使用實體,否則領域服務。
也許學習DDD最困難的方面之一就是能夠確定特定任務所需的工具。
在DDD中,儲存庫,資料對映器和DTO是實體生命週期的關鍵部分,使我們能夠儲存,重建和刪除域實體。這種型別的邏輯稱為“ 資料訪問邏輯 ”。
對於使用MVC構建REST-ful CRUD API而不太關注封裝ORM資料訪問邏輯的開發人員,您將學習:
- 當我們不封裝ORM資料訪問邏輯時發生的問題
- 如何使用DTO來穩定API
- 儲存庫如何充當複雜ORM查詢的外觀
- 建立儲存庫和方法的方法
- 資料對映器如何用於與DTO,域實體和ORM模型進行轉換
我們如何在MVC應用程式中使用ORM模型?
我們看看一個MVC控制器的程式碼:
class UserController extends BaseController { async exec (req, res) => { try { const { User } = models; const { username, email, password } = req.body; const user = await User.create({ username, email, password }); return res.status(201); } catch (err) { return res.status(500); } } } |
我們同意這種方法的好處是:
- 這段程式碼非常容易閱讀
- 在小型專案中,這種方法可以很容易地快速提高工作效率
但是,隨著我們的應用程式不斷髮展並變得越來越複雜,這種方法會帶來一些缺點,可能會引入錯誤。
主要原因是因為缺乏關注點分離。這段程式碼負責太多事情:
當我們為專案新增越來越多的程式碼時,我們注意為我們的類分配單一的責任變得非常重要。
場景:在3個單獨的API呼叫中返回相同的檢視模型
這是一個示例,其中缺乏對從ORM檢索資料的封裝可能導致引入錯誤。
假設我們正在開發我們的乙烯基交易應用程式,我們的任務是建立3個不同的API呼叫。
GET /vinyl?recent=6 - GET the 6 newest listed vinyl GET /vinly/:vinylId/ - GET a particular vinyl by it's id GET /vinyl/owner/:userId/ - GET all vinyl owned by a particular user |
在每個API呼叫中,我們都需要返回Vinyl檢視模型,所以讓我們做第一個控制器:返回最新的乙烯基。
export class GetRecentVinylController extends BaseController { private models: any; public constructor (models: any) { super(); this.models = models; } public async executeImpl(): Promise<any> { try { const { Vinyl, Track, Genre } = this.models; const count: number = this.req.query.count; const result = await Vinyl.findAll({ where: {}, include: [ { owner: User, as: 'Owner', attributes: ['user_id', 'display_name'] }, { model: Genre, as: 'Genres' }, { model: Track, as: 'TrackList' }, ], limit: count ? count : 12, order: [ ['created_at', 'DESC'] ], }) return this.ok(this.res, result); } catch (err) { return this.fail(err); } } } |
如果您熟悉Sequelize,這可能是您的標準。
再實現一個根據Id獲得實體的控制器:
export class GetVinylById extends BaseController { private models: any; public constructor (models: any) { super(); this.models = models; } public async executeImpl(): Promise<any> { try { const { Vinyl, Track, Genre } = this.models; const vinylId: string = this.req.params.vinylId; const result = await Vinyl.findOne({ where: {}, include: [ { model: User, as: 'Owner', attributes: ['user_id', 'display_name'] }, { model: Genre, as: 'Genres' }, { model: Track, as: 'TrackList' }, ] }) return this.ok(this.res, result); } catch (err) { return this.fail(err); } } } |
這兩個類之間沒有太大的不同,呃?
所以這絕對不遵循DRY原則,因為我們在這裡重複了很多。
並且您可以預期第三個API呼叫將與此類似。
到目前為止,我們注意到的主要問題是:程式碼重複;另一個問題是......缺乏資料一致性!
請注意我們如何直接傳回ORM查詢結果?
return this.ok(this.res, result); |
這就是為了響應API呼叫而返回給客戶端的內容。
那麼,當我們 在資料庫上執行遷移並新增新列時會發生什麼?更糟糕的是 - 當我們刪除列或更改列的名稱時會發生什麼?
我們剛剛破壞了依賴它的每個客戶端的API。
嗯......我們需要一個工具。
讓我們進入我們的企業工具箱,看看我們發現了什麼......
啊,DTO(資料傳輸物件)。
資料傳輸物件
資料傳輸物件是在兩個獨立系統之間傳輸資料的物件的(奇特)術語。
當我們關注Web開發時,我們認為DTO是View Models,因為它們是虛假模型。它們不是真正的域模型,但它們包含檢視需要了解的儘可能多的資料。
例如,Vinyl檢視模型/ DTO可以構建為如下所示:
type Genre = 'Post-punk' | 'Trip-hop' | 'Rock' | 'Rap' | 'Electronic' | 'Pop'; interface TrackDTO { number: number; name: string; length: string; } type TrackCollectionDTO = TrackDTO[]; // Vinyl view model / DTO, this is the format of the response interface VinylDTO { albumName: string; label: string; country: string; yearReleased: number; genres: Genre[]; artistName: string; trackList: TrackCollectionDTO; } |
之所以如此強大是因為我們只是標準化了我們的API響應結構。
我們的DTO是一份資料合同。我們告訴任何使用此API的人,“嘿,這將是您始終期望從此API呼叫中看到的格式”。
這就是我的意思。讓我們看一下如何在通過id檢索Vinyl的例子中使用它。
export class GetVinylById extends BaseController { private models: any; public constructor (models: any) { super(); this.models = models; } public async executeImpl(): Promise<any> { try { const { Vinyl, Track, Genre, Label } = this.models; const vinylId: string = this.req.params.vinylId; const result = await Vinyl.findOne({ where: {}, include: [ { model: User, as: 'Owner', attributes: ['user_id', 'display_name'] }, { model: Label, as: 'Label' }, { model: Genre, as: 'Genres' } { model: Track, as: 'TrackList' }, ] }); // Map the ORM object to our DTO const dto: VinylDTO = { albumName: result.album_name, label: result.Label.name, country: result.Label.country, yearReleased: new Date(result.release_date).getFullYear(), genres: result.Genres.map((g) => g.name), artistName: result.artist_name, trackList: result.TrackList.map((t) => ({ number: t.album_track_number, name: t.track_name, length: t.track_length, })) } // Using our baseController, we can specify the return type // for readability. return this.ok<VinylDTO>(this.res, dto) } catch (err) { return this.fail(err); } } } |
那很好,但現在讓我們考慮一下這類的責任。
這是一個controller,但它負責:
- 定義如何堅持ORM模型對映到VinylDTO,TrackDTO和Genres。
- 定義需要從Sequelize ORM呼叫中檢索多少資料才能成功建立DTO。
這比controllers應該做的要多得多。
我們來看看Repositories和Data Mappers。
我們將從儲存庫開始。
儲存庫Repositories
儲存庫是永續性技術的外觀(例如ORM),Repository是實體生命週期的關鍵部分,它使我們能夠儲存,重建和刪除域實體。Facade是一種設計模式術語,指的是為更大的程式碼體提供簡化介面的物件。在我們的例子中,更大的程式碼體是域實體永續性和 域實體檢索邏輯。
儲存庫在DDD和清潔架構中的作用:
在DDD和清潔體系結構中,儲存庫是基礎架構層關注的問題。
一般來說,我們說repos 持久化並檢索域實體。
1. 儲存持久化
- 跨越交叉點和關係表的腳手架複雜永續性邏輯。
- 回滾失敗的事務
- 單擊save(),檢查實體是否已存在,然後執行建立或更新。
關於“建立如果不存在,否則更新”,這就是我們不希望我們域中的任何其他構造必須知道的複雜資料訪問邏輯的型別:只有repos應該關心它。
2.檢索
檢索建立域實體所需的全部資料,我們已經看到了這一點,選擇了include: []Sequelize的內容,以便建立DTO和域物件。將實體重建的責任委託給一個對映器。
編寫儲存庫的方法
在應用程式中建立儲存庫有幾種不同的方法。
1. 通用儲存庫介面
您可以建立一個通用儲存庫介面,定義您必須對模型執行的各種常見操作getById(id: string),save(t: T)或者delete(t: T)。
interface Repo<T> { exists(t: T): Promise<boolean>; delete(t: T): Promise<any>; getById(id: string): Promise<T>; save(t: T): Promise<any>; } |
從某種意義上說,這是一種很好的方法,我們已經定義了建立儲存庫的通用方法,但我們最終可能會看到資料訪問層的細節洩漏到呼叫程式碼中。
原因是因為getById感覺就像感冒一樣。如果我正在處理一個VinylRepo,我寧願任務,getVinylById因為它對域的泛在語言更具描述性。如果我想要特定使用者擁有的所有乙烯基,我會使用getVinylOwnedByUserId。
喜歡的方法getById是相當YAGNI。
這導致我們成為建立儲存庫的首選方式。
2.按實體/資料庫表的儲存庫
我喜歡能夠快速新增對我正在工作的域有意義的方便方法,所以我通常會從一個苗條的基礎儲存庫開始:
interface Repo<T> { exists(t: T): Promise<boolean>; delete(t: T): Promise<any>; save(t: T): Promise<any>; } |
然後使用其他更多關於域的方法擴充套件它。
export interface IVinylRepo extends Repo<Vinyl> { getVinylById(vinylId: string): Promise<Vinyl>; findAllVinylByArtistName(artistName: string): Promise<VinylCollection>; getVinylOwnedByUserId(userId: string): Promise<VinylCollection>; } |
為什麼總是將儲存庫定義為介面是有益的,因為它遵循Liskov Subsitution Principle(可以使結構被替換),並且它使結構成為依賴注入!
讓我們繼續建立我們的IVinylRepo:
import { Op } from 'sequelize' import { IVinylRepo } from './IVinylRepo'; import { VinylMap } from './VinyMap'; class VinylRepo implements IVinylRepo { private models: any; constructor (models: any) { this.models = models; } private createQueryObject (): any { const { Vinyl, Track, Genre, Label } = this.models; return { where: {}, include: [ { model: User, as: 'Owner', attributes: ['user_id', 'display_name'], where: {} }, { model: Label, as: 'Label' }, { model: Genre, as: 'Genres' }, { model: Track, as: 'TrackList' }, ] } } public async exists (vinyl: Vinyl): Promise<boolean> { const VinylModel = this.models.Vinyl; const result = await VinylModel.findOne({ where: { vinyl_id: vinyl.id.toString() } }); return !!result === true; } public delete (vinyl: Vinyl): Promise<any> { const VinylModel = this.models.Vinyl; return VinylModel.destroy({ where: { vinyl_id: vinyl.id.toString() } }) } public async save(vinyl: Vinyl): Promise<any> { const VinylModel = this.models.Vinyl; const exists = await this.exists(vinyl.id.toString()); const rawVinylData = VinylMap.toPersistence(vinyl); if (exists) { const sequelizeVinyl = await VinylModel.findOne({ where: { vinyl_id: vinyl.id.toString() } }); try { await sequelizeVinyl.update(rawVinylData); // scaffold all of the other related tables (VinylGenres, Tracks, etc) // ... } catch (err) { // If it fails, we need to roll everything back this.delete(vinyl); } } else { await VinylModel.create(rawVinylData); } return vinyl; } public getVinylById(vinylId: string): Promise<Vinyl> { const VinylModel = this.models.Vinyl; const queryObject = this.createQueryObject(); queryObject.where = { vinyl_id: vinyl.id.toString() }; const vinyl = await VinylModel.findOne(queryObject); if (!!vinyl === false) return null; return VinylMap.toDomain(vinyl); } public findAllVinylByArtistName (artistName: string): Promise<VinylCollection> { const VinylModel = this.models.Vinyl; const queryObject = this.createQueryObject(); queryObjectp.where = { [Op.like]: `%${artistName}%` }; const vinylCollection = await VinylModel.findAll(queryObject); return vinylCollection.map((vinyl) => VinylMap.toDomain(vinyl)); } public getVinylOwnedByUserId(userId: string): Promise<VinylCollection> { const VinylModel = this.models.Vinyl; const queryObject = this.createQueryObject(); queryObject.include[0].where = { user_id: userId }; const vinylCollection = await VinylModel.findAll(queryObject); return vinylCollection.map((vinyl) => VinylMap.toDomain(vinyl)); } } |
看到我們封裝了我們的sequelize資料訪問邏輯?我們已經不再需要重複編寫includes,因為現在所有必需的 include語句都在這裡。
我們也提到過VinylMap。讓我們快速看一下資料對映器Mapper的責任。
資料對映器
Mapper的職責是進行所有轉換:
- 從Domain到DTO
- 從域到永續性
- 從永續性到域
這是我們的VinylMap樣子:
class VinylMap extends Mapper<Vinyl> { public toDomain (raw: any): Vinyl { const vinylOrError = Vinyl.create({ }, new UniqueEntityID(raw.vinyl_id)); return vinylOrError.isSuccess ? vinylOrError.getValue() : null; } public toPersistence (vinyl: Vinyl): any { return { album_name: vinyl.albumName.value, artist_name: vinyl.artistName.value } } public toDTO (vinyl: Vinyl): VinylDTO { return { albumName: vinyl.albumName, label: vinyl.Label.name.value, country: vinyl.Label.country.value yearReleased: vinyl.yearReleased.value, genres: result.Genres.map((g) => g.name), artistName: result.artist_name, trackList: vinyl.TrackList.map((t) => TrackMap.toDTO(t)) } } } |
好的,現在讓我們回過頭來使用我們的VinylRepo和重構我們的控制器VinylMap。
export class GetVinylById extends BaseController { private vinylRepo: IVinylRepo; public constructor (vinylRepo: IVinylRepo) { super(); this.vinylRepo = vinylRepo; } public async executeImpl(): Promise<any> { try { const { VinylRepo } = this; const vinylId: string = this.req.params.vinylId; const vinyl: Vinyl = await VinylRepo.getVinylById(vinylId); const dto: VinylDTO = VinylMap.toDTO(vinyl); return this.ok<VinylDTO>(this.res, dto) } catch (err) { return this.fail(err); } } } |
原始碼:
相關文章
- NodeJS的DDD與CRUD對比案例 - Khalil StemmlerNodeJS
- mmap共享儲存對映(儲存I/O對映)系列詳解
- 怎樣在資料庫中儲存貨幣資料庫
- 如何高效地將SQL資料對映到NoSQL儲存系統中SQL
- 在關聯式資料庫中儲存RDF (轉)資料庫
- 在K8S中,共享儲存的作用?K8S
- DDD、Wardley對映和團隊拓撲
- 檔案系統儲存與oracle資料庫儲存對比Oracle資料庫
- .NET Core Dto對映(AutoMapper)APP
- QTP中對映驅動器和複製資料夾的指令碼QT指令碼
- 淺談資料庫中的儲存過程資料庫儲存過程
- 在瀏覽器上儲存資料(轉)瀏覽器
- 層次結構資料的資料庫儲存和使用資料庫
- 樹型結構資料在資料庫基本表中的儲存及維護 (轉)資料庫
- iOS中的資料儲存iOS
- 列式儲存資料庫資料庫
- 使用儲存過程(PL/SQL)向資料庫中儲存BLOB物件儲存過程SQL資料庫物件
- Flutter持久化儲存之資料庫儲存Flutter持久化資料庫
- 在伺服器建立 git 儲存庫伺服器Git
- 伺服器資料的儲存伺服器
- 資料庫伺服器的作用資料庫伺服器
- 資料儲存(1):從資料儲存看人類文明-資料儲存器發展歷程
- 【資料庫】資料庫儲存過程(一)資料庫儲存過程
- 現在後端都在用什麼資料庫儲存資料?後端資料庫
- 執行中請求對應在資料庫和OS中的id資料庫
- Android中的資料儲存之檔案儲存Android
- Prometheus時序資料庫-磁碟中的儲存結構Prometheus資料庫
- Android中的資料儲存Android
- MySQL 資料庫儲存引擎MySql資料庫儲存引擎
- 資料庫儲存過程資料庫儲存過程
- Salesforce的多型儲存和SAPC4C的後設資料儲存倉庫Salesforce多型
- 大資料的儲存和管理大資料
- 在雲伺服器儲存資料的10個好處伺服器
- MySQL資料庫的儲存引擎(轉)MySql資料庫儲存引擎
- DDD設計工具:上下文對映器ContextMapperContextAPP
- 資料庫檔案儲存(DBFS),是一款針對資料庫場景的雲原生共享檔案儲存服務資料庫
- 儲存資料鍵和專案對的類(Dictionary物件) (轉)物件
- 043、Vue3+TypeScript基礎,pinia庫使用action,在函式中對儲存資料進行修改VueTypeScript函式