it-swarm.asia

كتابة مخطط مصرفي بسيط: كيف يمكنني الحفاظ على تزامن أرصدي مع سجل المعاملات؟

أنا أكتب مخطط قاعدة البيانات المصرفية البسيطة. فيما يلي المواصفات الأساسية:

  • ستقوم قاعدة البيانات بتخزين المعاملات مقابل مستخدم وعملة.
  • لكل مستخدم رصيد واحد لكل عملة ، لذا فإن كل رصيد هو ببساطة مجموع كل المعاملات مقابل مستخدم وعملة معينة.
  • لا يمكن أن يكون الرصيد سالبًا.

سيتواصل التطبيق المصرفي مع قاعدة بياناته حصرا من خلال الإجراءات المخزنة.

أتوقع أن تقبل قاعدة البيانات هذه مئات الآلاف من المعاملات الجديدة في اليوم ، بالإضافة إلى استفسارات الرصيد على مستوى أعلى من حيث الحجم. لتقديم أرصدة بسرعة كبيرة ، أحتاج إلى تجميعها مسبقًا. في الوقت نفسه ، أحتاج إلى ضمان ألا يتعارض الرصيد أبدًا مع تاريخ معاملاته.

خياراتي هي:

  1. احصل على جدول balances منفصل وقم بأحد الإجراءات التالية:

    1. تطبيق المعاملات على الجدولين transactions و balances. استخدم منطق TRANSACTION في طبقة الإجراءات المخزنة للتأكد من أن الأرصدة والمعاملات متزامنة دائمًا. (مدعوم من Jack .)

    2. قم بتطبيق المعاملات على جدول transactions ولديك مشغل يقوم بتحديث جدول balances لي بمبلغ المعاملة.

    3. قم بتطبيق المعاملات على جدول balances ولديك مشغل يضيف إدخالًا جديدًا في جدول transactions لي مع مبلغ المعاملة.

    يجب أن أعتمد على الأساليب القائمة على الأمان للتأكد من عدم إمكانية إجراء تغييرات خارج الإجراءات المخزنة. خلاف ذلك ، على سبيل المثال ، يمكن لبعض العمليات إدراج معاملة مباشرة في الجدول transactions وتحت المخطط 1.3 سوف يكون الرصيد ذو الصلة غير متزامن.

  2. لديك طريقة عرض مفهرسة balances تجمع المعاملات بشكل مناسب. يتم ضمان الأرصدة بواسطة محرك التخزين للبقاء متزامنة مع معاملاتهم ، لذلك لست بحاجة إلى الاعتماد على الأساليب القائمة على الأمان لضمان ذلك. من ناحية أخرى ، لا يمكنني فرض أرصدة غير سلبية بعد الآن لأن طرق العرض - حتى طرق العرض المفهرسة - لا يمكن أن تحتوي على قيود CHECK. (مدعوم من Denny .)

  3. لديك جدول transactions فقط ولكن مع عمود إضافي لتخزين الرصيد ساريًا بعد تنفيذ المعاملة مباشرة. وبالتالي ، فإن أحدث سجل للمعاملات للمستخدم والعملة يحتوي أيضًا على رصيدهما الحالي. (مقترح أدناه بواسطة Andrew ؛ البديل مقترح بواسطة garik .)

عندما عالجت هذه المشكلة لأول مرة ، قرأت هذهاثنين مناقشات وقررت الخيار 2. كمرجع ، يمكنك رؤية تنفيذ عظام عارية هنا .

  • هل قمت بتصميم أو إدارة قاعدة بيانات مثل هذه مع ملف تعريف تحميل عالي؟ ماذا كان حلك لهذه المشكلة؟

  • هل تعتقد أنني قمت باختيار التصميم الصحيح؟ هل هناك أي شيء يجب أن أتذكره؟

    على سبيل المثال ، أعلم أن تغييرات المخطط لجدول transactions ستتطلب إعادة إنشاء عرض balances. حتى إذا كنت أرشفة المعاملات للحفاظ على قاعدة البيانات صغيرة (على سبيل المثال عن طريق نقلها إلى مكان آخر واستبدالها بالمعاملات الموجزة) ، فإن الحاجة إلى إعادة إنشاء العرض لعشرات الملايين من المعاملات مع كل تحديث مخطط سيعني على الأرجح المزيد من وقت التوقف عن العمل لكل عملية نشر.

  • إذا كانت طريقة العرض المفهرسة هي الطريقة المناسبة ، كيف يمكنني ضمان عدم وجود رصيد سلبي؟


معاملات الأرشفة:

اسمحوا لي أن أوضح قليلاً عن أرشفة المعاملات و "المعاملات الموجزة" التي ذكرتها أعلاه. أولاً ، ستكون الأرشفة المنتظمة ضرورة في نظام عالي التحميل مثل هذا. أريد الحفاظ على الاتساق بين الأرصدة وتاريخ المعاملات الخاصة بهم مع السماح بنقل المعاملات القديمة إلى مكان آخر. للقيام بذلك ، سأستبدل كل دفعة من المعاملات المؤرشفة بملخص لمبالغها لكل مستخدم وعملة.

لذا ، على سبيل المثال ، قائمة المعاملات هذه:

user_id    currency_id      amount    is_summary
------------------------------------------------
      3              1       10.60             0
      3              1      -55.00             0
      3              1      -12.12             0

تتم أرشفته واستبداله بما يلي:

user_id    currency_id      amount    is_summary
------------------------------------------------
      3              1      -56.52             1

بهذه الطريقة ، يحتفظ التوازن مع المعاملات المؤرشفة بسجل كامل ومتسق للمعاملات.

60
Nick Chammas

لست على دراية بالمحاسبة ، ولكني قمت بحل بعض المشكلات المماثلة في بيئات من نوع المخزون. أقوم بتخزين المجاميع الجارية في نفس الصف مع المعاملة. أستخدم قيودًا ، حتى لا تكون بياناتي خاطئة أبدًا حتى في ظل التزامن العالي. لقد كتبت الحل التالي في ذلك الوقت في عام 2009: :

حساب مجاميع التشغيل بطيء بشكل ملحوظ ، سواء كنت تفعل ذلك بمؤشر أو بربط مثلث. من المغري جدًا إلغاء التطابق ، لتخزين الإجماليات العاملة في عمود ، خاصة إذا قمت بتحديده بشكل متكرر. ومع ذلك ، وكما هو معتاد عند قيامك بعملية إزالة البيانات ، تحتاج إلى ضمان سلامة بياناتك غير المخصصة. لحسن الحظ ، يمكنك ضمان سلامة تشغيل الإجماليات مع قيود - طالما أن جميع القيود الخاصة بك موثوق بها ، فإن جميع الإجماليات قيد التشغيل صحيحة. أيضًا بهذه الطريقة يمكنك التأكد بسهولة من أن الرصيد الحالي (إجماليات التشغيل) ليس سلبيًا أبدًا - يمكن أن يكون فرضه بطرق أخرى بطيئًا جدًا. يوضح البرنامج النصي التالي التقنية.

CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
  ItemID INT NOT NULL,
  ChangeDate DATETIME NOT NULL,
  ChangeQty INT NOT NULL,
  TotalQty INT NOT NULL,
  PreviousChangeDate DATETIME NULL,
  PreviousTotalQty INT NULL,
  CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
  CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
  CONSTRAINT UNQ_Inventory_Previous_Columns 
     UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
  CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
    REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
  CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(
         TotalQty >= 0 
     AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)
  ),
  CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
  CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK(
        (PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
     OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL)
  )
);

-- beginning of inventory for item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090101', 10, 10, NULL, NULL);

-- cannot begin the inventory for the second time for the same item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090102', 10, 10, NULL, NULL);


Msg 2627, Level 14, State 1, Line 10

Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. 
Cannot insert duplicate key in object 'Data.Inventory'.

The statement has been terminated.


-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order
SET @ChangeQty = 5;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4

The INSERT statement conflicted with the CHECK constraint 
"CHK_Inventory_Valid_Dates_Sequence". 
The conflict occurred in database "Test", table "Data.Inventory".

The statement has been terminated.

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;

-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update
DECLARE @IncreaseQty INT;

SET @IncreaseQty = 2;

UPDATE Data.Inventory 
SET 
     ChangeQty = ChangeQty 
   + CASE 
        WHEN ItemID = 1 AND ChangeDate = '20090103' 
        THEN @IncreaseQty 
        ELSE 0 
     END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + 
     CASE 
        WHEN ItemID = 1 AND ChangeDate = '20090103' 
        THEN 0 
        ELSE @IncreaseQty 
     END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20
17
A-K

نهج مختلف قليلاً (مشابه لخيارك الثاني ) للنظر هو أن يكون لديك جدول المعاملات فقط ، مع تعريف:

CREATE TABLE Transaction (
      UserID              INT
    , CurrencyID          INT 
    , TransactionDate     DATETIME  
    , OpeningBalance      MONEY
    , TransactionAmount   MONEY
);

قد تحتاج أيضًا إلى معرّف/أمر معاملة ، حتى تتمكن من معالجة عمليتين بنفس التاريخ وتحسين استعلام الاسترداد الخاص بك.

للحصول على الرصيد الحالي ، كل ما عليك الحصول عليه هو الرقم القياسي الأخير.

طرق الحصول على السجل الأخير :

/* For a single User/Currency */
Select TOP 1 *
FROM dbo.Transaction
WHERE UserID = 3 and CurrencyID = 1
ORDER By TransactionDate desc

/* For multiple records ie: to put into a view (which you might want to index) */
SELECT
    C.*
FROM
    (SELECT 
        *, 
        ROW_NUMBER() OVER (
           PARTITION BY UserID, CurrencyID 
           ORDER BY TransactionDate DESC
        ) AS rnBalance 
    FROM Transaction) C
WHERE
    C.rnBalance = 1
ORDER BY
    C.UserID, C.CurrencyID

السلبيات:

  • عند إدراج معاملة خارج التسلسل (أي: لتصحيح مشكلة/رصيد بداية غير صحيح) ، قد تحتاج إلى تسلسل التحديثات لجميع المعاملات اللاحقة.
  • يجب إجراء تسلسل لمعاملات المستخدم/العملة للحفاظ على توازن دقيق.

    -- Example of getting the current balance and locking the 
    -- last record for that User/Currency.
    -- This lock will be freed after the Stored Procedure completes.
    SELECT TOP 1 @OldBalance = OpeningBalance + TransactionAmount  
    FROM dbo.Transaction with (rowlock, xlock)   
    WHERE UserID = 3 and CurrencyID = 1  
    ORDER By TransactionDate DESC;
    

الإيجابيات:

  • لم يعد لديك للحفاظ على جدولين منفصلين ...
  • يمكنك التحقق من صحة الرصيد بسهولة ، وعندما يخرج الرصيد من المزامنة ، يمكنك تحديد الوقت الذي خرج منه عن العمل تمامًا عندما يصبح سجل المعاملات توثيقًا ذاتيًا.

تحرير: بعض الاستفسارات النموذجية حول استرداد الرصيد الحالي وإبراز الاشتراكات (ThanksJack Douglas)

15
Andrew Bickerton

إن عدم السماح للعملاء بالحصول على رصيد أقل من 0 هو قاعدة عمل (والتي ستتغير بسرعة حيث أن رسوم أشياء مثل الإفراط في السحب هي الطريقة التي تحقق بها البنوك معظم أموالها). ستحتاج إلى معالجة ذلك في معالجة التطبيق عند إدراج الصفوف في سجل المعاملات. خاصة وأنك قد تنتهي ببعض العملاء الذين لديهم حماية على المكشوف وبعضهم يحصلون على رسوم مفروضة والبعض الآخر لا يسمح بإدخال مبالغ سلبية.

حتى الآن أحب المكان الذي تذهب إليه مع هذا ، ولكن إذا كان هذا لمشروع فعلي (وليس مدرسة) ، فيجب أن يكون هناك الكثير من التفكير في قواعد العمل ، وما إلى ذلك. بمجرد أن يكون لديك نظام مصرفي والجري ليس هناك مجال كبير لإعادة التصميم نظرًا لوجود قوانين محددة جدًا بشأن الأشخاص الذين يمكنهم الوصول إلى أموالهم.

14
mrdenny

بعد قراءة هاتين النقاشتين ، قررت الخيار 2

بعد قراءة هذه المناقشات أيضًا ، لست متأكدًا من سبب اختيارك الحل DRI على أكثر الخيارات المعقولة التي تحددها:

تطبيق المعاملات على كل من جداول المعاملات والأرصدة. استخدم منطق المعاملات في طبقة الإجراءات المخزنة للتأكد من أن الأرصدة والمعاملات متزامنة دائمًا.

هذا النوع من الحلول له فوائد عملية هائلة إذا كان لديك ترف تقييد all الوصول إلى البيانات من خلال واجهة برمجة التطبيقات للمعاملات. ستفقد الفائدة المهمة جدًا من DRI ، وهي أن النزاهة مضمونة من خلال قاعدة البيانات ، ولكن في أي نموذج من التعقيد الكافي سيكون هناك بعض قواعد العمل التي لا يمكن فرضها بواسطة DRI .

أنصح باستخدام DRI حيثما أمكن لفرض قواعد العمل دون ثني نموذجك كثيرًا لجعل ذلك ممكنًا:

حتى إذا كنت أرشفة المعاملات (على سبيل المثال ، بنقلها إلى مكان آخر واستبدالها بمعاملات موجزة)

بمجرد أن تبدأ في التفكير في تلويث نموذجك بهذا الشكل ، أعتقد أنك تنتقل إلى المنطقة حيث تفوق ميزة DRI الصعوبات التي تقدمها. ضع في اعتبارك على سبيل المثال أن الخلل في عملية الأرشفة الخاصة بك يمكن أن يتسبب نظريًا في القاعدة الذهبية (تلك الأرصدة دائمًا تساوي مجموع المعاملات) إلى كسر بصمت بمحلول DRI .

فيما يلي ملخص لمزايا نهج المعاملات كما أراها:

  • يجب علينا القيام بذلك على أي حال إذا كان ذلك ممكناً. أيًا كان الحل الذي تختاره لهذه المشكلة تحديدًا ، فإنه يمنحك المزيد من مرونة التصميم والتحكم في بياناتك. عندئذٍ يصبح كل وصول "معاملات" من حيث منطق الأعمال ، وليس فقط من حيث منطق قاعدة البيانات.
  • يمكنك الاحتفاظ بنموذجك أنيقًا
  • يمكنك "فرض" نطاق وتعقيد أكبر لقواعد العمل (مع ملاحظة أن مفهوم "فرض" هو مفهوم أكثر مرونة من DRI)
  • لا يزال بإمكانك استخدام DRI حيثما كان ذلك عمليًا لإعطاء النموذج تكاملًا أساسيًا أكثر قوة - ويمكن أن يكون هذا بمثابة التحقق من منطق المعاملات الخاص بك
  • معظم مشاكل الأداء المقلقة ستذوب
  • قد يكون تقديم متطلبات جديدة أسهل كثيرًا - على سبيل المثال: القواعد المعقدة للمعاملات المتنازع عليها قد تجبرك على الابتعاد عن نهج DRI النقي في المستقبل ، مما يعني الكثير من الجهد الضائع
  • يصبح تقسيم البيانات التاريخية أو أرشفتها أقل خطورة وألمًا

--تعديل

للسماح بالأرشفة دون إضافة التعقيد أو المخاطرة ، يمكنك اختيار الاحتفاظ بصفوف التلخيص في جدول ملخص منفصل ، يتم إنشاؤه باستمرار (الاقتراض منAndrew وGarik)

على سبيل المثال ، إذا كانت الملخصات شهرية:

  • في كل مرة تكون هناك معاملة (من خلال واجهة برمجة التطبيقات) ، يكون هناك تحديث مقابل أو إدراج في جدول الملخص
  • جدول الملخص = لا مؤرشف ، لكن أرشفة المعاملات تصبح مجرد حذف (أو إسقاط القسم؟)
  • يتضمن كل صف في جدول الملخص "الرصيد الافتتاحي" و "المبلغ"
  • يمكن التحقق من القيود مثل "الرصيد الافتتاحي" + "المبلغ"> 0 و "الرصيد الافتتاحي"> 0 على جدول الملخص
  • يمكن إدراج صفوف التلخيص في دفعة شهرية لتسهيل قفل صف الملخص الأخير (سيكون هناك دائمًا صف للشهر الحالي)
12
Jack says try topanswers.xyz

نيك.

الفكرة الرئيسية هي تخزين سجلات الرصيد والمعاملات في نفس الجدول. حدث ذلك تاريخيا اعتقدت. لذلك في هذه الحالة يمكننا الحصول على التوازن فقط عن طريق تحديد آخر سجل ملخص.

 id   user_id    currency_id      amount    is_summary (or record_type)
----------------------------------------------------
  1       3              1       10.60             0
  2       3              1       10.60             1    -- summary after transaction 1
  3       3              1      -55.00             0
  4       3              1      -44.40             1    -- summary after transactions 1 and 3
  5       3              1      -12.12             0
  6       3              1      -56.52             1    -- summary after transactions 1, 3 and 5 

البديل الأفضل هو تقليل عدد المحاضر الموجزة. يمكن أن يكون لدينا سجل رصيد واحد في نهاية (و/أو يبدأ) اليوم. كما تعلم كل بنك لديه operational day لفتحه وإغلاقه للقيام ببعض العمليات الموجزة لهذا اليوم. يسمح لنا بسهولة حساب الفائدة باستخدام سجل الرصيد اليومي ، على سبيل المثال:

user_id    currency_id      amount    is_summary    oper_date
--------------------------------------------------------------
      3              1       10.60             0    01/01/2011 
      3              1      -55.00             0    01/01/2011
      3              1      -44.40             1    01/01/2011 -- summary at the end of day (01/01/2011)
      3              1      -12.12             0    01/02/2011
      3              1      -56.52             1    01/02/2011 -- summary at the end of day (01/02/2011)

حظ.

6
garik

بناءً على متطلباتك ، سيظهر الخيار 1 الأفضل. على الرغم من أن لدي تصميمي للسماح فقط بإدراج في جدول المعاملات. ولديك الزناد على جدول المعاملات لتحديث جدول التوازن في الوقت الحقيقي. يمكنك استخدام أذونات قاعدة البيانات للتحكم في الوصول إلى هذه الجداول.

في هذا النهج ، يتم ضمان أن يكون الرصيد في الوقت الحقيقي متزامنًا مع جدول المعاملات. ولا يهم إذا تم استخدام الإجراءات المخزنة أو psql أو jdbc. يمكنك التحقق من رصيدك السلبي إذا لزم الأمر. لن يكون الأداء مشكلة. للحصول على التوازن في الوقت الحقيقي ، هو استعلام مفرد.

لن تؤثر الأرشفة على هذا النهج. يمكنك الحصول على جدول ملخص أسبوعي ، شهري ، سنوي أيضًا إذا لزم الأمر لأشياء مثل التقارير.

4
Elan Fisoc

في Oracle ، يمكنك القيام بذلك باستخدام جدول المعاملات فقط مع طريقة عرض مادية سريعة قابلة للتحديث عليها تقوم بالتجميع لتشكيل التوازن. يمكنك تحديد الزناد في العرض المادي. إذا تم تعريف "العرض المادي" بـ "ON COMMIT" ، فإنه يمنع بشكل فعال إضافة/تعديل البيانات في الجداول الأساسية. يكتشف الزناد البيانات [في] الصالحة ويثير استثناء ، حيث يتراجع عن المعاملة. مثال جميل هنا http://www.sqlsnippets.com/en/topic-12896.html

لا أعرف sqlserver ولكن ربما لديها خيار مماثل؟

3
ik_zelf