Страницы

Поиск по вопросам

суббота, 21 декабря 2019 г.

Настройка отношения one to zero-or-one совместно с one-to-many

#c_sharp #aspnet_mvc #entity_framework


Есть работающее приложение/вебсайт, в котором можно в паспорт автомобиля подгружать
картинки, одна из картинок считается "заглавной". Первая загруженная картинка становится
заглавной, впоследствии можно поменять.

Соответственно, были следующие классы:

public class Car
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }
}


и

public class CarImage
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    [Required]
    public int CarID { get; set; }

    [ForeignKey("CarID")]
    public virtual Car Car { get; set; }

    public string FileName { get; set; }

    public bool IsPrimaryImage { get; set; }
}


И как-то знакомые предложили мне попробовать отрефакторить это следующим образом:
не помечать заглавную картинку как IsPrimaryImage = true, а вынести в свойство PrimaryImage: 

public class CarImage
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    [Required]
    public int CarID { get; set; }

    [ForeignKey("CarID")]
    public virtual Car Car { get; set; }

    public string FileName { get; set; }
}


и

public class Car
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }

    public int? PrimaryImageID { get; set; }

    public virtual CarImage PrimaryImage { get; set; }
}


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

Сначала я думал обойтись только data anntotation (не очень люблю fluent API, когда
определения где-то отдельно от таблиц), есть мой топик на en-so, где я пытался расставить
атрибуты.

Однако проблемы начались когда я начал пытаться сохраниться в базу:

using (var db = new DataContext())
{
    var car = db.Car.First(x => x.ID == CarID);

    var image = new CarImage
    {
        CarID = car.ID,
        //Car = car,
        IsDeleted = false,
    };

    db.CarImages.Add(image);

    db.SaveChanges();

    if (isFirstImage)
    {
        car.PrimaryImage = image;
        car.PrimaryImageID = image.ID;
    }

    db.SaveChanges();
}


(Ошибок было много разных, могу привести, но как мне кажется, что там нет ничего
полезного для дальнейшего обсуждения)

После чего я решил оставить эти попытки и использовать fluent API:

modelBuilder.Entity()
    .HasRequired(x => x.Car)
    .WithOptional(x => x.PrimaryImage)
    .Map(x => x.MapKey("PrimaryImageID"));


И... тоже как ни странно стал получать те или иные ошибки. Тоже не привожу, т.к.
в этот момент я понял, что нужно разбираться основательно и сначала.

Итак, для начала разберёмся с отношениями в базе.

У одного автомобиля может быть несколько изображений -- это очевидно, связь one-to-many.

Но, с другой стороны, у каждого автомобиля может быть одна заглавная картинка --
это очевидно связь one to zero-or-one.

Как правильно прописать эти связки в моих классах?

В контроле у меня собственно всего лишь две вьюхи:


нужно вывести список машин с заглавными картинками (или картинкой-заглушкой, если
картинок ещё нет);
нужно вывести карточку машины со списком параметров и всеми картинками, которые относятся
к этой машине.


И, как раз в первой будут намного лучше план запроса к БД, если сделать рефакторинг.

Пробовал такой вариант решения:

public class Car
{
    [Key]
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }

    public int? PrimaryImageID { get; set; }

    [ForeignKey("PrimaryImageID")]
    public virtual CarImage PrimaryImage { get; set; }

    public virtual ICollection AllImages { get; set; }
}


и

public class CarImage
{
    [Key]
    [ForeignKey("Car")]
    public int ID { get; set; }

    [Required]
    public int CarID { get; set; }

    [ForeignKey("CarID")]
    public virtual Car Car { get; set; }

    public string FileName { get; set; }
}


я описывал в чате, как я к нему шёл, там же можно найти проблемы, с которыми я столкнулся
и пока не смог решить.
    


Ответы

Ответ 1



Вы слишком все усложнили. Атрибут [ForeignKey("Car")] public int ID { get; set; } Т.е. ок, вы скопировали его из статьи про one-to-one or zero - но нужно понимать, за счет чего достигается эта связь. По сути, вы просто говорите "ID картинки должен быть равен ID существующей машины". Достаточно очевидно, что при этом у машины может быть не больше одной картинки. Вы физически не можете записать в базу картинку с ID = 2, если в базе нет машины с ID = 2. Никак. Вы явно обозначили это в своей модели. В вашей модели в базе не может быть ровно одна машина и две картинки - то ID у картинок должны быть равны, и вы просто не сможете их различить. И тут же вы пытаетесь заявить "но у машины (как-то!) может быть несколько картинок!" - что делает вашу модель противоречивой. И EF умирает. Не усложняйте. Просто уберите лишнее - лишние атрибуты, лишние вызовы SaveChanges, возможность достать машину из CarImage (можно оставить, но скорее всего она не нужна) и оставить работу через сущности - все заработает именно так, как вы хотите. Вот минимальный рабочий пример: public class Car { public int ID { get; set; } public virtual CarImage PrimaryImage { get; set; } public virtual ICollection AllImages { get; set; } } public class CarImage { public int ID { get; set; } } public class Model1 : DbContext { public Model1() : base("name=Model1") { } public DbSet Cars { get; set; } } using (var db = new Model1()) { var car = db.Cars.First(x => x.ID == 1); bool isFirstImage = !car.AllImages.Any(); var image = new CarImage(); car.AllImages.Add(image); if (isFirstImage) { car.PrimaryImage = image; } db.SaveChanges(); } По желанию можно вернут на место все PrimaryImageId, CarID, Car DbSet - все будет работать точно так же (т.е. вы этим сможете явно сделать это позволит вам сделать связь CarImage -> Car обязательной, но сейчас она все равно практически обязательна - т.к. у CarImage нет Car - вы не можете его занулить :) )

Ответ 2



Классы, приведенные в вопросе выглядят правильными, и должны по идее решать поставленную задачу. Следует только на уровне приложения следить за тем, чтобы PrimaryImage у разных машин не ссылался на один и тот же CarImage, и чтобы этот CarImage в свою очередь ссылался именно на эту машину. Такую логику хорошо бы поместить прямо в свойство PrimaryImage (Entity Framework позволяет так делать). private CarImage fPrimaryImage; public virtual CarImage PrimaryImage { get { return fPrimaryImage; } set { if (fPrimaryImage == value) return; fPrimaryImage = value; if (fPrimaryImage != null) fPrimaryImage.Car = this; } Можно сделать еще такую вещь - добавить таблицу в которой хранить ссылки на Car и CarImage. В этой таблице хранить PrimaryImage всех машин. При таком подходе, будет сложнее запутаться при объявлении one to zero-or-one связи. public class Car { public virtual PrimaryImage PrimaryImage {get;set;} } public class CarImage { [Required] public virtual Car Car { get;set;} } public class PrimaryImage { [Required] public virtual Car Car {get;set;} [Required] public virtual CarImage Image {get;set;} }

Комментариев нет:

Отправить комментарий