DBA: په وړتیا سره همغږي او واردات تنظیم کړئ

د لوی ډیټا سیټونو پیچلي پروسس کولو لپاره (مختلف د ETL پروسې: واردول، تبادلې او د یوې بهرنۍ سرچینې سره همغږي کول) ډیری وختونه اړتیا وي په لنډمهاله توګه "یاد" او سمدلاسه په چټکۍ سره پروسس کړئ یو څه لوی.

د دې ډول یوه عادي دنده معمولا داسې ښکاري: "همدلته د محاسبې څانګه د پیرودونکي بانک څخه پورته شوې وروستي ترلاسه شوي تادیات، تاسو اړتیا لرئ ژر تر ژره ویب پاڼې ته پورته کړئ او د خپلو حسابونو سره یې وصل کړئ"

مګر کله چې د دې "یو څه" حجم په سلګونو میګابایټ کې اندازه کول پیل شي ، او خدمت باید د ډیټابیس 24x7 سره کار کولو ته دوام ورکړي ، ډیری اړخیزې اغیزې رامینځته کیږي چې ستاسو ژوند به خرابوي.
DBA: په وړتیا سره همغږي او واردات تنظیم کړئ
په PostgreSQL کې د دوی سره معامله کولو لپاره (او نه یوازې پدې کې) ، تاسو کولی شئ ځینې اصلاحونه وکاروئ چې تاسو ته به اجازه درکړي هرڅه ګړندي او د سرچینو لږ مصرف سره پروسس کړئ.

1. چیرته لیږدول کیږي؟

لومړی، راځئ چې پریکړه وکړو چې چیرته موږ کولی شو هغه ډاټا اپلوډ کړو چې موږ یې "پروسس" غواړو.

۱.۱. لنډمهاله جدول (موقتي جدول)

په اصولو کې، د PostgreSQL لنډمهاله جدولونه د نورو په څیر دي. له همدې امله، توهین خوښوي "هلته هر څه یوازې په حافظه کې زیرمه شوي ، او دا پای ته رسیدلی شي". مګر یو شمیر مهم توپیرونه هم شتون لري.

د ډیټابیس سره د هرې اړیکې لپاره ستاسو خپل "نوم ځای"

که دوه اړیکې په ورته وخت کې د نښلولو هڅه وکړئ CREATE TABLE x، بیا به یو څوک خامخا ترلاسه کړي د بې ساري تېروتنه ډیټابیس توکي.

خو که دواړه د عملي کولو هڅه وکړي CREATE TEMPORARY TABLE xنو دواړه به دا په نورمال ډول ترسره کړي، او هرڅوک به ترلاسه کړي ستاسو کاپي میزونه او د دوی تر مینځ به هیڅ شی مشترک نه وي.

"ځان ویجاړول" کله چې منحل کیږي

کله چې پیوستون وتړل شي، ټول لنډمهاله میزونه په اتوماتيک ډول حذف کیږي، نو په لاسي ډول DROP TABLE x هیڅ معنی نلري پرته ...

که تاسو له لارې کار کوئ pgbouncer د راکړې ورکړې په حالت کې، بیا ډیټابیس باور ته دوام ورکوي چې دا اړیکه لاهم فعاله ده ، او پدې کې دا لنډمهاله جدول لاهم شتون لري.

له همدې امله، د بیا جوړولو هڅه کول، د پی جی باونسر ته د مختلف ارتباط څخه، د تېروتنې پایله به وي. مګر دا د کارولو له لارې مخنیوی کیدی شي CREATE TEMPORARY TABLE IF NOT EXISTS x.

ریښتیا ، دا غوره ده چې دا په هرصورت ونه کړئ ، ځکه چې بیا تاسو کولی شئ هلته "ناڅاپه" د "مخکیني مالک" څخه پاتې ډیټا ومومئ. پرځای یې ، دا خورا ښه دی چې لارښود ولولئ او وګورئ چې د میز رامینځته کولو پرمهال دا اضافه کول ممکن دي ON COMMIT DROP - دا دی، کله چې معامله بشپړه شي، جدول به په اوتومات ډول حذف شي.

غیر نقل کول

ځکه چې دوی یوازې په یو ځانګړي پیوستون پورې اړه لري، لنډمهاله میزونه نقل شوي ندي. خو دا د معلوماتو دوه ځله ثبتولو اړتیا له مینځه وړي په هپ + وال کې، نو په دې کې INSERT/UPDATE/DELETE د پام وړ چټک دی.

مګر څرنګه چې یو لنډمهاله میز لاهم یو "نږدې عادي" میز دی، دا په نقل کې هم نشي رامینځته کیدی. لږترلږه د اوس لپاره ، که څه هم اړوند پیچ ​​د اوږدې مودې لپاره جریان لري.

1.2. نه لګیدلی جدول

مګر تاسو باید څه وکړئ، د بیلګې په توګه، که تاسو یو ډول پیچلي ETL پروسه لرئ چې نشي کولی په یوه معامله کې پلي شي، مګر تاسو لاهم لرئ pgbouncer د راکړې ورکړې په حالت کې؟ ..

یا د معلوماتو جریان دومره لوی دی چې په یوه اړیکه کې کافي بینډ ویت شتون نلري د ډیټابیس څخه (لوستل، په هر CPU کې یوه پروسه)؟

یا ځینې عملیات روان دي په متناسب ډول په مختلفو اړیکو کې؟ ..

دلته یوازې یو اختیار شتون لري - په لنډمهاله توګه یو غیر لنډمهاله میز جوړ کړئ. پن، هو. هغه دی:

  • "زما خپل" جدولونه په اعظمي ډول تصادفي نومونو سره رامینځته کړي ترڅو د هیچا سره متقابل نشي
  • استخراجول: دوی د یوې بهرنۍ سرچینې څخه ډاټا ډک کړي
  • بدلول: بدل شوی، د کلیدي لینک کولو ساحو کې ډک شوی
  • بار: چمتو شوي معلومات په نښه شوي میزونو کې واچول
  • "زما" میزونه حذف کړل

او اوس - په مرهم کې مچۍ. په حقیقت کی، په PostgreSQL کې ټول لیکونه دوه ځله پیښیږي - لومړی په WAL کې، بیا په جدول / شاخصونو کې. دا ټول د ACID مالتړ لپاره ترسره کیږي او تر منځ د معلوماتو لید سم کړي COMMITمغز او ROLLBACK'ناسمه راکړه ورکړه.

مګر موږ دې ته اړتیا نلرو! موږ ټوله پروسه لرو یا دا په بشپړ ډول بریالی و یا دا نه و.. دا مهمه نده چې څومره منځمهاله لیږدونه به وي - موږ د "مینځ څخه پروسې ته دوام ورکولو" سره علاقه نه لرو، په ځانګړې توګه کله چې دا روښانه نده چې دا چیرته وه.

د دې کولو لپاره، د PostgreSQL پراختیا کونکي، بیرته په 9.1 نسخه کې، داسې شی معرفي کړ لکه څنګه چې نالګ شوي میزونه:

د دې اشارې سره، جدول د غیر تړل شوي په توګه رامینځته کیږي. هغه معلومات چې په غیر ثبت شوي جدولونو کې لیکل شوي د لیکلو دمخه لاګ نه تیریږي (29 څپرکی وګورئ) ، د دې لامل کیږي چې دا ډول میزونه د معمول په پرتله ډیر ګړندی کار وکړئ. په هرصورت، دوی د ناکامۍ څخه خوندي ندي؛ د سرور د ناکامۍ یا اضطراري بندیدو په صورت کې، یو غیر تړل شوی میز په اتوماتيک ډول پرې شوی. سربیره پردې، د unlogged جدول منځپانګه نه نقل شوی د غلام سرورونو ته. هر هغه شاخصونه چې په غیر ننوتل شوي میز کې رامینځته شوي په اوتومات ډول غیر ننوتل کیږي.

په لنډه توګه، دا به ډیر چټک وي، مګر که چیرې د ډیټابیس سرور "راشي" نو دا به ناخوښه وي. مګر دا څو ځله پیښیږي ، او ایا ستاسو د ETL پروسه پوهیږي چې دا د ډیټابیس د "بیا ژوندي کولو" وروسته "له مینځ څخه" سم سم کړي؟ ...

که نه، او پورته قضیه ستاسو سره ورته ده، وکاروئ UNLOGGEDمګر هیڅکله دا خاصیت په ریښتیني میزونو کې مه فعالوئ، هغه معلومات چې تاسو ته ګران دي.

1.3. په ژمنتیا { قطارونه حذف کړئ | غورځول}

دا جوړښت تاسو ته اجازه درکوي اتوماتیک چلند مشخص کړئ کله چې د میز رامینځته کولو پرمهال معامله بشپړه شي.

په ON COMMIT DROP ما دمخه پورته لیکلي ، دا رامینځته کوي DROP TABLE، مګر سره ON COMMIT DELETE ROWS وضعیت ډیر په زړه پوری دی - دا دلته رامینځته شوی TRUNCATE TABLE.

څرنګه چې د لنډمهاله جدول د میټا توضیحاتو ذخیره کولو لپاره ټول زیربنا په بشپړ ډول د منظم میز سره ورته ده، نو بیا د موقتي جدولونو دوامداره جوړول او حذف کول د سیسټم جدولونو د شدید "پړسوب" لامل کیږي pg_class, pg_attribute, pg_attrdef, pg_depend,…

اوس تصور وکړئ چې تاسو د ډیټابیس سره په مستقیم اړیکه کې یو کارګر لرئ، کوم چې په هره ثانیه کې یو نوی لیږد پرانیزي، یو لنډمهاله جدول رامینځته کوي، ډکوي، پروسس کوي او حذف کوي ... د سیسټم میزونو کې به د کثافاتو اضافه وي، او دا به د هر عملیات لپاره اضافي بریکونه رامینځته کړي.

په عموم کې، دا مه کوئ! په دې حالت کې، دا به ډیر اغیزمن وي CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS دا د راکړې ورکړې دورې څخه وباسئ - بیا د هرې نوې معاملې په پیل کې میزونه دمخه وي شتون به ولري (یو تلیفون خوندي کړئ CREATE) ، مګر خالي به وي، مننه TRUNCATE (موږ د هغې زنګ هم خوندي کړ) کله چې پخوانۍ معامله بشپړه شوه.

1.4. لکه ... په شمول ...

ما په پیل کې یادونه وکړه چې د لنډمهاله میزونو لپاره د کارولو معمول قضیې مختلف ډوله واردات دي - او پراختیا کونکی په ستړیا سره د هدف میز د ساحو لیست د هغه لنډمهاله اعلامیې کې کاپي کوي ...

خو سستي د پرمختګ انجن دی! د همدې لپاره یو نوی میز جوړ کړئ "د نمونې پر بنسټ" دا خورا ساده کیدی شي:

CREATE TEMPORARY TABLE import_table(
  LIKE target_table
);

له هغه ځایه چې تاسو کولی شئ پدې جدول کې ډیری ډیټا تولید کړئ ، نو د دې له لارې لټون به هیڅکله ګړندی نه وي. مګر د دې لپاره دودیز حل شتون لري - شاخصونه! او، هو، یو لنډمهاله جدول هم شاخصونه لري.

څرنګه چې، ډیری وختونه، اړین شاخصونه د هدف جدول شاخصونو سره سمون لري، تاسو کولی شئ په ساده ډول ولیکئ LIKE target_table INCLUDING INDEXES.

که تاسو هم اړتیا لرئ DEFAULT- ارزښتونه (د مثال په توګه، د لومړني کلیدي ارزښتونو ډکولو لپاره)، تاسو کولی شئ وکاروئ LIKE target_table INCLUDING DEFAULTS. یا په ساده ډول - LIKE target_table INCLUDING ALL - کاپي ډیفالټ، شاخصونه، خنډونه، ...

مګر دلته تاسو اړتیا لرئ پوه شئ چې که تاسو جوړ کړی د شاخصونو سره سمدلاسه جدول وارد کړئ ، بیا به ډیټا ډیر وخت ونیسيکه تاسو لومړی هرڅه ډک کړئ، او یوازې بیا شاخصونه پورته کړئ - وګورئ چې دا څنګه د مثال په توګه ترسره کوي pg_dump.

په عموم کې RTFM!

2. څنګه لیکل؟

اجازه راکړئ یوازې ووایم - دا وکاروئ COPY- د "پیک" پرځای جریان INSERT, په وخت کې سرعت. تاسو حتی کولی شئ مستقیم له مخکې جوړ شوي فایل څخه.

3. څنګه پروسس کول؟

نو، راځئ چې زموږ سریزه داسې یو څه وګورو:

  • تاسو په خپل ډیټابیس کې د پیرودونکي ډیټا سره یو میز لرئ 1M ریکارډونه
  • هره ورځ یو پیرودونکی تاسو ته یو نوی لیږئ بشپړ "انځور"
  • د تجربې څخه تاسو پوهیږئ چې وخت په وخت له 10K څخه ډیر ریکارډونه نه بدلیږي

د دې ډول حالت یو کلاسیک مثال دی د KLADR اډه - په ټولیز ډول ډیری پتې شتون لري، مګر په هره اونۍ کې په اپلوډ کې خورا لږ بدلونونه شتون لري (د استوګنې نوم بدلول، د سړکونو یوځای کول، د نویو کورونو بڼه) حتی په ملي کچه.

3.1. د بشپړ ترکیب الګوریتم

د سادګۍ لپاره، راځئ چې ووایو چې تاسو حتی د معلوماتو بیا تنظیم کولو ته اړتیا نلرئ - یوازې میز په مطلوب شکل کې راوړو، دا دی:

  • لرې کول هرڅه چې نور شتون نلري
  • اوسمهالول هرڅه چې دمخه شتون لري او نوي کولو ته اړتیا لري
  • دننه کړئ هرڅه چې تر اوسه ندي شوي

ولې باید عملیات په دې ترتیب ترسره شي؟ ځکه چې دا څنګه د میز اندازه به لږترلږه وده وکړي (MVCC په یاد ولرئ!).

له dst څخه حذف کړئ

نه، البته تاسو کولی شئ یوازې د دوو عملیاتو سره ترلاسه کړئ:

  • لرې کول (DELETE) هرڅه په عمومي توګه
  • دننه کړئ ټول د نوي عکس څخه

مګر په ورته وخت کې، د MVCC څخه مننه، د میز اندازه به دقیقا دوه ځله زیاته شي! د 1K تازه کولو له امله په جدول کې د ریکارډونو +10M عکسونه ترلاسه کول خورا بې ځایه دي ...

TRUNCATE dst

یو ډیر تجربه لرونکی پرمخ وړونکی پوهیږي چې ټول ټابلیټ په خورا ارزانه توګه پاک کیدی شي:

  • پاک (TRUNCATE) ټول میز
  • دننه کړئ ټول د نوي عکس څخه

طریقه اغیزمنه ده، ځینې ​​​​وختونه خورا د تطبیق وړ، مګر یوه ستونزه شتون لري ... موږ به د اوږدې مودې لپاره د 1M ریکارډونه اضافه کړو، نو موږ نشو کولی د دې ټول وخت لپاره میز خالي پریږدو (لکه څنګه چې په یوه معامله کې د لپاس کولو پرته پیښیږي).

یعنې:

  • موږ پیل کوو اوږدمهاله معامله
  • TRUNCATE لګوي Exclusive- بندول
  • موږ د اوږدې مودې لپاره داخل کوو، او په دې وخت کې هرڅوک حتی نشي کولی SELECT

یو څه ښه نه کیږي ...

جدول بدل کړئ… نوم بدل کړئ… / میز پریږدئ…

یو بدیل دا دی چې هرڅه په جلا نوي جدول کې ډک کړئ ، او بیا یې په ساده ډول د زاړه په ځای نوم بدل کړئ. یو څو کوچني شیان:

  • اوس هم Exclusiveکه څه هم د پام وړ لږ وخت
  • د دې جدول لپاره ټول پوښتنې پالنونه/احصایې بیا تنظیم شوي، د تحلیل چلولو ته اړتیا لري
  • ټولې بهرنۍ کیلي مات شوي (FK) میز ته

د سایمن ریګز څخه د WIP پیچ شتون درلود چې د جوړولو وړاندیز یې وکړ ALTER- د فایل په کچه د میز بدن بدلولو لپاره عملیات، پرته له دې چې احصایې او FK لمس کړي، مګر کورم راټول نه کړي.

ړنګول، تازه کول، داخلول

نو، موږ د دریو عملیاتو غیر بلاک کولو اختیار باندې بسنه کوو. نږدې درې ... دا څنګه په خورا اغیزمنه توګه ترسره کړئ؟

-- все делаем в рамках транзакции, чтобы никто не видел "промежуточных" состояний
BEGIN;

-- создаем временную таблицу с импортируемыми данными
CREATE TEMPORARY TABLE tmp(
  LIKE dst INCLUDING INDEXES -- по образу и подобию, вместе с индексами
) ON COMMIT DROP; -- за рамками транзакции она нам не нужна

-- быстро-быстро вливаем новый образ через COPY
COPY tmp FROM STDIN;
-- ...
-- .

-- удаляем отсутствующие
DELETE FROM
  dst D
USING
  dst X
LEFT JOIN
  tmp Y
    USING(pk1, pk2) -- поля первичного ключа
WHERE
  (D.pk1, D.pk2) = (X.pk1, X.pk2) AND
  Y IS NOT DISTINCT FROM NULL; -- "антиджойн"

-- обновляем оставшиеся
UPDATE
  dst D
SET
  (f1, f2, f3) = (T.f1, T.f2, T.f3)
FROM
  tmp T
WHERE
  (D.pk1, D.pk2) = (T.pk1, T.pk2) AND
  (D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); -- незачем обновлять совпадающие

-- вставляем отсутствующие
INSERT INTO
  dst
SELECT
  T.*
FROM
  tmp T
LEFT JOIN
  dst D
    USING(pk1, pk2)
WHERE
  D IS NOT DISTINCT FROM NULL;

COMMIT;

3.2. د پروسس وروسته واردول

په ورته KLADR کې، ټول بدل شوي ریکارډونه باید اضافي د پوسټ پروسس کولو له لارې پرمخ وړل شي - نورمال شوي، کلیدي ټکي روښانه شوي، او اړین جوړښتونو ته کم شوي. مګر تاسو څنګه پوهیږئ - په حقیقت کې څه بدل شویپرته له دې چې د همغږي کوډ پیچلي کړي، په مثالي توګه پرته له دې چې دا په بشپړه توګه لمس کړي؟

که یوازې ستاسو پروسې د همغږي کولو په وخت کې لیکلو ته لاسرسی ولري ، نو تاسو کولی شئ یو محرک وکاروئ چې زموږ لپاره به ټول بدلونونه راټول کړي:

-- целевые таблицы
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);

-- таблицы с историей изменений
CREATE TABLE kladr$log(
  ro kladr, -- тут лежат целые образы записей старой/новой
  rn kladr
);

CREATE TABLE kladr_house$log(
  ro kladr_house,
  rn kladr_house
);

-- общая функция логирования изменений
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
  dst varchar = TG_TABLE_NAME || '$log';
  stmt text = '';
BEGIN
  -- проверяем необходимость логгирования при обновлении записи
  IF TG_OP = 'UPDATE' THEN
    IF NEW IS NOT DISTINCT FROM OLD THEN
      RETURN NEW;
    END IF;
  END IF;
  -- создаем запись лога
  stmt = 'INSERT INTO ' || dst::text || '(ro,rn)VALUES(';
  CASE TG_OP
    WHEN 'INSERT' THEN
      EXECUTE stmt || 'NULL,$1)' USING NEW;
    WHEN 'UPDATE' THEN
      EXECUTE stmt || '$1,$2)' USING OLD, NEW;
    WHEN 'DELETE' THEN
      EXECUTE stmt || '$1,NULL)' USING OLD;
  END CASE;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

اوس موږ کولی شو د ترکیب پیل کولو دمخه محرکونه پلي کړو (یا یې له لارې فعال کړو ALTER TABLE ... ENABLE TRIGGER ...):

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

CREATE TRIGGER log
  AFTER INSERT OR UPDATE OR DELETE
  ON kladr_house
    FOR EACH ROW
      EXECUTE PROCEDURE diff$log();

او بیا موږ په آرامۍ سره ټول هغه بدلونونه چې موږ ورته اړتیا لرو د لاګ جدولونو څخه استخراج کوو او د اضافي هینډلرونو له لارې یې چلوو.

3.3. د لینک شوي سیټونو واردول

پورته موږ هغه قضیې په پام کې نیولي کله چې د سرچینې او منزل ډیټا جوړښتونه ورته وي. مګر که چیرې د بهرني سیسټم څخه اپلوډ زموږ په ډیټابیس کې د ذخیره کولو جوړښت څخه توپیر ولري؟

راځئ چې د مثال په توګه د پیرودونکو ذخیره کولو او د دوی حسابونو ته واخلو، د کلاسیک "ډیری څخه یو" اختیار:

CREATE TABLE client(
  client_id
    serial
      PRIMARY KEY
, inn
    varchar
      UNIQUE
, name
    varchar
);

CREATE TABLE invoice(
  invoice_id
    serial
      PRIMARY KEY
, client_id
    integer
      REFERENCES client(client_id)
, number
    varchar
, dt
    date
, sum
    numeric(32,2)
);

مګر د بهرنۍ سرچینې څخه ډاونلوډ موږ ته د "ټول په یوه کې" په بڼه راځي:

CREATE TEMPORARY TABLE invoice_import(
  client_inn
    varchar
, client_name
    varchar
, invoice_number
    varchar
, invoice_dt
    date
, invoice_sum
    numeric(32,2)
);

په ښکاره ډول، د پیرودونکي ډاټا په دې نسخه کې نقل کیدی شي، او اصلي ریکارډ "حساب" دی:

0123456789;Вася;A-01;2020-03-16;1000.00
9876543210;Петя;A-02;2020-03-16;666.00
0123456789;Вася;B-03;2020-03-16;9999.00

د ماډل لپاره، موږ به په ساده ډول زموږ د ازموینې ډاټا داخل کړو، مګر په یاد ولرئ - COPY ډیر اغیزمن!

INSERT INTO invoice_import
VALUES
  ('0123456789', 'Вася', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', 'Петя', 'A-02', '2020-03-16', 666.00)
, ('0123456789', 'Вася', 'B-03', '2020-03-16', 9999.00);

لومړی، راځئ هغه "کټونه" روښانه کړو چې زموږ "حقایق" ورته اشاره کوي. زموږ په قضیه کې، رسیدونه پیرودونکو ته راجع کیږي:

CREATE TEMPORARY TABLE client_import AS
SELECT DISTINCT ON(client_inn)
-- можно просто SELECT DISTINCT, если данные заведомо непротиворечивы
  client_inn inn
, client_name "name"
FROM
  invoice_import;

د دې لپاره چې حسابونه په سمه توګه د پیرودونکي IDs سره شریک کړو، موږ باید لومړی دا پیژندونکي پیدا کړو یا پیدا کړو. راځئ چې د دوی لاندې ساحې اضافه کړو:

ALTER TABLE invoice_import ADD COLUMN client_id integer;
ALTER TABLE client_import ADD COLUMN client_id integer;

راځئ چې د یو کوچني تعدیل سره پورته ذکر شوي د میز همغږي کولو میتود وکاروو - موږ به په نښه شوي جدول کې هیڅ شی تازه یا حذف نه کړو ، ځکه چې موږ پیرودونکي "یوازې ضمیمه" واردوو:

-- проставляем в таблице импорта ID уже существующих записей
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  client D
WHERE
  T.inn = D.inn; -- unique key

-- вставляем отсутствовавшие записи и проставляем их ID
WITH ins AS (
  INSERT INTO client(
    inn
  , name
  )
  SELECT
    inn
  , name
  FROM
    client_import
  WHERE
    client_id IS NULL -- если ID не проставился
  RETURNING *
)
UPDATE
  client_import T
SET
  client_id = D.client_id
FROM
  ins D
WHERE
  T.inn = D.inn; -- unique key

-- проставляем ID клиентов у записей счетов
UPDATE
  invoice_import T
SET
  client_id = D.client_id
FROM
  client_import D
WHERE
  T.client_inn = D.inn; -- прикладной ключ

په حقیقت کې، هرڅه په کې دي invoice_import اوس موږ د تماس ساحه ډکه کړه client_id، د کوم سره چې موږ به انوائس داخل کړو.

سرچینه: www.habr.com

Add a comment