EF Core 2.1 пытается включить поле первичного ключа в запрос INSERT при добавлении в DbContext и сохранении - PullRequest
0 голосов
/ 13 сентября 2018

В ASP .Net Core 2.1 Web API (с базой данных MySQL и использованием Pomelo), когда я добавляю новую сущность в базу данных в одном из моих действий контроллера, если сущность, полученная API от потребляющегоу клиента есть значение в первичном ключе, похоже, что EF Core пытается добавить первичный ключ вместо того, чтобы позволить базе данных дать ему новое значение.

Итак ... в базе данных у меня естьтаблица с именем person, имеющая целочисленное поле с именем id, для которого установлено значение PRIMARY KEY и AUTO-INCREMENT.

Модель:

public partial class Person
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>(entity =>
    {
        entity.ToTable("person");

        entity.HasKey(e => e.Id);

        entity.Property(e => e.Id)
            .HasColumnName("id")
            .HasColumnType("int(11)");

        entity.Property(e => e.Name)
            .HasColumnName("name")
            .HasColumnType("varchar(45)");

        entity.Property(e => e.Surname)
            .HasColumnName("surname")
            .HasColumnType("varchar(45)");
    }
}

Действие контроллера

// POST: api/Person
[HttpPost]
public async Task<IActionResult> AddPerson([FromBody]Person person)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    _context.Person.Add(person);
    await _context.SaveChangesAsync();
    return CreatedAtAction("GetPerson", new { id = person.Id }, person);
}

Если я не удаляю идентификатор пользователя, прежде чем пытаться вставить егов базу данных (то есть person.Id = null), тогда я получаю исключение с жалобой на дубликат первичного ключа.Это нормальное поведение EF Core?Или я что-то не так делаю?

1 Ответ

0 голосов
/ 13 сентября 2018

Честно говоря, да, вы делаете что-то не так. По целому ряду причин вы никогда не должны сохранять экземпляр, созданный из пользовательского ввода (то есть экземпляр Person, передаваемый в ваше действие и создаваемый из тела запроса сообщения), непосредственно в вашу базу данных. Одна из таких причин заключается в том, что это приводит к хаосу с ORM, такими как EF, которые используют отслеживание сущностей для оптимизации запросов.

Просто этот Person экземпляр здесь не отслеживается - EF ничего не знает об этом. Затем вы используете Add, чтобы добавить его в свой контекст, что сигнализирует EF, чтобы начать отслеживать его как новую вещь. Когда вы позже сохраняете, EF, то покорно выдает оператор вставки, но поскольку в эту вставку входит идентификатор, вы получаете конфликт первичного ключа. Вместо этого вы хотели, чтобы EF сделал обновление, но он не знает, что должен.

Есть способы, которыми вы можете технически это исправить. Например, вы можете использовать Attach вместо Add. Это просто слепо говорит EF, что это то, что он должен отслеживать, не обязательно сообщая, что он должен что-то делать с ним. Если вы внесете какие-либо изменения в этот экземпляр после того, как он будет отслежен, EF изменит его на «измененный», и вы получите инструкцию обновления, которая будет выдана при сохранении. Однако, если вы не вносите какие-либо изменения, а просто сохраняете их напрямую, вам также необходимо явно установить его состояние на «измененный», иначе EF по существу ничего не сделает. Приятно то, что если вы изменяете состояние неотслеживаемой сущности, EF автоматически присоединяет его для отслеживания указанного состояния, поэтому вам не нужно делать Attach вручную. Если коротко, вы можете очистить исключение, просто заменив строку Add на:

_context.Entry(person).State = EntityState.Modified;

Однако это может вызвать проблемы, если вы попытаетесь полностью добавить нового человека. Более серьезная проблема, с которой вы столкнулись, заключается в том, что у вас есть одно действие, выполняющее двойной долг Согласно REST, POST не может быть воспроизведен и должен выполняться только для тех ресурсов, которые являются идемпотентными. Проще говоря, вы POST только к ресурсу, подобному /api/person (а не к чему-то вроде /api/person/1, и каждый раз, когда вы делаете это , должен быть создан новый человек . Для обновления вы должны сделать запрос к этому фактическому ресурсу, то есть /api/person/1, и вместо этого HTTP-глагол должен быть PUT. Один и тот же запрос PUT к одному и тому же ресурсу всегда будет иметь один и тот же результат, что имеет место при обновлении определенного ресурса.

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

[HttpPost("")]
public async Task<IActionResult> AddPerson([FromBody]Person person)

[HttpPut("{id}")]
public async Task<IActionResult> UpdatePerson(int id, [FromBody]Person person)

Наконец, даже при всем этом, сохранение параметра person напрямую ставит слишком большое доверие пользователю при выполнении обновления. Может быть любое количество свойств, которые конечный пользователь не может изменять с помощью обновления (например, что-то вроде «созданной» даты), но они могут это делать, когда вы делаете это. В некотором смысле хуже, даже если пользователь не является злонамеренным, вы по-прежнему полагаетесь на то, что он отправит все данных для этого объекта. Например, если у вас действительно было свойство созданной даты, но пользователь не публикует его со своим обновлением (честно, почему будет , вы публикуете созданную дату вместе с запросом на обновление ресурса), тогда это приведет к очистке этого свойства. Если есть значение по умолчанию, оно будет возвращено к этому, а если нет, вы можете получить исключение при сохранении, если столбец NOT NULL.

Короче говоря, это не очень хорошая идея. Вместо этого используйте модель представления, DTO или подобное. Этот класс должен содержать только те свойства, которые вы хотите разрешить пользователю изменять или вообще влиять на создание. Затем, в случае обновления, вы извлекаете ресурс из базы данных и отображаете на него значения из вашего экземпляра param. Наконец, вы сохраняете версию из базы данных обратно в базу данных. Это гарантирует, что 1) пользователь не может изменять то, что вы явно не разрешаете, 2) пользователю нужно только публиковать то, что он действительно хочет изменить, и 3) объект будет правильно отслеживаться, а EF будет корректно выдавать оператор обновления при сохранении.

...