Powrót do strony głównej

Deklaratywny Data Pipeline w Spark | Klasy i dekoratory

Artykuł opisuje podejście do tworzenia deklaratywnych data pipeline z użyciem klas i dekoratorów w Python. Omówiono integrację z Spark i realizację poprzez meta-programowanie. Szczegółowo rozłożono kroki pipeline'a, zarządzanie stanem i użycie SQL.

Jak stworzyć deklaratywny data pipeline w Spark od zera
Advertisement 728x90

# Deklaratywny sposób budowania potoków danych: klasy i dekoratory w Spark

W projektach data engineering często dochodzi do fragmentacji logiki biznesowej z powodu braku jednolitej architektury. Proponowana metoda wykorzystuje metaprogramowanie Pythona do tworzenia deklaratywnych potoków danych, w których transformacje danych opisuje się za pomocą klas i dekoratorów. Takie podejście upraszcza utrzymanie kodu, zwiększa jego reużywalność i czyni procesy biznesowe jawne.

Podstawowe zasady deklaratywnego potoku danych

Kluczowy pomysł polega na wyodrębnieniu procesu biznesowego (Flow) jako głównej encji, a traktowaniu tabel jako obiektów pomocniczych. Pozwala to uniknąć rozmycia logiki przy skalowaniu. Flow to sekwencja kroków (steps), z których każdy:

  • Przyjmuje tabele wejściowe
  • Wykonuje transformację
  • Zwraca wynik

Ważne jest, że tabele są implementowane jako klasy, a nie jako obiekty pierwotne. Na przykład za klasą MyTable może stać Spark DataFrame. Taki poziom abstrakcji umożliwia zmianę implementacji bez przepisywania logiki biznesowej. Kroki powinny być atomowe i mieć jasno określone wejścia/wyjścia, co zapewnia przewidywalność wykonania.

Google AdInline article slot

Implementacja za pomocą klas i dekoratorów

Podstawowa klasa Flow zawiera logikę frameworka. Konkretne potoki tworzy się przez dziedziczenie:

class MyFlow(Flow):
    @classmethod
    @Flow.step(order=1)
    @Flow.input([MyTable])
    @Flow.output(MyTable2)
    def step_one(cls, context: Context) -> DataFrame:
        print(f" Step 1: sozdayom/update MyTable,{context.id}")

Dekoratory nie wykonują kodu, lecz gromadzą metainformacje:

  • @Flow.step(order=1) — określa kolejność wykonania
  • @Flow.input() — wskazuje tabele wejściowe
  • @Flow.output() — definiuje wynik

Podczas tworzenia podklasy uruchamia się metoda __init_subclass__, która automatycznie zbiera kroki, sortuje je według kolejności i tworzy wykonywalną sekwencję. Osiąga się to poprzez analizę atrybutów klasy i wyodrębnienie metadanych z udekorowanych metod.

Google AdInline article slot

Zarządzanie stanem: kontekst i tabele

Do przekazywania danych między krokami służy obiekt Context, zaimplementowany za pomocą modelu Pydantic:

class Context(BaseModel):
    config: Dict[Type[Config], Config] = {}
    data: Dict[Union[str, Type[Table]], DataFrame] = {}
    diff: Dict[Any, Any] = {}

Zalety takiego podejścia:

  • Minimalizacja parametrów w metodach
  • Zcentralizowane zarządzanie stanem
  • Elastyczność rozszerzeń

Tabele opisuje się w stylu deklaratywnym SQLAlchemy. Konwerter przekształca modele ORM w schematy Spark:

Google AdInline article slot
class Table(Base):
    __abstract__ = True

    @classmethod
    def get_schema(cls) -> T.StructType:
        fields = []
        for column in cls.__table__.columns:
            spark_type = _convert_type(column.type)
            fields.append(
                T.StructField(column.name, spark_type, column.nullable)
            )
        return T.StructType(fields)

Pozwala to używać jednolitego modelu danych w różnych warstwach systemu. W razie potrzeby schemat można nadpisać bezpośrednio w metodzie get_schema().

Uruchamianie potoku i obsługa SQL

Wykonanie potoku odbywa się za pomocą metody run(), która kolejno wywołuje kroki w określonej kolejności. Do transformacji zaleca się używanie SQL jako głównego narzędzia:

class MyFlow(Flow):
    @classmethod
    @Flow.step(order=3)
    @Flow.input([MyTable])
    @Flow.output(AnotherTable)
    @Flow.sql("step_three.sql")
    def step_three(cls, context: Context):
        df = cls.execute_sql(context, "step_three.sql", vars={"id": 1})

Dekorator @Flow.sql przygotowuje zapytanie parametryzowane. Przed wykonaniem SQL automatycznie tworzy tymczasowe widoki dla tabel wejściowych za pomocą metody create_temp_views(). Zapewnia to przezroczyste używanie tabel w zapytaniach bez ręcznego zarządzania kontekstem.

Co jest ważne

  • Deklaratywność czyni logikę biznesową jawną i ułatwia debugowanie
  • Kontekst jako jednolity punkt dostępu do stanu zmniejsza liczbę błędów
  • Priorytet SQL nad API DataFrame przyspiesza rozwój i poprawia czytelność
  • Metaprogramowanie za pomocą __init_subclass__ automatyzuje składanie potoku

— Editorial Team

Advertisement 728x90

Czytaj dalej