实体框架核心跟踪问题:“无法跟踪实体类型‘产品’的实例”

问题描述 投票:0回答:1

我尝试在 ASP.NET Core 应用程序中创建多个

Product
实体时,在 Entity Framework Core 中遇到跟踪问题。我收到的错误消息是:
System.InvalidOperationException:无法跟踪实体类型“Product”的实例,因为已跟踪键值“{Brand:hteye,Style:wqefwef,ColorName:Red}”的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

这就是我想要实现的目标:

  • 根据
    Product
    Colors
    的不同组合创建多个
    Sizes
    实体。
  • 在添加新的
    Product
    之前,我会检查具有相同
    Brand
    Style
    ColorName
    Size
    的产品是否已存在。
  • 如果产品已经存在,我应该跳过创建它并返回
    BadRequest

以下是相关代码:

产品创建模型:

public class ProductCreateModel
{
    public string Brand { get; set; }
    public string Style { get; set; }
    public string ColorCode { get; set; }
    public List<string> Colors { get; set; } = new List<string>();
    public List<string> Sizes { get; set; } = new List<string>();
    public string Description { get; set; }
    public string Specs { get; set; }
    public string Vendor { get; set; }
    public string Keywords { get; set; }
    public string Features { get; set; }
    public string ImageName { get; set; }
    public string Image { get; set; }
    public string Thumbnail { get; set; }
    public string PmsValue { get; set; }
    public decimal? ListPrice { get; set; }
    public bool AutoOrder { get; set; }
    public List<string> Divisions { get; set; } = new List<string>();
    public Dictionary<string, List<string>> Categories { get; set; } = new Dictionary<string, List<string>>();
    public string PriceCategory { get; set; }
    public string GenderAge { get; set; }
    public string SleeveLength { get; set; }
    public string Fabric { get; set; }
    public string StyleType { get; set; }
    public string Weight { get; set; }
    public bool NewProduct { get; set; }
    public string IndigoCategory { get; set; }
    public bool Public { get; set; }
    public int? ColorCount { get; set; }
    public int? CollegiateRank { get; set; }
    public int? CorporateRank { get; set; }
    public int? IndigoRank { get; set; }
    public string RealCampusCategory { get; set; }
    public int? RealRank { get; set; }
}

产品实体:

public class Product
{
    [Key]
    public int Id { get; set; }
    public string Brand { get; set; }
    public string Style { get; set; }  // This is the Name of the product
    public string ColorCode { get; set; }
    public string ColorName { get; set; }
    public string Size { get; set; }
    public string Description { get; set; }
    public string Specs { get; set; }
    public string Vendor { get; set; }
    public string Keywords { get; set; }
    public string Features { get; set; }
    public string ImageName { get; set; }
    public string Image { get; set; }
    public string Thumbnail { get; set; }
    public string PmsValue { get; set; }
    public decimal? ListPrice { get; set; }  // This is the Price of the product
    public string AutoOrder { get; set; }
    public string Divisions { get; set; }
    public string CollegiateCategory { get; set; }
    public string CorporateCategory { get; set; }
    public string PriceCategory { get; set; }
    public string GenderAge { get; set; }
    public string SleeveLength { get; set; }
    public string Fabric { get; set; }
    public string StyleType { get; set; }
    public string Weight { get; set; }
    public string NewProduct { get; set; }
    public string IndigoCategory { get; set; }
    public bool Public { get; set; }
    public int? ColorCount { get; set; }
    public int? CollegiateRank { get; set; }
    public int? CorporateRank { get; set; }
    public int? IndigoRank { get; set; }
    public string RealCampusCategory { get; set; }
    public int? RealRank { get; set; }
}

创建产品方法:

[HttpPost("Products/CreateProduct")]
public async Task<IActionResult> CreateProduct(ProductCreateModel productModel)
{
    try
    {
        foreach (var color in productModel.Colors)
        {
            foreach (var size in productModel.Sizes)
            {
                var existingProduct = await _context.Products
                    .FirstOrDefaultAsync(p => p.Brand == productModel.Brand
                        && p.Style == productModel.Style
                        && p.ColorName == color
                        && p.Size == size);

                if (existingProduct != null)
                {
                    return BadRequest($"Product with Brand '{productModel.Brand}', Style '{productModel.Style}', Color '{color}', and Size '{size}' already exists.");
                }

                var newProduct = new Product
                {
                    Brand = productModel.Brand,
                    Style = productModel.Style,
                    ColorCode = productModel.ColorCode,
                    ColorName = color,
                    Size = size,
                    Description = productModel.Description,
                    Specs = productModel.Specs,
                    Vendor = productModel.Vendor,
                    Keywords = productModel.Keywords,
                    Features = productModel.Features,
                    ImageName = productModel.ImageName,
                    Image = productModel.Image,
                    Thumbnail = productModel.Thumbnail,
                    PmsValue = productModel.PmsValue,
                    ListPrice = productModel.ListPrice,
                    AutoOrder = productModel.AutoOrder.ToString(),
                    Divisions = string.Join(",", productModel.Divisions),
                    CollegiateCategory = productModel.Categories.ContainsKey("Collegiate") ? string.Join(",", productModel.Categories["Collegiate"]) : null,
                    CorporateCategory = productModel.Categories.ContainsKey("Corporate") ? string.Join(",", productModel.Categories["Corporate"]) : null,
                    PriceCategory = productModel.PriceCategory,
                    GenderAge = productModel.GenderAge,
                    SleeveLength = productModel.SleeveLength,
                    Fabric = productModel.Fabric,
                    StyleType = productModel.StyleType,
                    Weight = productModel.Weight,
                    NewProduct = productModel.NewProduct.ToString(),
                    IndigoCategory = productModel.IndigoCategory,
                    Public = productModel.Public,
                    ColorCount = productModel.ColorCount,
                    CollegiateRank = productModel.CollegiateRank,
                    CorporateRank = productModel.CorporateRank,
                    IndigoRank = productModel.IndigoRank,
                    RealCampusCategory = productModel.RealCampusCategory,
                    RealRank = productModel.RealRank
                };

                _context.Products.Add(newProduct);
            }
        }

        await _context.SaveChangesAsync();
        return Ok("Products created successfully.");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Error creating products. Product model: {productModel}");
        return StatusCode(500, "Internal server error");
    }
}

我尝试遵循各种解决方案,例如分离实体或使用 .AsNoTracking() 但似乎没有任何效果。任何解决此问题的帮助或指示将不胜感激。
前端代码(React Admin):

const dataProvider: DataProvider = {
    create: async (resource: string, params: CreateParams): Promise<CreateResult<any>> => {
        const url = resource === 'Products' ? `Resources/Products/CreateProduct` : `Resources/${resource}`;
        try {
            const response = await httpClient.post(url, params.data);
            return { data: { ...params.data, id: response.data.id } };
        } catch (error) {
            console.error("Error in create:", error); // Debug output
            throw error;
        }
    },
};

创建组件(React Admin):

import React, { useState, useEffect, useCallback } from 'react';
import {
  Create,
  SimpleForm,
  TextInput,
  SelectInput,
  ImageInput,
  ImageField,
  BooleanInput,
  useDataProvider,
  useNotify,
  useRedirect,
} from 'react-admin';
import { Box, TextField, FormLabel, FormGroup, FormControlLabel, Checkbox, Button, Typography, Paper, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Cancel';
import { styled } from '@mui/material/styles';

const sizes = [
  { id: 'XS', name: 'XS' },
  { id: 'S', name: 'S' },
  { id: 'M', name: 'M' },
  { id: 'L', name: 'L' },
  { id: 'XL', name: 'XL' },
  { id: '2XL', name: '2XL' },
  { id: '3XL', name: '3XL' },
];

const divisions = [
  { id: 'Corporate', name: 'Corporate' },
  { id: 'Campus', name: 'Campus' },
  { id: 'Indigo', name: 'Indigo' },
];

interface Color {
  name: string;
  thumbnail?: File | null;
  thumbnailPreview?: string;
}

const StyledPaper = styled(Paper)(({ theme }) => ({
  padding: theme.spacing(4),
  backgroundColor: theme.palette.background.paper,
  borderRadius: theme.shape.borderRadius,
  maxWidth: '900px',
  margin: 'auto',
  marginTop: theme.spacing(4),
  boxShadow: theme.shadows[3],
}));

const StyledHeading = styled(Typography)(({ theme }) => ({
  marginBottom: theme.spacing(3),
  color: theme.palette.primary.main,
  textAlign: 'center',
  fontWeight: 'bold',
  fontSize: '2rem',
}));

const FlexContainer = styled(Box)(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  marginBottom: theme.spacing(3),
}));

const StyledButton = styled(Button)(({ theme }) => ({
  marginLeft: theme.spacing(2),
  height: '40px',
  fontWeight: 'bold',
  '&:hover': {
    backgroundColor: theme.palette.primary.dark,
    boxShadow: theme.shadows[2],
  },
}));

const FileInputLabel = styled('label')(({ theme }) => ({
  backgroundColor: theme.palette.primary.main,
  color: 'white',
  padding: theme.spacing(1, 2),
  borderRadius: theme.shape.borderRadius,
  cursor: 'pointer',
  marginLeft: theme.spacing(2),
  '&:hover': {
    backgroundColor: theme.palette.primary.dark,
  },
}));

const ThumbnailPreview = styled('img')(({ theme }) => ({
  width: '60px',
  height: '60px',
  marginLeft: theme.spacing(2),
  border: '1px solid #ddd',
  borderRadius: theme.shape.borderRadius,
  objectFit: 'cover',
  '&:hover': {
    transform: 'scale(1.1)',
  },
}));

const SelectAllButton = styled(Button)(({ theme }) => ({
  marginLeft: 'auto',
  backgroundColor: theme.palette.primary.main,
  color: 'white',
  padding: theme.spacing(1, 2),
  borderRadius: theme.shape.borderRadius,
  cursor: 'pointer',
  '&:hover': {
    backgroundColor: theme.palette.primary.dark,
  },
}));

const FormControlStyled = styled(Box)(({ theme }) => ({
  flexGrow: 1,
  display: 'flex',
  flexWrap: 'wrap',
  gap: theme.spacing(2),
}));

const ProductCreate: React.FC<any> = (props) => {
  const dataProvider = useDataProvider();
  const notify = useNotify();
  const redirect = useRedirect();
  const [brands, setBrands] = useState<{ id: string; name: string }[]>([]);
  const [newBrand, setNewBrand] = useState<string>('');
  const [selectedSizes, setSelectedSizes] = useState<string[]>([]);
  const [newColor, setNewColor] = useState<string>('');
  const [colors, setColors] = useState<Color[]>([]);
  const [editColorIndex, setEditColorIndex] = useState<number | null>(null);
  const [editedColor, setEditedColor] = useState<string>('');
  const [selectedCategories, setSelectedCategories] = useState<{ [key: string]: string[] }>({});
  const [selectedDivisions, setSelectedDivisions] = useState<string[]>([]);
  const [categoriesByDivision, setCategoriesByDivision] = useState<{ [key: string]: { id: string; name: string }[] }>({});

  const fetchBrands = useCallback(async () => {
    try {
      const { data } = await dataProvider.getList('Products/Brands', {
        pagination: { page: 1, perPage: 100 },
        sort: { field: 'name', order: 'ASC' },
        filter: {},
      });
      setBrands(data);
    } catch (error) {
      notify('Error fetching brands', { type: 'error' });
    }
  }, [dataProvider, notify]);

  const fetchCategories = useCallback(async (division: string) => {
    try {
      const { data } = await dataProvider.getList('Products/Categories', {
        pagination: { page: 1, perPage: 100 },
        sort: { field: 'name', order: 'ASC' },
        filter: { divisions: division },
      });

      setCategoriesByDivision((prev) => ({
        ...prev,
        [division]: data,
      }));
    } catch (error) {
      notify(`Error fetching categories for ${division}`, { type: 'error' });
    }
  }, [dataProvider, notify]);

  useEffect(() => {
    fetchBrands();
  }, [fetchBrands]);

  useEffect(() => {
    selectedDivisions.forEach((division) => {
      if (division) {
        fetchCategories(division);
      }
    });
  }, [selectedDivisions, fetchCategories]);

  const toggleAllSizes = () => {
    if (selectedSizes.length === sizes.length) {
      setSelectedSizes([]);
    } else {
      setSelectedSizes(sizes.map((size) => size.id));
    }
  };

  const handleSizeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    if (selectedSizes.includes(value)) {
      setSelectedSizes(selectedSizes.filter((size) => size !== value));
    } else {
      setSelectedSizes([...selectedSizes, value]);
    }
  };

  const handleNewBrandChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setNewBrand(event.target.value);
  };

  const handleAddNewBrand = () => {
    if (newBrand && !brands.some((brand) => brand.id === newBrand)) {
      setBrands([...brands, { id: newBrand, name: newBrand }]);
      setNewBrand('');
    }
  };

  const handleNewColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setNewColor(event.target.value);
  };

  const handleAddColor = () => {
    if (newColor.trim() === '') return;

    setColors([...colors, { name: newColor, thumbnail: null, thumbnailPreview: '' }]);
    setNewColor('');
  };

  const handleEditColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setEditedColor(event.target.value);
  };

  const handleSaveColor = (index: number) => {
    const updatedColors = [...colors];
    updatedColors[index].name = editedColor;
    setColors(updatedColors);
    setEditColorIndex(null);
    setEditedColor('');
  };

  const handleEditColor = (index: number) => {
    setEditedColor(colors[index].name);
    setEditColorIndex(index);
  };

  const handleCancelEdit = () => {
    setEditColorIndex(null);
    setEditedColor('');
  };

  const handleDeleteColor = (index: number) => {
    setColors(colors.filter((_, i) => i !== index));
  };

  const handleThumbnailChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (file) {
      const updatedColors = [...colors];
      updatedColors[index].thumbnail = file;
      updatedColors[index].thumbnailPreview = URL.createObjectURL(file);
      setColors(updatedColors);
    }
  };

  const handleCategoryChange = (division: string, event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    setSelectedCategories((prev) => {
      const divisionCategories = prev[division] || [];
      if (divisionCategories.includes(value)) {
        return {
          ...prev,
          [division]: divisionCategories.filter((category) => category !== value),
        };
      } else {
        return {
          ...prev,
          [division]: [...divisionCategories, value],
        };
      }
    });
  };

  const handleDivisionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    if (selectedDivisions.includes(value)) {
      setSelectedDivisions(selectedDivisions.filter((division) => division !== value));
    } else {
      setSelectedDivisions([...selectedDivisions, value]);
    }
  };

  const handleSubmit = async (data: any) => {
    const payload = {
      ...data,
      sizes: selectedSizes,
      colors: colors.map((color) => color.name),
      divisions: selectedDivisions,
      categories: selectedCategories,
    };

    await dataProvider.create('Products', { data: payload });
    notify('Product created successfully', { type: 'success' });
    redirect('list', 'Products');
  };

  return (
    <Create {...props}>
      <StyledPaper>
        <StyledHeading variant="h4">Create New Product</StyledHeading>
        <SimpleForm onSubmit={handleSubmit}>
          <FlexContainer>
            <SelectInput source="brand" choices={brands} fullWidth label="Brand" />
            <TextField value={newBrand} onChange={handleNewBrandChange} placeholder="New brand" />
            <StyledButton onClick={handleAddNewBrand} variant="contained" color="primary" startIcon={<AddIcon />}>Add Brand</StyledButton>
          </FlexContainer>
          <TextInput source="style" fullWidth label="Style" />
          <TextInput source="description" fullWidth label="Description" multiline />
          <TextInput source="price" fullWidth label="Price" />
          <FlexContainer>
            <FormControlStyled>
              <FormLabel component="legend">Sizes</FormLabel>
              <FormGroup>
                {sizes.map((size) => (
                  <FormControlLabel
                    control={<Checkbox checked={selectedSizes.includes(size.id)} onChange={handleSizeChange} value={size.id} />}
                    label={size.name}
                    key={size.id}
                  />
                ))}
              </FormGroup>
            </FormControlStyled>
            <SelectAllButton onClick={toggleAllSizes} variant="contained" startIcon={<CheckCircleOutlineIcon />}>
              {selectedSizes.length === sizes.length ? 'Deselect All' : 'Select All'}
            </SelectAllButton>
          </FlexContainer>
          <FlexContainer>
            <FormControlStyled>
              <FormLabel component="legend">Colors</FormLabel>
              <FlexContainer>
                <TextField
                  value={newColor}
                  onChange={handleNewColorChange}
                  placeholder="Enter color"
                />
                <StyledButton
                  onClick={handleAddColor}
                  variant="contained"
                  color="primary"
                  startIcon={<AddIcon />}
                >
                  Add Color
                </StyledButton>
              </FlexContainer>
              <FormGroup>
                {colors.map((color, index) => (
                  <FlexContainer key={index}>
                    <TextField
                      value={editColorIndex === index ? editedColor : color.name}
                      onChange={(e) => handleEditColorChange(e as React.ChangeEvent<HTMLInputElement>)}
                      fullWidth
                      disabled={editColorIndex !== index}
                    />
                    <FileInputLabel htmlFor={`fileInput-${index}`}>
                      {color.thumbnail ? 'Change Image' : 'Upload Image'}
                    </FileInputLabel>
                    <input
                      type="file"
                      accept="image/*"
                      id={`fileInput-${index}`}
                      onChange={(e) => handleThumbnailChange(index, e)}
                      style={{ display: 'none' }}
                    />
                    {color.thumbnailPreview && (
                      <ThumbnailPreview src={color.thumbnailPreview} alt="Color Thumbnail" />
                    )}
                    {editColorIndex === index ? (
                      <>
                        <IconButton onClick={() => handleSaveColor(index)}>
                          <SaveIcon />
                        </IconButton>
                        <IconButton onClick={handleCancelEdit}>
                          <CancelIcon />
                        </IconButton>
                      </>
                    ) : (
                      <>
                        <IconButton onClick={() => handleEditColor(index)}>
                          <EditIcon />
                        </IconButton>
                        <IconButton onClick={() => handleDeleteColor(index)}>
                          <DeleteIcon />
                        </IconButton>
                      </>
                    )}
                  </FlexContainer>
                ))}
              </FormGroup>
            </FormControlStyled>
          </FlexContainer>
          <FlexContainer>
            <FormControlStyled>
              <FormLabel component="legend">Divisions</FormLabel>
              <FormGroup>
                {divisions.map((division) => (
                  <FormControlLabel
                    control={<Checkbox checked={selectedDivisions.includes(division.id)} onChange={handleDivisionChange} value={division.id} />}
                    label={division.name}
                    key={division.id}
                  />
                ))}
              </FormGroup>
            </FormControlStyled>
          </FlexContainer>
          <FlexContainer>
            {selectedDivisions.map((division) => (
              <FormControlStyled key={division}>
                <FormLabel component="legend">{division} Categories</FormLabel>
                <FormGroup>
                  {categoriesByDivision[division]?.map((category) => (
                    <FormControlLabel
                      control={<Checkbox checked={selectedCategories[division]?.includes(category.id) || false} onChange={(e) => handleCategoryChange(division, e)} value={category.id} />}
                      label={category.name}
                      key={category.id}
                    />
                  ))}
                </FormGroup>
              </FormControlStyled>
            ))}
          </FlexContainer>
          <FlexContainer>
            <ImageInput source="image" label="Product image" accept="image/*">
              <ImageField source="src" title="title" />
            </ImageInput>
          </FlexContainer>
          <FlexContainer>
            <BooleanInput source="public" label="Public" />
          </FlexContainer>
        </SimpleForm>
      </StyledPaper>
    </Create>
  );
};

export default ProductCreate;

问题解决尝试

我尝试了什么以及我期待什么?

采取的步骤

  1. 检查现有产品: 在添加新产品之前,我使用

    FirstOrDefaultAsync
    查询来检查数据库中是否已存在具有相同品牌、样式、颜色名称和尺寸的产品。这是为了确保不会创建重复项。

    var existingProduct = await _context.Products
        .FirstOrDefaultAsync(p => p.Brand == productModel.Brand && p.Style == productModel.Style && p.ColorName == color && p.Size == size);
    
    if (existingProduct != null) {
        return BadRequest($"Product with Brand '{productModel.Brand}', Style '{productModel.Style}', Color '{color}', and Size '{size}' already exists.");
    }
    
  2. 添加新产品: 如果产品不存在,我将创建一个新的产品实体并将其添加到上下文中。

    var newProduct = new Product {
        // initialization
    };
    _context.Products.Add(newProduct);
    
  3. 保存更改: 最后,我调用

    SaveChangesAsync
    将新产品持久化到数据库中。

    await _context.SaveChangesAsync();
    

我在期待什么?

我希望上述方法能够防止将重复的 Product 实体添加到数据库中,并避免在 Entity Framework Core 中跟踪问题。具体来说,我预计:

  1. FirstOrDefaultAsync
    查询将正确识别现有产品。
  2. 只有新的、独特的产品才会添加到上下文中并保存。

我如何知道数据库中当前不存在该产品?

我通过直接对数据库运行以下 SQL 查询来验证数据库中当前不存在我尝试创建的产品:

SELECT TOP (1000) 
    [Id], 
    [Brand], 
    [Style], 
    [ColorCode], 
    [ColorName], 
    [Size], 
    [Description], 
    [Specs], 
    [Vendor], 
    [Keywords], 
    [Features], 
    [ImageName], 
    [Image], 
    [Thumbnail], 
    [Pms...
FROM [dbo].[Products] 
WHERE Brand = 'hteye'

结果返回零行,确认数据库中不存在指定品牌的产品。

还尝试添加类似的内容

                var existingProduct = await _context.Products
                    .AsNoTracking()
                    .FirstOrDefaultAsync(p => p.Brand == productModel.Brand 
                                              && p.Style == productModel.Style 
                                              && p.ColorName == color 
                                              && p.Size == size);

尽管执行了这些步骤,我仍然遇到 Entity Framework Core 中的跟踪问题。任何解决此问题的帮助或指示将不胜感激。

c# reactjs asp.net-core entity-framework-core react-admin
1个回答
0
投票

基于这部分代码

foreach (var color in productModel.Colors)
        {
            foreach (var size in productModel.Sizes)
            {

问题似乎在于组合键中缺少尺寸。

{Brand: hteye, Style: wqefwef, ColorName: Red}

您还可以通过使用

AnyAsync
方法而不是加载整个实体来提高性能

var exists = await _context.Products
                    .AnyAsync(p => p.Brand == productModel.Brand
                        && p.Style == productModel.Style
                        && p.ColorName == color
                        && p.Size == size);
© www.soinside.com 2019 - 2024. All rights reserved.