EFCore PostgreSQL: Ошибка ограничения первичного ключа
Я использую:
- PostgreSQL 13.0, compiled by Visual C++ build 1914, 64-bit;
- Npgsql.EntityFrameworkCore.PostgreSQL, Version=3.1.4.0
Модели:
public class BaseModel
{
[Key]
public long Id { get; set; }
...
}
public class UserModel : BaseModel
{
[Required]
[StringLength(ModelValidationConstants.User.LOGIN_STRING_LENGTH)]
public string Login { get; set; }
[StringLength(ModelValidationConstants.User.PASSWORD_STRING_LENGTH)]
public string Password { get; set; }
[StringLength(ModelValidationConstants.User.PASSWORD_SALT_STRING_LENGTH)]
public string PasswordSalt { get; set; }
[Required]
public UserOriginEntity UserOrigin { get; set; }
public string Description { get; set; }
...
}
Конфигурация модели:
public class UserModelConfiguration : IEntityTypeConfiguration<UserModel>
{
public void Configure(EntityTypeBuilder<UserModel> builder)
{
builder.ToTable("user");
builder.HasIndex(entity => entity.Login)
.IsUnique(false);
builder.HasIndex(entity => new { entity.Login, entity.Password })
.IsUnique(true);
builder.HasIndex(entity => new { entity.Login, entity.UserOrigin })
.IsUnique(true);
}
}
После добавления миграций и обновления БД, получилась следующая таблица:
CREATE TABLE tsdb."user"
(
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
login character varying(100) COLLATE pg_catalog."default" NOT NULL,
password character varying(100) COLLATE pg_catalog."default",
passwordsalt character varying(128) COLLATE pg_catalog."default",
userorigin integer NOT NULL,
description text COLLATE pg_catalog."default",
CONSTRAINT pk_user PRIMARY KEY (id)
)
TABLESPACE pg_default;
Когда выполняется следующий код:
using(var context = new PostgreContext())
{
if (context.Users.FirstOrDefault(u => u.Login == TEST_USER_LOGIN) != null)
return;
context.Users.Add(new UserModel()
{
Login = TEST_USER_LOGIN,
Password = "TestPassword",
PasswordSalt = "TestPasswordSalt",
UserOrigin = Entities.Enums.UserOriginEntity.InternalSystem
});
context.SaveChanges();
}
Я получаю exception: PostgresException: 23505: : повторяющееся значение ключа нарушает ограничение уникальности "pk_user". Такое поведение наблюдается на "свежей" БД. Так же в БД вставлены начальные данные, в частности пользователь-админ. Не могу понять что не так.
Ответы (2 шт):
в БД вставлены начальные данные
вставлены как? Ну, я и так знаю, что id вы сами при этом какие-то выбрали и использовали. В частности, использовали 1 вместо механизма IDENTITY, что особенно важно, объявленного именно как GENERATED BY DEFAULT.
melkij=> create temp table foo (
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (
INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1
) , val int);
CREATE TABLE
melkij=> insert into foo (id, val) values(1,1);
INSERT 0 1
melkij=> insert into foo (val) values(2);
INSERT 0 1
melkij=> table foo;
id | val
----+-----
1 | 1
1 | 2
(2 строки)
Иначе говоря - а кто будет сообщать этому sequence о том, что какие-то значения вы присвоили самостоятельно? Никто. Вы (вернее ваш фреймворк) попросили BY DEFAULT, именно это поведение и получили, новое значение последовательности генерируется только если при добавлении строки не было указано явное значение.
Вам нужно изменить значение следующего генерируемого значения:
select max(id)+1 from tsdb."user";
ALTER TABLE tsdb."user" ALTER COLUMN id RESTART WITH /* результат предыдущего запроса */;
Если бы вы попросили GENERATED ALWAYS AS IDENTITY - то получили бы любезную ошибку при попытке делать странное:
melkij=> create temp table foo (id bigint NOT NULL GENERATED always AS IDENTITY (
INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1) , val int);
CREATE TABLE
melkij=> insert into foo (id, val) values(1,1);
ERROR: cannot insert into column "id"
ПОДРОБНОСТИ: Column "id" is an identity column defined as GENERATED ALWAYS.
ПОДСКАЗКА: Use OVERRIDING SYSTEM VALUE to override.
А GENERATED BY DEFAULT и должен себя ввести так как ведёт.
Решение данной проблемы рассматривается тут:github.com/npgsql/efcore.pg/issues/367. Предлагается два пути решения:
- Для сидирования данных использовать отрицательные идентификаторы, например:
modelBuilder.Entity<UserModel>().HasData(new UserModel[]
{
new UserModel()
{
Id = -1,
Login = "admin",
PasswordSalt = "admin salt",
Password = "admin password",
}
});
- Либо воспользоватся .HasIdentityOptions(startValue:<требуемое начальное значение>), где "требуемое начальное значение" равно "количество сидируемых данных"+1, например:
modelBuilder.Entity<UserModel>().Property(u => u.Id)
.UseIdentityAlwaysColumn()
.HasIdentityOptions(startValue:2);