0

I have a class, LogRequest, which contains parameters (members) for an object.
This object is turned to an sql query using another class, DataAccess.
And lastly, I have the class Search which run the sql query.

In Search :

'Create the request
Dim myRequest As LogRequest = MakeRequest()
'Does the SQL query part
responseList = DataAccess.ReadLogs(myRequest)

My problem is that the Where part is built up ("x=1","b=2","date between x and y"...) in LogRequest.

In DataAccess :

' request is a LogRequest object  
For Each filter As String In request.GetFilters()
    sqlfilters.AppendLine(String.Format("{0} {1}", IIf(first, "WHERE", "AND"), filter))
    first = False
Next
sql = String.Format("SELECT * FROM table_x {0} ", sqlfilters)    

Any suggestions on how I could break those strings to use sqlParameters instead?

Is this a design issue, should I move the "GetFilters" part to the dataAccess class?

Edit:

Also, any suggestions on how to pass sqlParameters from a function to another would be appreciated. (The second one creating and handling the command.)

4

2 回答 2

1

First of all it's a good thing to replace your string parameters by sql parameters, especially if you incorporate user input in it. This prevents so called SQL Injection attacks!

The problem you try to solve looks a lot like a implementation of query object. I would keep all the logic that represents a query inside a query object, in your case, this would be the LogRequest object.

I would then make a (SQL) query translator class that transforms a given query object into a sql representation which you then can execute. In your case this would be the ReadLogs() method. You could think about setting this up in a more generic way so that it doesn't mind if you want to read the logs or do another kind of query. This gives you a flexible way to define, transform and execute queries.

In order to accomiplish want you want you will have to get rid of your current filter implementation. You will have to seperate the columnname, operator and operand in seperate properties. The best way is by defining a criterion class.

The purpose of the following examples is to only put you in the right direction. They are not a complete out of the box solution.

BTW I am sorry that this code is in C#, while I should know some VB.NET it takes me more time which I don't have at the moment.

public class Criterion
{
    public string PropertyName { get; set; }
    public object Value { get; set; }
    public CriterionOperator Operator { get; set; }
}

public enum CriterionOperator
{
    Equals        
}

As my example supports only the equality operator, you can add more to your needs. This may need additional Criterion classes as well.

For holding our queries we need to define some interfaces, enums and abstract classes as well.

public enum QueryOperator
{
    And,
    Or
}

public interface IQuery
{
    IEnumerable<Criterion> Criteria { get; }
    QueryOperator QueryOperator { get; set; }
    void Add(Criterion criterion);
}

public interface IQuery<TResult> : IQuery
{
    TResult Execute();
}

public abstract class BaseQuery<TResult> : IQuery<TResult>
{
    private readonly List<Criterion> _criteria = new List<Criterion>();

    public IEnumerable<Criterion> Criteria
    {
        get { return _criteria; }
    }

    public QueryOperator QueryOperator
    {
        get;
        set;
    }

    public void Add(Criterion criterion)
    {
        _criteria.Add(criterion);
    }

    public abstract TResult Execute();
}

public abstract class SqlQuery<TResult> : BaseQuery<TResult>
{
    protected string _baseSelectQuery = String.Empty;

    protected SqlQuery(string baseSelectQuery)
    {
        _baseSelectQuery = baseSelectQuery;
    }
}

We can now create a SQL Query Translator class which is responsible for transforming a IQuery plus a base select query to a proper SQLCommand.

public static class SqlQueryTranslator
{
    public static void Translate(IQuery query, string baseSelectQuery, SqlCommand command)
    {
        var sqlQuery = new StringBuilder();

        sqlQuery.Append(baseSelectQuery);

        if (query.Criteria.Count() > 0)
        {
            sqlQuery.Append("WHERE ");
        }

        var isNotFirst = false;

        foreach (Criterion criterion in query.Criteria)
        {
            if (isNotFirst)
                sqlQuery.Append(query.QueryOperator == QueryOperator.And ? "AND " : "OR ");

            sqlQuery.Append(GetQueryPartFrom(criterion));

            command.Parameters.Add(new SqlParameter("@" + criterion.PropertyName, criterion.Value));

            isNotFirst = true;
        }

        command.CommandType = CommandType.Text;
        command.CommandText = sqlQuery.ToString();
    }

    private static string GetQueryPartFrom(Criterion criterion)
    {
        return string.Format("{0} {1} @{2}",
                             criterion.PropertyName,
                             GetSqlOperatorFor(criterion.Operator),
                             criterion.PropertyName);
    }

    private static string GetSqlOperatorFor(CriterionOperator criterionOperator)
    {
        switch (criterionOperator)
        {
          case CriterionOperator.Equals:
                return "=";
            default:
                throw new ApplicationException("Not supported Operator");
        }
    }
}

Take a look at the part:

sqlQuery.Append(GetQueryPartFrom(criterion));

command.Parameters.Add(new SqlParameter("@" + criterion.PropertyName, criterion.Value));

If you think this is all to much you can use only this part to replace the string parameters with SQL parameters.

Ok now we have all types available to create your ReadLogs query (in this example the query returns a sequence of strings):

public class ReadLogsQuery : SqlQuery<IEnumerable<string>>
{
    public ReadLogsQuery(): base("SELECT * FROM table_x ")
    {
        Add(new Criterion() { PropertyName = "x", Operator = CriterionOperator.Equals, Value = 1 });
        Add(new Criterion() { PropertyName = "y", Operator = CriterionOperator.Equals, Value = 2 });
        QueryOperator = QueryOperator.And;
    }

    public override IEnumerable<string> Execute()
    {
        //define result;
        var result = new List<string>();

        using (var conn = new SqlConnection("PUT IN YOU CONNECTION STRING"))
        {
            SqlCommand command = conn.CreateCommand();

            SqlQueryTranslator.Translate(this, _baseSelectQuery, command);

            conn.Open();

            using (SqlDataReader reader = command.ExecuteReader())
            {
                while(reader.Read())
                {
                    //read from the datareader
                    result.Add(reader["colname"].ToString());
                }
            }
        }

        return result;
    }
}

You could then execute the query as follows:

var query = new ReadLogsQuery();

IEnumerable<string> result = query.Execute();

Like I said before, this is no out of the box solution! For example you could extend the query classes to support sorting, transactions, named queries (stored procedure/functions), more criterion operators (gt, lt, in, etc) or add a beter level of abstractness to the data accessing part.

I hope other people can help you translate it to VB.NET it shouldn't be that hard. If it's necessary I can do this tommorow in the evening. Let me know!

Could you also elaborate some more about your question on passing sqlparameters. What is the problem with it? There is no problem passing for example a array of SQL parameters to antoher function.

于 2011-10-19T18:28:05.383 回答
0

使用存储过程

在存储过程中使用布尔代数或动态 SQL 来打开和关闭部分查询。例如,在下面的示例中(使用布尔逻辑来启用/禁用参数),如果三个参数的长度为零,则不会在查询中使用它们。

ALTER Procedure [dbo].[Report_ExportReworkData]
@x as nvarchar(50),
@b as nvarchar(50),
@n as nvarchar(50)

-- Make sure this gets compiled each time to avoid incorrect parmeter sniffing
WITH RECOMPILE
as

select
    FieldName1,
    FieldName2
from tablename where
(len(@x) = 0 or x = @x) and
(len(@b) = 0 or b = @b) and
(len(@n) = 0 or n = @n)

这三个参数最终是可选的,如果您在其中传递一个空白字符串,它们不会在 where 子句中使用。如果你想忽略一个数字参数,你可以传入你知道它不可能的值并进行比较,或者你可以传入额外的参数来单独打开和关闭每个参数。如果您在表中的所有记录中传递三个零长度参数,则将返回。

确保在 sp 中包含 WITH RECOMPILE 以在以多种不同方式运行此 sp 时停止不正确的参数嗅探/性能损失。我有一篇关于为什么在某些情况下应该这样做的博客文章。

然后,您可以将此存储过程用作带有参数的普通存储过程。

在 c# 代码中动态构建参数可能非常糟糕,因为您可能会面临 SQL 注入攻击。

于 2011-10-19T15:54:22.420 回答