数据模型与代码生成
如何修改 herd/_model.goat 并重新生成应用。
数据模型与代码生成
文件:
herd/_model.goat描述应用程序的结构,是 Goat 生成器使用的主要信息来源。
模型定义的内容包括:
- 应用程序元数据,
- 支持的语言,
- 用户角色,
- 实体,
- 实体属性,
- 关系,
- 数据约束,
- 读写权限,
- 分配给实体的模块,
- CRUD 行为,
- SEO 和 SSR 元素。
基于这些信息,Goat 可以生成应用程序多个层的一致性组件,包括:
- Go 模型,
- DAO 和仓储,
- DTO,
- API,
- CRUD 命令,
- SQL 迁移,
- 表单,
- 列表和管理视图,
- 前端元素。
最重要的优势并不只是生成文件本身。该模型允许你一次描述某个结构,然后在应用程序的多个部分中使用该定义。
无需手动同步后端、数据库、API 和前端;你可以修改模型,并让生成器准备由此变更产生的修改。
作为应用程序描述的模型
herd/_model.goat 应主要描述应用程序的意图和结构,而不是每个生成文件的实现细节。
简而言之:
_model.goat
↓
领域模型
↓
生成器
↓
后端 + 数据库 + API + 前端例如,字段定义:
add --name=title --type=web_title不仅可能影响后端模型,还会影响数据存储方式、生成的 DTO、表单和视图。
因此,相同的信息无需在不同技术中重复声明多次。
应用程序元数据
模型从应用程序的基本配置开始:
app --name goat \
--version 0.0.1 \
--path app \
--go-prefix code.pozoga.eu/spozoga/goatcms.com \
--terminal-prefix APP \
--default-language=en \
--supported-languages=pl,en,de该定义指定的内容包括:
- 应用程序名称,
- 版本,
- 项目路径,
- Go 模块前缀,
- 终端使用的前缀,
- 默认语言,
- 支持的语言列表。
这些信息之后可由生成器及应用程序的各个模块使用。
角色与权限
角色直接在模型中定义。
示例:
role:add --name=admin --super
role:add --name=manager
role:add --name=user在此情况下,应用程序具有三个角色:
admin,manager,user。
标志:
--super表示拥有扩展系统权限的角色。
之后可以在定义实体属性的访问权限时使用角色。
示例:
def --write=admin,manager --read=user允许指定哪些角色可以修改数据,哪些角色可以读取数据。
这样,基本访问规则可以直接在模型层定义,而无需在 API、表单和后端中分别重复定义。
基础实体
如果多个实体具有共同的结构,建议将其提取为基础实体。
content 就是一个示例:
entity:base:add --name content \
--label slug \
--base base_entity基础实体可以定义通用的:
- 属性,
- 关系,
- 约束,
- 访问规则。
在 content 项目中,它表示应用程序中发布内容的基本结构。
随后可供以下内容使用:
- 页面,
- 文档,
- 文章,
- 其他内容类型。
实体属性
属性在 --properties 部分中定义。
示例:
--properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=web_title \
--doc="Title is a short heading that describes the content."
add --name=slug \
--type=web_slug \
--required \
--doc="Slug is a URL-friendly name for the content."
add --name=lang \
--type=language \
--required \
--doc="Language of the current content."
PROPERTIESEOF每个属性可以指定的内容包括:
- 名称,
- 类型,
- 是否必填,
- 呈现方式,
- 访问权限,
- 文档说明,
- 生成器的附加行为。
属性类型携带的信息比数据库中的列类型本身更多。
例如:
web_title
web_slug
language
seo_description
block_content
file_set还可以决定数据在界面中的验证、序列化和呈现方式。
实体继承
实体可以扩展基础实体。
示例文档实体:
entity:extended --name doc --base content表示 doc 继承了为 content 定义的属性和行为。
这样可以避免在每个表示内容的实体中重复定义以下字段:
title
slug
lang
description
body实体还可以同时添加自己的属性。
示例:
--properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=ver \
--type=short_text \
--doc="Goat CLI version number for the page"
PROPERTIESEOF这样,doc 保留了完整的 content 模型,同时还存储文档版本。
关系
实体之间的关系可以直接在模型中定义。
示例:
--relations=<<RELATIONSEOF
def --onremove
add --name=owner \
--to=user \
--doc="Owner is the author of the article."
RELATIONSEOF该定义指定了指向实体 user 的 owner 关系。
生成器可以在多个位置使用此类信息:
- 数据模型,
- SQL 模式,
- API,
- DAO,
- 表单,
- 管理视图。
因此,关系只需在一个位置描述,而不必在应用程序的每一层中分别定义。
数据约束
模型还可以定义数据相关的约束。
示例:
--constraints=<<CONSTRAINTSEOF
unique:add --fields=lang,slug
CONSTRAINTSEOF表示以下组合:
lang + slug必须唯一。
这使得可以为不同语言保存相同的 slug:
pl / architektura
en / architecture
de / architektur同时防止创建语言和 slug 均相同的两条记录。
实体模块
实体可以通过模块获得额外行为。
例如,实体 doc 使用:
module:add --name=seo
module:add --name=ssr
module:add --name=crud模块允许扩展实体,而无需重复整个实现。
因此,在实践中,实体模型不仅可以描述其数据,还可以描述应用程序应如何使用它。
SEO 模块
模块:
module:add --name=seo添加与页面元数据相关的行为。
对于继承自 content 的实体,它可以使用例如:
title
description来准备搜索引擎和内容分享机制所需的信息。
这样,基础 SEO 支持可以由模型生成,而无需为每个页面手动实现。
SSR 渲染
模块:
module:add --name=ssr允许将实体记录关联到应用程序的公开路由。
文档示例:
module:add \
--name=ssr \
--slug=doc \
--route="/doc/{lang}/{slug}" \
--property:list:list="title,description" \
--property:list:details="title,body"该定义指定了:
- 公开路径,
- 记录识别方式,
- 列表中所需的字段,
- 详情视图中所需的字段。
对于示例文档,生成的路径可能如下:
/doc/zh/project-architecture随后,SSR 模块可以使用实体数据来准备服务端渲染的页面。
CRUD 模块
模块:
module:add --name=crud定义通过生成的管理操作处理实体的方式。
示例:
module:add \
--name=crud \
--property:list:list="ver,lang,title" \
--property:list:persist="title,slug,lang,description,body,ver"property:list:list 指定展示记录列表时使用的属性。
property:list:persist 指定创建和更新记录时使用的字段。
crud 模块定义 WebUI/API 契约。控制台命令字段则通过 crud_cli 单独配置:
module:add \
--name=crud_cli \
--property:list:list="username,email" \
--property:list:persist="password,role,username,email"生成的命令会保留命名空间 crud:<entity>:*。crud_cli.list 指定 crud:<entity>:list 返回的字段,而 crud_cli.persist 指定创建和更新标志。list 命令每行输出一个 JSON 对象,并支持 --limit、--order-by、--order-direction 和 --search。
可扩展模块
Goat 还支持定义自定义模块类型。
示例:
entity:module:def \
--type=top_box_counter \
--query:count=<<QUERYDEFEOF
def --required
QUERYDEFEOF \
--short_text:label=<<LABELDEFEOF
def --required
LABELDEFEOF该定义创建了模块类型 top_box_counter,它需要:
- 查询
count, - 标签
label。
随后可基于该类型创建具体模块。
示例:
module:add \
--name=admin_counter \
--type=top_box_counter \
--label=Administrators \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=admin
WHEREEOF
QUERYEOF这使得可以在更高的抽象层级上构建可复用的应用组件。
无需每次都在后端和前端实现用户计数器,而是可以将其结构定义为模型模块。
仪表盘模块示例
多个模块可以组合成更大的结构。
示例:
entity:module:def \
--type=summary_cards \
--modules:list=<<MODULESLISTEOF
def --types=top_box_counter --required
MODULESLISTEOF然后,实体 user 可以获得一组计数器:
module:add \
--name=summary_cards \
--type=summary_cards \
--modules:list=<<SUMMARYCARDSOF
module:add \
--name=admin_counter \
--type=top_box_counter \
--label=Administrators \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=admin
WHEREEOF
QUERYEOF
module:add \
--name=manager_counter \
--type=top_box_counter \
--label=Managers \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=manager
WHEREEOF
QUERYEOF
module:add \
--name=user_counter \
--type=top_box_counter \
--label=Users \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=user
WHEREEOF
QUERYEOF
SUMMARYCARDSOF这里的模型不仅描述数据结构,也描述仪表盘组件及为其提供数据所需的查询。
这表明 herd/_model.goat 并不只是数据库模式定义的对应物。它还可以描述应用行为的更高层级。
独立实体
并非每个实体都必须继承自 content。
例如,图库:
entity:add \
--name gallery \
--label title \
--base base_entity其属性可以如下:
--properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=nice_name \
--doc="Title is a short heading that describes the content."
def --write=admin,manager --read=user
add --name=slug \
--type=web_slug \
--unique \
--doc="Slug is a URL-friendly name for the content."
add --name=files \
--type=file_set \
--doc="Files for the gallery"
PROPERTIESEOF这表明模型既可以描述基于共同基础的实体,也可以描述完全独立的领域结构。
代码生成
修改后:
herd/_model.goat运行生成器:
goat re或直接运行:
go run ./scripts re生成器读取当前模型,并准备由此产生的应用变更。
该过程可简化表示如下:
herd/_model.goat
↓
herd/gen.goat
↓
模板
↓
生成器
↓
应用代码可生成的内容包括:
- 模型,
- 仓储,
- API,
- CLI 命令,
- 前端,
- SQL 迁移。
基础数据库模式还会生成到:
app/static/raw/data/sql/migrations/0001_schema.sql生成器配置
在对生成方式进行较大修改之前,建议检查:
herd/gen.goat该文件描述生成过程、所使用的模板以及其生成结果写入的位置。
如果想要修改大量相似元素的生成方式,通常最好修改相应模板,而不是在多个文件中手动重复相同的改动。
简而言之:
_model.goat决定生成什么,
而:
herd/gen.goat
herd/gen/templates/决定结果如何呈现。
生成的代码可以编辑
Goat 创建的代码仍然是项目的普通源代码。
你可以:
- 修改它,
- 重构它,
- 扩展它,
- 添加自己的逻辑,
- 像提交其他代码一样提交它。
生成器的目的是加快工作,而不是将用户限制在其生成模型中。
因此,并非每项变更都必须映射到 herd/_model.goat。
如果你需要一次性的、特定于某项功能的修改,直接编辑生成的代码可能是最简单的解决方案。
不过,如果你发现自己反复执行同一种修改,就值得将其上移一层——放到模型或生成器模板中。
何时修改模型?
当修改涉及领域结构或行为时,应修改 herd/_model.goat。
示例:
- 添加实体,
- 添加属性,
- 修改关系,
- 添加约束,
- 修改权限,
- 添加模块,
- 修改 CRUD 配置,
- 修改实体的公开路由。
示例:
add \
--name=published_at \
--type=datetime如果 published_at 是领域模型的一部分,就应在此处进行描述。
何时修改模板?
当修改应影响某一特定类型的所有元素时,应修改生成器模板。
示例:
- 所有表单都应获得额外的结构,
- 每个生成的端点都应使用新的机制,
- 所有实体都应获得额外的辅助代码,
- 希望修改生成的前端约定。
这类修改最好在以下位置进行:
herd/gen/templates/而不是分别修正多个生成的文件。
何时直接编辑代码?
当修改仅适用于特定情况时,直接编辑是合适的。
例如:
- 非典型的业务逻辑,
- 额外的集成,
- 特殊端点,
- 某个表单的特殊行为,
- 复杂查询,
- 手动优化,
- 自定义 UI 元素。
没有必要仅仅为了处理一个例外而让生成器变得复杂。
一个很好的判断标准是:
这项修改描述的是项目规则,还是某项功能特有的例外?
规则值得迁移到模型或生成器中。例外则可以直接保留在代码中。
安全的变更流程
典型的模型变更如下:
修改 _model.goat
↓
goat re
↓
git diff
↓
手动完善代码
↓
测试
↓
提交在实践中:
# 修改模型
vim herd/_model.goat
# 生成变更
goat re
# 检查结果
git diff
# 运行测试
goat run:script --path=herd/test.goatgit diff 尤其重要,因为它可以让你查看生成器准备的实际变更范围。
不要将生成视为一种无需检查便可接受其结果的操作。
生成器负责准备代码,但最终是否接受变更的决定权仍在开发者手中。
模型与现有数据库
修改模型可能会导致生成的 SQL 模式发生变化。
这并不自动意味着它对现有数据库是安全的。
例如:
修改 _model.goat
↓
修改目标模式并不等同于:
对现有数据进行安全迁移以下情况尤其需要注意:
- 删除字段,
- 修改类型,
- 添加必填字段,
- 修改关系,
- 新增约束,
- 修改索引。
在部署此类变更之前,请检查生成的 SQL,并准备合适的迁移路径。
作为 AI 层的模型
声明式应用模型在与 AI 协作时还具有额外优势。
在许多情况下,模型无需分别分析:
Go 模型
DAO
DTO
API
迁移
表单
Angular 视图而是可以修改一个定义:
herd/_model.goat然后让 Goat 生成该修改带来的后续结果。
例如:
“文档应具有发布日期”
↓
AI 修改 _model.goat
↓
goat re
↓
更新所需层级这减少了分析所需的代码量,也降低了遗漏某一层级的风险。
不过,并非每项变更都应通过模型完成。如果任务只涉及实现中的单个片段,直接编辑代码可能更高效。
示例模型
以下片段展示了 Goat 模型的一些基本功能:
# App metadata
app --name goat \
--version 0.0.1 \
--path app \
--go-prefix code.pozoga.eu/spozoga/goatcms.com \
--terminal-prefix APP \
--default-language=en \
--supported-languages=pl,en,de
# Roles
role:add --name=admin --super
role:add --name=manager
role:add --name=user
# Dashboard module types
entity:module:def --type=top_box_counter --query:count=<<QUERYDEFEOF
def --required
QUERYDEFEOF --short_text:label=<<LABELDEFEOF
def --required
LABELDEFEOF
entity:module:def --type=summary_cards --modules:list=<<MODULESLISTEOF
def --types=top_box_counter --required
MODULESLISTEOF
# User
entity:overwrite --name=user --base=user_base_entity --system=<<ENTITYSYSTEMEOF
module:add --name=summary_cards --type=summary_cards --modules:list=<<SUMMARYCARDSOF
module:add --name=admin_counter --type=top_box_counter --label=Administrators --query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=admin
WHEREEOF
QUERYEOF
module:add --name=manager_counter --type=top_box_counter --label=Managers --query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=manager
WHEREEOF
QUERYEOF
module:add --name=user_counter --type=top_box_counter --label=Users --query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=user
WHEREEOF
QUERYEOF
SUMMARYCARDSOF
module:add --name=crud \
--property:list:list="username,email" \
--property:list:persist="role,username,email"
module:add --name=crud_cli \
--property:list:list="username,email" \
--property:list:persist="password,role,username,email"
ENTITYSYSTEMEOF
# Base content entity
entity:base:add --name content --label slug --base base_entity --properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=web_title \
--doc="Title is a short heading that describes the content."
add --name=slug \
--type=web_slug \
--required \
--doc="Slug is a URL-friendly name for the content."
add --name=lang \
--type=language \
--required \
--doc="Language of the current content."
def --write=admin,manager --read=user
add --name=description \
--type=seo_description \
--doc="Short description for SEO and social media."
add --name=body \
--type=block_content \
--doc="Body contains the content data structured into blocks."
PROPERTIESEOF --relations=<<RELATIONSEOF
def --onremove
add --name=owner \
--to=user \
--doc="Owner is the author of the article."
RELATIONSEOF --constraints=<<CONSTRAINTSEOF
unique:add --fields=lang,slug
CONSTRAINTSEOF
# Page
entity:extended --name page --base content --doc=<<DOCEOF
Pages are the main sections of the website.
They contain essential content that rarely changes and
plays an important role in the overall structure and context of the website.
DOCEOF --system=<<ENTITYSYSTEMEOF
module:add --name=seo
module:add --name=ssr \
--slug=page \
--property:list:list="title,description" \
--property:list:details="title,body"
module:add --name=crud \
--property:list:list="title,description" \
--property:list:persist="title,slug,lang,description,body"
module:add --name=crud_cli \
--property:list:list="title,description" \
--property:list:persist="title,slug,lang,description,body"
ENTITYSYSTEMEOF
# Documentation
entity:extended --name doc --base content --doc=<<DOCEOF
The document contains comprehensive project documentation,
including available commands, configuration details,
usage examples and other information required to work
with the project effectively.
DOCEOF --properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=ver \
--type=short_text \
--doc="Goat CLI version number for the page"
PROPERTIESEOF --system=<<ENTITYSYSTEMEOF
module:add --name=seo
module:add --name=ssr \
--slug=doc \
--route="/doc/{lang}/{slug}" \
--property:list:list="title,description" \
--property:list:details="title,body"
module:add --name=crud \
--property:list:list="ver,lang,title" \
--property:list:persist="title,slug,lang,description,body,ver"
module:add --name=crud_cli \
--property:list:list="ver,lang,title" \
--property:list:persist="title,slug,lang,description,body,ver"
ENTITYSYSTEMEOF
# Gallery
entity:add --name gallery --label title --base base_entity --properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=nice_name \
--doc="Title is a short heading that describes the content."
def --write=admin,manager --read=user
add --name=slug \
--type=web_slug \
--unique \
--doc="Slug is a URL-friendly name for the content."
add --name=files \
--type=file_set \
--doc="Files for the gallery"
PROPERTIESEOF该示例展示了 Goat 模型最重要的理念:一个声明式定义不仅可以描述数据库,还可以描述权限、关系、CRUD、路由、SEO、SSR 和界面元素。
最重要的原则
模型应接管应用中那些可重复且能够以声明式方式描述的部分。
生成器会将其转换为具体实现,而生成的代码仍完全由开发者掌控。
在实践中,你可以在三个层级上工作:
模型 — 当变更涉及应用的结构和规则时,
生成器和模板 — 当你希望改变整类元素的生成方式时,
应用代码 — 当你需要个性化实现时。
目标并不是不惜一切代价生成尽可能多的项目部分。
Goat 应生成那些可重复的内容,从而让你能将更多时间投入到真正使应用脱颖而出的代码中。