ODPS2.0重装上阵,优化提升SQL语言表达能力

MaxCompute(原ODPS)是阿里云自主研发的具有业界领先水平的分布式大数据处理平台, 尤其在集团内部得到广泛应用,支撑了多个BU的核心业务。 MaxCompute除了持续优化性能外,也致力于提升SQL语言的用户体验和表达能力,提高广大ODPS开发者的生产力。

MaxCompute基于ODPS2.0新一代的SQL引擎,显著提升了SQL语言编译过程的易用性与语言的表达能力。我们在此推出MaxCompute(ODPS2.0)重装上阵系列文章

  • 第一弹 - 善用MaxCompute编译器的错误和警告
  • 第二弹 - 新的基本数据类型与内建函数
  • 第三弹 - 复杂类型
  • 第四弹 - CTE,VALUES,SEMIJOIN
  • 第五弹 - SELECT TRANSFORM
  • 第六弹 - User Defined Type
  • 第七弹 - Grouping Set, Cube and Rollup
  • 第八弹 - 动态类型函数

MaxCompute自定义函数的参数和返回值不够灵活,是数据开发过程中时常被提及的问题。Hive 提供给了 GenericUDF 的方式,通过调用一段用户代码,让用户来根据参数类型决定返回值类型。MaxCompute 出于性能、安全性等考虑,没有支持这种方式。但是MaxCompute也提供了许多方式,让您能够灵活地自定义函数。

  • 场景1
    需要实现一个UDF,可以接受任意类型的输入,但是MaxCompute的UDF不支持泛型,要做一个接受任何类型的函数,就必须为每种类型都写一个evaluate函数。
  • 场景2
    MaxCompute的UDAF和UDTF使用@Resolve的注解来指定输入输出类型,无法重载。要做一个接受多种类型的自定义功能,就需要定义多个不同的函数。
  • 场景3
    MaxCompute支持了参数化视图,能够把一些公共的SQL提出来。参数化视图的表值参数要求输入表的列数和类型与视图定义时完全一致,如果想要写一个能够接受具有相似特征的不同的表的视图,还无法定义出来。

本文带大家一起看看MaxCompute对这些大家关心的问题都做了哪些改进。

参数化视图

问题

参数化视图是MaxCompute自己设计的一种视图。允许用户定义参数,从而能够大大视图代码的复用率。很多用户都利用这一功能,将一些公共SQL提取到视图中,形成公共SQL代码池。

参数化视图在声明过程中具有局限性:参数类型,长度都是固定的。尤其是参数化视图允许传入表值参数,表值参数要求形参与实参在列的个数和类型上都一致。这一点限制了许多使用场景,如下面的例子:

CREATE VIEW paramed_view (@a TABLE(key bigint)) AS SELECT @a.* FROM @a JOIN dim on a.key = dim.key;

这个例子封装了一段使用dim表来过滤输入表的逻辑,本来这个是个通用的逻辑,任何包含key这一列的表,都可以用来做输入表。但是由于定义视图时只能确定输入中包含key列,因此声明的参数类型只包含这一列。导致了视图的调用者传递的表参数必须只能有一列,而返回的数据集也只包含一列,这显然与这个视图的设计初衷不合。

改进

最新的MaxCompute版本对参数化视图做了一些改进,可以大大提升参数化视图定义的灵活性。

首先,参数化视图的参数可以使用ANY关键字,表示任意类型。如

CREATE VIEW paramed_view (@a ANY) AS SELECT * FROM src WHERE case when @a is null then key1 else key2 end = key3;

这里定义的视图,第一个参数可以接受任意类型。注意ANY类型不能参与如 '+', 'AND' 之类的需要明确类型才能做的运算。ANY类型更多是在TABLE参数中做passthrough列,如

CREATE VIEW paramed_view (@a TABLE(name STRING, id ANY, age BIGINT)) AS SELECT * FROM @a WHRER name = 'foo' and age < 25;-- 调用示例
SELECT * FROM param_view((SELECT name, id, age from students));

上面的视图接受一个表值参数,但是并不关心这个表的第二列,那么这个列可以直接定义为ANY类型。参数化视图在调用时,每次都会根据输入参数的实际类型重新推算返回值类型。比如上面的视图,当输入的表是 TABLE(c1 STRING, c2 DOUBLE, c3 BIGINT),那么输出的数据集的第二列也会自动变成DOUBLE类型,让视图的调用者可以使用任何可用于DOUBLE类型的操作来操作这一列。

需要注意的一点是,我们用CREATE VIEW创建了视图后,可以用DESC来获取视图的描述,这个描述中会包含视图的返回类型信息。但是由于视图的返回类型是在调用的时候重新推算的,重新推算出来的类型可能与创建视图时推导出来的不一致。一个例子就是上面的ANY类型。

在ANY之外,参数化视图中的表值参数还支持了*,表示任意多个列。这个 * 可以带类型,也可以使用ANY类型。如

CREATE VIEW paramed_view (@a TABLE(key STRING, * ANY), @b TABLE(key STRING, * STRING)) AS SELECT a.* FROM @a JOIN @b ON a.key = b.key; -- 调用示例
SELECT name, address FROM param_view((SELECT school, name, age, address FROM student), school) WHERE age < 20;

上面这个视图接受两个表值参数,第一个表值参数第一列是string类型,后面可以是任意多个任意类型的列,而第二个表值参数的第一列是string,后面可以是任意多个STRING类型的列。这其中有几点需要注意:

  • 变长部分必须要写在表值参数定义的最后面,即在 * 的后面不允许再有其他列。这也间接导致了一个表值参数中最多只有一个变长列列表。
  • 由于变长部分必须在最后,有的时候输入表的列不一定是按照这种顺序排列的,这时候需要对输入表的列做一定重排,可以以subquery作为参数(参考上面的例子),注意subquery外面要加一层括号。
  • 由于表值参数中变长部分没有名字,因此在视图定义过程中没办法获得对这部分数据的引用,也就没有办法对这些数据做运算。这个限制是特意设置的,如果需要对变长部分的数据做运算,需要把要运算的列声明在定长部分,而编译器会对调用时传入的参数进行检查。
  • 虽然不能对变长部分做运算,但是 SELECT * 这种通配符的使用依旧可以将变长部分的列传递出去,如上面的例子在paramed_view中将 @a 的所有列返回,虽然创建视图的时候,a中只有key这一列,但是调用视图的时候,编译器推算出@a中还包含了name, age, address,因此视图返回的数据集中也包含这三列,而视图的调用者也可以对着三列进行操作(如 WHERE age < 20)。
  • 表值参数的列与视图声明时指定的定长列部分不一定完全一致。如果名字不一样,编译器会自动做重命名,如果类型不一样,编译器会做隐式转换(不能隐式转换则会报错)。

上面提到的第4点非常有用,一方面保证了调用视图是输入参数的灵活性,另一方面又不降低数据的信息量。好好利用能够很大程度上增加公共代码的复用率。

下面是一个调用示例。该例子使用的视图是:

CREATE VIEW paramed_view (@a TABLE(key STRING, * ANY), @b TABLE(key STRING, * STRING)) AS SELECT a.* FROM @a JOIN @b ON a.key = b.key; 

在MaxCompute Studio中调用,可以享受语法高亮和错误提示等功能。执行的调用代码如下:

image

执行的状态图如下:

image

放大执行过程仔细观察,图中可以发现几点有意思的地方:

image

上述执行输出的结果如下:

+------+---------+
| name | address |
+------+---------+
| 小明 | 杭州 |
+------+---------+

其他用法

经常有用户误用参数化视图,将参数化视图的参数当做是宏替换参数来使用。这里说明一下。参数化视图实际上是函数调用,而不是宏替换。如下面的例子:

CREATE VIEW paramed_view(@a TABLE(key STRING, value STRING), @b STRING) 
AS SELECT * FROM @a ORDER BY @b;-- 调用示例
select * from paramed_view(src, 'key');

上面的例子中,用户的期望是 ORDER BY @b 被宏替换为 ORDER BY key,即根据输入参数,决定了按照key列做排序。然而,实际上参数@b是作为一个值来传递的,ORDER BY @b 相当于 ORDER BY 'key',即 ORDER BY一个字符串常量('key')而不是一列。要想实现"让调用者决定排序列"这一功能,可以考虑下述做法。

CREATE VIEW orderByFirstCol(@a TABLE(columnForOrder ANY, * ANY)) AS SELECT `(columnForOrder)?+.+` FROM (SELECT * FROM @a ORDER BY columnForOrder) t;-- 调用示例
select * from orderByFirstCol((select key, * from src));

上面的例子,要求调用者将要排序的列放在第一列,于是在调用的时候使用子查询将src的需要排序的列抽取到最前面。视图返回的 (columnForOrder)?+.+ 是一个正则通配符,匹配columnForOrder之外的所有列,列表达式使用正则表达式可参考SELECT语法介绍>列表达式关于正则表达式的说明。

UDF:函数重载方式

问题

MaxCompute 的 UDF 使用重载 evalaute 方法的方式来重载函数,如下面的UDF定义了两个重载,当输入是 String 类型时,输出String类型,输入是BIGINT类型时,输出DOUBLE类型。

public UDFClass extends UDF {public String evaluate(String input) { return input + "123";  }public Double evaluate(Long input) { return input + 123.0; }
}

这种方式固然能解决一些问题,但有一定的局限性。比如不支持泛型,要做一个接受任何类型的函数,就必须为每种类型都写一个evaluate函数。有的时候重载甚至是不能实现的,比如ARRAY 和 ARRAY 的重载是做不到的。

public UDFClass extends UDF {public String evaluate(List<Long> input) { return input.size(); }// 这里会报错,因为在java类型擦除后,这个函数和 String evaluate(List<Long> input) 的参数是一样的public Double evaluate(List<Double> input) { input.size(); } // UDF 不支持下面这种定义方式public String evaluate(List<Object> input) { return input.size(); }
}

PYTHON UDF 或 UDTF 在不提供 Resolve 注解(annotation)的时候,会根据参数个数决定输入参数,也支持变长,因此非常灵活。但也因为过于灵活,编译器无法静态找到某些错误。比如

class Substr(object):def evaluate(self, a, b):return a[b:];

上面的函数接受两个参数,从实现上看,第一个参数需要是STRING类型,第二个参数应该是整形。而这个限制需要用户在调用时自己去把握。即使用户传错了参数,编译器也没有办法报错。同时,这种方式定义的UDF返回值类型只能是STRING,不够灵活。

改进

要解决上面的问题。可以考虑使用UDT。 UDT经常被简单在调用JDK中的方法的时候使用,比如 java.util.Objects.toString(x) 将任何对象 x 转成STRING类型。但是在自定义函数方面同样也有很好的用途。 UDT支持泛型,支持类继承,支持变长等功能,让定义函数更方便。如下面的例子:

public class UDTClass {// 这个函数接受一个数值类型(可以是 TINYINT, SMALLINT, INT, BIGINT, FLOAT, DOUBLE 以及任何以Number为基类的UDT),返回DOUBLEpublic static Double doubleValue(Number input) {return input.doubleValue();}// 这个方法,接受一个数值类型参数和一个任意类型的参数,返回值类型与第二个参数的类型相同public static <T extends Number, R> R nullOrValue(T a, R b) {return a.doubleValue() > 0 ? b : null;}// 这个方法接受一个任意元素类型的array或List,返回BIGINTpublic static Long length(java.util.List<? extends Object> input) {return input.size();}// 注意这个在不做强制转换的情况下参数只能接受 UDT 的 java.util.Map<Object, Object> 对象。如果需要传入任何map对象,比如 map<bigint,bigint> 可以考虑:// 1. 定义函数时使用java.util.Map<? extends Object, ? extends Object>// 2. 调用时强转,比如 UDTClass.mapSize(cast(mapObj as java.util.Map<Object, Object>))public static Long mapSize(java.util.Map<Object, Object> input) {return input.size();}
}

UDT 能够提供灵活的函数定义方式。但是有的时候UDF 需要通过 com.aliyun.odps.udf.ExecutionContext(在setup方法中传入)来获取一些上下文。现在UDT也可以通过 com.aliyun.odps.udt.UDTExecutionContext.get() 方法来或者这样的一个 ExecutionContext 对象。

Aggregator 与 UDTF:Annotation方式

问题

MaxCompute 的 UDAF 和 UDTF 使用Resolve注解来决定函数Signature。比如下面的方式定义了一个UDTF,该UDTF接受一个BIGINT参数,返回DOUBLE类型。

@com.aliyun.odps.udf.annotation.Resolve("BIGINT->DOUBLE")
public class UDTFClass extends UDTF {...
}

这种方式的局限性很明显,输入参数和输出参数都是固定的,没办法重载。

改进

MaxCompute对Resolve注解的语法做了许多扩展,现在能够支持一定的灵活性。

  • 参数列表中可以使用星号('*'),表示接受任意长度的,任意类型的输入参数。比如 @Resolve('double,*->String'),接受第一个是double,后接任意类型,任意个数的参数列表。这里需要UDF的作者在代码里面自己去判断输入的个数和类型,然后做出相应的动作(可以对比 C 语言里面的 printf 函数来理解)。注意星号用在返回值列表中时,表示的是不同的含义,在后续第三点中说明。
  • 参数列表中可以使用 ANY 关键字,表示任意类型的参数。比如 @Resolve('double,any->string'),接受第一个是double,第二个任意类型的参数列表。注意,ANY在返回值列表中不能使用,也不能在复杂类型的子类型中使用(如不能写ARRAY)。
  • UDTF的返回值可以使用星号,表示返回任意多个string类型。这里需要注意,返回值的个数并非真的是任意多个,而是与调用函数时给出的alias个数有关。比如@Resolve("ANY,ANY->DOUBLE,*"),调用方式是 UDTF(x, y) as (a, b, c),这里as后面给出了三个alias (a, b, c),编译器会认定a为double类型(annotation中返回值第一列的类型是给定的),b,c为string类型,而因为这里给出了三个返回值,所以UDTF在forward的时候,也一定要forward长度为3的数组,否则会出现运行时错误。注意这个错误是无法在编译时给出的,因此通常需要UDTF的作者与调用者互相沟通好,调用者在SQL中给出alias个数的时候,一定要按照UDTF的需要来写。由于Aggregator返回值个数固定是1,所以这个功能对UDAF无意义。

用一个例子来说明。如下UDTF:

import com.aliyun.odps.udf.UDFException;
import com.aliyun.odps.udf.UDTF;
import com.aliyun.odps.udf.annotation.Resolve;
import org.json.JSONException;
import org.json.JSONObject;@Resolve("STRING,*->STRING,*")
public class JsonTuple extends UDTF {private Object[] result = null;@Overridepublic void process(Object[] input) throws UDFException {if (result == null) {result = new Object[input.length];}try {JSONObject obj = new JSONObject((String)input[0]);for (int i = 1; i < input.length; i++) {// 返回值要求变长部分都是STRINGresult[i] = String.valueOf(obj.get((String)(input[i])));}result[0] = null;} catch (JSONException ex) {for (int i = 1; i < result.length; i++) {result[i] = null;}result[0] = ex.getMessage();}forward(result);}
}

这个UDTF的返回值个数会根据输入参数的个数来决定。输出参数的第一个是一个JSON文本,后面是需要从JSON中解析的key。返回值第一个是解析JSON过程中的出错信息,如果没有出错,则后续根据输入的key依次输出从json中解析出来的内容。使用示例如下。

-- 根据输入参数的个数定制输出alias个数
SELECT my_json_tuple(json, ’a‘, 'b') as exceptions, a, b FROM jsons;-- 变长部分可以一列都没有
SELECT my_json_tuple(json) as exceptions, a, b FROM jsons;-- 下面这个SQL会出现运行时错误,因为alias个数与实际输出个数不符
-- 注意编译时无法发现这个错误
SELECT my_json_tuple(json, 'a', 'b') as exceptions, a, b, c FROM jsons;

上面虽然做出了许多扩展,但是这些扩展并不一定能满足所有的需求。这时候依然可以考虑使用UDT。UDT也是可以用来实现Aggregator和UDTF的功能的。详细可以参考UDT示例文档,“聚合操作的实现示例” 及 “表值函数的实现示例” 的内容。

总结

MaxCompute自定义函数的函数原型不够灵活,在数据开发过程中带来诸多不便利,本文列举了各种函数定义方式存在的问题与解决方案,希望对大家有帮助,同时也告诉大家MaxCompute一直在努力为大家提供更好的服务。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/516707.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

野鸡大学怎么知道考生电话的?

来源 | 隐小卫责编 | 晋兆雨封图 | CSDN 下载自视觉中国当你在某度频繁搜索“高考”、“志愿”、“大学”、“本科”等关键词时&#xff0c;你的手机号码等信息有可能被非法抓取。并且打包进行交易&#xff0c;卖给培训班、网校、医院等机构进行所谓的“精准获客”。当然&#…

快速入门ECS快照功能,助力大数据容灾保护

阿里云快照是云盘数据在某个时刻完整的只读拷贝&#xff0c;是一种便捷高效的数据容灾手段&#xff0c;常用于数据备份、制作自定义镜像、应用容灾等。 应用场景 推荐您在以下场景中使用快照&#xff1a; 容灾备份&#xff1a;为云盘创建快照&#xff0c;再使用快照创建云盘获…

Docker JFrog Artifactory 7.27.10 maven私服(IDEA 实战篇01) linux

文章目录一、私服配置1. 账户密码2. 本地仓库3. ip/port二、IntelliJ IDEA2.1. 创建项目2.2. 指定配置2.3. 下载依赖2.4. 依赖查看2.5. 注意事项一、私服配置 将服务端生成的配置复制下来&#xff0c;进行修改 1. 账户密码 2. 本地仓库 3. ip/port 内容&#xff1a; <?xml…

一文看懂专有网络和交换机的定义及关系

在专有网络&#xff08;Virtual Private Cloud&#xff0c;简称VPC&#xff09;中使用云资源前&#xff0c;您必须先创建一个专有网络和交换机。您可以在一个专有网络中创建多个交换机来划分子网。一个专有网络内的子网默认私网互通。 专有网络和交换机 专有网络VPC是您独有的…

2020中关村论坛未来青年论坛:聚焦科技与产业数字化转型,让创新成果落地开花

8月26日&#xff0c;由中关村科技园区管理委员会指导&#xff0c;朝阳区人民政府、未来论坛联合主办&#xff0c;中关村朝阳园管委会承办的“2020中关村论坛未来青年论坛”&#xff0c;在北京举行。 作为2020中关村论坛的首场先锋论坛&#xff0c;2020中关村论坛未来青年论坛聚…

JFrog Artifactory 7.27 上传应用到私服和从maven私服下载制品

文章目录一、上传微服务应用1. 生成配置2. 拷贝配置3. 执行上传4. 验证5. 自定义配置二、下载制品2.1. 获取密文密码2.2. 执行下载一、上传微服务应用 1. 生成配置 生成Artifactory仓库上传配置文件&#xff0c;选择仓库&#xff0c;点击‘Set Me Up’查看部署配置 2. 拷…

阿里云2020上云采购季,你适合买什么云产品?

线下IDC机房成本高? 自建数据库卡、慢&#xff0c;延迟高? 被黑客攻击了怎么办&#xff1f; 今年IT预算没多少? 不知道怎么过等保2.0&#xff1f; 你遇到了哪些问题&#xff1f;来阿里云2020上云采购季&#xff01;主会场请戳&#xff1a;https://www.aliyun.com/sale-…

从零开始入门 K8s | 理解 CNI 和 CNI 插件

作者 | 溪恒 阿里巴巴高级技术专家 本文整理自《CNCF x Alibaba 云原生技术公开课》第 26 讲&#xff0c;点击直达课程页面。 关注“阿里巴巴云原生”公众号&#xff0c;回复关键词“入门”&#xff0c;即可下载从零入门 K8s 系列文章 PPT。 导读&#xff1a;网络架构是 K8s…

老码农:这段代码绝了,切勿模仿!

作为一名老码农&#xff0c;我的心这次凉透了&#xff01;事情起因很简单&#xff1a;我在某Hub上浏览时候&#xff0c;发现这样的一条信息&#xff1a;Python 超过 C、JS 薪酬排行第一&#xff08;最大招聘网站Indeed.com数据&#xff09;噗&#xff0c;996刚下班的我&#xf…

揭秘 RocketMQ 新特性以及在金融场景下的实践

2019 年末&#xff0c; RocketMQ 正式发布了 4.6.0 版本&#xff0c;增加了“ Request-Reply ”的同步调用的新特性。“ Request-Reply ”这个新特性是由微众银行的开发者们总结实践经验&#xff0c;并反馈给社区的。接下来本文会详细介绍此新特性。 “ Request-Reply ”是什么…

docker 安装部署 Jenkins 2.322

文章目录一、镜像容器1. 安装docker2. 镜像搜索3. 远程拉取镜像4. 创建挂载目录5. 修改权限6. 启动容器二、配置修改2.1. 镜像地址2.2. 核查url2.3. 重启容器2.4. 效果访问2.5. 密码获取2.6. 插件选择2.7. 创建用户2.8. 核查URL一、镜像容器 1. 安装docker yum install docke…

闲鱼的云原生故事:靠什么支撑起万亿的交易规模?

来源 | 阿里巴巴中间件作者 | 王树彬&#xff0c;阿里巴巴闲鱼架构负责人责编 | Carol2014年6月28日&#xff0c;阿里即将赴美上市的这一年&#xff0c;西溪园区的一个茶水间里&#xff0c;28个人日夜赶工了三个月后&#xff0c;上线了一个闲置交易平台——闲鱼。今年5月份&…

Kubernetes operator 模式开发实践

0. 前言 近日我们在开发符合我们业务自身需求的微服务平台时&#xff0c;使用了 Kubernetes 的 Operator Pattern 来实现其中的运维系统&#xff0c;在本文&#xff0c;我们将实现过程中积累的主要知识点和技术细节做了一个整理。 读者在阅读完本文之后&#xff0c;会对 Oper…

Jenkins 2.322 安装 自定义插件

文章目录Jenkins自定义安装插件1. Rebuilder2. Safe Restart3. Artifactory4. Build Timeout5. Credentials Binding6. Email Extension7. Git8. Pipeline9. SonarQube Scanner10. SSH Build Agents11. Workspace Cleanup12. TimestamperJenkins自定义安装插件 在jenkins管理页…

从国际站 - M 站建设谈开发者产品思维

前言 作为一个开发者我们在持续不断地交付我们负责的需求&#xff0c;可我们很少从产品的角度来看待我们交付的需求&#xff0c;比方说一直被强调的需求类型、需求价值、需求目标。作为产品需要具备的能力&#xff1a;看到用户、倾听用户、判断用户、与用户连接、在用户的交互…

2020人工智能应用挑战赛前瞻 | 专家委员会强大阵容,震撼发布!

截止2019年8月份全国344所高校已经开办、正在开办、即将开办人工智能方向相关专业。还记得当时浙江大学在新增机器人工程和人工智能两个超级热门专业时&#xff0c;不仅引起国内教育界的讨论&#xff0c;国外媒体也争相报道。其中日本《日经亚洲评论》认为&#xff0c;中国教育…

超18万人次下载使用的 Cloud Toolkit 的成长历程

在文章的开始&#xff0c;先介绍一下 Cloud Toolkit 是什么&#xff1f; Cloud Toolkit 是阿里云发布的免费本地 IDE 插件&#xff0c;帮助开发者更高效地开发、测试、诊断并部署应用。通过插件&#xff0c;可以将本地应用一键部署到任意服务器&#xff0c;甚至云端&#xff0…

新华智云基于MaxCompute建设媒体大数据开放平台

摘要&#xff1a;随着自媒体的发展&#xff0c;传统媒体面临着巨大的压力和挑战&#xff0c;新华智云运用大数据和人工智能技术&#xff0c;致力于为媒体行业赋能。通过媒体大数据开放平台&#xff0c;将媒体行业全网数据汇总起来&#xff0c;借助平台数据处理能力和算法能力&a…

docker Gitlab14.5.0 初始化账号以及密码是什么呢?

文章目录1. 进入容器2. 进入bin目录3. 执行命令4. 执行命令5. 修改密码6. 确认密码7. 保存修改8. 操作记录&#xff08;全&#xff09;9. 操作截图&#xff08;全&#xff09;这时&#xff0c;gitlab已经跑起来了&#xff0c;通过浏览器能正常访问了&#xff0c;那么问题来了&a…

坦白讲!90%的数据分析师都不合格!!

你还要在家待多久&#xff0c;之前托人给你找的工作你也不去&#xff0c;你到底想干什么呀&#xff01;”大明的女友又开始了每日的说教&#xff0c;大明漠然的关上了门&#xff0c;隔绝了声音。大明是一名技术开发工程师&#xff0c;不知道这是第几次和女友的争吵了&#xff0…