Как закодировать ViewModel с ключом для удобочитаемых значений вместо идентификаторов

#sql #asp.net-mvc #entity-framework

#sql #asp.net-mvc #entity-framework

Вопрос:

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

У меня есть 2 таблицы, одна для стран и одна для штатов. Обычно я бы использовал идентификатор для первичного ключа, но мой клиент попросил, чтобы я сделал ассоциации удобочитаемыми. Например, вместо пользователя, имеющего значение для своей страны как 1, они хотят, чтобы оно было «США». Вместо значения 14 для их состояния они видят «CA» или «TX» и т.д.

Мой Migration Configuration.cs содержит начальную процедуру для добавления стран и штатов.

Когда я запускаю Update-Database, я получаю следующую ошибку:

Запуск начального метода. System.Data.Entity.Инфраструктура.Исключение DbUpdateException: невозможно определить основной конец ‘XXX_XXX_XXX.Models.Связь State_Country. Несколько добавленных объектов могут иметь один и тот же первичный ключ. —> System.Data.Entity.Core.UpdateException: невозможно определить основной конец ‘XXXX_XXXX_XXXX.Модели.Связь State_Country. Несколько добавленных объектов могут иметь один и тот же первичный ключ.

Моя ViewModel содержит:

 namespace XXXX_XXXX_XXXX.Models
{
    public class Country
    {
        [Key]
        public string CountryCode { get; set; }

        public int CountryId { get; set; }
        public string CountryName { get; set; }

        public virtual ICollection<State> States { get; set; }
    }

    public class State
    {
        [Key]
        public string StateCode { get; set; }

        public int StateId { get; set; }
        public string StateName { get; set; }

        [ForeignKey("Country")]
        public string CountryCode { get; set; }

        public virtual Country Country { get; set; }
    }
}
  

Моя начальная процедура выглядит так:

 protected override void Seed(ProgramDbContext context)
{
    // Initialize Countries
    context.Countries.AddOrUpdate(c => c.CountryCode, 
        new Country { CountryId = 1, 
                      CountryName = "United States", 
                      CountryCode = "USA" });

    context.Countries.AddOrUpdate(c => c.CountryCode, 
        new Country { CountryId = 2, 
                      CountryName = "Canada", 
                      CountryCode = "CAN" });

    {  ... }

    // Initialize States
    context.States.AddOrUpdate(c => c.StateId, 
        new State { StateId = 1, 
                    StateName = "Iowa", 
                    StateCode = "IA", 
                    CountryCode = "USA" });

    context.States.AddOrUpdate(c => c.StateId, 
        new State { StateId = 2, 
                    StateName = "Illinois", 
                    StateCode = "IL", 
                    CountryCode = "USA" });

    context.States.AddOrUpdate(c => c.StateId, 
        new State { StateId = 3, 
                    StateName = "California", 
                    StateCode = "CA", 
                    CountryCode = "USA" });

    {  ... }

}
  

I DO understand that I am getting the error because CountryCode doesn’t contain unique values for every column. If I set the CountryId to the Key and change the Foreign Key for States to CountryId I don’t get the error, but then it seems I have to perform a lookup on the Id fields to get the CountryCode whenever I need it.

I also foresee that it’s possible and even likely that the StateCode will eventually have a duplicate, though it doesn’t currently.

Is it possible to have 2 [Key] attributes in a single table using ViewModel coding? I couldn’t get that to work.

Do I need to place the [Key] attribute on the Id columns of each of these tables and then look-up the CountryCode and StateCode each time I need to update a user profile or create a dropdown list? Btw, I’ve created custom Identity fields for CountryCode and StateCode.

How should my ViewModel for Country and State be coded so that I can conveniently have my user profiles show «CA, USA» instead of 3, 1 — for example?

EDIT

@David offered to look at my View code for the Country and State dropdowns. I populate the State dropdown using jQuery

I populate Model.Countries in my controller with:

 public IEnumerable<SelectListItem> GetCountryList()
{
    var countries = new SelectList(dbLocations.Countries, "CountryCode", "CountryName").ToList();
    countries.Insert(0, (new SelectListItem { Text = "Select Country", Value = "-1" }));
    countries.Insert(1, (new SelectListItem { Text = "United Sates", Value = "54" }));
    countries.Insert(2, (new SelectListItem { Text = "Canada", Value = "13" }));
    countries.Insert(3, (new SelectListItem { Text = "Mexico", Value = "12" }));
    countries.Insert(4, (new SelectListItem { Text = "Brazil", Value = "36" }));
    countries.Insert(5, (new SelectListItem { Text = "------------------------", Value = "-1" }));

    IEnumerable<SelectListItem> countrylist =
        countries.Select(m => new SelectListItem()
        {
            Text = m.Text,
            Value = m.Value
        });

    return countrylist;
}
  

An example of how I use in a View:

 <div class="form-group">
    @Html.LabelFor(m => m.CountryId, new { @class = "col-md-3 control-label" })
    <div class="col-md-9">
        @Html.DropDownListFor(
        x => x.CountryId,
        new SelectList(Model.Countries, "Value", "Text"), 
        new { @class = "form-control" })
    </div>
</div>

<div class="form-group">
    @Html.LabelFor(m => m.StateId, new { @class = "col-md-3 control-label" })
    <div class="col-md-9">
        <div id="stateid_select">
            <select id="StateId" class="form-control" name="StateId"></select>
        </div>
    </div>
</div>
  

jQuery для изменения страны (извлекает состояния):

 <script type="text/javascript">

// Populate State/Province dropdown on Country select
$(function () {
     $('#CountryCode').change(function () {

         $.getJSON('/Account/StateList/'   $('#CountryCode').val(), function (data) {

             var cnt = 0;
             var items = '<option>Select a State/Province</option>';
             $.each(data, function (i, state) {
                items  = "<option value='"   state.Value   "'>"   state.Text   "</option>";
                cnt = cnt   1;
            });

            // Hide SELECT and show TEXT field if no state/provinces to choose from.
            // Else show SELECT and hide TEXT field.
            if (!cnt == 0) {
                $('#stateprovince_select select').html(items).show().attr("disabled", false).removeClass("hide");
                $('#stateprovince_text input[type="text"]').hide().attr("disabled", true).addClass("hide");
             } else {
                $('#stateprovince_select select').html('').hide().attr("disabled", true).addClass("hide");
                $('#stateprovince_text input[type="text"]').show().attr("disabled", true).removeClass("hide");
                $('#StateProvinceCode').hide();
            }
        });
    });
});
</script>
  

jQuery вызывает эту процедуру в контроллере:

 [AllowAnonymous]
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult StateList(int countryid)
    {
        var state = from s in dbLocations.States
                    where s.CountryId == countryid
                    select s;
        return Json(new SelectList(state.ToArray(), "StateId", "StateName"), JsonRequestBehavior.AllowGet);
    }
  

Может быть больше, чем вам нужно было видеть. Я внес некоторые изменения, чтобы отразить изменения ModelView, хотя я еще не смог запустить приложение — все еще внося изменения. Я знаю, что jQuery нуждается в некоторой очистке. 😉

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

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

2. В целом я понимаю, что вы советуете, просто не знаю, как это сделать.

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

4. Я отредактировал вопрос и включил еще немного своего кода.

Ответ №1:

Вы должны оставить Key индекс в столбце Id, но добавить уникальное ограничение к CountryCode . Это имеет следующие преимущества:

  1. Все ваши отношения используют целочисленные столбцы идентификаторов, которые будут работать лучше, чем объединение по строкам.

  2. Код страны теперь не будет допускать повторяющихся записей.

  3. Код страны может быть показан пользователям и скрывает тот факт, что ваша база данных связывается с целыми числами.

Итак, теперь ваши модели выглядят так:

 public class Country
{
    [Key]
    public int Id { get; set; }

    [Index("UX_Country_CountryCode", IsUnique = true)]
    [MaxLength(10)]
    public string CountryCode { get; set; }

    public string CountryName { get; set; }

    public virtual ICollection<State> States { get; set; }
}

public class State
{
    [Key]
    public int Id { get; set; }

    [Index("UX_State_StateCode", IsUnique = true)]
    [MaxLength(10)]
    public string StateCode { get; set; }

    public string StateName { get; set; }

    [ForeignKey("Country")]
    public int CountryId { get; set; }

    public virtual Country Country { get; set; }
}
  

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

 public class State
{
    [Key]
    public int Id { get; set; }

    [Index("UX_State_StateCodeCountryId", IsUnique = true, Order = 1)]
    [MaxLength(10)]
    public string StateCode { get; set; }

    public string StateName { get; set; }

    [ForeignKey("Country")]
    [Index("UX_State_StateCodeCountryId", IsUnique = true, Order = 0)]
    public int CountryId { get; set; }

    public virtual Country Country { get; set; }
}
  

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

1. Я не уверен, насколько важно ограничение для кода состояния — я не думаю, что могу указать это, поскольку страна может иметь тот же двухбуквенный код для состояния, что и другая страна. Я вижу, что вы сделали. Спасибо.

2. Я внес изменения, чтобы помочь.

3. Я получаю эту ошибку при запуске Update-Database: Column 'CountryCode' in table 'dbo.Countries' is of a type that is invalid for use as a key column in an index.

4. Ах, поставьте на него атрибут максимальной длины.

5. Понял, MaxLength(3) по коду страны и MaxLength(2) по коду состояния. Теперь я вижу эту ошибку: Validation failed for one or more entities. See 'EntityValidationErrors' property for more details. Я подозреваю, что сейчас есть проблема в начальном коде?