SQL Server למפתחים: Unique Index ו- IGNORE_DUP_KEY

·

אחד הפיצ’רים הבסיסיים לסכימה של מסדי נתונים, היא היכולת להגדיר unique constraint.
מאחורי הקלעים, Unique Constraints מתבססים על Unique Indexes שנוצרים כדי לשרת את ה- constraint, כלומר – כדי לאפשר ל- SQL Server לבדוק בזמן ביצוע פעולות הוספה ועדכון שה- constraint לא מופר.

את הפוסט הזה אני רוצה להקדיש לפיצ’ר שימושי שמפתחים צריכים גם כן להכיר – שימוש ב- unique index על טבלה לסינון כפילויות בזמן ההכנסה. צריך להבדיל גם בין שני תתי-תרחישים אפשריים: לפעמים, נכונות המידע דורשת מאיתנו שלא יהיו שורות כפולות. למשל, אם אנחנו מנהלים טבלה של אזרחי ישראל – לא יכולים להיות שני אזרחים עם אותו מספר ת”ז.

עם זאת, לפעמים נרצה לסנן שורות כפולות כדי להקל על השימוש במידע. למשל, אם יש לנו טבלת קישור שמקשרת בין אזרח בישראל לאבא שלו. ונניח שאנחנו מקבלים כל הזמן מידע על אזרחי ישראל ומי האבא שלהם, כאשר לעיתים המידע יחדש לנו (יספר לנו על קשר שלא ידענו עליו) ולעיתים לא (נקבל שוב מידע על קשר שאנחנו מכירים).
נניח גם (לטובת התרחיש הקצת מופרך) שעצם קבלת המידע לא מעניינת, כלומר אין לנו עניין לשמור תיעוד של עצם העובדה שקיבלנו קישור כזה (נניח כי הוא נשמר במקום אחר).
בתרחיש הזה – אין בעייה מבחינת נכונות המידע לשמור שורות כפולות. א’ הוא אבא של ב’ גם אם זה מופיע תיעוד לזה פעם אחת וגם אם מופיע לזה תיעוד מיליון פעמים. עם זאת, יש יתרונות להחזיק טבלה קטנה לעומת טבלה גדולה, ככה שיש לנו יתרון לשמור את הקישור הזה באופן יחיד.
כדי לעשות את זה אנחנו יכולים לנקוט באחת מהרבה הדרכים שקיימות כדי לשמור מידע סיכומי, או תצוגה סיכומית של המידע (כתבתי למשל על Indexed Views, שבהחלט אפשר לעשות בהם שימוש במקרה כזה).  אבל מה אם אנחנו מראש לא רוצים להחזיק את המידע ה-“מיותר” (כלומר, כל כפילות שהיא)?

לכאורה אנחנו נמצאים פה עם בעיית “UPSERT” מנוונת: אנחנו לא רוצים להכניס-או-לעדכן,  אנחנו רוצים להכניס-או-לוותר [אם יש כפילות].

לפני שאני אמשיך בהצגת Unique Indexes, אני רוצה להתייחס רגע לפיתרון הנאיבי של “נבדוק אם לא קיים ואז נכניס”. כלומר, נעשה תנאי של IF NOT EXISTS וע”ס הערכים שלו נפעל.
פיתרון הזה פחות טוב, ולא רק בגלל שהוא פחות אלגנטי ונוח. הוא גם לא מאפשר לנו לעבוד בצורה נוחה עם BULK INSERT-ים (כי אז אנחנו לא יכולים לשלב את הבדיקת IF NOT EXISTS), הוא מקשה עלינו מבחינת טרנזקציות שרצות במקביל (צריך לבחור isolation level כזה שיאפשר לנו לוודא שבין הבדיקה של ה- IF NOT EXISTS להכנסה לא הוכנס הערך מטרנזקציה אחרת) – ובנוסף, הוא גם לא חוסך לנו את האינדקס (כי אם יש לנו הרבה שורות בטבלה, הבדיקת IF NOT EXISTS עצמה תהיה יקרה). ואם כבר אינדקס, אז עדיף שזה יהיה תוך שימוש בפיצ’ר שמובנה במנגנון ומאפשר לנו להנות מיתרונות נוספים : –)

Unique Indexes והתנהגות במקרה של כפילויות

בגדול, unique index, זה אינדקס רגיל שמסומן ל- SQL Server שכל קומבינציה של ה- key columns שלו (לשם פשטות, אני אקרא לזה “הערך” שלו – למרות שכמובן זה יכול להיות שילוב של ערכים) הוא יוניקי. הוא למעשה “הרוח החיה” מאחורי כל unique constraint (כי הוא זה שמאפשר גישה מהירה כדי לבדוק שה- constraint נשמר).
Unique Indexes יכולים לשמש אותנו לאכיפת constraint, ובנוסף הם מספקים מידע חשוב לטובת ייצור Execution Plan. כאשר מובטחת ייחודיות של ערכים מדובר למעשה באמירה חזקה מאד על המידע שמאפשרת לבצע פעולות מסויימות בצורה יעילה יותר מה שמאפשר לייצר execution plan טוב יותר.

כאשר אנחנו יוצרים unique index, ההתנהגות הדיפולטית במקרה שאנחנו מכניסים ערך שקיים כבר היא פשוטה: לזרוק שגיאה (וכמובן שהשורה לא מתווספת). אם השגיאה הזאת קרתה כחלק מטרנזקציה (למשל, כאשר אנחנו עושים רצף של פעולות בטרנזקציה אחת, או למשל עושים שימוש ב- Bulk Insert ומכניסים מס’ שורות בבת אחת) – יתבצע rollback של הטרנזקציה, כלומר המצב יחזור לקדמותו ואף אחת מהפעולות שהתבצעה כחלק מהטרנזקציה לא תקרה. ההיגיון מאחורי זה ברור – אם רצף הפעולות הזה הביא למצב שבו ניסינו להכניס שורה עם ערך כפול, למרות שהוא אמור להיות ייחודי, אז משהו לא נכון קרה.

אבל, אנחנו יכולים להשפיע על ההתנהגות הזאת, ולהגיד ל- SQL Server שבמקום שגיאה יוציא אזהרה בלבד. מה המשמעות של אזהרה? לא מתבצע rollback. אנחנו מקבלים חיווי לכך שהיו שורות כפולות, אבל זה לא מונע מהטרנזקציה להתבצע בהצלחה.

אם אנחנו מחזיקים את ה- index כאמצעי לאכוף איזשהו constraint ולשמור על נכונות המידע, כנראה נצטרך לחשוב פעמיים אם נשנה את ההתנהגות הזאת. אבל, אם המטרה היא לעזור לנו לשימוש במידע ולשמור מידע סיכומי, נוכל (ואף נצטרך) להשתמש בזה בלב שקט

 

יצירת Unique Index עם IGNORE_DUP_KEY

.איך עושים את זה? משתמשים ב- flag שנקרא IGNORE_DUP_KEY ומגדירים אותו ל- ON בעת יצירת ה- index.

CREATE UNIQUE NONCLUSTERED INDEX [IX_FatherSonInfo_Unique_SonID_FatherID] ON [dbo].[FatherSonInfo]

(

    [SonID] ASC,

    [FatherID] ASC

)WITH (IGNORE_DUP_KEY = ON)

מה אמרנו בפועל? ייצרנו אינדקס יוניקי חדש, ואמרנו שבמידה שמתבצעת פעולת הכנסה/עדכון לאינדקס הזה שתפגע בייחודיות – הוא ימנע את זה, מעצם הגדרתו כאינדקס יוניקי, אבל יעשה את זה עם אזהרה ולא עם שגיאה (לטובת הבהרה: כאשרמתבצעת פעולת הכנסה/עדכון על הטבלה אז מתבצעת פעולה על האינדקס – כי כל פעולה על הטבלה כרוכה בפעולה מתאימה על כל אינדקס שמוגדר בטבלה [למעט היוצא מין הכלל של Filtered Index, ועל זה בפוסט אחר]).

כמובן, שכאשר מריצים את ה- statement הזה חייבים שבטבלה לא יהיו כפילויות, אחרת נקבל שגיאה כזאת:

Msg 1505, Level 16, State 1, Line 5

The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.FatherSonInfo' and the index name 'IX_FatherSonInfo_Unique_FatherID_SonID'. The duplicate key value is (1, 1).

The statement has been terminated.

——————

כטיפ קטן (ופחות קשור למטרה המרכזית של הפוסט), הנה דוגמא לאיך אני מנקה את הטבלה שלי שמכילה כמה עמודות מכל השורות הכפולות, ומשאיר רק את השורה עם ה- ID הנמוך ביותר:

with cte as (

    SELECT    ID,

            DateCreated,

            FatherID,

            SonID,

            RN = ROW_NUMBER()OVER(PARTITION BY FatherID, SonID ORDER BY ID)

    FROM FatherSonInfo

)

 

DELETE FROM cte

WHERE RN > 1

(כמובן שאם מדובר בטבלה גדולה זאת יכולה להיות שאילתה מאד כבדה,  ככה שאל תנסו את זה בסביבה ייצורית בלי להיות באמת מודעים להשלכות. אני אכתוב בהזדמנות פוסט על איך עושים פעילויות עדכון נתונים רחבות בסביבת production).

——————

אם לא היו שורות כפולות בטבלה נקבל הודעה שהאינדקס נוצר בהצלחה.

שימוש באינדקס שמוגדר עם IGNORE_DUP_KEY=ON

אז אחרי שיצרנו את האינדקס הזה, שכאמור הוא אינדקס יוניקי שמסומן ב IGNORE_DUP_KEY = ON, כך שההתנהגות במקרה של כפילויות היא אזהרה בלבד, בואו נראה באמת מה קורה.

נתבונן רגע בשאילתה הבאה:

 

INSERT INTO FatherSonInfo(DateCreated, FatherID, SonID)

VALUES

(GETDATE(), 1, 1),

(GETDATE(), 1,1),

(GETDATE(), 1,2)

אנחנו מכניסים פה באותו statement (שקל וחומר שזה אומר שזה באותה טרנזקציה) מס’ ערכים לטבלת FatherSonInfo – כאשר הראשון והשני כפולים.  התוצאה שנקבל היא ההודעה הבאה:

Duplicate key was ignored.

 

(2 row(s) affected)

כלומר, קיבלנו אזהרה – אבל השורות הוכנסו. בצורה הזאת, היינו יכולים להבטיח כבר בשלב ההכנסה שמצד אחד לא יהיו לנו שורות “מיותרות” בטבלה, מצד שני לא להעסיק את עצמנו עצמאית בשאילתה של IF NOT EXISTS ובכל זאת להבטיח ייחודיות של ערכים.

לעומת זאת, אם זה היה מתבצע מול טבלה שהאינדקס היוניקי מוגדר בה בתור IGNORE_DUP_KEY = OFF (או פשוט בלי אזכור של IGNORE_DUP_KEY, כי ה- default הוא OFF) אז היינו מקבלים שגיאה שנראית ככה:

Msg 2601, Level 14, State 1, Line 2

Cannot insert duplicate key row in object 'dbo.FatherSonInfo' with unique index 'IX_FatherSonInfo_Unique_FatherID_SonID'. The duplicate key value is (1, 1).

The statement has been terminated.

ולא הייתה מוכנסת אף שורה לטבלה.

אם הכפילות הייתה למשל בתאריך ובשעה, שהם לא חלק מה-key columns של ה- unique index שלנו – אז כמובן שבאף אחד מהמקרים זה לא היה מכשיל את הפעולה.

חשוב לציין גם שבעוד שהדוגמאות שהראיתי היו כשה- IGNORE_DUP_KEY מוגדר על nonclustered index, הוא יכול להיות מוגדר גם על unique clustered index באותה הצורה (וגם על ה- primary key).

מה עושים כאשר יש הרבה Key Columns?

אם יש לכם הרבה Key Columns שלפיהם אתם רוצים לעשות את היוניקיזציה – אתם עלולים להיתקל באחת משתי בעיות: (1) שמכיוון שיש הרבה עמודות, שלוקחות פוטנציאלית הרבה נפח, האינדקס יהיה גדול מבחינת הנפח שלו (וגם פעולות שונות עליו יהיו יותר איטיות, כי יערבו יותר IO), או ש- (2) תעברו את מגבלת 900 הבתים של ה- Key Column ואז בכלל לא תוכלו להגדיר את האינדקס כמו שאתם רוצים.

במקרה כזה, אפשר להוסיף עמודה, נקרא לה UniqueHash, שבה בזמן ההכנסה תחשבו MD5 hash (למשל) על ה- string שנוצר מ- concat של השדות שאתם רוצים שיהיו ה- key שלכם מופרדים באיזשהו seperator. למשל, נניח שהערכים שלי הם DataA, DataB, DataC,…DataZ אז נחשב hash על DataA~DataB~DataC ונשמור את ה- hash, כאשר את ה- unique index נגדיר על ה- hash בלבד.

היתרון של השיטה הזאת הוא שגודל האינדקס שלנו קטן, כי אם למשל הגודל המצטבר של כל העמודות הוא 300 בתים, אנחנו יכולים לחשב hash שייוצג בתור string בגודל של 32 תווים, כלומר 32 בתים, ולשמור אותו. ככה נחסוך גם בעלויות storage, וגם בעלויות למשל של אורך פעולות התחזוקה על האינדקס (למשל, כמה זמן לוקח REBUILD).

החסרונות בשיטה הזאת הן שאינדקס כזה לא עוזר לנו לשליפות שמתשאלות את הערכים עצמם, כי הם לא כלולים בו. בנוסף, באינדקס כזה מובטח אחוז פרגמנטציה גבוהה מאד באופן קבוע (כל עוד יש בו שינויים).

אם אתם הולכים על השיטה הזאת, חשוב לוודא ששמורים לכם כל הערכים ששימשו אתכם כדי לייצר את ה- hash עבור שורה נתונה. זאת כדי שאם בזמן עתידי כלשהו תחליטו להוסיף ערך, או להוריד ערך, או כל מניפולציה אחרת שמשנה את ה- hash, יהיו לכם כל הנתונים שאתם צריכים לטובת חישוב חוזר.

 

השפעה של IGNORE_DUP_KEY על עבודה מהקוד

אני רוצה להתייחס לכמה אספקטים חשובים שנוגעים לעבודה מקוד מול טבלאות שיש עליהם unique index שמוגדר עליו IGNORE_DUP_KEY = ON.

למעשה, המשמעות של ההגדרה הזאת על אינדקס קצת יותר עמוקה משנראה ממבט ראשון: היא אומרת שיכול להיות שאתם מכניסים שורה, שהיא לא תיכנס ושזאת לא שגיאה.
זה למעשה מצב לא שגרתי כשכותבים קוד, ולכן ברגע שמוסיפים שימוש בזה ברמת ה-DB, צריכים להיות ביקורתיים ולחשוב שלא דפקנו לעצמנו שום הנחות יסוד, או דברים שנשענים על ההנחה (הדי לגיטימית ביום יום) שמבחינתנו הצלחה בהכנסת שורה היא שהשורה באמת שם בסוף.

ADO.NET “קלאסי”

אם אתם משתמשים באובייקטים הבסייים של ADO.NET לטובת עבודה, כלומר עובדים עם SqlCommand / SqlConnection ישירות, אז אם היה מוגדר unique index עם IGNORE_DUP_KEY = OFF (הדיפולט) והייתם מכניסים ערכים כפולים, היה עף SqlException.
אם אתם מכניסים ערכים כפולים לאינדקס שמוגדר עם IGNORE_DUP_KEY = ON, תוכלו להירשם ל-event של SqlConnection שנקרא InfoMessage כדי לקבל את כל הטקסט של ה- messages, ובכלל זה את המידע על ה- duplicate key ignored.
דרך יותר אלגנטית תהיה לשלוף את הערך של @@ROWCOUNT, כלומר מספר השורות שהושפעו ע”י השאילתות הכנסה שלכם. אם הוא קטן ממספר השורות שהכנסתם בטבלה שיש לה אינדקס שמוגדר עם IGNORE_DUP_KEY=ON, הגיוני שלשם “נעלמו” השורות החסרות.

BULK INSERT

אם אתם משתמשים ב- SqlBulkCopy לטובת ההכנסות לטבלה שמוגדרת עם IGNORE_DUP_KEY=ON – הכל יהיה שקוף לכם. פשוט תזרקו את המידע, וייכנס רק מה שצריך להיכנס.

Entity Framework

כאשר אתם מבצעים הכנסת שורות באמצעות Entity Framework(ועדכון, כמובן) אז כשאתם עושים SaveChanges, מאחורי הקלעים Entity Framework בודק שהפעולה של ההוספה/עדכון אכן התבצעה. אם היא לא, נזרק exception מ- SaveChanges ששמו DbUpdateException. כלומר, כשננסה להריץ את הקוד הבא:

using (var data = new check1Entities())

{

    data.FatherSonInfoes.Add(new FatherSonInfo() {FatherID = 1, SonID = 2, DateCreated = DateTime.Now});

    data.FatherSonInfoes.Add(new FatherSonInfo() { FatherID = 1, SonID = 2, DateCreated = DateTime.Now });

    data.SaveChanges();

}

נקבל:

image

ה- Inner Exception פה הוא מסוג OptimisticConcurrencyException.

ננסה להבין קצת יותר מה קרה פה, ע”י זה שנסתכל נראית השאילתה שהוא מריץ מאחורי הקלעים .

חשוב לזכור שכשאנחנו עושים Save Changes, מאחורי הקלעים entity framework לוקח את רשימת השינויים שהוא מכיר (כברירת מחדל הוא עושה change tracking אוטומטי, אם מבטלים את זה [למשל כדי לשפר ביצועים של קריאה למתודה Add], חשוב לסמן את ה- Entity ב- state המתאים [Added/Modified/Deleted…]).
בהסתמך על רשימת השינויים, Entity Framework מבין איזה שאילתות צריך להריץ וגם באיזה סדר. למשל, אם יש לנו אובייקט A ש-“מכיל” הרבה אובייקטים מסוג B (כלומר, ל-B שמור המפתח של A) – אז קודם כל נדרשת הכנסה של A, קבלת ה-ID (המפתח) ואז ההכנסה של כל האובייקטים מסוג B ששוייכו ל-A, תוך שימוש במפתח כדי לעשות את הקישור ברמת ה-DB.

מכאן, למעשה כל ההכנסות קורות אחת אחת. נתבונן רגע איך נראית שאילתה שמתיגרמת להכנסה של אובייקט FatherSonInfo בודד, כפי שעשיתי בקוד:

exec sp_executesql N'INSERT [dbo].[FatherSonInfo]([DateCreated], [FatherID], [SonID])

VALUES (@0, @1, @2)

SELECT [ID]

FROM [dbo].[FatherSonInfo]

WHERE @@ROWCOUNT > 0 AND [ID] = scope_identity()',N'@0 datetime2(7),@1 int,@2 int',@0='2016-10-26 21:37:55.0438727',@1=1,@2=2

 

נשים לב שהוא עושה INSERT, ואז שולף את ה-ID שהוקצה לישות (כי זה ה- key שמתמלא ע”י ה-DB, והוא רוצה למלא את ה- entity בצד של ה- client), אבל עושה את זה רק בתנאי שה- @@ROWCOUNT (כמות השורות שהושפעו) גדול מ-0.

המטרה של Entity Framework בבדיקות הללו ב-WHERE, היא ששיטת העבודה של Entity Framework עצמו מבוססת Optimistic Concurrecy (וכמובן, גם השאילתה יכולה לרוץ ב- Isolation Level מבוסס optimistic concurrency ברמת הדטאבייס). ולמעשה, באמצעות ה- WHERE הזה הוא בודק שההנחות שלו התקיימו בפועל. ששורה שהוא חשב שהתווספה אכן התווספה, או ששורה שהוא רוצה לעדכן באמת נראית טרם תחית העדכון כמו שהוא חושב שהיא נראית (כלומר, שהיא לא השתנתה במקביל “מתחת לרגליים”).

במקרה שיש כישלון, ולמעשה לא מתאמת שום דבר על ה-WHERE כמו במקרה שלנו, הוא מניח שהאופטימיות נכשלה – ושזה מעיד על בעייה. ואז נזרק DbUpdateException שעוטף InnerException מסוג OptimisticConcurrencyException.

עם זאת, במקרה שלנו, אנחנו מראש תכננו שכך זה יהיה. יש שורות שלא מתווספות, וזה מבחינתנו לא תקלה. כלומר, עף פה exception בלי שבאמת היה מצב חריג או מצב שלא רצינו.

אנחנו יכולים לכתוב קוד בסגנון הזה:

using (var data = new check1Entities())

{

    data.FatherSonInfoes.Add(new FatherSonInfo() { FatherID = 1, SonID = 2, DateCreated = DateTime.Now });

    data.FatherSonInfoes.Add(new FatherSonInfo() { FatherID = 1, SonID = 2, DateCreated = DateTime.Now });

    data.FatherSonInfoes.Add(new FatherSonInfo() { FatherID = 91, SonID = 8, DateCreated = DateTime.Now });

    try

    {

        data.SaveChanges();

    }

    catch (DbUpdateException ex)

    {

        var optimisticConcurrencyException = ex.InnerException as OptimisticConcurrencyException;

        if (optimisticConcurrencyException != null)

        {

            if (optimisticConcurrencyException.StateEntries.Any(k => k.State != EntityState.Added))

            {

                throw;

            }

        }

    }

}

שבו אנחנו זורקים את ה- exception מחדש רק אם הוא נוגע ל-entity שהוא לא Added (ואז זה יכול להיות רלוונטי), אבל מכיוון ש- SaveChanges מבצע את הפעולות אחת-אחת, אם הוא נכשל בהכנסה של השורה הראשונה הוא למעשה לא יכניס את השורה השנייה והשלישית.

אפשר לעקוף את זה בכל מיני דרכים, למשל להפריד לחלקים ולהכניס כל פעם entity בודד ולעשות SaveChanges. או לחלופין, במקרה של exception, לעשות לולאה של retries ולהוריד כל פעם את ה- entity הבעייתי. אבל, כאמור, כל אלה הם מעקפים – כי אין באמת פיתרון אלגנטי (לפחות לא כזה שאני מכיר).

הבעייה המהותית פה היא שגם אם הייתה דרך לבטל את המנגנון של הבדיקה של העדכונים – לא היה טוב להחיל את ההתנהגות הזאת באופן גורף, כי יש סיכוי שמתישהו (אפילו בלי שנשים לב) ה- SaveChanges שלנו יגלם בתוכו שינויים נוספים, מעבר ל- INSERT-ים האלה שאנחנו יודעים שגורמים ל- exception הנ”ל, שעבורם השגיאה כן תהיה רלוונטית.

מעבר לזה, ה- exception הזה הוא לא הבעייה היחידה שלנו. כאשר אנחנו מיישמים את הטריק הזה ברמת ה-DB, אנחנו צריכים להיות מודעים לו ברמת הקוד כאשר אנחנו עובדים עם Entity Framework, כי חלק מה-“קסמים” שהוא עושה עבורנו עלולים להיפגע. למשל, אם הישות שאנחנו מוסיפים לטבלה מהסוג הזה מכילה relationship מול טבלאות אחרות שבה לידי ביטוי בתור object graph, גם אם “נבלע” את הדילוג על השורה הספיציפית, נשאלת השאלה מה אנחנו עושים עם כל האובייקטים שהוא מפנה אליהם ולהיפך? במקרה כזה, לא ניתן יהיה לייצג את ה- object graph של ה- entities בתור קשרים בין שורות שונות בטבלאות שלנו – כי למשל אחת מהשורות לא הוכנסה.

למעשה, ברגע שאנחנו מגדירים IGNORE_DUP_KEY = ON, זה מכריח אותנו להתמודד עם מצב שבו אנחנו “מדלגים” על הכנסת שורה מסויימת. וזה אומר שאנחנו לא יכולים להשתמש בחלק מהפיצ’רים של Entity Framework אם entities שייכנסו לטבלה שבה קיים אינדקס כזה.

מה שמביא אותנו להמלצה שלי – אם אתם מיישמים את הטריק הזה עם Entity Framework, תדאגו להבדיל בקוד שלכם את ההכנסה לטבלה המדוברת ביחס לטבלאות אחרות.

עבור פריטים בודדים

אם אתם בד”כ מכניסים פריטים בודדים בכל פעם, אתם יכולים להפריד את זה ליחידת קוד (מתודה/מחלקה/whatever) שאחראית על ההוספה, בצורה שיהיה ברור למי שמשתמש בה שלא מדובר בהוספה סטנדרטית של Entity Framework, כי מכיוון שאנחנו מראש מודעים למצב שיש סיכוי שההכנסה שלנו לא תתבצע ואנחנו רוצים שזה יקרה, זה מגביל אותנו בפיצ’רים מסויימים (כמו relationship שמתמפה ל- object graph) שלא נרצה לעשות בהם שימוש.

במקרה כזה מה שנעשה זה נוודא שאת ההכנסה אנחנו עושים מ- instance נפרד של ה-context שלנו, ואז אנחנו יודעים שרק הפריט המסויים הזה כלול למעשה במשימה שמגולמת ב- SaveChanges, ולכן אם עף exception של OptimisticConcurrencyException, נוכל להתעלם ממנו בבטחה.

static void Main(string[] args)

{

    AddSingleItem(new FatherSonInfo() { FatherID = 1, SonID = 2, DateCreated = DateTime.Now });

    AddSingleItem(new FatherSonInfo() { FatherID = 1, SonID = 2, DateCreated = DateTime.Now });

    AddSingleItem(new FatherSonInfo() {FatherID = 91, SonID = 81, DateCreated = DateTime.Now});

}

 

static void AddSingleItem(FatherSonInfo item)

{

    using (var data = new check1Entities())

    {

        data.FatherSonInfoes.Add(item);

        try

        {

            data.SaveChanges();

        }

        catch (DbUpdateException ex) when (ex.InnerException is OptimisticConcurrencyException)

        {

            //ignore.

        }

    }

}

 

אם אנחנו מכניסים מספר פריטים

במקרה כזה, מומלץ שכבר נעשה שימוש בפיצ’ר ה- Bulk Insert ואז אפשר להשתמש ב- EntityFramework.BulkInsert לטובת ההכנסה, כמו בקוד הבא (אפשר לקרוא עוד על BULK INSERT פה):

using (var data = new check1Entities())

{

    List<FatherSonInfo> items = new List<FatherSonInfo>()

    {

        new FatherSonInfo() {FatherID = 1, SonID = 2, DateCreated = DateTime.Now},

        new FatherSonInfo() {FatherID = 1, SonID = 2, DateCreated = DateTime.Now},

        new FatherSonInfo() {FatherID = 91, SonID = 8, DateCreated = DateTime.Now}

    };

 

    data.BulkInsert(items);

}

 

ובכל אופן – חייבים להיות מודעים לכך ששורות מסויימות יכולות לא להיכנס, ולוודא שאנחנו לא נופלים על זה ברמה הלוגית איפשהו באפליקציה שלנו.

 

בהצלחה!

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *