Как обойти ограничение уникального ключа при обновлении через Entity Framework (используя dbcontext.SaveChanges ()) - PullRequest
2 голосов
/ 29 июня 2019

У меня проблема при обновлении некоторых данных через EF.

Допустим, у меня есть таблица в моей базе данных:

Table T (ID int, Rank int, Name varchar)

У меня есть ограничение уникального ключа для Rank.

Например, у меня есть эти данные втаблица:

Link to example data image

Мой объект C # выглядит примерно так: Person (name, rank), поэтому на внешнем интерфейсе пользователь хочет изменить рангДжо и Марк.

Когда я делаю обновление через EF, я получаю ошибку из-за уникального ключа.
Я подозреваю, что это потому, что dbContext.SaveChanges использует обновление в этом стиле:

UPDATE Table SET rank = 5 where Name = Joe
UPDATE Table SET rank = 1 where Name = Mark

С помощью SQL-запроса я могу выполнить это обновление, выполнив следующее:

Передайте пользовательскую таблицу (ранг, имя) со стороны C # в запрос и затем:

  update T 
  set T.Rank = Updated.Rank
  from Table T 
  inner join @UserDefinedTable Updated on T.Name = Temp.Name

и это не вызывает ограничение уникального ключа

Однако я хочу использовать EF для этой операции, что мне делать?

До сих пор я думал об этих других решениях:

  • Удалить старые записи, добавить "новые" записи из обновленных объектов с помощью EF

  • Удаление уникального ограничения для баз данныхи написание функции C # для выполнения уникального ограничения

  • Просто используйте запрос SQL, как в примере выше, вместо EF

Примечание : структура таблицы и данные, которые я использовал выше, являются лишь примером

Есть идеи?

Ответы [ 2 ]

0 голосов
/ 18 июля 2019

Вы сосредоточились на SQL-стороне, но вы можете сделать то же самое в чистом EF.
В следующий раз это поможет вам предоставить код EF, чтобы мы могли предоставить вам более конкретныйответ.

ПРИМЕЧАНИЕ: не используйте эту логику в EF в тех случаях, когда будут существовать большие наборы данных, поскольку процесс ReOrder загружает все записи в память, этооднако полезно для управления порядковым числом в дочерних или вложенных списках, ограниченных дополнительным предложением фильтра (не для всей таблицы!)

Изолированный процесс ReOrder является хорошим кандидатомсамостоятельно перейти к БД в качестве StoredProc, если вам нужно выполнить уникальную логику ранжирования по всей таблице

Здесь есть два основных варианта (для уникальных значений):

  1. Ранг всегда должен быть последовательным / непрерывным
    • Это упрощает логику вставки и замены, но вам, скорее всего, придется управлять сценариями добавления, вставки, обмена и удаления в коде.
    • Code для перемещения элементов вверх и вниз по рангу очень легко реализовать
    • ДОЛЖЕН управлять удалениями, чтобы пересчитать ранг для всех элементов
  2. Ранг может иметь пробелы (невсе значения являются смежными)
    • Звучит так, как будто это должно быть проще, но для оценки перемещения вверх и вниз по списку необходимо учитывать пробелы.

      Я не буду публиковать код для этого варианта, но учтите, что его обычно сложнее поддерживать.

    • С другой стороны, вам не нужно активно беспокоитьсяуправление удалениями.

Я использую следующую процедуру, когда необходимо управлять порядковым номером:
ПРИМЕЧАНИЕ: Эта процедура не сохраняет изменения,он просто загружает все записи, которые могут быть затронуты, в память, чтобы мы могли правильно обработать новый рейтинг.

public static void ReOrderTableRecords(Context db)
{
    // By convention do not allow the DB to do the ordering. this type of query will load missing DB values into the current dbContext,  
    // but will not replace the objects that are already loaded.
    // The following query would be ordered by the original DB values:
    //      db.Table.OrderBy(x => x.Order).ToList()
    // Instead we want to order by the current modified values in the db Context. This is a very important distinction which is why I have left this comment in place.
    // So, load from the DB into memory and then order:
    //      db.Table[.Where(...optional filter by parentId...)].ToList().OrderBy(x => x.Order)
    // NOTE: in this implementation we must also ensure that we don't include the items that have been flagged for deletion. 
    var currentValues = db.Table.ToList()
                                .Where(x => db.Entry(x).State != EntityState.Deleted)
                                .OrderBy(x => x.Rank);
    int order = 1;
    foreach (var item in currentValues)
        item.Order = order++;
}

Допустим, вы можете уменьшить свой код до функции, которая вставляет новый элемент с определенным рангом.в список или вы хотите поменять ранг двух элементов в списке:

public static Table InsertItem(Context db, Table item, int? Rank = 1)
{
    // Rank is optional, allows override of the item.Rank
    if (Rank.HasValue)
        item.Rank = Rank;

    // Default to first item in the list as 1
    if (item.Rank <= 0)
        item.Rank = 1;

    // re-order first, this will ensure no gaps.
    // NOTE: the new item is not yet added to the collection yet
    ReOrderTableRecords(db);

    var items = db.Table.ToList()
                        .Where(x => db.Entry(x).State != EntityState.Deleted)
                        .Where(x => x.Rank >= item.Rank);
    if (items.Any())
    {
        foreach (var i in items)
            i.Rank = i.Rank + 1;
    }
    else if (item.Rank > 1)
    {
        // special case
        // either ReOrderTableRecords(db) again... after adding the item to the table
        item.Rank = db.Table.ToList()
                            .Where(x => db.Entry(x).State != EntityState.Deleted)
                            .Max(x => x.Rank) + 1;
    }

    db.Table.Add(item);
    db.SaveChanges();
    return item;
}

/// <summary> call this when Rank value is changed on a single row </summary>
public static void UpdateRank(Context db, Table item)
{
    var rank = item.Rank;
    item.Rank = -1; // move this item out of the list so it doesn't affect the ranking on reOrder
    ReOrderTableRecords(db); // ensure no gaps

    // use insert logic
    var items = db.Table.ToList()
                        .Where(x => db.Entry(x).State != EntityState.Deleted)
                        .Where(x => x.Rank >= rank);
    if (items.Any())
    {
        foreach (var i in items)
            i.Rank = i.Rank + 1;
    } 
    item.Rank = rank;

    db.SaveChanges();
}

public static void SwapItemsByIds(Context db, int item1Id, int item2Id)
{
    var item1 = db.Table.Single(x => x.Id == item1Id);
    var item2 = db.Table.Single(x => x.Id == item2Id);

    var rank = item1.Rank;
    item1.Rank = item2.Rank;
    item2.Rank = rank;

    db.SaveChanges();
}

public static void MoveUpById(Context db, int item1Id)
{
    var item1 = db.Table.Single(x => x.Id == item1Id);
    var rank = item1.Rank - 1;
    if (rank > 0) // Rank 1 is the highest
    {
        var item2 = db.Table.Single(x => x.Rank == rank);
        item2.Rank = item1.Rank;
        item1.Rank = rank;
        db.SaveChanges();
    }
}
public static void MoveDownById(Context db, int item1Id)
{
    var item1 = db.Table.Single(x => x.Id == item1Id);
    var rank = item1.Rank + 1;
    var item2 = db.Table.SingleOrDefault(x => x.Rank == rank);
    if (item2 != null) // item 1 is already the lowest rank
    {
        item2.Rank = item1.Rank;
        item1.Rank = rank;
        db.SaveChanges();
    }
}

Чтобы убедиться, что пробелы не вводятся, вы должны позвонить ReOrder после удаление элементов из таблицы, но перед вызовом SaveChanges()

В качестве альтернативы вызовите ReOrder перед каждым из Swap / MoveUp / MoveDown, аналогичным insert.


Имейте в виду, что это очень простоr, чтобы разрешить дублирование значений ранга, особенно для больших списков данных, но ваши бизнес-требования определят, является ли это жизнеспособным решением.

0 голосов
/ 29 июня 2019

Идея - вы можете сделать это как двухэтапную операцию (в виде одной транзакции)

1) установить значения для всех сущностей, которые должны быть обновлены до отрицательных (Джо, -1; Марк -5)

2) установить правильные значения (Joe, 5, Mark 1)


Эквивалент SQL Server:

SELECT 1 AS ID, 1 AS [rank], 'Joe' AS name INTO t
UNION SELECT 2,2,'Ann'
UNION SELECT 3,5,'Mark'
UNION SELECT 4,7,'Sam';

CREATE UNIQUE INDEX uq ON t([rank]);

SELECT * FROM t;

/* Approach 1
UPDATE t SET [rank] = 5 where Name = 'Joe';
UPDATE t SET [rank] = 1 where Name = 'Mark';

Cannot insert duplicate key row in object 'dbo.t' with unique index 'uq'.
The duplicate key value is (5). Msg 2601 Level 14 State 1 Line 2
Cannot insert duplicate key row in object 'dbo.t' with unique index 'uq'.
The duplicate key value is (1).
*/

BEGIN TRAN
-- step 1

UPDATE t SET [rank] = -[rank] where Name = 'Joe';
UPDATE t SET [rank] = -[rank] where Name = 'Mark';


-- step 2
UPDATE t SET [rank] = 5 where Name = 'Joe'
UPDATE t SET [rank] = 1 where Name = 'Mark';
COMMIT;

ДБ <> Fiddle demo

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...