Wie kann man aufhören Angst zu haben und sich in das Parsen verlieben?

    Wie oft haben Sie sich beim Programmieren eines anderen Geschäftsfeatures Gedanken gemacht: Es gibt Menschen auf der Erde, die Datenbanken schreiben, Gesichter auf Fotografien erkennen, Frameworks erstellen und interessante Algorithmen implementieren. Warum läuft alles in meiner Arbeit darauf hinaus, eine Datenbank von einer Tabelle in eine andere zu verschieben, http-Dienste aufzurufen, ein HTML-Formular zu erstellen und andere "Business-Nudeln"? Vielleicht mache ich etwas falsch oder arbeite in der falschen Firma?


    Die gute Nachricht ist, dass uns überall interessante Aufgaben umgeben. Ein starkes Verlangen und Mut wirken sich auf dem Weg zum Ziel positiv aus - eine Aufgabe jeder Größenordnung wird zu Ihrer Stärke. Beginnen Sie einfach damit.

    Kürzlich haben wir einen 1C-Parser für Abfragesprachen und dessen Übersetzer in reguläres SQL geschrieben. Dies ermöglichte es uns, Anfragen an 1C ohne die Teilnahme von 1C zu erfüllen :) Die Mindestarbeitsversion auf regexp betrug zwei Wochen. Ein weiterer Monat wurde mit einem vollwertigen Parser für Grammatiken verbracht, bei dem die Feinheiten der Datenbankstruktur verschiedener 1C-Objekte analysiert und bestimmte Operatoren und Funktionen implementiert wurden. Daher unterstützt die Lösung fast alle Sprachkonstrukte, der Quellcode wird auf GitHub gepostet .

    Im Folgenden werden wir Ihnen erklären, warum wir es brauchten, wie es möglich war, und einige interessante technische Details ansprechen.

    Wie hat alles angefangen?


    Wir arbeiten in einer großen Wirtschaftsprüfungsgesellschaft Button . Wir haben 1005 Kunden, 75 Buchhalter und 11 Entwickler. Unsere Buchhalter führen Aufzeichnungen über Tausende von Kunden im 1C: Accounting-System . Zur Verwaltung der Datenbanken verwenden wir die Cloud-Technologie 1cFresh, deren Daten in PostgreSQL gespeichert werden.

    Die schwierigste Phase in der Arbeit eines Buchhalters ist die Berichterstattung. Es scheint, dass 1C beliebige Berichte erstellen kann, dafür wird jedoch der aktuelle Status der Datenbank benötigt. Jemand muss alle primären Belege in das System eingeben, einen Kontoauszug importieren, die erforderlichen Buchhaltungsbelege erstellen und durchführen. Darüber hinaus sind die Fristen für die Meldung in unserem geliebten Staat streng begrenzt, sodass Buchhalter in der Regel von einem schlaflosen Zeitdruck zum anderen leben.

    Wir dachten: Wie können Buchhalter das Leben leichter machen?

    Es stellte sich heraus, dass viele Berichterstattungsprobleme auf geringfügige Fehler in der Rechnungslegungsgrundlage zurückzuführen sind:

    • doppelte Gegenpartei oder Vertrag;
    • Duplikate von Primärdokumenten;
    • Gegenpartei ohne TIN;
    • Ein Dokument mit einem Datum aus einer fernen Vergangenheit oder Zukunft.

    Die aufgeführten Probleme lassen sich mit der 1C-Abfragesprache leicht finden, sodass die Idee einer automatisierten Prüfung von Client-Datenbanken entstand. Wir haben mehrere Anfragen geschrieben und angefangen, diese jede Nacht an allen 1C-Basen auszuführen. Wir haben die gefundenen Probleme den Buchhaltern in einem praktischen Google-Kennzeichen gezeigt und auf jede erdenkliche Weise gefordert, dass das Kennzeichen leer bleibt.

    Das Ausführen dieser Anforderungen über das Standard-COM-API 1C ist keine gute Idee. Erstens dauert es sehr lange, bis ungefähr tausend Datenbanken vorhanden sind und alle Abfragen für jede von ihnen 10 Stunden lang ausgeführt wurden. Zweitens wird der 1C-Server erheblich belastet, der normalerweise nicht zufrieden ist. Es ist aus Prüfungsgründen unangenehm, die derzeitige tägliche Arbeit der Menschen zu verlangsamen.

    In der Zwischenzeit sieht eine typische 1C-Anfrage folgendermaßen aus:

    select
    	doc.Дата as Date,
    	doc.Номер as Number,
    	doc.Организация.ИНН as Inn,
    	doc.Контрагент.ИНН as CounterpartyInn,
    	Представление(doc.Контрагент.ЮридическоеФизическоеЛицо) as CounterpartyType,
    	doc.НазначениеПлатежа as Description,
    	doc.СуммаДокумента as Sum
    from Документ.ПоступлениеНаРасчетныйСчет doc
    where
    not doc.ДоговорКонтрагента.ПометкаУдаления
    and doc.Проведен
    and doc.видоперации = Значение(Перечисление.ВидыОперацийПоступлениеДенежныхСредств.ОплатаПокупателя)	
    and ГОД(doc.Дата) = ГОД(&Now)
    

    Trotz der Tatsache, dass es SQL sehr ähnlich ist, funktioniert so etwas nicht nur, um direkt durch die Datenbank zu laufen.

    Dafür gibt es drei echte Gründe:

    1. Die magischen Namen von Tabellen und Spalten in der Datenbank. Dies ist leicht zu lösen, da 1C ihre Entsprechung zu den Namen aus der Anfrage dokumentiert .
    2. Geschachtelte Eigenschaften. Zum Beispiel doc.Организация.ИННin SQL entspricht left joindie beiden Platten Документ.ПоступлениеНаРасчетныйСчетund Справочник.Организации.
    3. 1C-spezifische Operatoren und Funktionen , wie z Значение, Представление и Год. Sie müssen außerdem zusätzlich in die entsprechenden DBMS-Entwürfe übersetzt werden.

    Nachdem wir all dies erkannt haben, haben wir ein Hilfsprogramm geschrieben , das eine Abfrage aus dem 1C-Dialekt in reguläres SQL konvertiert, sie auf allen physischen PostgreSQL-Servern parallel ausführt, das Ergebnis kombiniert und in MS SQL in einer separaten Tabelle ablegt. Infolgedessen wurde die Datenerfassungszeit von 10 Stunden auf 3 Minuten reduziert.

    Reguläre Ausdrücke


    In der ersten Version haben wir die Anforderungsumwandlungslogik vollständig über regexp implementiert. Das COM - api 1C ist eine Funktion PoluchitStrukturuHraneniyaBazyDannyh . Es gibt Informationen darüber zurück, welche Tabellen und Felder Objekten und Eigenschaften in einer 1C-Abfrage entsprechen. Mit mehreren regulären Ausdrücken haben wir einfach einen Namen durch einen anderen ersetzt. Dies war recht einfach zu erreichen, vorausgesetzt, alle Aufrufe von Objekten und Eigenschaften hatten Aliase.

    Am allermeisten wurde der Ärger durch die angefügten Eigenschaften geliefert. 1C speichert sie in verwandten Tabellen, so fromdass der ursprüngliche Name des Objekts im Entwurf durch eine Unterabfrage ersetzt werden musste, in der alle erforderlichen enthalten waren left join-ы.

    Beispiel anfordern
    select
    doc.Контрагент.ИНН
    from Документ.ПоступлениеТоваровУслуг doc
    -- конвертировалось в
    select
    	doc.gen0
    from (select
    	tContractor.inn gen0
    from tDoc
    left join tContractor on tDoc.contractorId = tContractor.id) doc
    


    Neben dem Umbenennen von Eigenschaften und dem Generieren von Links verwendete joinder Übersetzer eine Reihe von Transformationen. So musste beispielsweise jeder joinin der ursprünglichen Abfrage eine zusätzliche Bedingung für die Gleichheit des Feldes erhalten Область (area). Tatsache ist, dass wir in einer PostgreSQL-Datenbank mehrere 1C-Client-Datenbanken haben und die Daten eines Clients sich von den Daten eines anderen durch eine spezielle Kennung unterscheiden, die 1C die Region nennt . In der Datenbank erstellt 1C eine Reihe von Standardindizes. Alle von ihnen, die erste Komponente des Schlüssels, haben einen Bereich, da alle Anforderungen innerhalb desselben Clients ausgeführt werden. Damit unsere Abfragen Standardindizes verwenden und beim Schreiben nicht darüber nachdenken, haben wir begonnen, diese Bedingung beim Senden einer Abfrage automatisch hinzuzufügen.

    Die Verwendung von Regexp hat sich als die richtige Lösung erwiesenDies ermöglichte es uns, schnell das Endergebnis zu erhalten und zu verstehen, dass aus diesem ganzen Unternehmen etwas Nützliches hervorgeht. Wir raten jedem, Konzepte und Experimente auf diese Weise nachzuweisen - mit den einfachsten verfügbaren Mitteln. Und was kann beim Arbeiten mit Texten einfacher und effektiver sein als reguläre Ausdrücke?

    Natürlich gibt es auch Nachteile. Das erste und offensichtliche Problem sind abgeschnittene Ecken und Syntaxbeschränkungen. RegExps für Eigenschaften und Tabellen erfordern ein Aliasing in der Abfrage und könnten im Allgemeinen versehentlich ein anderes Konstrukt aufholen, z. B. eine konstante Zeichenfolge.

    Ein weiteres Problem ist die Verwirrung der Textanalyse-Logik und ihre Umwandlung gemäß den erforderlichen Regeln. Jedes Mal, wenn eine neue Funktion implementiert wurde, war es notwendig, eine neue höllische Mischung von regulären Ausdrücken mit Herausforderungen zu erfindenIndexOfin Zeilen, die die entsprechenden Elemente in der ursprünglichen Abfrage isolieren.

    So sah zum Beispiel der Code so aus, dass allen Verknüpfungen eine Bedingung für die Gleichheit von Domänen hinzugefügt wurde:

    private string PatchJoin(string joinText, int joinPosition, string alias)
    {
        var fromPosition = queryText.LastIndexOf("from", joinPosition, StringComparison.OrdinalIgnoreCase);
        if (fromPosition < 0)
            throw new InvalidOperationException("assertion failure");
        var tableMatch = tableNameRegex.Match(queryText, fromPosition);
        if (!tableMatch.Success)
            throw new InvalidOperationException("assertion failure");
        var mainTableAlias = tableMatch.Groups[3].Value;
        var mainTableEntity = GetQueryEntity(mainTableAlias);
        var joinTableEntity = GetQueryEntity(alias);
        var condition = string.Format("{0}.{1} = {2}.{3} and ", mainTableAlias,
            mainTableEntity.GetAreaColumnName(), alias, joinTableEntity.GetAreaColumnName());
        return joinText + condition;
    }
    

    Im Code wollte ich mich mit dem Objektmodell der ursprünglichen Anforderung befassen, mit ColumnReferenceund JoinClause, und stattdessen wurden im Anforderungstext nur Teilzeichenfolgen und Offsets gefunden, die vom regulären Ausdruck angegeben wurden.

    Stimmen Sie zu, dass diese Option viel einfacher und verständlicher aussieht als die vorherige:

    private void PatchJoin(SelectClause selectClause, JoinClause joinClause)
    {
        joinClause.Condition = new AndExpression
        {
            Left = new EqualityExpression
            {
                Left = new ColumnReferenceExpression
                {
                    Name = PropertyNames.area,
                    Table = selectClause.Source
                },
                Right = new ColumnReferenceExpression
                {
                    Name = PropertyNames.area,
                    Table = joinClause.Source
                }
            },
            Right = joinClause.Condition
        };
    }
    

    Ein solches Objektmodell heißt Abstract Syntax Tree ( AST ).

    AST


    Interessanterweise wurde AST zum ersten Mal nicht beim Analysieren der ursprünglichen Abfrage angezeigt, sondern beim Formatieren des Ergebnisses in SQL. Tatsache ist, dass die Logik zum Erstellen einer Unterabfrage für verschachtelte Eigenschaften sehr aufwändig geworden ist. Um sie zu vereinfachen (und gemäß SRP ), haben wir den gesamten Prozess in zwei Phasen unterteilt: Zuerst erstellen wir einen Baum von Objekten, die die Unterabfrage beschreiben, und serialisieren ihn dann separat in SQL. Irgendwann wurde uns klar, dass dies AST ist, und um Probleme mit regulären Ausdrücken zu lösen, müssen Sie nur lernen, wie Sie es für die ursprüngliche Anforderung erstellen.

    Der Begriff AST wird häufig verwendet, um die Nuancen der Syntaxanalyse zu erörtern. Baumes wird genannt, weil diese Datenstruktur die für Programmiersprachen typischen Konstrukte gut beschreibt, die normalerweise die Eigenschaft der Rekursivität und des Fehlens von Schleifen besitzen (obwohl dies nicht immer der Fall ist ).

    Betrachten Sie beispielsweise diese Abfrage:

    select p.surname as 'person surname'
    from persons p
    where p.name = 'иван'
    

    In der Form von AST sieht es so aus:


    In der Abbildung sind Knoten - Instanzen von Klassen, Pfeilen und Signaturen - Eigenschaften dieser Klassen.

    Ein solches Objektmodell kann wie folgt über den Code zusammengestellt werden:

    var table = new TableDeclarationClause
    {
        Name = "PersonsTable",
        Alias = "t"
    };
    var selectClause = new SelectClause
    {
        FromExpression = table,
        WhereExpression = new EqualityExpression
        {
            Left = new ColumnReferenceExpression
            {
                Table = table,
                Name = "name"
            },
            Right = new LiteralExpression
            {
                Value = "иван"
            }
        }
    };
    selectClause.Fields.Add(new SelectFieldExpression
    {
        Expression = new ColumnReferenceExpression
        {
            Table = table,
            Name = "surname"
        }
    });
    

    Es ist erwähnenswert, dass das obige AST-Beispiel nicht das einzig richtige ist. Die spezifische Struktur der Klassen und die Beziehungen zwischen ihnen werden durch die Besonderheiten der Aufgabe bestimmt. Das Hauptziel eines AST ist es, die Lösung des Problems zu erleichtern und die Durchführung typischer Operationen so komfortabel wie möglich zu gestalten. Je einfacher und natürlicher es ist, die Konstruktionen der gewünschten Sprache zu beschreiben, desto besser.

    Der Übergang von regulären Ausdrücken zu AST ermöglichte es uns, viele Hacks loszuwerden, um den Code übersichtlicher und verständlicher zu machen. Gleichzeitig sollte unser Dienstprogramm jetzt über alle Konstruktionen der Quellsprache Bescheid wissen, um einen entsprechenden Knoten für sie im Baum zu erstellen. Dazu musste ich eine Grammatik der Abfragesprache 1C und einen Parser dafür schreiben.

    Grammatik


    Irgendwann wurde klar, dass wir den AST der ursprünglichen Anfrage benötigen. Es gibt viele Bibliotheken im Internet, die SQL analysieren und AST dafür erstellen können, aber bei näherer Betrachtung stellen sich heraus, dass sie entweder kostenpflichtig sind oder nur eine Teilmenge von SQL unterstützen. Außerdem ist nicht klar, wie sie für die Erkennung des 1C-Dialekts von SQL angepasst werden sollen, da er eine Reihe spezifischer Erweiterungen enthält.

    Aus diesem Grund haben wir uns entschlossen, einen eigenen Parser zu schreiben. Parser beginnen normalerweise damit, die Grammatik der Sprache zu beschreiben, die Sie erkennen möchten. Die formale Grammatik ist ein klassisches Werkzeug zur Beschreibung der Struktur von Programmiersprachen. Es basiert auf den Inferenzregeln, dh den rekursiven Definitionen jedes Sprachkonstrukts.

    Diese Regeln können beispielsweise die Sprache von arithmetischen Ausdrücken beschreiben:

    E → number | (E) | E + E | E - E | E * E | E / E

    Ein solcher Datensatz kann wie folgt gelesen werden:

    • jede Zahl (number)ist ein Ausdruck (E);
    • Wenn der Ausdruck in eckigen Klammern steht, ist dies alles zusammen mit den eckigen Klammern auch ein Ausdruck.
    • zwei durch eine arithmetische Operation verbundene Ausdrücke bilden ebenfalls einen Ausdruck.

    Symbole, für die Ausgaberegeln definiert sind, werden als Nicht-Terminals bezeichnet . Zeichen , die nicht durch die Regeln definiert sind, und welche Elemente der Sprache - Terminals . Anwenden der Regeln von Nicht-Terminals ist es möglich, Zeichenfolgen zu erhalten, die aus anderen Nicht-Terminals und Terminals bestehen, bis nur die Terminals übrig bleiben. Im obigen Beispiel Eist dies ein Nichtterminal, und die Symbole +, -, *, /und numberTerminals bilden die Sprache der arithmetischen Ausdrücke.

    Es gibt spezielle Tools - Parser-Generatoren, die gemäß der Beschreibung der Sprache in Form einer Grammatik Code erzeugen können, der diese Sprache erkennt. Die bekanntesten sind Yacc , Bisons und Ameisen. Es gibt eine weniger verbreitete Irony- Bibliothek für C # . Es gab bereits einen kleinen Artikel über sie auf Habré , aber Scott Hanselmans Post über sie.

    Das Hauptmerkmal der Irony- Bibliothek ist, dass Grammatikregeln direkt beschrieben werden können C#, indem der Operator überladen wird. Das Ergebnis ist ein hübsches DSL in einer Form, die der klassischen Form der Schreibregeln sehr ähnlich ist:

    var e = new NonTerminal("expression");
    var number = new NumberLiteral("number");
    e.Rule = e + "+" + e | e + "-" + e | e + "*" + e | e + "/" + e | "(" + e + ")" | number;
    

    Symbol | bedeutet, dass eine der Regeloptionen angewendet werden kann (logisch oder). Das + Symbol ist Verkettung, die Zeichen müssen aufeinander folgen.

    Ironie trennt die Konzepte von Parse Tree und Abstract Syntax Tree . Parse Tree ist ein Artefakt des Texterkennungsprozesses, das aus der konsequenten Anwendung von Grammatikregeln resultiert. Nichtterminale befinden sich in den internen Knoten, und Symbole aus den rechten Teilen der entsprechenden Regeln gehören zu den Nachkommen.

    Zum Beispiel ein Ausdruck 1+(2+3)beim Anwenden von Regeln:
    e 1 : E → E + E
    e 2 : E → (E)
    e 3 : E → Nummer

    entspricht einem solchen Parse Tree:



    Parse Tree sind sprachunabhängig und werden in Irony in einer Klasse beschrieben ParseTreeNode.

    Der abstrakte Syntaxbaum hingegen wird vollständig von einer bestimmten Aufgabe bestimmt und besteht aus Klassen, die für diese Aufgabe und die Beziehungen zwischen ihnen spezifisch sind.

    Beispielsweise kann der AST für die obige Grammatik aus nur einer Klasse bestehen BinaryOperator:

    public enum OperatorType
    {
        Plus,
        Minus,
        Mul,
        Div
    }
    public class BinaryOperator
    {
        public object Left { get; set; }
        public object Right { get; set; }
        public OperatorType Type { get; set; }
    }
    

    Eigenschaften Leftund Righthaben einen Typ object, weil Sie können sich entweder auf eine Zahl oder eine andere beziehen BinaryOperator:


    Mit Irony können Sie nacheinander einen AST erstellen, der von den Blättern zur Wurzel aufsteigt, während Sie die Grammatikregeln anwenden. Zu diesem Zweck kann AstNodeCreatorein Delegierter an jedes Nicht-Terminal gehängt werden , das Irony zum Zeitpunkt der Anwendung der mit diesem Nicht-Terminal verbundenen Regeln aufruft. Dieser Delegat sollte ParseTreeNodeden AST-Knoten erstellen, der ihm entspricht, und eine Verknüpfung zu diesem Knoten wieder herstellen ParseTreeNode. Zum Zeitpunkt des Aufrufs des Delegaten AstNodeCreatorwaren alle untergeordneten Knoten des Parse-Baums bereits verarbeitet und wurden bereits für sie aufgerufen, sodass wir im Delegatentext die Eigenschaft der bereits ausgefüllten AstNodeuntergeordneten Knoten verwenden können.

    Wenn wir so das Wurzel-Nichtterminal erreichen, wird der AstNodeAST-Wurzelknoten darin gebildet, in unserem Fall - SqlQuery.

    Für die Grammatik der obigen arithmetischen Ausdrücke könnte AstNodeCreator folgendermaßen aussehen:

    var e = new NonTerminal("expression",
    delegate(AstContext context, ParseTreeNode node)
    {
        //соответствует правилу E → number,
        if (node.ChildNodes.Count == 1)
        {
            node.AstNode = node.ChildNodes[0].Token.Value;
            return;
        }
        //правила вида E → E op E
        if (node.ChildNodes[0].AstNode != null && node.ChildNodes[2].AstNode != null)
        {
            node.AstNode = new BinaryOperator
            {
                Left = node.ChildNodes[0].AstNode,
                Operator = node.ChildNodes[1].FindTokenAndGetText(),
                Right = node.ChildNodes[2].AstNode
            };
            return;
        }
        //правило со скобками
        node.AstNode = node.ChildNodes[1].AstNode;
    });
    

    Mit der Hilfe von Irony haben wir also gelernt, wie man auf Anfrage einen AST erstellt. Es gibt nur eine große Frage: Wie lässt sich der Code für die AST-Konvertierung effizient strukturieren? Schließlich müssen wir die resultierende SQL-Abfrage AST vom Quell-AST abrufen. Das Besuchermuster hilft uns dabei.

    Besucher


    Das Besucher- (oder Doppelversand- ) Muster ist eines der komplexesten in GoF und möglicherweise eines der seltensten. Aus unserer Erfahrung haben wir nur eine aktive Verwendung gesehen - für die Konvertierung verschiedener ASTs. Ein konkretes Beispiel ist die ExpressionVisitor- Klasse in .NET, die zwangsläufig auftritt , wenn Sie den Linq-Provider ausführen oder den vom Compiler generierten Ausdrucksbaum nur geringfügig korrigieren möchten .

    Welches Problem lösen Besucher?
    Das Natürlichste und Notwendigste, was Sie bei der Arbeit mit AST häufig tun müssen, ist, daraus eine Zeichenfolge zu machen. Nehmen wir zum Beispiel unseren AST: Nachdem wir russische Tabellennamen durch englische ersetzt left join-овund 1C-Operatoren in Datenbankoperatoren generiert und konvertiert haben, müssen wir am Ausgang einen String abrufen, den wir zur Ausführung in PostgreSQL senden können.

    Eine mögliche Lösung für dieses Problem ist wie folgt:

    internal class SelectClause : ISqlElement
    {
        //...
        public void BuildSql(StringBuilder target)
        {
            target.Append("select ");
            for (var i = 0; i < Fields.Count; i++)
            {
                if (i != 0)
                    target.Append(",");
                Fields[i].BuildSql(target);
            }
            target.Append("\r\nfrom ");
            From.BuildSql(target);
            foreach (var c in JoinClauses)
            {
                target.Append("\r\n");
                c.BuildSql(target);
            }
        }
    }
    

    Zu diesem Code können zwei wichtige Beobachtungen gemacht werden:

    • Alle Knoten des Baums müssen über eine Methode verfügen, BuildSqldamit die Rekursion funktioniert.
    • Methode BuildSqlzum SelectClauseerneuten Aufrufen BuildSqlaller untergeordneten Knoten.

    Betrachten Sie nun eine andere Aufgabe. Angenommen, wir müssen eine Bedingung für die Gleichheit des Feldes areazwischen der Haupttabelle und allen verknüpften hinzufügen , um in die PostgreSQL-Indizes zu gelangen. Dazu müssen wir alles JoinClausein der Anfrage durchgehen, aber angesichts der möglichen Unterabfragen müssen wir uns daran erinnern, alle anderen Knoten zu betrachten.

    Dies bedeutet, dass wir, wenn wir der gleichen Codestruktur wie oben folgen, Folgendes tun müssen:

    • füge AddAreaToJoinClauseallen Knoten des Baumes eine Methode hinzu ;
    • Die Implementierung muss auf allen Knoten außer JoinClausean ihre Nachkommen weitergeleitet werden.

    Das Problem bei diesem Ansatz ist klar: Je mehr logische Operationen im Baum vorhanden sind, desto mehr Methoden befinden sich in den Knoten und desto mehr können Sie kopieren und einfügen.

    Besucher lösen dieses Problem durch:

    • Logische Operationen sind keine Methoden mehr auf Knoten, sondern werden zu separaten Objekten - Erben einer abstrakten Klasse SqlVisitor(siehe folgende Abbildung).
    • Jeder Knoten entspricht eine andere Art von Verfahren Visitin einer Base SqlVisitor-е, beispielsweise VisitSelectClause(SelectClause clause)oder VisitJoinClause(JoinClause clause).
    • Methoden BuildSqlund AddAreaToJoinClausewerden durch eine gemeinsame Methode ersetzt Accept.
    • Jeder Knoten implementiert es, indem er an die entsprechende Methode weiterleitet SqlVisitor-е, die als Parameter geliefert wird.
    • Konkrete Operationen werden von SqlVisitorden für sie interessanten Methoden geerbt und neu definiert.
    • Implementierungen von Methoden Visitin der Basis werden SqlVisitor-еeinfach Visitfür alle untergeordneten Knoten neu aufgerufen , da dadurch doppelter Code beseitigt wird.


    Das Serialisierungsbeispiel in SQL passt sich wie folgt an:

    internal interface ISqlElement
    {
        void Accept(SqlVisitor visitor);
    }
    internal class SqlVisitor
    {
        public virtual void Visit(ISqlElement sqlElement)
        {
            sqlElement.Accept(this);
        }
        public virtual void VisitSelectClause(SelectClause selectClause)
        {
        }
        //...
    }
    internal class SqlFormatter : SqlVisitor
    {
        private readonly StringBuilder target = new StringBuilder();
        public override void VisitSelectClause(SelectClause selectClause)
        {
            target.Append("select ");
            for (var i = 0; i < selectClause.Fields.Count; i++)
            {
                if (i != 0)
                    target.Append(",");
                Visit(selectClause.Fields[i]);
            }
            target.Append("\r\nfrom ");
            Visit(selectClause.Source);
            foreach (var c in selectClause.JoinClauses)
            {
                target.Append("\r\n");
                Visit(c);
            }
        }
    }
    internal class SelectClause : ISqlElement
    {
        //...
        public void Accept(SqlVisitor visitor)
        {
            visitor.VisitSelectClause(this);
        }
    }
    

    Der Name Doppelversand beschreibt dieses Schema recht genau:

    • Erster Versand erfolgt in der Klasse SqlVisitoram Übergang von Visitbis Acceptzu einem bestimmten Knoten, zu diesem Zeitpunkt bekannte Art von Knoten wird.
    • Der zweite Versand folgt dem ersten, wenn von Accepteinem Knoten zu einer bestimmten Methode gewechselt wird SqlVisitor, hier wird die Operation bekannt, die auf den ausgewählten Knoten angewendet werden muss.

    Total


    Der Artikel beschreibt ausführlich das Rezept zum Vorbereiten des Übersetzers der Abfragesprache 1C in SQL-Abfragen. Wir haben Experimente mit regulären Ausdrücken durchgeführt, einen funktionierenden Prototyp erhalten und bestätigt, dass die Sache nützlich ist und es sich lohnt, weiterzumachen. Und als es unmöglich wurde, den Code ohne Scham und Schmerz zu betrachten, und das Jonglieren mit Regexp nicht zum gewünschten Ergebnis führte, machten wir einen ernsthaften Schritt - wir wechselten zu AST und Grammatik. Mithilfe der Besucher haben wir außerdem gelernt, wie man AST transformiert, um die Logik der Übersetzung von einer Sprache in eine andere zu verwirklichen.

    Es ist erwähnenswert, dass wir diesen Weg nicht alleine gegangen sind und nicht einmal das Drachenbuch öffnen mussten. Zum Parsen und Erstellen des AST haben wir die fertige Irony-Bibliothek verwendet, die es uns ermöglichte, das Rad nicht neu zu erfinden, sondern sofort mit der Lösung des angewendeten Problems fortzufahren.

    Das praktische Ergebnis für das Unternehmen besteht darin, die Geschwindigkeit des Datenempfangs von 10 Stunden auf 3 Minuten zu reduzieren. Dies ermöglichte unseren Analysten, Hypothesen über das Geschäft von Kunden und die Arbeit von Buchhaltern schnell zu experimentieren und zu testen. Dies ist besonders praktisch, da wir viele Clients haben und deren Datenbanken auf die fünf physischen PostgreSQL-Server verteilt sind.

    Alles zusammenfassend:

    1. Experimentieren Sie und sichern Sie sich so schnell und kostengünstig wie möglich einen Proof of Concept.
    2. Setzen Sie sich ehrgeizige Ziele und bewegen Sie sich in kleinen Schritten auf diese zu, um das Instrument schrittweise in den gewünschten Zustand zu versetzen.
    3. Für die meisten Aufgaben gibt es bereits eine fertige Lösung oder zumindest ein Fundament.
    4. Parsing und Grammatik sind in normalen Geschäftsanwendungen anwendbar.
    5. Lösen Sie ein bestimmtes Problem, und die allgemeine Lösung wird von selbst kommen.

    Auf GitHub warten Bibliothekscode und Verwendungsbeispiele auf Sie .

    In Ihrer Freizeit empfehlen wir zu lesen:


    Jetzt auch beliebt: