How to use JsonObject of Pomelo.EntityFramework - mysql

I want to store list of string into mysql table as json. I saw there is support for this in pomelo entityframework. I followed this https://libraries.io/github/tuanbs/Pomelo.EntityFrameworkCore.MySql
Here is my entity
public class Project
{
public int Id {get;set;}
public string Title {get;set;}
public JsonObject<List<string>> Tags {get;set;}
}
But when _context.Database.EnsureDeleted(); is called it gives below error
Navigation property 'Tags' on entity type 'Project' is not virtual.
UseLazyLoadingProxies requires all entity types to be public,
unsealed, have virtual navigation properties, and have a public or
protected constructor.
But it is not navigation property that I have to add virtual keyword with it but is a column. Don't know what am I missing here.

Take a look at the following sample code, that is taken from my post on our GitHub repository, and works without issues:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Storage;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
public JsonObject<Energy> Energy { get; set; }
public JsonObject<List<string>> Comments { get; set; }
}
public class Energy
{
public double Kilojoules { get; set; }
public double Kilocalories { get; set; }
}
public class Context : DbContext
{
public virtual DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql("server=127.0.0.1;port=3306;user=root;password=;database=So62301095",
b => b.ServerVersion(new ServerVersion("8.0.20-mysql")))
.UseLoggerFactory(LoggerFactory.Create(b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
}
internal class Program
{
private static void Main()
{
using (var context = new Context())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.IceCreams.AddRange(
new IceCream
{
Name = "Vanilla",
Energy = new Energy
{
Kilojoules = 866.0,
Kilocalories = 207.0
},
Comments = new List<string>
{
"First!",
"Delicious!"
}
},
new IceCream
{
Name = "Chocolate",
Energy = new Energy
{
Kilojoules = 904.0,
Kilocalories = 216.0
},
Comments = new List<string>
{
"My husband likes this one a lot."
}
});
context.SaveChanges();
}
using (var context = new Context())
{
var result = context.IceCreams
.OrderBy(e => e.IceCreamId)
.ToList();
Debug.Assert(result.Count == 2);
Debug.Assert(result[0].Name == "Vanilla");
Debug.Assert(result[0].Energy.Object.Kilojoules == 866.0);
Debug.Assert(result[0].Comments.Object.Count == 2);
Debug.Assert(result[0].Comments.Object[0] == "First!");
}
}
}
}
It generates the following SQL:
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 3.1.3 initialized 'Context' using provider 'Pomelo.EntityFrameworkCore.MySql' with options: ServerVersion 8.0.20 MySql SensitiveDataLoggingEnabled DetailedErrorsEnabled
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (81ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
DROP DATABASE `So62301095`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (12ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE DATABASE `So62301095`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (66ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE `IceCreams` (
`IceCreamId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
`Energy` json NULL,
`Comments` json NULL,
CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
);
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [Parameters=[#p0='["First!","Delicious!"]', #p1='{"Kilojoules":866.0,"Kilocalories":207.0}', #p2='Vanilla' (Size = 4000)], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`Comments`, `Energy`, `Name`)
VALUES (#p0, #p1, #p2);
SELECT `IceCreamId`
FROM `IceCreams`
WHERE ROW_COUNT() = 1 AND `IceCreamId` = LAST_INSERT_ID();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[#p0='["My husband likes this one a lot."]', #p1='{"Kilojoules":904.0,"Kilocalories":216.0}', #p2='Chocolate' (Size = 4000)], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`Comments`, `Energy`, `Name`)
VALUES (#p0, #p1, #p2);
SELECT `IceCreamId`
FROM `IceCreams`
WHERE ROW_COUNT() = 1 AND `IceCreamId` = LAST_INSERT_ID();
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `i`.`IceCreamId`, `i`.`Comments`, `i`.`Energy`, `i`.`Name`
FROM `IceCreams` AS `i`
ORDER BY `i`.`IceCreamId`
Take a close look at the IceCream.Comments property, that does exactly what you want.
On the same GitHub issue further below, you find another post by me, with a much more sophisticated example.
Also, we are going to implement full JSON support next for Pomelo (probably within a week).

Related

Pomelo.EntityFrameworkCore.MySql problem when access property

Today I migrate my project to .NET 6 with MySql db.
I tried Pomelo.EntityFrameworkCore.MySql for the first time but several errors had occur.
I fixed some of them but the last one, I couldn`t.
System.InvalidOperationException: The property 'SqlClass.Disabled' is of type 'byte' which is not supported by the current database provider. Either change the property CLR type, or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
entity.Property(e => e.Disabled)
//.HasConversion<int>()
.IsRequired()
.HasMaxLength(255)
.HasColumnName("disabled")
.HasColumnType("tinyint(1)");
[Required]
[MaxLength(255)]
public byte Disabled { get; set; }
Any ideas ? Thank you.
There are basically 3 simple options, all demonstrated here with the IceCream.Available property:
1. Use System.Boolean instead of System.Byte
Pomelo translates tinyint(1) to System.Boolean by default. So if you change the CLR type of your property from byte to bool, it works out-of-the box:
Program.cs
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
[Required]
[Column(TypeName = "tinyint(1)")] // <-- redundant (bool is translated to tinyint(1) by default)
public bool Available { get; set; } // <-- use bool
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=So70198786";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder
.UseMySql(connectionString, serverVersion)
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
// Not needed if you are using data annotations:
//
// entity.Property(e => e.Available)
// .IsRequired()
// .HasColumnType("tinyint(1)"); // <-- redundant (bool is translated to tinyint(1) by default)
entity.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Available = true, // <-- bool
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Available = false, // <-- bool
});
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var availableIceCreams = context.IceCreams
.Where(i => i.Available) // <-- bool
.ToList();
Trace.Assert(availableIceCreams.Count == 1);
Trace.Assert(availableIceCreams[0].Name == "Vanilla");
}
}
}
Output (SQL)
warn: Microsoft.EntityFrameworkCore.Model.Validation[10400]
Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.0 initialized 'Context' using provider 'Pomelo.EntityFrameworkCore.MySql:6.0.0-rtm.1' with options: ServerVersion 8.0.25-mysql SensitiveDataLoggingEnabled DetailedErrorsEnabled
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (38ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
DROP DATABASE `So70198786`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (13ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE DATABASE `So70198786`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
ALTER DATABASE CHARACTER SET utf8mb4;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (47ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE `IceCreams` (
`IceCreamId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
`Available` tinyint(1) NOT NULL,
CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
) CHARACTER SET=utf8mb4;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Available`, `Name`)
VALUES (1, TRUE, 'Vanilla');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (10ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Available`, `Name`)
VALUES (2, FALSE, 'Chocolate');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `i`.`IceCreamId`, `i`.`Available`, `i`.`Name`
FROM `IceCreams` AS `i`
WHERE `i`.`Available`
2. Use tinyint instead of tinyint(1)
In case you definitely want to use System.Byte as the CLR type of your property, use tinyint instead of tinyint(1). All tinyint except tinyint(1) are translated to System.Byte by default:
Program.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
[Required]
[Column(TypeName = "tinyint")] // <-- redundant (byte is translated to tinyint by default)
public byte Available { get; set; }
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=So70198786_01";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder
.UseMySql(connectionString, serverVersion)
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
// Not needed if you are using data annotations:
//
// entity.Property(e => e.Available)
// .IsRequired()
// .HasColumnType("tinyint"); // <-- redundant (byte is translated to tinyint by default)
entity.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Available = 1, // <-- byte
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Available = 0, // <-- byte
});
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var availableIceCreams = context.IceCreams
.Where(i => i.Available != 0) // <-- byte
.ToList();
Trace.Assert(availableIceCreams.Count == 1);
Trace.Assert(availableIceCreams[0].Name == "Vanilla");
}
}
}
Output (SQL)
warn: Microsoft.EntityFrameworkCore.Model.Validation[10400]
Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.0 initialized 'Context' using provider 'Pomelo.EntityFrameworkCore.MySql:6.0.0-rtm.1' with options: ServerVersion 8.0.25-mysql SensitiveDataLoggingEnabled DetailedErrorsEnabled
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (42ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
DROP DATABASE `So70198786_01`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (41ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE DATABASE `So70198786_01`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (19ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
ALTER DATABASE CHARACTER SET utf8mb4;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (48ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE `IceCreams` (
`IceCreamId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
`Available` tinyint NOT NULL,
CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
) CHARACTER SET=utf8mb4;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Available`, `Name`)
VALUES (1, 1, 'Vanilla');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (10ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Available`, `Name`)
VALUES (2, 0, 'Chocolate');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `i`.`IceCreamId`, `i`.`Available`, `i`.`Name`
FROM `IceCreams` AS `i`
WHERE `i`.`Available` <> 0
3. Change default System.Boolean mapping to bit(1) or remove it altogether
Finally, if you need to map exactly tinyint(1) to System.Byte, then you can change the default mapping that Pomelo is using for System.Boolean by setting a DbContext option in your UseMySql() call:
Program.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
namespace IssueConsoleTemplate
{
public class IceCream
{
public int IceCreamId { get; set; }
public string Name { get; set; }
[Required]
[Column(TypeName = "tinyint(1)")] // <-- necessary (otherwise gets translated to tinyint)
public byte Available { get; set; }
}
public class Context : DbContext
{
public DbSet<IceCream> IceCreams { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=So70198786_02";
var serverVersion = ServerVersion.AutoDetect(connectionString);
optionsBuilder
.UseMySql(
connectionString,
serverVersion,
options => options.DefaultDataTypeMappings( // <-- change default data type mappings
m => m.WithClrBoolean(MySqlBooleanType.Bit1))) // <-- or even MySqlBooleanType.None
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IceCream>(
entity =>
{
// Not needed if you are using data annotations:
//
// entity.Property(e => e.Available)
// .IsRequired()
// .HasColumnType("tinyint(1)"); // <-- necessary (otherwise gets translated to tinyint)
entity.HasData(
new IceCream
{
IceCreamId = 1,
Name = "Vanilla",
Available = 1, // <-- byte
},
new IceCream
{
IceCreamId = 2,
Name = "Chocolate",
Available = 0, // <-- byte
});
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var availableIceCreams = context.IceCreams
.Where(i => i.Available != 0) // <-- byte
.ToList();
Trace.Assert(availableIceCreams.Count == 1);
Trace.Assert(availableIceCreams[0].Name == "Vanilla");
}
}
}
Output (SQL)
warn: Microsoft.EntityFrameworkCore.Model.Validation[10400]
Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 6.0.0 initialized 'Context' using provider 'Pomelo.EntityFrameworkCore.MySql:6.0.0-rtm.1' with options: ServerVersion 8.0.25-mysql SensitiveDataLoggingEnabled DetailedErrorsEnabled
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (40ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
DROP DATABASE `So70198786_02`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (13ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE DATABASE `So70198786_02`;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (19ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
ALTER DATABASE CHARACTER SET utf8mb4;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (86ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE `IceCreams` (
`IceCreamId` int NOT NULL AUTO_INCREMENT,
`Name` longtext CHARACTER SET utf8mb4 NULL,
`Available` tinyint(1) NOT NULL,
CONSTRAINT `PK_IceCreams` PRIMARY KEY (`IceCreamId`)
) CHARACTER SET=utf8mb4;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (7ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Available`, `Name`)
VALUES (1, 1, 'Vanilla');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO `IceCreams` (`IceCreamId`, `Available`, `Name`)
VALUES (2, 0, 'Chocolate');
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (7ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `i`.`IceCreamId`, `i`.`Available`, `i`.`Name`
FROM `IceCreams` AS `i`
WHERE `i`.`Available` <> 0
Unrelated to the topic of this question, [MaxLength(255)] or .HasMaxLength(255) have no effect on non-string columns.

Spring Boot- Duplicate entry for key 'PRIMARY'

I am new to Spring Boot. I am trying to use the save() functionality via the JPA library using Postman for the first time. My database is a legacy Mysql database. Generically speaking, this table contains data of baseball players who have been drafted into a fantasy baseball league. The primary key of my table is 'play_id', and I also track the player's 'mlb_id' (Major League Baseball's unique identifier) in the same table.
Here is my code:
Table setup in Mysql:
CREATE TABLE `mlb_rosters` (
`play_id` int(10) NOT NULL,
`mlb_id` int(10) NOT NULL,
`name_first` varbinary(255) NOT NULL,
`name_last` varbinary(255) NOT NULL,
`bats` varchar(1) NOT NULL,
`throws` varchar(1) NOT NULL,
`birthday` date NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `mlb_rosters`
ADD PRIMARY KEY (`play_id`),
ADD UNIQUE KEY `mlb_id` (`mlb_id`),
ADD UNIQUE KEY `mlb_id_2` (`mlb_id`);
ALTER TABLE `mlb_rosters`
MODIFY `play_id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6730;
I also ran insert statements for approximately ~1500 players, so this is not a blank table.
My object in Springboot:
package com.example.demo.entities;
import javax.persistence.*;
#Entity
#Table(name="mlb_rosters")
public class IbcMlbPlayer {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name="play_id", columnDefinition = "int(10)")
private Integer playId;
#Column(name="mlb_id")
private Integer mlbId;
#Column(name="name_first", columnDefinition = "varbinary(255)")
private String nameFirst;
#Column(name="name_last", columnDefinition = "varbinary(255)")
private String nameLast;
#Column(name="bats")
private String bats;
#Column(name="throws")
private String thrws;
#Column(name="birthday")
private String birthday;
public IbcMlbPlayer(){
}
public Integer getPlayId() {
return playId;
}
public void setPlayId(Integer playId) {
this.playId = playId;
}
public Integer getMlbId() {
return mlbId;
}
public void setMlbId(Integer mlbId) {
this.mlbId = mlbId;
}
public String getNameFirst() {
return nameFirst;
}
public void setNameFirst(String nameFirst) {
this.nameFirst = nameFirst;
}
public String getNameLast() {
return nameLast;
}
public void setNameLast(String nameLast) {
this.nameLast = nameLast;
}
public String getBats() {
return bats;
}
public void setBats(String bats) {
this.bats = bats;
}
public String getThrws() {
return thrws;
}
public void setThrws(String thrws) {
this.thrws = thrws;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
}
The relevant path of my controller:
#PostMapping(value = "/saveIbcMlbPlayer")
public IbcMlbPlayer saveIbcMlbPlayer(#RequestBody IbcMlbPlayer ibcMlbPlayer){
return ibcMlbPlayerDao.save(ibcMlbPlayer);
}
My Dao:
package com.example.demo.dao;
import com.example.demo.entities.IbcMlbPlayer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface IbcMlbPlayerDao extends JpaRepository<IbcMlbPlayer, Integer> {
}
When I attempt to do a Post request to the save path and pass in the JSON object of the player who I'm attempting to create, I get the following error:
Duplicate entry '25' for key 'PRIMARY'
In this case, I've tried this 25 times, so Postman/Spring Boot keep incrementing the 'play_id' field by 1 (this number goes up in the error message by one each time I test).
I understand the error, for whatever reason, Spring Boot isn't getting the max value of the 'play_id' field, incrementing it by one, and then attempting to do the insert. I would have expected 'play_id' to be 6730, which I believe is the table's max play_id plus one. Does anyone know how to fix this? Any help would be really appreciated!
AUTO shouldn't be used as GenerationType you must use IDENTITY
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="play_id", columnDefinition = "int(10)")
private Integer playId;

Entity Framework Core: Update() method INSERTs instead of UPDATEs on dependent entity

It seems like EF Core is doing an INSERT instead of an UPDATE, and thus MySQL complains about a duplicate key. However, I am using the Update method on the DbSet and the entities do have primary keys set. This results in a DUPLICATE ENTRY error in MySql.
Running VS 2019, EF Core 3.1.1 and ASP.NET Core 3.1
Model (I do not use Fluent config for relationships, just Convention):
public class Vehicle
{
public long Id {get; set; } // PRIMARY KEY, AUTO INCREMENT
public string RegistrationNumber { get; set; }
public VehicleSession Session { get; set; }
}
public class VehicleSession
{
public long VehicleId { get; set; }
public Vehicle Vehicle { get; set; }
public string DeviceUuid { get; set; } // PRIMARY KEY
public string AuthenticationToken { get; set; }
public string OSName { get; set; }
public string OSVersion { get; set; }
public string DeviceModel { get; set; }
}
Database:
Table 'vehiclesessions' where the PRIMARY KEY is DeviceUuid and has key 'device2':
Controller:
A request comes in from somewhere. DeviceUuid is fetched from HTTP headers.
public async Task<ActionResult<VehicleLoginResponse>> Login(VehicleLogin login)
{
Request.Headers.TryGetValue(BaseConstants.DEVICE_UUID, out StringValues deviceUuid);
Vehicle vehicle = await vehicleService.Authenticate(login.Username, login.Password); // returns a Vehicle by asking dbContet: dbContext.Vehicles.FirstOrDefault(v => v.Username.Equals(username));
VehicleSession vs = await vehicleService.CreateSession(vehicle, login, deviceUuid);
// ...
}
Service:
This service creates a new VehicleSession object, assigns it to the Vehicle property, and does an .Update on the Vehicle, so that the new session is saved with it.
public async Task<VehicleSession> CreateSession(Vehicle vehicle, VehicleLogin vehicleLogin, string deviceUuid)
{
VehicleSession vs = new VehicleSession()
{
Vehicle = vehicle,
AuthenticationToken = someTokenFetchedFromSomewhere,
DeviceModel = vehicleLogin.DeviceModel,
DeviceUuid = deviceUuid, // is 'device2'
OSName = vehicleLogin.OSName,
OSVersion = vehicleLogin.OSVersion
};
vehicle.Session = vs;
dbContext.Vehicles.Update(vehicle);
await dbContext.SaveChangesAsync();
return vs;
}
It doesn't matter if I replace the new VehicleSession and assignment, with just editing an already existing Vehicle.Session, same error:
public async Task<VehicleSession> CreateSession(Vehicle vehicle, VehicleLogin vehicleLogin, string deviceUuid)
{
if (vehicle.Session == null)
vehicle.Session = new VehicleSession(); // never executes this line
vehicle.Session.AuthenticationToken = someTokenFetchedFromSomewhere;
vehicle.Session.DeviceModel = vehicleLogin.DeviceModel;
vehicle.Session.DeviceUuid = deviceUuid;
vehicle.Session.OSName = vehicleLogin.OSName;
vehicle.Session.OSVersion = vehicleLogin.OSVersion;
await dbContext.SaveChangesAsync();
return vehicle.Session;
}
When doing so, I get an error saying:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred
while updating the entries. See the inner exception for details. --->
MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry
'device2' for key 'vehiclesessions.PRIMARY' --->
MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry
'device2' for key 'vehiclesessions.PRIMARY'
The SQL produced:
Failed executing DbCommand (125ms) [Parameters=[#p0='?' (Size = 95), #p1='?' (Size = 95), #p2='?' (Size = 4000), #p3='?' (Size = 4000), #p4='?' (Size = 4000), #p5='?' (DbType = Int64)], CommandType='Text', CommandTimeout='30']
INSERT INTO `VehicleSessions` (`DeviceUuid`, `AuthenticationToken`, `DeviceModel`, `OSName`, `OSVersion`, `VehicleId`)
VALUES (#p0, #p1, #p2, #p3, #p4, #p5);
fail: Microsoft.EntityFrameworkCore.Update[10000]
An exception occurred in the database while saving changes for context type 'blabla'.
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry 'device2' for key 'vehiclesessions.PRIMARY'
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Duplicate entry 'device2' for key 'vehiclesessions.PRIMARY'
Why am I getting this error? It seems like an INSERT is done instead of an UPDATE, even though the Vehicle object is fetched from dbContext, passed along and has PRIMARY id set.
So, I think I might have found the issue.
As noted above, I first did a search to retrieve the Vehicle, like this:
Vehicle vehicle = dbContext.Vehicles.FirstOrDefault(v => v.Username.Equals(username));
In this case, the Vehicle.Session, if one existed, was not populated. Later on, I did the update as the code above shows, and it failed as noted above.
But, if I change the fetch code to this:
Vehicle entityVehicle = dbContext.Vehicles
.Include(x => x.Session)// <-- NEW!
.FirstOrDefault(v => v.Username.Equals(username));
then it works.
It doesn't matter if I assign the .Session a new VehicleSession(...) or if I change the properties in the existing object. It also doesn't matter if I use dbContext.Vehicles.Update(vehicle); before await dbContext.SaveChangesAsync();, the call to .Update(...) is not needed.
The only thing that made a difference, was to use .Include(...).
My own theory is:
When fetching the object from db, the EF Core "tracker" starts tracking. Property Session is NULL. When Session later is populated, the tracker detects that it was NULL but now is not NULL, thus, it figures that an INSERT is needed.
And then, if you populate the Session on fetch, tracker sees it is there, and should be updated.

Force engine=innodb when using Entity Framework Code First with mysql

I have created a new .NET MVC 5 web application using Entity Framework 6 and a msyql database. I am using code/model first. The database server has a default storage engine of MyISAM, but I would like for the tables that EF creates to be InnoDb. Does anyone know if there is as way to specify the storage engine that EF will use in the CREATE TABLE statement?
Actually the engine used by MySQL EF provider is ALWAYS InnoDB and you can't change it without rewriting the DDL generator.
To try you can create a simple project and enable log on MySQL. You will notice that every create statement will terminate with engine=InnoDb auto_increment=0
For example this class
public class Blog
{
public int BlogId { get; set; }
[MaxLength(200)]
public string Name { get; set; }
[MaxLength(200)]
public string Topic { get; set; }
public DateTime LastUpdated { get; set; }
[DefaultValue(0)]
public int Order { get; set; }
public virtual List<Post> Posts { get; set; }
}
with standard MySQL EF provider migration, generates this MySQL DDL statement
CREATE TABLE `Blogs` (
`BlogId` INT NOT NULL auto_increment,
`Name` NVARCHAR(200),
`Topic` NVARCHAR(200),
`LastUpdated` DATETIME NOT NULL,
`Order` INT NOT NULL,
PRIMARY KEY (`BlogId`)
) engine = InnoDb auto_increment = 0
Where is engine = InnoDb from? It's hard coded in migration source code.
You can have a look at the migration source code
https://github.com/mysql/mysql-connector-net/blob/6.9/Source/MySql.Data.EntityFramework5/MySqlMigrationSqlGenerator.cs
method MySqlMigrationSqlGenerator.Generate(CreateTableOperation op).
The last statement is sb.Append(") engine=InnoDb auto_increment=0");
So, the right question should be how can I change the engine from InnoDB to another engine.
You can inherit MySqlMigrationSqlGenerator class and override the method, i.e.:
internal class MyOwnMigrationSqlGenerator : MySqlMigrationSqlGenerator
{
public MyOwnMigrationSqlGenerator()
{
Engine = "InnoDB";
}
public MyOwnMigrationSqlGenerator(string engine)
{
Engine = engine;
}
private readonly List<MigrationStatement> _specialStatements = new List<MigrationStatement>();
public string Engine { get; set; }
public override IEnumerable<MigrationStatement> Generate(IEnumerable<MigrationOperation> migrationOperations, string providerManifestToken)
{
List<MigrationStatement> migrationStatements = base.Generate(migrationOperations, providerManifestToken).ToList();
migrationStatements.AddRange(_specialStatements);
return migrationStatements;
}
protected override MigrationStatement Generate(CreateTableOperation op)
{
StringBuilder sb = new StringBuilder();
string tableName = TrimSchemaPrefix(op.Name);
var autoIncrementCols = (List<string>)(typeof(MySqlMigrationSqlGenerator).GetProperty("autoIncrementCols", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this));
var primaryKeyCols = (List<string>)(typeof(MySqlMigrationSqlGenerator).GetProperty("primaryKeyCols", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this));
sb.Append("create table " + "`" + tableName + "`" + " (");
if (op.PrimaryKey != null)
{
op.PrimaryKey.Columns.ToList().ForEach(col => primaryKeyCols.Add(col));
}
//columns
sb.Append(string.Join(",", op.Columns.Select(c => "`" + c.Name + "` " + Generate(c))));
// Determine columns that are GUID & identity
List<ColumnModel> guidCols = new List<ColumnModel>();
ColumnModel guidPk = null;
foreach (ColumnModel columnModel in op.Columns)
{
if (columnModel.Type == PrimitiveTypeKind.Guid && columnModel.IsIdentity && String.Compare(columnModel.StoreType, "CHAR(36) BINARY", true) == 0)
{
if (primaryKeyCols.Contains(columnModel.Name))
guidPk = columnModel;
guidCols.Add(columnModel);
}
}
if (guidCols.Count != 0)
{
var createTrigger = new StringBuilder();
createTrigger.AppendLine(string.Format("DROP TRIGGER IF EXISTS `{0}_IdentityTgr`;", TrimSchemaPrefix(tableName)));
createTrigger.AppendLine(string.Format("CREATE TRIGGER `{0}_IdentityTgr` BEFORE INSERT ON `{0}`", TrimSchemaPrefix(tableName)));
createTrigger.AppendLine("FOR EACH ROW BEGIN");
foreach (ColumnModel opCol in guidCols)
createTrigger.AppendLine(string.Format("SET NEW.{0} = UUID();", opCol.Name));
createTrigger.AppendLine(string.Format("DROP TEMPORARY TABLE IF EXISTS tmpIdentity_{0};", TrimSchemaPrefix(tableName)));
createTrigger.AppendLine(string.Format("CREATE TEMPORARY TABLE tmpIdentity_{0} (guid CHAR(36))ENGINE=MEMORY;", TrimSchemaPrefix(tableName)));
createTrigger.AppendLine(string.Format("INSERT INTO tmpIdentity_{0} VALUES(New.{1});", TrimSchemaPrefix(tableName), guidPk.Name));
createTrigger.AppendLine("END");
var sqlOp = new SqlOperation(createTrigger.ToString());
_specialStatements.Add(Generate(sqlOp));
}
if (op.PrimaryKey != null) // && !sb.ToString().Contains("primary key"))
{
sb.Append(",");
sb.Append("primary key ( " + string.Join(",", op.PrimaryKey.Columns.Select(c => "`" + c + "`")) + ") ");
}
string keyFields = ",";
autoIncrementCols.ForEach(col => keyFields += (!primaryKeyCols.Contains(col) ? string.Format(" KEY (`{0}`),", col) : ""));
sb.Append(keyFields.Substring(0, keyFields.LastIndexOf(",")));
sb.Append(string.Format(") engine={0} auto_increment=0", Engine));
return new MigrationStatement() { Sql = sb.ToString() };
}
private string TrimSchemaPrefix(string table)
{
if (table.StartsWith("dbo.") || table.Contains("dbo."))
return table.Replace("dbo.", "");
return table;
}
}
Then, in your migration configuration you can specify your own sql generator.
internal sealed class MyContextMigrationConfiguration : DbMigrationsConfiguration<MyContext>
{
public MyContextMigrationConfiguration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
SetSqlGenerator("MySql.Data.MySqlClient", new MyOwnMigrationSqlGenerator("MyPreferredEngine"));
}
}
EDIT
There was a bug on MyOwnMigrationSqlGenerator class. Probably the best thing is to rewrite all MySqlMigrationSqlGenerator. In this case I just fixed the class accessing private fields of MySqlMigrationSqlGenerator (that is quite bad).

Entity Framework does not update an auto-increment field of a computed key in mySql

I'm trying to save an entity using entityframework, although the EF is not update the auto-increment field
public class Address
{
public int Id { get; set; }
public int DatacenterId { get; set; }
public virtual Datacenter Datacenter { get; set; }
}
public class AddressMapping : EntityTypeConfiguration<Address>
{
public AddressMapping()
{
this.HasKey(k => new { k.Id, k.DatacenterId });
this.HasRequired(itr => itr.Datacenter)
.WithMany()
.HasForeignKey(fk => fk.DatacenterId);
this.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
using (var context = new MyContext())
{
var address = new Address(); //Initialize and set all values. Include the FK
context.Address.Add(address);
context.SaveChanges();
//address.Id is still 0
}
I profiled the following command from mySql:
INSERT INTO `address`(
`Id`,
`DatacenterId`,
)VALUES(
0,
1,
)
I've changed the order of mapping commands and it is working. I didn't know the EF mapping commands could change the mapping result.
public class AddressMapping : EntityTypeConfiguration<Address>
{
public AddressMapping()
{
this.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.HasKey(k => new { k.Id, k.DatacenterId });
this.HasRequired(itr => itr.Datacenter)
.WithMany()
.HasForeignKey(fk => fk.DatacenterId);
}
}
And finally it generated the correct sql command:
INSERT INTO `address`(
`DatacenterId`,
)VALUES(
1,
);
SELECT
`Id`
FROM
`address`
WHERE
row_count() > 0 AND `Id` = last_insert_id() AND `DatacenterId` = 1