[{"data":1,"prerenderedAt":1010},["ShallowReactive",2],{"post-dwa-flushe-dwie-transakcje-outbox-doctrine":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"author":11,"tags":12,"readingTime":19,"body":20,"_type":1004,"_id":1005,"_source":1006,"_file":1007,"_stem":1008,"_extension":1009},"\u002Fblog\u002Fdwa-flushe-dwie-transakcje-outbox-doctrine","blog",false,"","Dwa flushe, dwie transakcje: jak niechcący zabić Outbox w Doctrine","persist() planuje, flush() wykonuje SQL i — bez jawnej transakcji — commituje. Dwa flushe to dwie transakcje, czyli okno, w którym event ginie. Dlaczego to rozbraja cały sens Outbox pattern, jak flush działa pod spodem i ile realnie kosztuje commit.","2026-07-07","Kamil Sułek",[13,14,15,16,17,18],"Outbox","Doctrine","EDA","transakcje","PHP","Symfony",8,{"type":21,"children":22,"toc":993},"root",[23,31,187,207,227,232,302,314,320,332,356,376,395,401,413,419,463,469,474,499,553,558,568,658,684,690,724,736,742,770,855,860,910,930,936,948,967,987],{"type":24,"tag":25,"props":26,"children":27},"element","p",{},[28],{"type":29,"value":30},"text","Oto okaz do code review. Kod się kompiluje, w happy pathie działa, nic nie rzuca wyjątkiem\n— a mimo to jest w nim defekt poprawności, który wywraca cały wzorzec, którego używa.",{"type":24,"tag":32,"props":33,"children":37},"pre",{"className":34,"code":35,"language":36,"meta":7,"style":7},"language-php shiki shiki-themes github-dark","public function createOrder(OrderDTO $dto): Order\n{\n    $order = new Order($dto);\n    $this->em->persist($order);\n    $this->em->flush();          \u002F\u002F ①\n\n    $event = new OutboxMessage(\n        type: 'order.created',\n        payload: json_encode($dto),\n        aggregateId: $order->getId(),\n    );\n    $this->em->persist($event);\n    $this->em->flush();          \u002F\u002F ②\n\n    return $order;\n}\n","php",[38],{"type":24,"tag":39,"props":40,"children":41},"code",{"__ignoreMap":7},[42,53,62,71,80,89,99,108,116,125,134,143,152,161,169,178],{"type":24,"tag":43,"props":44,"children":47},"span",{"class":45,"line":46},"line",1,[48],{"type":24,"tag":43,"props":49,"children":50},{},[51],{"type":29,"value":52},"public function createOrder(OrderDTO $dto): Order\n",{"type":24,"tag":43,"props":54,"children":56},{"class":45,"line":55},2,[57],{"type":24,"tag":43,"props":58,"children":59},{},[60],{"type":29,"value":61},"{\n",{"type":24,"tag":43,"props":63,"children":65},{"class":45,"line":64},3,[66],{"type":24,"tag":43,"props":67,"children":68},{},[69],{"type":29,"value":70},"    $order = new Order($dto);\n",{"type":24,"tag":43,"props":72,"children":74},{"class":45,"line":73},4,[75],{"type":24,"tag":43,"props":76,"children":77},{},[78],{"type":29,"value":79},"    $this->em->persist($order);\n",{"type":24,"tag":43,"props":81,"children":83},{"class":45,"line":82},5,[84],{"type":24,"tag":43,"props":85,"children":86},{},[87],{"type":29,"value":88},"    $this->em->flush();          \u002F\u002F ①\n",{"type":24,"tag":43,"props":90,"children":92},{"class":45,"line":91},6,[93],{"type":24,"tag":43,"props":94,"children":96},{"emptyLinePlaceholder":95},true,[97],{"type":29,"value":98},"\n",{"type":24,"tag":43,"props":100,"children":102},{"class":45,"line":101},7,[103],{"type":24,"tag":43,"props":104,"children":105},{},[106],{"type":29,"value":107},"    $event = new OutboxMessage(\n",{"type":24,"tag":43,"props":109,"children":110},{"class":45,"line":19},[111],{"type":24,"tag":43,"props":112,"children":113},{},[114],{"type":29,"value":115},"        type: 'order.created',\n",{"type":24,"tag":43,"props":117,"children":119},{"class":45,"line":118},9,[120],{"type":24,"tag":43,"props":121,"children":122},{},[123],{"type":29,"value":124},"        payload: json_encode($dto),\n",{"type":24,"tag":43,"props":126,"children":128},{"class":45,"line":127},10,[129],{"type":24,"tag":43,"props":130,"children":131},{},[132],{"type":29,"value":133},"        aggregateId: $order->getId(),\n",{"type":24,"tag":43,"props":135,"children":137},{"class":45,"line":136},11,[138],{"type":24,"tag":43,"props":139,"children":140},{},[141],{"type":29,"value":142},"    );\n",{"type":24,"tag":43,"props":144,"children":146},{"class":45,"line":145},12,[147],{"type":24,"tag":43,"props":148,"children":149},{},[150],{"type":29,"value":151},"    $this->em->persist($event);\n",{"type":24,"tag":43,"props":153,"children":155},{"class":45,"line":154},13,[156],{"type":24,"tag":43,"props":157,"children":158},{},[159],{"type":29,"value":160},"    $this->em->flush();          \u002F\u002F ②\n",{"type":24,"tag":43,"props":162,"children":164},{"class":45,"line":163},14,[165],{"type":24,"tag":43,"props":166,"children":167},{"emptyLinePlaceholder":95},[168],{"type":29,"value":98},{"type":24,"tag":43,"props":170,"children":172},{"class":45,"line":171},15,[173],{"type":24,"tag":43,"props":174,"children":175},{},[176],{"type":29,"value":177},"    return $order;\n",{"type":24,"tag":43,"props":179,"children":181},{"class":45,"line":180},16,[182],{"type":24,"tag":43,"props":183,"children":184},{},[185],{"type":29,"value":186},"}\n",{"type":24,"tag":25,"props":188,"children":189},{},[190,192,198,200,205],{"type":29,"value":191},"Intencja jest dobra: zapisz zamówienie i dołóż wpis do outboxa, żeby osobny proces\nopublikował event. Problem jest w tych dwóch ",{"type":24,"tag":39,"props":193,"children":195},{"className":194},[],[196],{"type":29,"value":197},"flush()",{"type":29,"value":199},". Żeby go zobaczyć, trzeba wiedzieć,\nco ",{"type":24,"tag":39,"props":201,"children":203},{"className":202},[],[204],{"type":29,"value":197},{"type":29,"value":206}," naprawdę robi pod spodem.",{"type":24,"tag":208,"props":209,"children":211},"h2",{"id":210},"co-robi-persist-a-co-flush",[212,214,220,222],{"type":29,"value":213},"Co robi ",{"type":24,"tag":39,"props":215,"children":217},{"className":216},[],[218],{"type":29,"value":219},"persist()",{"type":29,"value":221},", a co ",{"type":24,"tag":39,"props":223,"children":225},{"className":224},[],[226],{"type":29,"value":197},{"type":24,"tag":25,"props":228,"children":229},{},[230],{"type":29,"value":231},"W Doctrine to są dwie różne rzeczy:",{"type":24,"tag":233,"props":234,"children":235},"ul",{},[236,260],{"type":24,"tag":237,"props":238,"children":239},"li",{},[240,250,252,258],{"type":24,"tag":241,"props":242,"children":243},"strong",{},[244],{"type":24,"tag":39,"props":245,"children":247},{"className":246},[],[248],{"type":29,"value":249},"persist($entity)",{"type":29,"value":251}," niczego nie zapisuje. Mówi tylko UnitOfWork: „weź tę encję pod\nzarząd, przy najbliższym flushu zaplanuj jej INSERT\". Żadnego INSERT-u. (Jedyny wyjątek:\nprzy generatorze ",{"type":24,"tag":39,"props":253,"children":255},{"className":254},[],[256],{"type":29,"value":257},"SEQUENCE",{"type":29,"value":259}," persist pobiera z bazy następny numer sekwencji — o tym niżej —\nale samej encji i tak nie zapisuje.)",{"type":24,"tag":237,"props":261,"children":262},{},[263,271,273,278,280,285,287,292,294,300],{"type":24,"tag":241,"props":264,"children":265},{},[266],{"type":24,"tag":39,"props":267,"children":269},{"className":268},[],[270],{"type":29,"value":197},{"type":29,"value":272}," liczy change-set wszystkich zarządzanych encji i ",{"type":24,"tag":241,"props":274,"children":275},{},[276],{"type":29,"value":277},"wykonuje SQL",{"type":29,"value":279},". I tu\nkluczowy fakt: jeśli nie ma jawnie otwartej transakcji, ",{"type":24,"tag":39,"props":281,"children":283},{"className":282},[],[284],{"type":29,"value":197},{"type":29,"value":286}," ",{"type":24,"tag":241,"props":288,"children":289},{},[290],{"type":29,"value":291},"sam ją otwiera,\nwykonuje zapisy i commituje",{"type":29,"value":293},". Jeden flush = ",{"type":24,"tag":39,"props":295,"children":297},{"className":296},[],[298],{"type":29,"value":299},"BEGIN … COMMIT",{"type":29,"value":301}," wokół całej paczki zmian.",{"type":24,"tag":25,"props":303,"children":304},{},[305,307,312],{"type":29,"value":306},"Doctrine robi to celowo — pojedynczy flush jest atomowy: albo wszystkie zaplanowane zmiany\nwejdą razem, albo (przy wyjątku) całość się wycofa. To dobra własność. Problem pojawia się,\ngdy flushujesz ",{"type":24,"tag":241,"props":308,"children":309},{},[310],{"type":29,"value":311},"dwa razy",{"type":29,"value":313},".",{"type":24,"tag":208,"props":315,"children":317},{"id":316},"dwa-flushe-dwie-transakcje",[318],{"type":29,"value":319},"Dwa flushe = dwie transakcje",{"type":24,"tag":25,"props":321,"children":322},{},[323,325,330],{"type":29,"value":324},"Skoro każdy flush bez otwartej transakcji sam się commituje, powyższy kod to nie jedna\ntransakcja — to ",{"type":24,"tag":241,"props":326,"children":327},{},[328],{"type":29,"value":329},"dwie",{"type":29,"value":331},":",{"type":24,"tag":32,"props":333,"children":336},{"className":334,"code":335,"language":29,"meta":7,"style":7},"language-text shiki shiki-themes github-dark","flush ① → BEGIN, INSERT orders,  COMMIT\nflush ② → BEGIN, INSERT outbox,  COMMIT\n",[337],{"type":24,"tag":39,"props":338,"children":339},{"__ignoreMap":7},[340,348],{"type":24,"tag":43,"props":341,"children":342},{"class":45,"line":46},[343],{"type":24,"tag":43,"props":344,"children":345},{},[346],{"type":29,"value":347},"flush ① → BEGIN, INSERT orders,  COMMIT\n",{"type":24,"tag":43,"props":349,"children":350},{"class":45,"line":55},[351],{"type":24,"tag":43,"props":352,"children":353},{},[354],{"type":29,"value":355},"flush ② → BEGIN, INSERT outbox,  COMMIT\n",{"type":24,"tag":25,"props":357,"children":358},{},[359,361,367,369,374],{"type":29,"value":360},"Między nimi jest okno. Jeśli cokolwiek padnie po ① a przed ② — wyjątek, zerwane połączenie,\n",{"type":24,"tag":39,"props":362,"children":364},{"className":363},[],[365],{"type":29,"value":366},"kill",{"type":29,"value":368}," procesu, timeout, deploy w złym momencie — masz ",{"type":24,"tag":241,"props":370,"children":371},{},[372],{"type":29,"value":373},"zamówienie w bazie i zero eventu\nw outboxie",{"type":29,"value":375},". Event zaginął bezpowrotnie: proces publikujący nigdy się o zamówieniu nie\ndowie, bo nie ma czego czytać.",{"type":24,"tag":25,"props":377,"children":378},{},[379,381,386,388,393],{"type":29,"value":380},"To jest dokładnie ",{"type":24,"tag":241,"props":382,"children":383},{},[384],{"type":29,"value":385},"dual-write problem",{"type":29,"value":387}," — dwa niezależne commity do jednego logicznego\nfaktu, z oknem awarii pomiędzy. Ironia jest bolesna: jedynym zadaniem Outboxa jest\n",{"type":24,"tag":241,"props":389,"children":390},{},[391],{"type":29,"value":392},"usunięcie",{"type":29,"value":394}," tego okna (event zapisany atomowo razem ze zmianą stanu). Ten kod wziął\nwzorzec, którego cała wartość to atomowość, i zaimplementował go tak, że tę atomowość\nzniszczył. Odtworzył problem wewnątrz mechanizmu, który miał go rozwiązać.",{"type":24,"tag":208,"props":396,"children":398},{"id":397},"dlaczego-jest-podstępny",[399],{"type":29,"value":400},"Dlaczego jest podstępny",{"type":24,"tag":25,"props":402,"children":403},{},[404,406,411],{"type":29,"value":405},"Bo to nie jest błąd, który „wywala\" test. Każdy flush z osobna jest poprawny składniowo,\nkod się kompiluje, w happy pathie zamówienie i event lądują w bazie. Defekt ujawnia się\n",{"type":24,"tag":241,"props":407,"children":408},{},[409],{"type":29,"value":410},"tylko przy awarii",{"type":29,"value":412}," — a te w happy-path code review nie występują. Dlatego przechodzi\nrecenzję u kogoś, kto patrzy, „czy działa\", zamiast „co się stanie, gdy padnie między\nlinijkami\".",{"type":24,"tag":208,"props":414,"children":416},{"id":415},"skąd-w-ogóle-dwa-flushe",[417],{"type":29,"value":418},"Skąd w ogóle dwa flushe",{"type":24,"tag":25,"props":420,"children":421},{},[422,424,430,432,437,439,445,447,453,455,461],{"type":29,"value":423},"Jest realny powód, nie głupota: jeśli ",{"type":24,"tag":39,"props":425,"children":427},{"className":426},[],[428],{"type":29,"value":429},"Order",{"type":29,"value":431}," ma ",{"type":24,"tag":241,"props":433,"children":434},{},[435],{"type":29,"value":436},"ID generowane przez bazę",{"type":29,"value":438},"\n(IDENTITY \u002F auto-increment), to ",{"type":24,"tag":39,"props":440,"children":442},{"className":441},[],[443],{"type":29,"value":444},"$order->getId()",{"type":29,"value":446}," jest ",{"type":24,"tag":39,"props":448,"children":450},{"className":449},[],[451],{"type":29,"value":452},"null",{"type":29,"value":454}," aż do pierwszego flusha.\nAutor potrzebuje ID do ",{"type":24,"tag":39,"props":456,"children":458},{"className":457},[],[459],{"type":29,"value":460},"aggregateId",{"type":29,"value":462}," eventu — więc flushuje, żeby baza nadała ID, i dopiero\npotem buduje event. Intencja zrozumiała, wykonanie rozbraja atomowość.",{"type":24,"tag":208,"props":464,"children":466},{"id":465},"jak-zrobić-to-dobrze",[467],{"type":29,"value":468},"Jak zrobić to dobrze",{"type":24,"tag":25,"props":470,"children":471},{},[472],{"type":29,"value":473},"Dwa czyste wyjścia:",{"type":24,"tag":25,"props":475,"children":476},{},[477,482,484,490,492,497],{"type":24,"tag":241,"props":478,"children":479},{},[480],{"type":29,"value":481},"1. Tożsamość nadana w agregacie (idiomatyczne DDD).",{"type":29,"value":483}," Niech ID to UUID generowane\nw konstruktorze, nie przez bazę. Wtedy ",{"type":24,"tag":39,"props":485,"children":487},{"className":486},[],[488],{"type":29,"value":489},"getId()",{"type":29,"value":491}," istnieje ",{"type":24,"tag":241,"props":493,"children":494},{},[495],{"type":29,"value":496},"przed",{"type":29,"value":498}," flushem:",{"type":24,"tag":32,"props":500,"children":502},{"className":34,"code":501,"language":36,"meta":7,"style":7},"$order = new Order($dto);            \u002F\u002F ID już jest (UUID w konstruktorze)\n$event = new OutboxMessage(type: 'order.created', payload: ..., aggregateId: $order->getId());\n\n$this->em->persist($order);\n$this->em->persist($event);\n$this->em->flush();                  \u002F\u002F jeden flush → jedna transakcja → atomowo\n",[503],{"type":24,"tag":39,"props":504,"children":505},{"__ignoreMap":7},[506,514,522,529,537,545],{"type":24,"tag":43,"props":507,"children":508},{"class":45,"line":46},[509],{"type":24,"tag":43,"props":510,"children":511},{},[512],{"type":29,"value":513},"$order = new Order($dto);            \u002F\u002F ID już jest (UUID w konstruktorze)\n",{"type":24,"tag":43,"props":515,"children":516},{"class":45,"line":55},[517],{"type":24,"tag":43,"props":518,"children":519},{},[520],{"type":29,"value":521},"$event = new OutboxMessage(type: 'order.created', payload: ..., aggregateId: $order->getId());\n",{"type":24,"tag":43,"props":523,"children":524},{"class":45,"line":64},[525],{"type":24,"tag":43,"props":526,"children":527},{"emptyLinePlaceholder":95},[528],{"type":29,"value":98},{"type":24,"tag":43,"props":530,"children":531},{"class":45,"line":73},[532],{"type":24,"tag":43,"props":533,"children":534},{},[535],{"type":29,"value":536},"$this->em->persist($order);\n",{"type":24,"tag":43,"props":538,"children":539},{"class":45,"line":82},[540],{"type":24,"tag":43,"props":541,"children":542},{},[543],{"type":29,"value":544},"$this->em->persist($event);\n",{"type":24,"tag":43,"props":546,"children":547},{"class":45,"line":91},[548],{"type":24,"tag":43,"props":549,"children":550},{},[551],{"type":29,"value":552},"$this->em->flush();                  \u002F\u002F jeden flush → jedna transakcja → atomowo\n",{"type":24,"tag":25,"props":554,"children":555},{},[556],{"type":29,"value":557},"Agregat sam generuje swoją tożsamość — bonus: encja jest kompletna, zanim dotknie bazy.",{"type":24,"tag":25,"props":559,"children":560},{},[561,566],{"type":24,"tag":241,"props":562,"children":563},{},[564],{"type":29,"value":565},"2. Jawna transakcja.",{"type":29,"value":567}," Jeśli musisz zostać przy ID z bazy, owiń całość:",{"type":24,"tag":32,"props":569,"children":571},{"className":34,"code":570,"language":36,"meta":7,"style":7},"return $this->em->wrapInTransaction(function () use ($dto) {\n    $order = new Order($dto);\n    $this->em->persist($order);\n    $this->em->flush();              \u002F\u002F wewnątrz otwartej transakcji NIE commituje\n\n    $event = new OutboxMessage(..., aggregateId: $order->getId());\n    $this->em->persist($event);\n    $this->em->flush();              \u002F\u002F nadal ta sama transakcja\n\n    return $order;                   \u002F\u002F wrapInTransaction przekazuje wartość callable'a\n});                                  \u002F\u002F COMMIT dopiero tutaj — oba INSERTy albo żaden\n",[572],{"type":24,"tag":39,"props":573,"children":574},{"__ignoreMap":7},[575,583,590,597,605,612,620,627,635,642,650],{"type":24,"tag":43,"props":576,"children":577},{"class":45,"line":46},[578],{"type":24,"tag":43,"props":579,"children":580},{},[581],{"type":29,"value":582},"return $this->em->wrapInTransaction(function () use ($dto) {\n",{"type":24,"tag":43,"props":584,"children":585},{"class":45,"line":55},[586],{"type":24,"tag":43,"props":587,"children":588},{},[589],{"type":29,"value":70},{"type":24,"tag":43,"props":591,"children":592},{"class":45,"line":64},[593],{"type":24,"tag":43,"props":594,"children":595},{},[596],{"type":29,"value":79},{"type":24,"tag":43,"props":598,"children":599},{"class":45,"line":73},[600],{"type":24,"tag":43,"props":601,"children":602},{},[603],{"type":29,"value":604},"    $this->em->flush();              \u002F\u002F wewnątrz otwartej transakcji NIE commituje\n",{"type":24,"tag":43,"props":606,"children":607},{"class":45,"line":82},[608],{"type":24,"tag":43,"props":609,"children":610},{"emptyLinePlaceholder":95},[611],{"type":29,"value":98},{"type":24,"tag":43,"props":613,"children":614},{"class":45,"line":91},[615],{"type":24,"tag":43,"props":616,"children":617},{},[618],{"type":29,"value":619},"    $event = new OutboxMessage(..., aggregateId: $order->getId());\n",{"type":24,"tag":43,"props":621,"children":622},{"class":45,"line":101},[623],{"type":24,"tag":43,"props":624,"children":625},{},[626],{"type":29,"value":151},{"type":24,"tag":43,"props":628,"children":629},{"class":45,"line":19},[630],{"type":24,"tag":43,"props":631,"children":632},{},[633],{"type":29,"value":634},"    $this->em->flush();              \u002F\u002F nadal ta sama transakcja\n",{"type":24,"tag":43,"props":636,"children":637},{"class":45,"line":118},[638],{"type":24,"tag":43,"props":639,"children":640},{"emptyLinePlaceholder":95},[641],{"type":29,"value":98},{"type":24,"tag":43,"props":643,"children":644},{"class":45,"line":127},[645],{"type":24,"tag":43,"props":646,"children":647},{},[648],{"type":29,"value":649},"    return $order;                   \u002F\u002F wrapInTransaction przekazuje wartość callable'a\n",{"type":24,"tag":43,"props":651,"children":652},{"class":45,"line":136},[653],{"type":24,"tag":43,"props":654,"children":655},{},[656],{"type":29,"value":657},"});                                  \u002F\u002F COMMIT dopiero tutaj — oba INSERTy albo żaden\n",{"type":24,"tag":25,"props":659,"children":660},{},[661,663,668,669,674,676,682],{"type":29,"value":662},"Kluczowy niuans: wewnątrz jawnie otwartej transakcji ",{"type":24,"tag":39,"props":664,"children":666},{"className":665},[],[667],{"type":29,"value":197},{"type":29,"value":286},{"type":24,"tag":241,"props":670,"children":671},{},[672],{"type":29,"value":673},"nie commituje",{"type":29,"value":675}," — commit\njest odroczony do końca wrappera. Wtedy nawet podwójny flush jest w porządku i atomowy.\nGdyby autor powiedział „dwa flushe są celowe, bo potrzebuję ID, ale owinąłem w\n",{"type":24,"tag":39,"props":677,"children":679},{"className":678},[],[680],{"type":29,"value":681},"wrapInTransaction",{"type":29,"value":683},"\" — miałby rację. Bez tego owinięcia „każdy flush jest ok\" myli\npoprawność pojedynczego flusha z atomowością całości.",{"type":24,"tag":208,"props":685,"children":687},{"id":686},"ile-to-kosztuje-bo-to-też-koszt-wydajności",[688],{"type":29,"value":689},"Ile to kosztuje — bo to też koszt wydajności",{"type":24,"tag":25,"props":691,"children":692},{},[693,695,700,702,707,709,714,716,722],{"type":29,"value":694},"Podwójny flush to nie tylko utracona atomowość, to również ",{"type":24,"tag":241,"props":696,"children":697},{},[698],{"type":29,"value":699},"wolniej",{"type":29,"value":701},". Najdroższą częścią\nzapisu nie jest sam INSERT, tylko ",{"type":24,"tag":241,"props":703,"children":704},{},[705],{"type":29,"value":706},"COMMIT",{"type":29,"value":708}," — to on wymusza trwałość: fsync WAL-a na dysk,\na przy replikacji synchronicznej czekanie na potwierdzenie repliki. Dwa flushe to ",{"type":24,"tag":241,"props":710,"children":711},{},[712],{"type":29,"value":713},"dwa\ncommity",{"type":29,"value":715}," — podwajasz najkosztowniejszy element operacji, do tego dwa razy ",{"type":24,"tag":39,"props":717,"children":719},{"className":718},[],[720],{"type":29,"value":721},"BEGIN\u002FCOMMIT",{"type":29,"value":723},"\nround-trip do bazy i więcej pracy z blokadami.",{"type":24,"tag":25,"props":725,"children":726},{},[727,729,734],{"type":29,"value":728},"Batchowanie wszystkich zmian w jeden flush to jedna transakcja — jeden commit. To jest ten\nrzadki przypadek, gdzie poprawność i wydajność ciągną w tę samą stronę: ",{"type":24,"tag":241,"props":730,"children":731},{},[732],{"type":29,"value":733},"jeden flush jest\ni bardziej atomowy, i szybszy",{"type":29,"value":735},". Wielokrotne flushowanie „bo tak wygodniej\" fragmentuje\npracę na wiele transakcji — płacisz commitami i tracisz gwarancję.",{"type":24,"tag":208,"props":737,"children":739},{"id":738},"a-jeśli-handler-jest-już-w-transakcji-np-messenger",[740],{"type":29,"value":741},"A jeśli handler jest już w transakcji (np. Messenger)?",{"type":24,"tag":25,"props":743,"children":744},{},[745,747,753,755,761,763,768],{"type":29,"value":746},"Częsty przypadek w Symfony: ",{"type":24,"tag":39,"props":748,"children":750},{"className":749},[],[751],{"type":29,"value":752},"DoctrineTransactionMiddleware",{"type":29,"value":754}," (messenger ",{"type":24,"tag":39,"props":756,"children":758},{"className":757},[],[759],{"type":29,"value":760},"doctrine_transaction",{"type":29,"value":762},")\nowija cały handler w jedną transakcję. Wtedy oba flushe wpadają w nią, commit jest jeden i\n",{"type":24,"tag":241,"props":764,"children":765},{},[766],{"type":29,"value":767},"bug utraty eventu nie gryzie",{"type":29,"value":769}," — wygląda, jakby problem znikał sam. Nie znika: przesuwa się\nz „utracony event\" na „źle dobrana tożsamość\".",{"type":24,"tag":25,"props":771,"children":772},{},[773,775,780,782,794,796,802,804,810,812,817,819,825,827,832,834,840,841,847,849,853],{"type":29,"value":774},"Bo skoro handler ma być jedną transakcją, to idiomatycznie ma też ",{"type":24,"tag":241,"props":776,"children":777},{},[778],{"type":29,"value":779},"jeden flush",{"type":29,"value":781},". A wtedy\n",{"type":24,"tag":241,"props":783,"children":784},{},[785,787,792],{"type":29,"value":786},"nie możesz oprzeć ",{"type":24,"tag":39,"props":788,"children":790},{"className":789},[],[791],{"type":29,"value":460},{"type":29,"value":793}," na autoincrementcie z bazy",{"type":29,"value":795},": ID z ",{"type":24,"tag":39,"props":797,"children":799},{"className":798},[],[800],{"type":29,"value":801},"IDENTITY",{"type":29,"value":803}," powstaje\ndopiero przy INSERT, czyli dopiero w flushu — więc nie masz go w chwili, gdy budujesz\n",{"type":24,"tag":39,"props":805,"children":807},{"className":806},[],[808],{"type":29,"value":809},"OutboxMessage",{"type":29,"value":811},". A event niesie skalarny snapshot (",{"type":24,"tag":39,"props":813,"children":815},{"className":814},[],[816],{"type":29,"value":460},{"type":29,"value":818}," + ",{"type":24,"tag":39,"props":820,"children":822},{"className":821},[],[823],{"type":29,"value":824},"payload",{"type":29,"value":826},"), nie żywą relację\ndo agregatu, więc nie „doczyta\" sobie ID przy zapisie. Krótko: ",{"type":24,"tag":241,"props":828,"children":829},{},[830],{"type":29,"value":831},"z autoincrementu nie zbudujesz\neventu w tej samej, jednej jednostce pracy",{"type":29,"value":833}," — bez brzydkich hacków w listenerach\n(",{"type":24,"tag":39,"props":835,"children":837},{"className":836},[],[838],{"type":29,"value":839},"postPersist",{"type":29,"value":818},{"type":24,"tag":39,"props":842,"children":844},{"className":843},[],[845],{"type":29,"value":846},"recomputeSingleEntityChangeSet",{"type":29,"value":848},"), a to już nie jest kod, który chcesz\nutrzymywać. Middleware daje ci atomowość za darmo — ale\ndokładnie dlatego wymusza, żeby tożsamość istniała ",{"type":24,"tag":241,"props":850,"children":851},{},[852],{"type":29,"value":496},{"type":29,"value":854}," bazą.",{"type":24,"tag":25,"props":856,"children":857},{},[858],{"type":29,"value":859},"Zostają dwie drogi i obie znaczą to samo:",{"type":24,"tag":233,"props":861,"children":862},{},[863,873],{"type":24,"tag":237,"props":864,"children":865},{},[866,871],{"type":24,"tag":241,"props":867,"children":868},{},[869],{"type":29,"value":870},"UUID (najlepiej v7",{"type":29,"value":872}," — czasowo-uporządkowany, bez rozjazdu locality indeksu jak v4)\ngenerowany w konstruktorze agregatu,",{"type":24,"tag":237,"props":874,"children":875},{},[876,878,886,888,894,896,901,903,908],{"type":29,"value":877},"albo ",{"type":24,"tag":241,"props":879,"children":880},{},[881],{"type":24,"tag":39,"props":882,"children":884},{"className":883},[],[885],{"type":29,"value":257},{"type":29,"value":887}," na Postgresie — Doctrine pobiera ",{"type":24,"tag":39,"props":889,"children":891},{"className":890},[],[892],{"type":29,"value":893},"nextval",{"type":29,"value":895}," już przy ",{"type":24,"tag":39,"props":897,"children":899},{"className":898},[],[900],{"type":29,"value":219},{"type":29,"value":902},", bez\nINSERT-u, więc ",{"type":24,"tag":39,"props":904,"children":906},{"className":905},[],[907],{"type":29,"value":489},{"type":29,"value":909}," działa przed flushem.",{"type":24,"tag":25,"props":911,"children":912},{},[913,915,920,922,928],{"type":29,"value":914},"Autoincrement wyklucza się tu sam. A gdybyś mimo to został przy nim i pośrednim flushu — poza\nzłamaniem idiomu płacisz ",{"type":24,"tag":241,"props":916,"children":917},{},[918],{"type":29,"value":919},"kosztem wydajności innego rodzaju",{"type":29,"value":921}," niż podwójny commit powyżej:\nINSERT ",{"type":24,"tag":39,"props":923,"children":925},{"className":924},[],[926],{"type":29,"value":927},"orders",{"type":29,"value":929}," wykonuje się wcześnie i trzyma blokady na wierszu przez całą resztę handlera,\na Unit of Work rozpada się na dwie fazy zamiast jednej paczki. Poprawność masz z middleware,\nale tej ceny on nie zdejmuje.",{"type":24,"tag":208,"props":931,"children":933},{"id":932},"dlaczego-to-sedno-outbox-pattern",[934],{"type":29,"value":935},"Dlaczego to sedno Outbox pattern",{"type":24,"tag":25,"props":937,"children":938},{},[939,941,946],{"type":29,"value":940},"Cała wartość Outboxa mieści się w jednym zdaniu: ",{"type":24,"tag":241,"props":942,"children":943},{},[944],{"type":29,"value":945},"event i zmiana stanu są w tej samej\ntransakcji",{"type":29,"value":947},", więc commitują się albo razem, albo wcale. To ta atomowość sprawia, że\npublikacja zdarzeń jest niezawodna — nie ma zgubionych eventów (commit bez eventu) ani\neventów-widm (event bez commitu).",{"type":24,"tag":25,"props":949,"children":950},{},[951,953,958,960,965],{"type":29,"value":952},"W momencie, w którym rozbijasz to na dwa flushe \u002F dwie transakcje, wracasz do dual-write —\ndo dokładnie tej awarii, dla której wzorzec istnieje. Dlatego znajomość samej nazwy „Outbox\"\nnie wystarcza: trzeba rozumieć, że jego gwarancja ",{"type":24,"tag":241,"props":954,"children":955},{},[956],{"type":29,"value":957},"żyje w słowie „jedna transakcja\"",{"type":29,"value":959},".\n",{"type":24,"tag":39,"props":961,"children":963},{"className":962},[],[964],{"type":29,"value":197},{"type":29,"value":966}," to nie jest „zapisz\" — to, bez jawnie otwartej transakcji, jej granica. Kto tego\nnie widzi, ten napisze Outbox, który niczego nie gwarantuje.",{"type":24,"tag":25,"props":968,"children":969},{},[970,972,978,980,985],{"type":29,"value":971},"Reszta to już tylko konsekwencje tej jednej transakcji: osobny proces czyta outbox i publikuje\n",{"type":24,"tag":973,"props":974,"children":975},"em",{},[976],{"type":29,"value":977},"at-least-once",{"type":29,"value":979}," (po restarcie może wysłać duplikat), więc konsument musi być ",{"type":24,"tag":241,"props":981,"children":982},{},[983],{"type":29,"value":984},"idempotentny",{"type":29,"value":986},"\n— ale to osobny temat. Fundamentem jest atomowy zapis. Bez niego cały wzorzec jest\ndekoracją.",{"type":24,"tag":988,"props":989,"children":990},"style",{},[991],{"type":29,"value":992},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":55,"depth":55,"links":994},[995,997,998,999,1000,1001,1002,1003],{"id":210,"depth":55,"text":996},"Co robi persist(), a co flush()",{"id":316,"depth":55,"text":319},{"id":397,"depth":55,"text":400},{"id":415,"depth":55,"text":418},{"id":465,"depth":55,"text":468},{"id":686,"depth":55,"text":689},{"id":738,"depth":55,"text":741},{"id":932,"depth":55,"text":935},"markdown","content:blog:dwa-flushe-dwie-transakcje-outbox-doctrine.md","content","blog\u002Fdwa-flushe-dwie-transakcje-outbox-doctrine.md","blog\u002Fdwa-flushe-dwie-transakcje-outbox-doctrine","md",1783499642977]