当前位置: 首页 > news >正文

PostgreSQL create or replace view和重建视图 有什么区别?

一、 replace vs 重建

       遇到开发提了个问题,create or replace view和重建视图(drop+create)有什么区别,查询资料整理了一下。

1. create or replace

  • 当存在同名视图时,尝试将其替换
  • 新视图语句必须与现有视图查询具有相同的列(即相同的列名、列顺序和数据类型)
  • pg 8.1开始,之前向视图末尾添加新列
  • 总体而言,改动限制较大,但replace后不影响权限和依赖于该视图的对象

2. drop+create

  • 新视图定义不依赖原视图,灵活度高
  • 重建后权限丢失,必须重新授权
-- 对象授权查询
SELECT * FROM information_schema.table_privileges WHERE table_name='视图名' and grantor<>grantee;
  • 依赖于该视图的对象需要一起重建,复杂度可能较高

二、 源码学习

1. CREATE OR REPLACE VIEW

       按照 "CREATE OR REPLACE VIEW" 关键字搜索,这部分代码在ATExecCmd函数(tablecmds.c文件)。可以看到它对应的命令类型叫AT_AddColumnToView,对应操作为调用ATExecAddColumn函数为视图新加列。

/** ATExecCmd: dispatch a subcommand to appropriate execution routine*/
static void
ATExecCmd(List **wqueue, AlteredTableInfo *tab,AlterTableCmd *cmd, LOCKMODE lockmode, int cur_pass,AlterTableUtilityContext *context)
{ObjectAddress address = InvalidObjectAddress;Relation	rel = tab->rel;switch (cmd->subtype){case AT_AddColumn:		/* ADD COLUMN */case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */address = ATExecAddColumn(wqueue, tab, rel, &cmd,false, false,lockmode, cur_pass, context);break;
...

2. ATExecAddColumn函数

比较长的一个函数,主要操作如下:

  • 权限检查

/** Add a column to a table.  The return value is the address of the* new column in the parent relation.** cmd is pass-by-ref so that we can replace it with the parse-transformed* copy (but that happens only after we check for IF NOT EXISTS).*/
static ObjectAddress
ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,AlterTableCmd **cmd,bool recurse, bool recursing,LOCKMODE lockmode, int cur_pass,AlterTableUtilityContext *context)
{Oid			myrelid = RelationGetRelid(rel);ColumnDef  *colDef = castNode(ColumnDef, (*cmd)->def);bool		if_not_exists = (*cmd)->missing_ok;Relation	pgclass,attrdesc;
.../* At top level, permission check was done in ATPrepCmd, else do it */if (recursing)ATSimplePermissions(rel, ATT_TABLE | ATT_FOREIGN_TABLE);if (rel->rd_rel->relispartition && !recursing)ereport(ERROR,(errcode(ERRCODE_WRONG_OBJECT_TYPE),errmsg("cannot add column to a partition")));
  • 获取表锁,等级为RowExclusiveLock

attrdesc = table_open(AttributeRelationId, RowExclusiveLock);
  • 判断是否为递归子表加列,能否进行merge列

    若是merge,要求更新前后列类型及排序规则一致(Child column must match on type, typmod, and collation)

	/** Are we adding the column to a recursion child?  If so, check whether to* merge with an existing definition for the column.  If we do merge, we* must not recurse.  Children will already have the column, and recursing* into them would mess up attinhcount.*/if (colDef->inhcount > 0){HeapTuple	tuple;/* Does child already have a column by this name? */tuple = SearchSysCacheCopyAttName(myrelid, colDef->colname);if (HeapTupleIsValid(tuple)){Form_pg_attribute childatt = (Form_pg_attribute) GETSTRUCT(tuple);Oid			ctypeId;int32		ctypmod;Oid			ccollid;/* Child column must match on type, typmod, and collation */typenameTypeIdAndMod(NULL, colDef->typeName, &ctypeId, &ctypmod);if (ctypeId != childatt->atttypid ||ctypmod != childatt->atttypmod)ereport(ERROR,(errcode(ERRCODE_DATATYPE_MISMATCH),errmsg("child table \"%s\" has different type for column \"%s\"",RelationGetRelationName(rel), colDef->colname)));ccollid = GetColumnDefCollation(NULL, colDef, ctypeId);if (ccollid != childatt->attcollation)ereport(ERROR,(errcode(ERRCODE_COLLATION_MISMATCH),errmsg("child table \"%s\" has different collation for column \"%s\"",RelationGetRelationName(rel), colDef->colname),errdetail("\"%s\" versus \"%s\"",get_collation_name(ccollid),get_collation_name(childatt->attcollation))));/* Bump the existing child att's inhcount */childatt->attinhcount++;CatalogTupleUpdate(attrdesc, &tuple->t_self, tuple);heap_freetuple(tuple);/* Inform the user about the merge */ereport(NOTICE,(errmsg("merging definition of column \"%s\" for child \"%s\"",colDef->colname, RelationGetRelationName(rel))));table_close(attrdesc, RowExclusiveLock);return InvalidObjectAddress;}}
  • 一些检查,例如列名是否已存在,列数是否超出限制等

	/* skip if the name already exists and if_not_exists is true */if (!check_for_column_name_collision(rel, colDef->colname, if_not_exists)){table_close(attrdesc, RowExclusiveLock);return InvalidObjectAddress;}
.../* Determine the new attribute's number */newattnum = ((Form_pg_class) GETSTRUCT(reltup))->relnatts + 1;if (newattnum > MaxHeapAttributeNumber)ereport(ERROR,(errcode(ERRCODE_TOO_MANY_COLUMNS),errmsg("tables can have at most %d columns",MaxHeapAttributeNumber)));
  • 填充pg_attribute及pg_class信息

	/* construct new attribute's pg_attribute entry */attribute.attrelid = myrelid;namestrcpy(&(attribute.attname), colDef->colname);attribute.atttypid = typeOid;attribute.attstattarget = (newattnum > 0) ? -1 : 0;attribute.attlen = tform->typlen;attribute.attnum = newattnum;attribute.attndims = list_length(colDef->typeName->arrayBounds);attribute.atttypmod = typmod;attribute.attbyval = tform->typbyval;attribute.attalign = tform->typalign;attribute.attstorage = tform->typstorage;attribute.attcompression = GetAttributeCompression(typeOid,colDef->compression);attribute.attnotnull = colDef->is_not_null;attribute.atthasdef = false;attribute.atthasmissing = false;attribute.attidentity = colDef->identity;attribute.attgenerated = colDef->generated;attribute.attisdropped = false;attribute.attislocal = colDef->is_local;attribute.attinhcount = colDef->inhcount;attribute.attcollation = collOid;/* attribute.attacl is handled by InsertPgAttributeTuples() */ReleaseSysCache(typeTuple);tupdesc = CreateTupleDesc(lengthof(aattr), (FormData_pg_attribute **) &aattr);InsertPgAttributeTuples(attrdesc, tupdesc, myrelid, NULL, NULL);table_close(attrdesc, RowExclusiveLock);/** Update pg_class tuple as appropriate*/((Form_pg_class) GETSTRUCT(reltup))->relnatts = newattnum;CatalogTupleUpdate(pgclass, &reltup->t_self, reltup);heap_freetuple(reltup);/* Post creation hook for new attribute */InvokeObjectPostCreateHook(RelationRelationId, myrelid, newattnum);table_close(pgclass, RowExclusiveLock);/* Make the attribute's catalog entry visible */CommandCounterIncrement();
  • 填充默认值

/** Store the DEFAULT, if any, in the catalogs*/if (colDef->raw_default){RawColumnDefault *rawEnt;rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault));rawEnt->attnum = attribute.attnum;rawEnt->raw_default = copyObject(colDef->raw_default);/** Attempt to skip a complete table rewrite by storing the specified* DEFAULT value outside of the heap.  This may be disabled inside* AddRelationNewConstraints if the optimization cannot be applied.*/rawEnt->missingMode = (!colDef->generated);rawEnt->generated = colDef->generated;/** This function is intended for CREATE TABLE, so it processes a* _list_ of defaults, but we just do one.*/AddRelationNewConstraints(rel, list_make1(rawEnt), NIL,false, true, false, NULL);/* Make the additional catalog changes visible */CommandCounterIncrement();/** Did the request for a missing value work? If not we'll have to do a* rewrite*/if (!rawEnt->missingMode)tab->rewrite |= AT_REWRITE_DEFAULT_VAL;}/** Tell Phase 3 to fill in the default expression, if there is one.** If there is no default, Phase 3 doesn't have to do anything, because* that effectively means that the default is NULL.  The heap tuple access* routines always check for attnum > # of attributes in tuple, and return* NULL if so, so without any modification of the tuple data we will get* the effect of NULL values in the new column.** An exception occurs when the new column is of a domain type: the domain* might have a NOT NULL constraint, or a check constraint that indirectly* rejects nulls.  If there are any domain constraints then we construct* an explicit NULL default value that will be passed through* CoerceToDomain processing.  (This is a tad inefficient, since it causes* rewriting the table which we really don't have to do, but the present* design of domain processing doesn't offer any simple way of checking* the constraints more directly.)** Note: we use build_column_default, and not just the cooked default* returned by AddRelationNewConstraints, so that the right thing happens* when a datatype's default applies.** Note: it might seem that this should happen at the end of Phase 2, so* that the effects of subsequent subcommands can be taken into account.* It's intentional that we do it now, though.  The new column should be* filled according to what is said in the ADD COLUMN subcommand, so that* the effects are the same as if this subcommand had been run by itself* and the later subcommands had been issued in new ALTER TABLE commands.** We can skip this entirely for relations without storage, since Phase 3* is certainly not going to touch them.  System attributes don't have* interesting defaults, either.*/if (RELKIND_HAS_STORAGE(relkind) && attribute.attnum > 0){/** For an identity column, we can't use build_column_default(),* because the sequence ownership isn't set yet.  So do it manually.*/if (colDef->identity){NextValueExpr *nve = makeNode(NextValueExpr);nve->seqid = RangeVarGetRelid(colDef->identitySequence, NoLock, false);nve->typeId = typeOid;defval = (Expr *) nve;/* must do a rewrite for identity columns */tab->rewrite |= AT_REWRITE_DEFAULT_VAL;}elsedefval = (Expr *) build_column_default(rel, attribute.attnum);if (!defval && DomainHasConstraints(typeOid)){Oid			baseTypeId;int32		baseTypeMod;Oid			baseTypeColl;baseTypeMod = typmod;baseTypeId = getBaseTypeAndTypmod(typeOid, &baseTypeMod);baseTypeColl = get_typcollation(baseTypeId);defval = (Expr *) makeNullConst(baseTypeId, baseTypeMod, baseTypeColl);defval = (Expr *) coerce_to_target_type(NULL,(Node *) defval,baseTypeId,typeOid,typmod,COERCION_ASSIGNMENT,COERCE_IMPLICIT_CAST,-1);if (defval == NULL) /* should not happen */elog(ERROR, "failed to coerce base type to domain");}if (defval){NewColumnValue *newval;newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue));newval->attnum = attribute.attnum;newval->expr = expression_planner(defval);newval->is_generated = (colDef->generated != '\0');tab->newvals = lappend(tab->newvals, newval);}if (DomainHasConstraints(typeOid))tab->rewrite |= AT_REWRITE_DEFAULT_VAL;if (!TupleDescAttr(rel->rd_att, attribute.attnum - 1)->atthasmissing){/** If the new column is NOT NULL, and there is no missing value,* tell Phase 3 it needs to check for NULLs.*/tab->verify_new_notnull |= colDef->is_not_null;}}
  • 更新pg_attribute的atttypid及attcollation字段

	/** Add needed dependency entries for the new column.*/add_column_datatype_dependency(myrelid, newattnum, attribute.atttypid);add_column_collation_dependency(myrelid, newattnum, attribute.attcollation);
  • 若有子表,为每个子表递归调用该函数新增列

/** Propagate to children as appropriate.  Unlike most other ALTER* routines, we have to do this one level of recursion at a time; we can't* use find_all_inheritors to do it in one pass.*/children =find_inheritance_children(RelationGetRelid(rel), lockmode);/** If we are told not to recurse, there had better not be any child* tables; else the addition would put them out of step.*/if (children && !recurse)ereport(ERROR,(errcode(ERRCODE_INVALID_TABLE_DEFINITION),errmsg("column must be added to child tables too")));/* Children should see column as singly inherited */if (!recursing){childcmd = copyObject(*cmd);colDef = castNode(ColumnDef, childcmd->def);colDef->inhcount = 1;colDef->is_local = false;}elsechildcmd = *cmd;		/* no need to copy again */foreach(child, children){Oid			childrelid = lfirst_oid(child);Relation	childrel;AlteredTableInfo *childtab;/* find_inheritance_children already got lock */childrel = table_open(childrelid, NoLock);CheckTableNotInUse(childrel, "ALTER TABLE");/* Find or create work queue entry for this table */childtab = ATGetQueueEntry(wqueue, childrel);/* Recurse to child; return value is ignored */ATExecAddColumn(wqueue, childtab, childrel,&childcmd, recurse, true,lockmode, cur_pass, context);table_close(childrel, NoLock);}ObjectAddressSubSet(address, RelationRelationId, myrelid, newattnum);return address;
}

参考

PostgreSQL: Documentation: 16: CREATE VIEW

相关文章:

PostgreSQL create or replace view和重建视图 有什么区别?

一、 replace vs 重建 遇到开发提了个问题&#xff0c;create or replace view和重建视图&#xff08;dropcreate&#xff09;有什么区别&#xff0c;查询资料整理了一下。 1. create or replace 当存在同名视图时&#xff0c;尝试将其替换新视图语句必须与现有视图查询具有相…...

Selenium 连接到现有的 Firefox 示例

当前环境&#xff1a; python 3.7 selenium 3.14.1 urllib3 1.26.8 Frefox 115.1.0esr(32位) geckodriver.exe 0.33.0 1 下载 Firefox 浏览器&#xff0c;根据自己的需要选择。 下载 Firefox 浏览器&#xff0c;这里有简体中文及其他 90 多种语言版本…...

小程序如何进行版本回退

当商家决定回退小程序版本时&#xff0c;可能是因为新版本出现了一些问题或者不符合预期&#xff0c;需要恢复到之前的稳定版本。下面具体介绍怎么回退小程序的版本。 在小程序管理员后台->版本设置处&#xff0c;点击版本回退。确认后&#xff0c;小程序会回退到上一次的版…...

15:00面试,15:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到8月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…...

大数据-之LibrA数据库系统告警处理(ALM-37008 MPPDB服务不可用)

告警解释 告警模块每30秒周期性检测MPPDB服务健康状态&#xff0c;当检测到MPPDB健康状态为“故障”时产生告警。 当检测到MPPDB健康状态为“良好”时告警恢复。 告警属性 告警ID 告警级别 可自动清除 37008 致命 是 告警参数 参数名称 参数含义 ServiceName 产生…...

Pytorch-gpu环境篇

最最最头疼的就是配环境了 包之间的版本匹配问题 INSTALLING PREVIOUS VERSIONS OF PYTORCH 要考虑到pytorch和torchvision之间的匹配关系 显卡版本匹配问题...

互联网上门洗鞋店小程序

上门洗鞋店小程序门店版是基于原平台版进行增强的&#xff0c;结合洗鞋行业的线下实际运营经验和需求&#xff0c;专为洗鞋人和洗鞋店打造的高效、实用、有价值的管理软件系统。 它能够帮助洗鞋人建立自己的私域流量&#xff0c;实现会员用户管理&#xff0c;实现用户与商家的点…...

【深度学习笔记】04 概率论基础

04 概率论基础 概率论公理联合概率条件概率贝叶斯定理边际化独立性期望和方差模拟投掷骰子的概率随投掷次数增加的变化 概率论公理 概率&#xff08;probability&#xff09;可以被认为是将集合映射到真实值的函数。 在给定的样本空间 S \mathcal{S} S中&#xff0c;事件 A \m…...

45.113.200.1搜索引擎蜘蛛抓取不到网站内容页面可能的原因

以下是搜索引擎蜘蛛抓取不到网站内容页面的一些主要原因总结&#xff1a; 网站的 robots.txt 文件中禁止了搜索引擎蜘蛛访问网站某些页面或目录&#xff0c;导致搜索引擎无法抓取到相关页面的内容。 网站的页面存在重定向或者跳转&#xff0c;搜索引擎蜘蛛无法直接抓取到需要的…...

VMware 系列:vSphere Client安装配置常见问题及解决方案

vSphere Client安装配置常见问题及解决方案 1.本地通过远程桌面复制文件或者文件夹到windows服务器时出错提示“未指定的错误”问题描述:原因分析:解决方案:方法 1方法 2方法 32.远程桌面连接出现身份验证错误要求的函数不受支持问题描述:原因分析:解决方案:3.Windows se…...

FLASK博客系列5——模板之从天而降

我们啰啰嗦嗦讲了4篇&#xff0c;都是在调接口&#xff0c;啥时候能看到漂亮的页面呢&#xff1f;别急&#xff0c;今天我们就来实现。 来我们先来实现一个简单的页面。不多说&#xff0c;上代码。 app.route(/) def index():user {username: clannadhh}return <html>&…...

6.一维数组——用冒泡法将10个整数由大到小排序

文章目录 前言一、题目描述 二、题目分析 三、解题 程序运行代码 前言 本系列为一维数组编程题&#xff0c;点滴成长&#xff0c;一起逆袭。 一、题目描述 用冒泡法将10个整数由大到小排序 二、题目分析 三、解题 程序运行代码 #include<stdio.h> int main() {int …...

Wireshark的捕获过滤器

Wireshark的过滤器&#xff0c;顾名思义&#xff0c;作用是对数据包进行过滤处理。具体过滤器包括捕获过滤器和显示过滤器。本文对捕获过滤器进行分析。 捕获过滤器&#xff1a;当进行数据包捕获时&#xff0c;只有那些满足给定的包含/排除表达式的数据包会被捕获。 捕获过滤器…...

安陆FPGA调试中遇到的问题总结

参考链接&#xff1a;安陆FPGA踩坑填坑记录&#xff08;梦里呓语回忆录&#xff09;_fpga开发是个大坑-CSDN博客 上海安陆FPGA设置重上电启动速度_安路 fpga 加载时间-CSDN博客...

Springboot2+WebSocket

一、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency> 二、添加配置 新增配置文件 config/WebSocketConfig.java import org.springframewo…...

希尔伯特和包络变换

一、希尔伯特变换 Hilbert Transform&#xff0c;数学定义&#xff1a;在数学与信号处理的领域中&#xff0c;一个实值函数的希尔伯特变换是将信号x(t)与h(t)1/(πt)做卷积&#xff0c;以得到其希尔伯特变换。因此&#xff0c;希尔伯特变换结果可以理解为输入是x(t)的线性时不…...

国产Ai大模型和chtgpt3.5的比较

下面是针对国产大模型&#xff0c;腾讯混元&#xff0c;百度文心一言&#xff0c;阿里通义千问和chatgpt的比较&#xff0c;最基础的对一篇文章的单词书进行统计&#xff0c;只有文心一言和chatgpt回答差不多&#xff0c;阿里和腾讯差太多了...

机器学习ROC曲线中的阈值thresholds

在ROC&#xff08;Receiver Operating Characteristic&#xff09;曲线中&#xff0c;阈值&#xff08;thresholds&#xff09;是一个用于分类模型的概率或分数的截断值。ROC曲线的绘制涉及使用不同的阈值来计算真正例率&#xff08;True Positive Rate&#xff0c;TPR&#xf…...

MySOL常见四种连接查询

1、内联接 &#xff08;典型的联接运算&#xff0c;使用像 或 <> 之类的比较运算符&#xff09;。包括相等联接和自然联接。 内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行。例如&#xff0c;检索 students和courses表中学生标识号相同的所有行。 2、…...

数智融合 开启金融数据治理新时代

11月24日&#xff0c;由上海罗盘信息科技有限公司&#xff08;罗盘科技&#xff09;主办&#xff0c;北京酷克数据科技有限公司&#xff08;酷克数据&#xff09;支持协办的“博学近思 切问治理”数据治理分享会在上海成功举行。 本次会议深度聚焦金融行业数智化转型&#xff…...

数据链路层的主要功能是什么

数据链路层&#xff08;OSI模型第2层&#xff09;的核心功能是在相邻网络节点&#xff08;如交换机、主机&#xff09;间提供可靠的数据帧传输服务&#xff0c;主要职责包括&#xff1a; &#x1f511; 核心功能详解&#xff1a; 帧封装与解封装 封装&#xff1a; 将网络层下发…...

#Uniapp篇:chrome调试unapp适配

chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器&#xff1a;Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...

基于Java Swing的电子通讯录设计与实现:附系统托盘功能代码详解

JAVASQL电子通讯录带系统托盘 一、系统概述 本电子通讯录系统采用Java Swing开发桌面应用&#xff0c;结合SQLite数据库实现联系人管理功能&#xff0c;并集成系统托盘功能提升用户体验。系统支持联系人的增删改查、分组管理、搜索过滤等功能&#xff0c;同时可以最小化到系统…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

腾讯云V3签名

想要接入腾讯云的Api&#xff0c;必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口&#xff0c;但总是卡在签名这一步&#xff0c;最后放弃选择SDK&#xff0c;这次终于自己代码实现。 可能腾讯云翻新了接口文档&#xff0c;现在阅读起来&#xff0c;清晰了很多&…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 &#xff08;一&#xff09;项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台&#xff0c;其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言&#xff0c;首次接触 OpenBCI 设备时&#xff0c;往…...

Bean 作用域有哪些?如何答出技术深度?

导语&#xff1a; Spring 面试绕不开 Bean 的作用域问题&#xff0c;这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开&#xff0c;结合典型面试题及实战场景&#xff0c;帮你厘清重点&#xff0c;打破模板式回答&#xff0c…...

【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)

前言&#xff1a; 双亲委派机制对于面试这块来说非常重要&#xff0c;在实际开发中也是经常遇见需要打破双亲委派的需求&#xff0c;今天我们一起来探索一下什么是双亲委派机制&#xff0c;在此之前我们先介绍一下类的加载器。 目录 ​编辑 前言&#xff1a; 类加载器 1. …...

Rust 开发环境搭建

环境搭建 1、开发工具RustRover 或者vs code 2、Cygwin64 安装 https://cygwin.com/install.html 在工具终端执行&#xff1a; rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu ​ 2、Hello World fn main() { println…...

【Linux】自动化构建-Make/Makefile

前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具&#xff1a;make/makfile 1.背景 在一个工程中源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;mak…...