有什么方法可以避免域类和SQL查询之间的逻辑重复?


21

下面的示例完全是人为的,其唯一目的是使我理解我的观点。

假设我有一个SQL表:

CREATE TABLE rectangles (
  width int,
  height int 
);

域类:

public class Rectangle {
  private int width;
  private int height;

  /* My business logic */
  public int area() {
    return width * height;
  }
}

现在假设我有一个要求向用户显示数据库中所有矩形的总面积。我可以通过获取表的所有行,将它们变成对象并对其进行迭代来做到这一点。但这看起来很愚蠢,因为我的桌子上有很多矩形。

所以我这样做:

SELECT sum(r.width * r.height)
FROM rectangles r

这很容易,快速并且利用了数据库的优势。但是,它引入了重复的逻辑,因为我的域类中的计算也相同。

当然,对于这个例子,逻辑的重复根本不是致命的。但是,我的其他域类也面临同样的问题,它们更为复杂。


1
我怀疑最佳解决方案在不同的代码库之间会有很大的不同,因此您能否简要描述一个给您带来麻烦的更复杂的示例?
Ixrec 2015年

2
@lxrec:报告。一个业务应用程序具有在类中捕获的规则,并且我还需要创建显示相同信息但精简的报告。增值税的计算,付款,收入等等。
Escape Velocity 2015年

1
这不是在服务器和客户端之间分配负载的问题吗?当然,最好将计算的缓存结果转储到客户端是您的最佳选择,但是如果数据经常更改并且有很多请求,那么将成分和配方丢给客户端而不是客户端可能会更有利。为他们做饭。我认为在一个分布式系统中拥有多个可以提供某种功能的节点并不一定是一件坏事。
2015年

我认为最好的方法是生成这样的代码。我稍后再解释。
Xavier Combelle 2015年

Answers:


11

正如lxrec指出的那样,每个代码库都会有所不同。一些应用程序将允许您将此类业务逻辑放入SQL函数和/或查询中,并允许您在需要向用户显示这些值的任何时间运行它们。

有时看起来可能很愚蠢,但为实现正确性编写代码要比将性能作为主要目标要好。

在示例中,如果要在网络表单中显示用户的区域值,则必须:

1) Do a post/get to the server with the values of x and y;
2) The server would have to create a query to the DB Server to run the calculations;
3) The DB server would make the calculations and return;
4) The webserver would return the POST or GET to the user;
5) Final result shown.

对于简单的事情(例如样本中的问题),这是愚蠢的,但可能需要更复杂的事情,例如计算客户在银行系统中的投资的内部收益率。

正确性代码。如果您的软件是正确的,但速度很慢,则您将有机会(在分析之后)优化所需的位置。如果那意味着在数据库中保留一些业务逻辑,那就这样吧。这就是为什么我们拥有重构技术。

如果它变慢或无响应,则可能需要进行一些优化,例如违反DRY原则,如果您自己进行适当的单元测试和一致性测试,这并不是一个罪过。


1
将(过程)业务逻辑放入SQL中的麻烦在于,重构非常痛苦。即使您拥有一流的SQL重构工具,它们通常也不会与IDE中的代码重构工具进行交互(或者至少我还没有看到这样的工具集)
Roland Tepp 2015年

2

您说该示例是人为的,所以我不知道这里所说的内容是否适合您的实际情况,但是我的答案是-使用ORM(对象关系映射)层定义结构和查询/操作您的数据库。这样,您就不会有重复的逻辑,因为所有内容都将在模型中定义。

例如,使用Django(python)框架,您可以将矩形域类定义为以下模型

class Rectangle(models.Model):
    width = models.IntegerField()
    height = models.IntegerField()

    def area(self):
        return self.width * self.height

要计算总面积(不进行任何过滤),请定义:

def total_area():
    return sum(rect.area() for rect in Rectangle.objects.all())

正如其他人提到的那样,您应该首先为正确性编写代码,并且仅在遇到瓶颈时才进行优化。因此,如果您以后决定绝对必须进行优化,则可以切换到定义原始查询,例如:

def total_area_optimized():
    return Rectangle.objects.raw(
        'select sum(width * height) from myapp_rectangle')

1

我写了一个愚蠢的例子来解释一个想法:

class BinaryIntegerOperation
{
    public int Execute(string operation, int operand1, int operand2)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            var args = split[1].Split(',');
            var result = IsFirstOperand(args[0]) ? operand1 : operand2;
            for (var i = 1; i < args.Length; i++)
            {
                result *= IsFirstOperand(args[i]) ? operand1 : operand2;
            }
            return result;
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    public string ToSqlExpression(string operation, string operand1Name, string operand2Name)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            return string.Join("*", split[1].Split(',').Select(a => IsFirstOperand(a) ? operand1Name : operand2Name));
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    private bool IsFirstOperand(string code)
    {
        return code == "0";
    }
}

因此,如果您有一些逻辑:

var logic = "MULTIPLY:0,1";

您可以在域类中重用它:

var op = new BinaryIntegerOperation();
Console.WriteLine(op.Execute(logic, 3, 6));

或在您的sql-generation层中:

Console.WriteLine(op.ToSqlExpression(logic, "r.width", "r.height"));

而且,当然,您可以轻松更改它。尝试这个:

logic = "MULTIPLY:0,1,1,1";

-1

正如@Machado所说,最简单的方法是避免它,并在主Java中进行所有处理。但是,仍然有可能必须使用相似的代码进行编码,而不必通过为两个代码库都生成代码来重复自己的工作。

例如,使用cog enable从一个通用定义生成三个片段

片段1:

/*[[[cog
from generate import generate_sql_table
cog.outl(generate_sql_table("rectangle"))
]]]*/
CREATE TABLE rectangles (
    width int,
    height int
);
/*[[[end]]]*/

片段2:

public class Rectangle {
    /*[[[cog
      from generate import generate_domain_attributes,generate_domain_logic
      cog.outl(generate_domain_attributes("rectangle"))
      cog.outl(generate_domain_logic("rectangle"))
      ]]]*/
    private int width;
    private int height;
    public int area {
        return width * heigh;
    }
    /*[[[end]]]*/
}

片段3:

/*[[[cog
from generate import generate_sql
cog.outl(generate_sql("rectangle","""
                       SELECT sum({area})
                       FROM rectangles r"""))
]]]*/
SELECT sum((r.width * r.heigh))
FROM rectangles r
/*[[[end]]]*/

来自一个参考文件

import textwrap
import pprint

# the common definition 

types = {"rectangle":
    {"sql_table_name": "rectangles",
     "sql_alias": "r",
     "attributes": [
         ["width", "int"],
         ["height", "int"],
     ],
    "methods": [
        ["area","int","this.width * this.heigh"],
    ]
    }
 }

# the utilities functions

def generate_sql_table(name):
    type = types[name]
    attributes =",\n    ".join("{attr_name} {attr_type}".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])
    return """
CREATE TABLE {table_name} (
    {attributes}
);""".format(
    table_name=type["sql_table_name"],
    attributes = attributes
).lstrip("\n")


def generate_method(method_def):
    name,type,value =method_def
    value = value.replace("this.","")
    return textwrap.dedent("""
    public %(type)s %(name)s {
        return %(value)s;
    }""".lstrip("\n"))% {"name":name,"type":type,"value":value}


def generate_sql_method(type,method_def):
    name,_,value =method_def
    value = value.replace("this.",type["sql_alias"]+".")
    return name,"""(%(value)s)"""% {"value":value}

def generate_domain_logic(name):
    type = types[name]
    attributes ="\n".join(generate_method(method_def)
                   for method_def
                   in type["methods"])

    return attributes


def generate_domain_attributes(name):
    type = types[name]
    attributes ="\n".join("private {attr_type} {attr_name};".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])

    return attributes

def generate_sql(name,sql):
    type = types[name]
    fields ={name:value
             for name,value in
             (generate_sql_method(type,method_def)
              for method_def in type["methods"])}
    sql=textwrap.dedent(sql.lstrip("\n"))
    print (sql)
    return sql.format(**fields)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.