需求分析:
1、具体需求
本《筛选符合条件的订单输出明细》有如下需求:
- 本实践输出的信息将涉及到销售业务各表(销售订单抬头、销售订单明细、客户、物料、雇员);
- 在本程序执行时,出现选择界面以输入客户范围,然后根据此输入的客户范围列出符合条件的销售订单;
- 在输出界面的页眉中要输出选择的条件;
- 输出的订单数据中需要显示客户和员工的详细信息;
- 同时显示的订单明细能显示物料信息同时,能计算明细的金额并能统计整个订单的金额。
2、开发分析
要达成本实践目标,需要综合ABAP的数据输出实现方式:
- 通过SELECT-OPTIONS语句,指定选择界面的字段;
- 通过Open SQL语句连接相关表后读取并存储到内表中。
实践步骤:
本实践通过程序编辑器(SE38)即可完成,编写的代码将有如下几部分组成,按开发人员风格不同,其组成部分并非强制一致。
No | 部分 | 说明 |
---|---|---|
1 | 程序声明 | 声明本程序执行后是否包含标准标题,数据输出宽度和每页的行数量为多少,另需考虑页脚输出 |
2 | 对象定义 | 通过定义类型池以在程序中使用相应的图标,通过定义要使用的以变量或常量或要使用的表结构等,以在程序执行过程中计算和存储临时值 |
3 | 页眉页脚 | 通过代码设置输出页眉页脚 |
4 | 获取数据 | 从表中获得要输出的数据数据并存储到内表中以待后续输出 |
5 | 输出数据 | 将内表中的数据按要求输出,本代码可有2种方式实现:条件、AT NEW事件。 |
1、程序声明
程序声明部分的代码如下:
REPORT zu0404_order_detail NO STANDARD PAGE HEADING
LINE-SIZE 100 LINE-COUNT 80.
通过如上代码定义,程序输出时,页面宽度为100(能容纳100个数字或英文字符),每页输出区域为80行。
2、对象定义
对象定义部分的代码如下:
*****对象定义
TABLES: ztorders_h. "定义需要在程序中使用的表
TYPES: BEGIN OF order_i_type, " ORDER_I_TYPE-类型名称
orderid TYPE ztorders_h-orderid,
orderdate TYPE ztorders_h-orderdate,
customerid TYPE ztorders_h-customerid,
customername TYPE ztcustomer-customername,
city TYPE ztcustomer-city,
address TYPE ztcustomer-address,
contact TYPE ztcustomer-contact,
cphone TYPE ztcustomer-cphone,
employeeid TYPE ztorders_h-employeeid,
employeename TYPE ztemployee-employeename,
post TYPE ztemployee-post,
sex TYPE ztemployee-sex,
materialid TYPE ztorders_i-materialid,
nprice TYPE ztorders_i-nprice,
oquantity TYPE ztorders_i-oquantity,
discount TYPE ztorders_i-discount,
materialname TYPE ztmaterial-materialname,
iamount TYPE ztorders_h-oamount,
END OF order_i_type.
DATA: order_i_stru TYPE order_i_type,
order_i_itab TYPE STANDARD TABLE OF order_i_type,
prev_orderid TYPE ztorders_h-orderid,
oamount TYPE ztorders_h-oamount,
cnt TYPE i,
ocnt TYPE i,
lcnt TYPE i,
materialname TYPE ztmaterial-materialname.
*****选择条件
SELECT-OPTIONS: scustid FOR ztorders_h-customerid. "选择客户
因为需要在SELECT-OPTIONS中使用ztorders_h的客户字段以在选择界面可以选择客户数据,因此需要通过TABLES进行定义,同时此scustid在程序编辑完成后需要通过程序编辑器界面的“转到文本元素”设置选择文本。
3、页眉输出
页眉输出部分的代码如下:
*&----------------------------------------------------------------------*
*& TOP-OF-PAGE 输出页眉
*&----------------------------------------------------------------------*
TOP-OF-PAGE. "页眉事件
*****输出选择条件
FORMAT INVERSE ON COLOR COL_NEGATIVE.
WRITE:/5 '客户选择条件:',
/10 'SIGN', 15 'OPTION', 25 'LOW', 35 'HIGH'.
LOOP AT scustid.
WRITE:/10 scustid-sign, 15 scustid-option, 25 scustid-low, 35 scustid-high.
ENDLOOP.
FORMAT RESET.
*****输出标题行
WRITE:/5(95) sy-uline,
/5 '序号', 10(8) '订单编号', 20 '订单明细', 85 '页码:' , (3) sy-pagno,
/5(95) sy-uline.
如上代码定义了页眉的输出。在页眉中,除了输出标题行外,还将选择的条件也显示了出来。通过SELECT-OPTIONS指定的选择变量,在执行时也相应的建立了一个与变量名称相同的内表,此内表中包含了SIGN、OPT、LOW、HIGH四个字段,用于存储选择界面中操作的值,其中:
SIGN:符号,其值I为包含,E为排除;
OPTION:选择,EQ为等于,BT为范围
LOW :范围的低值
HIGH:范围的高值
4、获取数据
获取数据部分的代码如下:
*&----------------------------------------------------------------------*
*& START-OF-SELECTION 开始处理
*&----------------------------------------------------------------------*
START-OF-SELECTION. " 数据处理事件
*****获得数据
SELECT oh~orderid oh~orderdate oh~customerid
ct~customername ct~city ct~address ct~contact ct~cphone
oh~employeeid et~employeename et~post et~sex
oi~materialid oi~nprice oi~oquantity oi~discount
INTO TABLE order_i_itab
"APPENDING TABLE order_i_itab PACKAGE SIZE 50 " 读取时每包50条记录
FROM ( ztorders_h AS oh
LEFT OUTER JOIN ztorders_i AS oi ON oi~orderid = oh~orderid
LEFT OUTER JOIN ztcustomer AS ct ON ct~customerid = oh~customerid
LEFT OUTER JOIN ztemployee AS et ON et~employeeid = oh~employeeid )
WHERE oh~customerid IN scustid
ORDER BY oh~customerid oh~orderid.
"ENDSELECT. “ 如果SELECT语句为开放结构,则需要endselect
LOOP AT order_i_itab INTO order_i_stru.
SELECT SINGLE materialname " 获得物料名称
INTO order_i_stru-materialname
FROM ztmaterial
WHERE materialid = order_i_stru-materialid.
order_i_stru-iamount = order_i_stru-nprice * " 计算每个订单明细的金额
order_i_stru-oquantity *
( 1 - order_i_stru-discount ).
MODIFY order_i_itab FROM order_i_stru. " 更新内表记录的物料名称和金额
ENDLOOP.
CLEAR order_i_stru. " 释放内存
如上代码通过select语句,以销售订单抬头表ZTORDERS_H为主表,通过LEFT OUTER JOIN连接其他表(ZTORDERS_I、ZTCUSTOMER、ZTEMPLOYEE),并将获取的数据存储到内表。
在将数据通过select存储到内表的代码语句中,如果select语句为开放结构,包括select 数据后into的目的不是内表而是结构、或者into到内表后带了package size以分包读取,那么在select语句后还可以带其他处理语句,最后还要通过endselect结束。
物料名称是在物料表(ZTMATERIAL)中,与订单明细表(ZTORDERS_I)中物料编号关联,还有订单明细金额需要经过计算得到,因此这两个值不通过select语句中获得,而是通过LOOP AT循环对数据处理后用MODIFY更新得到。
5、输出数据
输出数据部分的代码如下:
*****输出数据
LOOP AT order_i_itab INTO order_i_stru.
* "检查是否为新的订单,如是,则输出订单抬头和明细标题行
IF order_i_stru-orderid <> prev_orderid. "每一个订单分别输出
IF cnt > 1 AND sy-tabix > 1. "在每个订单底部输出订单总金额
WRITE:/55 '订单金额:' INVERSE ON COLOR = 7,
(8) oamount UNDER order_i_stru-iamount INVERSE ON COLOR = 7.
SKIP.
ENDIF.
oamount = 0. " 新的订单金额重新计算
ocnt = ocnt + 1. " 订单序号
cnt = 1. " 明细序号
SELECT COUNT(*) FROM ztorders_i INTO lcnt " 获得订单明细行数量
WHERE orderid = order_i_stru-orderid.
lcnt = lcnt + 11. " 一个订单所需占用的行
RESERVE lcnt LINES. " 页面遗留行数不足,换页输出
WRITE: " 输出订单抬头信息
/20(80) sy-uline,
/5(5) ocnt, 10(10) order_i_stru-orderid,
20 sy-vline, (10) '订单日期:', order_i_stru-orderdate, 99 sy-vline,
/20 sy-vline, (10) '客户:', order_i_stru-customerid, '(',
(15) order_i_stru-customername, ') / ',(8) order_i_stru-city,
(20) order_i_stru-address, 99 sy-vline,
/20 sy-vline, (10) '联系方式:',(8) order_i_stru-contact, '/',
order_i_stru-cphone, 99 sy-vline,
/20 sy-vline, (10) '雇员:', (8) order_i_stru-employeename, '(',
order_i_stru-post, ') / ', order_i_stru-sex, 99 sy-vline,
/20(80) sy-uline.
WRITE: "输出明细标题行
/20 sy-vline, 40 '订单明细',99 sy-vline,
/20(80) sy-uline,
/20 sy-vline NO-GAP, (2) 'NO' NO-GAP, 24 sy-vline NO-GAP,
25(6) ' 物料' NO-GAP, 44 sy-vline NO-GAP,
45(6) '价格' NO-GAP, 51 sy-vline NO-GAP,
52(6) ' 数量' NO-GAP, 59 sy-vline NO-GAP,
60(5) ' 折扣' NO-GAP, 69 sy-vline NO-GAP,
70(8) ' 金额' NO-GAP, 99 sy-vline NO-GAP,
/20(80) sy-uline.
ENDIF.
WRITE: "输出明细行
/20 sy-vline NO-GAP, (2) cnt NO-GAP, 24 sy-vline NO-GAP,
25(6) order_i_stru-materialid NO-GAP, order_i_stru-materialname NO-GAP,
44 sy-vline NO-GAP,
45(6) order_i_stru-nprice NO-GAP, 51 sy-vline NO-GAP,
52(6) order_i_stru-oquantity NO-GAP,59 sy-vline NO-GAP,
60(5) order_i_stru-discount NO-GAP,69 sy-vline NO-GAP,
70(8) order_i_stru-iamount NO-GAP, 99 sy-vline,
/20(80) sy-uline.
prev_orderid = order_i_stru-orderid.
cnt = cnt + 1.
oamount = oamount + order_i_stru-iamount. "累计得到订单金额
ENDLOOP.
*----------------------------------------------------------------------*
*******最后一个订单订单金额
END-OF-SELECTION.
IF cnt > 1.
WRITE:/55 '订单金额:' INVERSE ON COLOR = 7, (8) oamount
UNDER order_i_stru-iamount INVERSE ON COLOR = 7.
ENDIF.
在遍历数据进行输出时,按需求,每一个订单输出的内容包括订单抬头信息、订单明细数据、订单总金额三个部分,其中订单抬头信息和订单总金额只输出一次,而订单明细是逐行输出的。
因此在语句中,首先判断是否是一个新的订单,如果是一个新的订单,然后会判断上一个订单的行数是否大于1,如果大于1则输出上一个订单的“订单金额:”;而最后一个订单数据(抬头、明细)输出后,已没有此订单金额输出语句,因此需要在END-OF-SELECTION事件中输出;另外也可以在LOOP AT后,不使用END-OF-SELECTION事件(将如上代码的END-OF-SELECTION行注释),如上只是为了方便代码可读。
其他部分的输出可看代码。
6、输出数据(使用AT事件)
而对输出数据部分,也可以通过实践A3小结中的AT NEW、 AT END OF事件替代处理,实现结果相同,而其代码的可读性更好,当然AT事件中的字段值需要考虑,具体代码如下。
******输出数据(通过AT事件)
LOOP AT order_i_itab INTO order_i_stru.
DATA str(40) TYPE c.
CONCATENATE '订单日期:' order_i_stru-orderdate INTO str.
* ”检查是否为新的订单,如是,则输出订单抬头和明细标题行
AT NEW orderid.
ocnt = ocnt + 1. " 订单序号
cnt = 0. " 明细序号
SELECT COUNT(*) FROM ztorders_i INTO lcnt " 获得订单明细行数量
WHERE orderid = order_i_stru-orderid.
lcnt = lcnt + 7. " 一个订单所需占用的行
RESERVE lcnt LINES. " 页面遗留行数不足,换页输出
WRITE: " 输出订单抬头信息
/20(80) sy-uline,
/5(5) ocnt, 10(10) order_i_stru-orderid,
20 sy-vline, str, 99 sy-vline,
/20(80) sy-uline.
WRITE: "输出明细标题行
/20 sy-vline, 40 '订单明细',99 sy-vline,
/20(80) sy-uline,
/20 sy-vline NO-GAP, (2) 'NO' NO-GAP, 24 sy-vline NO-GAP,
25(6) ' 物料' NO-GAP, 44 sy-vline NO-GAP,
45(6) '价格' NO-GAP, 51 sy-vline NO-GAP,
52(6) ' 数量' NO-GAP, 59 sy-vline NO-GAP,
60(5) ' 折扣' NO-GAP, 69 sy-vline NO-GAP,
70(8) ' 金额' NO-GAP, 99 sy-vline NO-GAP,
/20(80) sy-uline.
ENDAT.
materialname = order_i_stru-materialname. " 获得物料名称以待AT END OF中输出
AT END OF materialid.
cnt = cnt + 1.
SUM.
WRITE: "输出明细行
/20 sy-vline NO-GAP, (2) cnt NO-GAP, 24 sy-vline NO-GAP,
25(6) order_i_stru-materialid NO-GAP, materialname NO-GAP,
44 sy-vline NO-GAP,
45(6) order_i_stru-nprice NO-GAP, 51 sy-vline NO-GAP,
52(6) order_i_stru-oquantity NO-GAP,59 sy-vline NO-GAP,
60(5) order_i_stru-discount NO-GAP,69 sy-vline NO-GAP,
70(8) order_i_stru-iamount NO-GAP, 99 sy-vline,
/20(80) sy-uline.
ENDAT.
AT END OF orderid. "订单最后输出订单金额
SUM.
WRITE:/55 '订单金额:' INVERSE ON COLOR = 7, (8) order_i_stru-iamount
UNDER order_i_stru-iamount INVERSE ON COLOR = 7.
SKIP.
ENDAT.
ENDLOOP.
本实践小结:
在ABAP开发时,经常需要使用到SQL语句,从表、视图中读取、更新数据等处理;区别于跟使用数据库相关的Native SQL(原生SQL),使用Open SQL开发的程序适用于使用不同数据库系统的SAP系统,因此在ABAP开发时,尽量避免使用Native SQL,本小结将对Open SQL进行说明。
1、SELECT 语法
SELECT {SINGLE [FOR UPDATE]}|{[DISTINCT]{}} | *
| {{col1|{ MAX( [DISTINCT] col )| IN( [DISTINCT] col )| AVG( [DISTINCT] col )
| SUM( [DISTINCT] col )| COUNT( DISTINCT col )| COUNT( * ) } } [AS a1] ... } | (列语法)
FROM { {dbtab [AS 表别名]}
| [(] { {dbtab_left [AS左表别名]} | 连接条件 {[INNER] JOIN}|{LEFT [OUTER] JOIN}
{右表 [AS 右表别名] ON 连接条件} [)]
|{(表语法) [AS 表别名]} }
[CLIENT SPECIFIED] [UP TO n ROWS] [BYPASSING BUFFER]
{INTO {{[CORRESPONDING FIELDS OF] wa}|(dobj1, dobj2, ...)} }
| { INTO|APPENDING [CORRESPONDING FIELDS OF] TABLE itab [PACKAGE SIZE n] }
[[FOR ALL ENTRIES IN itab]
WHERE [... col {=|EQ|<>|NE|>|GT|<|LT|>=|GE|<=|LE}itab-comp ... ]
{ { col1 {=|EQ|<>|NE|>|GT|<|LT|>=|GE|<=|LE}
{ {dobj}|{col2}|{[ALL|ANY|SOME] (select … from …)} } }
| {col [NOT] BETWEEN dobj1 AND dobj2}
| {col [NOT] LIKE dobj [ESCAPE esc]}
| {col [NOT] IN (dobj1, dobj2 ...)}
| {col [NOT] IN seltab}
| {col IS [NOT] NULL}
| {(条件语法)}
| {[NOT] EXISTS (select … from …)}
| {col [NOT] IN (select … from …)} }]
[GROUP BY { {col1 col2 ...} | (列语法) }][HAVING sql_cond]
[ORDER BY { {PRIMARY KEY}| { col1 [ASCENDING|DESCENDING]col2 [ASCENDING|DESCENDING] ...}| (列语法) }].
...
[ENDSELECT].
2、读取数据
3、插入更改数据
4、WHERE列条件
5、SQL执行时间和优化
对于程序的执行,肯定是希望执行的越快越好,如果要输出一张报表需要几十秒甚至几分钟,那么对于用户来说是很差的体验。从如上SQL的语法看,要读取记录,可有多种方式,如表数据可以通过多表连接、子查询,WHERE条件可以一般条件也可以ALL ENTRIES IN……,如果数据库表记录不多,只要能获得准确的记录,用不同的语句都没问题;而对于上规模的企业,数据量非常巨大,此时则要尽可能用最优的SQL语句以提高效率。如下从获得SQL操作时间和优化两方面进行说明。
1)操作时间
可以通过记录SQL执行前和执行后的时间后,取的差异值作为SQL的执行时间。
DATA t TYPE i. 定义类型为i的变量t
GET RUN TIME FIELD t. 获得当前时间(到秒)后存储于t
2)SQL优化
- 优先使用ABAP字典的视图,以能使用缓存;其次使用Join多表联合查询;如ABAP字典和Join都不能满足,再考虑ALL ENTRIES IN;不得已的情况下才使用子查询;
- 尽可能使用游标读取数据,如此省掉从数据库中读取记录并INTO到内表的资源消耗;
- 在INSERT、UPDATE、DELETE时,使用内表的方式操作,以减少与数据库交互次数;
- 在需要经常访问的字段上创建索引,并且将重复数据最少的字段放在索引最开头的规则来确定索引中字段顺序;要在重复率少的字段上创建索引,而不应该在重复率高的字段上创建索引;对经常读取的表创建索引,而需要经常更新的表,则不适合创建索引,如果创建了索引在数据更新时会更新索引;创建索引时不要多于4个字段,一个表上创建的索引不要超过5个;
- 尽量不要将允许为NULL值的字段作为索引字段,因为某些数据库不会将NULL值存储到索引表中;
- 避免全表扫描,尽可能在条件语句中使用索引字段进行查询;如果组成索引的所有字段都用到,并且使用的是“=”表达式,同时各个表达式使用“AND”连接时,查找效率会最高;
- 在条件语句中,要将索引字段条件写在最前面(左边),非索引字段写在最后面,如此查询效率也会高些;
- 如果一个索引由多个字段组成,在只选择索引的部分字段来进行查询时,也可以使用到索引,但使用时需要注意按照索引定义的顺序来使用:如表中创建了 a,b,c,d,e 五个字段组成的索引,当使用c,d作为条件来查询时,查询条件前面也一定要带上a,b查询条件字段,如果只选择c,d两个字段,是用不到索引的;
- 在条件中尽量不要使用否定的逻辑表达式,如NE(<>)、 NOT LIKE,如此使用时不会使用索引,所以尽量使用肯定的逻辑表达式,如EQ、LIKE;
- 尽量不要使用OR来连接多个索引字段表达式,OR后面的表达式中的字段将不会用到索引,可以看能否使用IN操作符实现;
- 一般情况下不要使用ORDER BY,除非ORDER BY所使用的索引与WHERE语句所使用的索引一样。
(实践A4 End)