我已经分配了一个任务来创建(相对)简单的报告系统。在这些系统中,将向用户显示报告的表格结果。表有一些字段,每个字段为每个记录中的用户提供一些信息。但问题是,开发人员不会声明每个报告字段。它必须由系统的用户声明。所以我的报告表是动态的。
我在' Data Driven Custom View Engine in ASP.NET MVC'使用Asp.net MVC Framework创建动态表单,但我不知道这对我的系统是否合适。
UPDATE1:
目前我以下面的实体关系图结束:
在上图中,我将每条记录存储在Report
表中。我还在ReportType
中存储报告类型。对于将在报告记录中使用的每个字段,我将使用ReportFieldValue
。字段类型将存储在ReportField
。
所以如果我想先在我的数据库中添加一条记录,我会在Report
表中添加一行。然后,对于每个添加的记录字段,我将向ReportFieldValue
表添加一行。
但是您可能会注意到,在这些方法中,我必须将每个字段值存储在char(255)中。问题是像datetime
这样的字段类型不应该存储为字符串。这类系统有任何设计模式或架构吗?
答案 0 :(得分:13)
将VALUE
替换为NUMBER_VALUE
,DATE_VALUE
,STRING_VALUE
,以避免使用字符串类型的数据。这三种类型大多数时候都足够好。
如果需要,您可以稍后添加XMLTYPE和其他精美列。对于Oracle,使用VARCHAR2而不是CHAR来节省空间。
始终尝试将值存储为正确的类型。原生数据类型更快,更小,更易于使用,更安全。
Oracle有一个通用数据类型系统(ANYTYPE,ANYDATA和ANYDATASET),但这些类型很难使用,在大多数情况下应该避免使用。
建筑师通常认为对所有数据使用单个字段会使事情变得更容易。它可以更容易地生成数据模型的漂亮图片,但它可以创建所有内容 否则更难。请考虑以下问题:
针对字符串类型的数据开发类型安全查询非常痛苦。例如,让我们说你想找到"出生日期"对于在这个千禧年出生的人:
select *
from ReportFieldValue
join ReportField
on ReportFieldValue.ReportFieldid = ReportField.id
where ReportField.name = 'Date of Birth'
and to_date(value, 'YYYY-MM-DD') > date '2000-01-01'
你能发现这个错误吗?即使您以正确的格式存储日期,上述查询也很危险,很少有开发人员知道如何正确修复它。 Oracle进行了优化,难以强制执行特定的操作顺序。您需要这样的查询才能安全:
select *
from
(
select ReportFieldValue.*, ReportField.*
--ROWNUM ensures type safe by preventing view merging and predicate pushing.
,rownum
from ReportFieldValue
join ReportField
on ReportFieldValue.ReportFieldid = ReportField.id
where ReportField.name = 'Date of Birth'
)
where to_date(value, 'YYYY-MM-DD') > date '2000-01-01';
您不希望告诉每个开发人员以这种方式撰写查询。
答案 1 :(得分:8)
您的设计是实体属性值(EAV)数据模型的变体,通常被视为数据库设计中的反模式。
也许更好的方法是创建一个报告值表,例如300列(NUMBER_VALUE_1到NUMBER_VALUE_100,VARCHAR2_VALUE_1..100和DATE_VALUE_1..100)。
然后,设计其余的数据模型,围绕哪些报告使用哪些列以及每列使用哪些列。
这有两个好处:首先,您不是在字符串中存储日期和数字(已经指出了其好处),其次,您避免了与EAV模型相关的许多性能和数据完整性问题。
使用Oracle 11g2数据库,我将一个表中的30,000条记录移动到EAV数据模型中。然后我查询了模型以获得这30,000条记录。
SELECT SUM (header_id * LENGTH (ordered_item) * (SYSDATE - schedule_ship_date))
FROM (SELECT rf.report_type_id,
rv.report_header_id,
rv.report_record_id,
MAX (DECODE (rf.report_field_name, 'HEADER_ID', rv.number_value, NULL)) header_id,
MAX (DECODE (rf.report_field_name, 'LINE_ID', rv.number_value, NULL)) line_id,
MAX (DECODE (rf.report_field_name, 'ORDERED_ITEM', rv.char_value, NULL)) ordered_item,
MAX (DECODE (rf.report_field_name, 'SCHEDULE_SHIP_DATE', rv.date_value, NULL)) schedule_ship_date
FROM eav_report_record_values rv INNER JOIN eav_report_fields rf ON rf.report_field_id = rv.report_field_id
WHERE rv.report_header_id = 20
GROUP BY rf.report_type_id, rv.report_header_id, rv.report_record_id)
结果是:
1 row selected.
Elapsed: 00:00:22.62
Execution Plan
----------------------------------------------------------
----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2026 | 53 (67)|
| 1 | SORT AGGREGATE | | 1 | 2026 | |
| 2 | VIEW | | 130K| 251M| 53 (67)|
| 3 | HASH GROUP BY | | 130K| 261M| 53 (67)|
| 4 | NESTED LOOPS | | | | |
| 5 | NESTED LOOPS | | 130K| 261M| 36 (50)|
| 6 | TABLE ACCESS FULL | EAV_REPORT_FIELDS | 350 | 15050 | 18 (0)|
|* 7 | INDEX RANGE SCAN | EAV_REPORT_RECORD_VALUES_N1 | 130K| | 0 (0)|
|* 8 | TABLE ACCESS BY INDEX ROWID| EAV_REPORT_RECORD_VALUES | 372 | 749K| 0 (0)|
----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
7 - access("RV"."REPORT_HEADER_ID"=20)
8 - filter("RF"."REPORT_FIELD_ID"="RV"."REPORT_FIELD_ID")
Note
-----
- 'PLAN_TABLE' is old version
Statistics
----------------------------------------------------------
4 recursive calls
0 db block gets
275480 consistent gets
465 physical reads
0 redo size
307 bytes sent via SQL*Net to client
252 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
获得30,000行,每列4列,这是22秒。那是方式太长了。从一张平台上我们可以看到不到2秒,很容易。
答案 2 :(得分:3)
使用MariaDB,Dynamic Columns。实际上,这可以让您将所有杂项列放在一个列中,但仍然可以有效地访问它们。
我会在自己的专栏中保留一些常见字段。
More discussion of EAV和建议(以及如何在没有动态列的情况下执行此操作)。
答案 3 :(得分:1)
嗯,您对将数据存储在正确的数据类型中有一个非常好的观点 我同意这确实对用户定义的数据系统造成了问题。
解决此问题的一种方法是为每个数据类型组添加表(整数,浮点,字符串,二进制和日期,而不是将值保留在php
表中。
但是,这将使您的生活更加艰难,因为您必须选择并加入多个表才能获得单个结果。
另一种方法是在ReportFieldValue
中添加一个数据类型列,并创建一个用户定义的函数,将数据从字符串动态转换为适当的数据类型(使用数据类型列中的值),这样您可以使用它进行排序,搜索等。
Sql server也有一个名为sql_variant
的数据类型,它应该支持多种类型,虽然我从未使用它,但它的文档看起来很有希望。