Как избежать дублирования логики между классами домена и 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

Это легко, быстро и использует сильные стороны базы данных. Тем не менее, он вводит дублированную логику, потому что у меня есть те же вычисления в моем классе домена.

Конечно, для этого примера дублирование логики вовсе не фатально. Тем не менее, я сталкиваюсь с той же проблемой с другими моими классами домена, которые являются более сложными.

Escape Velocity
источник
1
Я подозреваю, что оптимальное решение будет довольно сильно отличаться от кодовой базы к кодовой базе, поэтому не могли бы вы кратко описать один из более сложных примеров, которые доставляют вам проблемы?
Иксрек
2
@lxrec: отчеты. Бизнес-приложение, которое имеет правила, которые я собираю в классах, и мне также нужно создавать отчеты, которые показывают ту же информацию, но сжатую. Расчеты НДС, выплаты, заработок, такие вещи.
Escape Velocity
1
Разве это не вопрос распределения нагрузки между сервером и клиентами? Конечно, лучше всего выгружать кэшированный результат вычисления клиенту, но если данные часто меняются и возникает много запросов, может быть полезно иметь возможность бросать ингредиенты и рецепт на клиента вместо готовить еду для них. Я думаю, что не обязательно плохо иметь более одного узла в распределенной системе, который может обеспечить определенную функциональность.
ноль
Я думаю, что лучший способ - генерировать такие коды. Я объясню позже.
Ксавье Комбель,

Ответы:

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.

Это глупо для простых вещей, таких как пример, но может потребоваться более сложные вещи, такие как расчет IRR инвестиций клиента в банковскую систему.

Код на правильность . Если ваше программное обеспечение работает правильно, но медленно, у вас будет шанс оптимизировать, где вам нужно (после профилирования). Если это означает сохранение части бизнес-логики в базе данных, пусть будет так. Вот почему у нас есть методы рефакторинга.

Если он становится медленным или не отвечает, то вам может потребоваться выполнить некоторые оптимизации, например, нарушить принцип DRY, что не является грехом, если вы окружаете себя надлежащим модульным тестированием и тестированием согласованности.

Мачадо
источник
1
Проблема с внедрением (процедурной) бизнес-логики в SQL заключается в том, что ее рефакторинг чрезвычайно болезнен. Даже если у вас есть первоклассные инструменты SQL-рефакторинга, они обычно не взаимодействуют с инструментами рефакторинга кода в вашей IDE (или, по крайней мере, я еще не видел такого набора инструментов)
Роланд Тепп,
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')
yoniLavi
источник
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-поколения:

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

И, конечно, вы можете легко это изменить. Попробуй это:

logic = "MULTIPLY:0,1,1,1";
astef
источник
-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)
Ксавье Комбель
источник