Jump to content
markus_ja

Generic FilterExpression class Show Case

Recommended Posts

Hello,

 

JFYI, if somebody is interessted. I developed a FilterExpression class, which can be used to create a generic filter expression to use for different DataSources, e.g. SQL, REST, TW3DataSet, ...

 

It's not optimized for speed. I just needed such a functionality in my project, quickly.

It should also be a show case for SMS. Which is my favority tool to create HTML + JavaScript WebApps!!

TFilterExpressionTree = class
  private
    FOperatorTree: array of TFilterOperatorExpression;
  public
    destructor Destroy; override;

    class function NullObject: TFilterExpressionTree;

    function Expr(aField: string; aExprType: TFilterExpressionType; aValue: Variant): TFilterExpressionTree;
    function &Begin(aField: string; aExprType: TFilterExpressionType; aValue: Variant): TFilterExpressionTree; overload;
    function &Begin: TFilterExpressionTree; overload;
    function &End: TFilterExpressionTree;
    function &And(aField: string; aExprType: TFilterExpressionType; aValue: Variant): TFilterExpressionTree; overload;
    function &And: TFilterExpressionTree; overload;
    function &Or(aField: string; aExprType: TFilterExpressionType; aValue: Variant): TFilterExpressionTree; overload;
    function &Or: TFilterExpressionTree; overload;

    procedure Clear;
    property Expressions: array of TFilterOperatorExpression read FOperatorTree;
    function Serialize(aSerializer: IFilterTreeSerializer): string;
  end;

If you need to serialze it to a specifc case, e.g. to a SQL where clause, just implement a IFilterTreeSerialize interface.

TFilterTreeSerializerSQL = class(IFilterTreeSerializer)
public
  function Serialize(aOperatorExpr: TFilterOperatorExpression): string;
end;
function TFilterTreeSerializerSQL.Serialize(aOperatorExpr: TFilterOperatorExpression): String;
begin
  result := '';

  case aOperatorExpr.OperatorType of
    foBegin: result := '(';
    foEnd: result := ')';
    foAnd: result := ' and ';
    foOr: result := ' or ';
  end;

  if aOperatorExpr.HasExpression then
  begin
    result += aOperatorExpr.Expression.Field;

    case aOperatorExpr.Expression.ExprType of
      feEqual:
        begin
          if VarIsNull(aOperatorExpr.Expression.Value) then
            Result += ' is '
          else
            Result += '=';
        end;
      feNotEqual:
        begin
          if VarIsNull(aOperatorExpr.Expression.Value) then
            Result += ' is not '
          else
            Result += '<>';
        end;
      feGreater:
        result += '>';
      feGreaterOrEqual:
        result += '>=';
      feLess:
        result += '<';
      feLessOrEqual:
        result += '<=';
    end;

    case VarType(aOperatorExpr.Expression.Value) of
      varString: Result += QuotedStr( aOperatorExpr.Expression.Value, '"' );
    else
      if VarIsNull(aOperatorExpr.Expression.Value) then
        Result += 'null'
      else
        Result += aOperatorExpr.Expression.Value;
    end;

  end;
end;

Or use it to filter a TW3DataSet

function FilterDataSet(aDataSet: TMyDataSet; aFilterExpr: TFilterExpressionTree): TMyDataSet;

The usage for a SQL where clause:

procedure TForm1.btnFilterExpressionClick(Sender: TObject);
var
  fExpr: TFilterExpressionTree;
  serializer: IFilterTreeSerializer;
begin
  fExpr := TFilterExpressionTree.Create;
  serializer := TFilterTreeSerializerSQL.Create;

  fExpr.Clear;
  fExpr.Expr('CARID', feEqual, 5);
  mmoLog.Text := fExpr.Serialize(serializer) + sLineBreak + sLineBreak;;

  fExpr.Clear;
  fExpr
    .Begin('ID', feGreaterOrEqual, 10).And('NAME', feEqual, 'markus').End
    .Or('AGE', feNotEqual, null);

  mmolog.text := mmolog.text + fExpr.Serialize(serializer)+ sLineBreak + sLineBreak;;

  fExpr.Free;

end;

Output:

 

 

CARID=5

(ID>=10 and NAME="markus") or AGE is not null

 

 

Or use it to filter a DataSet:

procedure TFilterDataSetTest.TestFilter01;
var
  expectedCount: Integer;
  actual: TMyDataSet;
begin
  //(f1 = 1) and (((f2 = 2) and (f3 = 3)) or (f4 = 4))
  expectedCount := 1;

  FExprTree.Clear;
  FExprTree
    .Begin('f1', feEqual, 1).End
    .And
    .Begin
      .Begin
        .Begin('f2', feEqual, 2).End
        .And
        .Begin('f3', feEqual, 3).End
      .End
      .Or
      .Begin('f4', feEqual, 4).End
    .End;

  actual := FilterDataSet(FDataSet, FExprTree);

  Equal('TestFilter01', expectedCount, actual.Count, '(f1 = 1) and (((f2 = 2) and (f3 = 3)) or (f4 = 4))');

end;

Share this post


Link to post
Share on other sites

I also work on a DataProvider and DataSource functionality. In order I have a generic way to read/update data. The filter expression class is required for a generic way to retrieve filtered data from any source.

In my current project, I work against local data (TW3DataSet), then in production, I just want to change my DataProvider to retrieve data from my own REST service.

 

When the project is finished, I want to open source it. It also depence, if somebody needs it.

  IDataProvider = interface
    function Read(aSourceURI: string; aFilterExpression: TFilterExpressionTree): TMyDataSet;
    procedure Insert(aDestURI: string; aDelta: TMyDataSet);
    procedure Update(aDestURI: string; aKeyFields: array of string; aKeyValues: array of variant; aDelta: TMyDataSet);
    procedure Delete(aDestURI: string; aKeyFields: array of string; aKeyValues: array of variant);
  end;

  TDataSetDataProvider = class(IDataProvider)
  private
    FDataSet: TMyDataSet;
  public
    constructor Create(aDataSet: TMyDataSet);
    function Read(aSourceURI: string; aFilterExpression: TFilterExpressionTree): TMyDataSet;
    procedure Insert(aDestURI: string; aDelta: TMyDataSet);
    procedure Update(aDestURI: string; aKeyFields: array of string; aKeyValues: array of variant; aDelta: TMyDataSet);
    procedure Delete(aDestURI: string; aKeyFields: array of string; aKeyValues: array of variant);
  end;

  //*********************************************************
  // Base Class
  //*********************************************************
  TDataSource = class
  protected
    FProvider: IDataProvider;

    function DoRead(aSourceURI: string; aFilterExpression: TFilterExpressionTree): TMyDataSet; virtual;
    procedure DoInsert(aDestURI: string; aDelta: TMyDataSet); virtual;
    procedure DoUpdate(aDestURI: string; aKeyFields: array of string; aKeyValues: array of variant; aDelta: TMyDataSet); virtual;
    procedure DoDelete(aDestURI: string; aKeyFields: array of string; aKeyValues: array of variant); virtual;
  public
    constructor Create(aProvider: IDataProvider);
  end;

Concrete Model implementation:

TCarDataSource = class(TDataSource)
  private
    const URI = 'my-car-uri';
  public
    function Read(aCarGUID: string): TCarModel;
    function ReadAll: TCarModels;
    function Delete(aCarGUID: string);
  end;

function TCarDataSource.Read(aCarGUID: String): TCarModel;
var
  fExpr: TFilterExpressionTree;
  ds: TMyDataSet;
begin
  fExpr := TFilterExpressionTree.Create;
  fExpr.Expr('CAR_GUID', feEqual, aCarGUID);

  ds := DoRead(URI, fExpr);

  if ds.count > 0 then
  begin
    Result.CarGUID := ds.Fields.FieldByName('CAR_GUID').AsString;
    Result.CarName := ds.Fields.FieldByName('CAR_NAME').AsString;
    Result.CarImageSrc := ds.Fields.FieldByName('CAR_IMG_SRC').AsString;
    Result.Rating := ds.Fields.FieldByName('RATING').AsInteger;
    Result.Brand:= ds.Fields.FieldByName('BRAND').AsString;
  end;

end;

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×