我尝试在 ASP.NET Core 应用程序中创建多个
Product
实体时,在 Entity Framework Core 中遇到跟踪问题。我收到的错误消息是:
这就是我想要实现的目标:
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;
检查现有产品: 在添加新产品之前,我使用
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.");
}
添加新产品: 如果产品不存在,我将创建一个新的产品实体并将其添加到上下文中。
var newProduct = new Product {
// initialization
};
_context.Products.Add(newProduct);
保存更改: 最后,我调用
SaveChangesAsync
将新产品持久化到数据库中。
await _context.SaveChangesAsync();
我希望上述方法能够防止将重复的 Product 实体添加到数据库中,并避免在 Entity Framework Core 中跟踪问题。具体来说,我预计:
FirstOrDefaultAsync
查询将正确识别现有产品。我通过直接对数据库运行以下 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);
基于这部分代码
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);