EF Core + Oracle 踩坑记
EF(Entity Framework)是微软标志性且成熟的ORM,从之前的.NET Framework时代就已经很常见了,但是给人的感觉还是偏“重”,性能被不少人吐槽,倒是像Dapper这类更轻量级的更受大家待见。但是进入.NET Core时代后,EF随之进化为EF Core(Entity Framework Core),除了是为ASP.NET Core量身打造外,在性能和功能上微软也是做了很多优化,已经算是一款十分实用的ORM了。在写本文时,EF Core目前处于3.x的版本阶段,并且支持很多主流数据库MySQL,SqlServer,Oracle,SQLite等等。本文讨论的主要对象就是EF Core和Oracle的搭配,虽然目前是支持Oracle数据库,但目前Oracle官方提供的Oracle.EntityFrameworkCore库依然是依赖于EF Core的2.1版本,并且在使用中也不像EF Core和亲儿子SqlServer配合的那样丝滑,其中有不少坑。Oracle目前已经将Oracle.EntityFrameworkCore更新到了3.19.80版本,提升了之前版本的稳定性,并且支持EF Core的新版本,可以说解决了之前一直困扰我的兼容性问题。这里做一些简单的记录,也有可能是本人才疏学浅,打开的方式不对,如有错误欢迎指正。
本文Demo源码地址:https://github.com/Xuhy0826/EFCoreTest (2020-09-09更新)
配置
若要使用EF Core操作Oracle数据库,首先需要安装Oracle.EntityFrameworkCore。可以直接在NuGet上直接搜索安装即可。
按照惯例创建Context类继承DbContext和其他的数据库没有区别,但是我们在配置连接字符串的时候的同时需要为其指定Oracle数据库的版本,传入“11”代表11g,传入“12”代表12c。
1 public class MyContext : DbContext 2 { 3 public MyContext(DbContextOptions options) : base(options) 4 { } 5 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 6 { 7 optionsBuilder.UseOracle( 8 "DATA SOURCE=LOCALHOST:1521/ORACLE1;USER ID= 你的ID ;PASSWORD=你的密码;" 9 , b => b.UseOracleSQLCompatibility("12")); //指定数据库版本 10 } 11 }
如果我们在调试期间希望EF Core输出执行的Sql,我使用的方式自定义一个日志类,将Sql输出到Console中。
1 public class EFLoggerProvider : ILoggerProvider 2 { 3 public ILogger CreateLogger(string categoryName) => new EFLogger(categoryName); 4 public void Dispose() { } 5 } 6 public class EFLogger : ILogger 7 { 8 private readonly string categoryName; 9 public EFLogger(string categoryName) => this.categoryName = categoryName; 10 public IDisposable BeginScope<TState>(TState state) => null; 11 public bool IsEnabled(LogLevel logLevel) => true; 12 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) 13 { 14 //ef core执行数据库查询时的categoryName为Microsoft.EntityFrameworkCore.Database.Command,日志级别为Information 15 if (categoryName == "Microsoft.EntityFrameworkCore.Database.Command") 16 { 17 var logContent = formatter(state, exception); 18 Console.WriteLine(); 19 Console.ForegroundColor = ConsoleColor.Green; 20 Console.WriteLine(logContent); 21 Console.ResetColor(); 22 } 23 } 24 }
并在配置时将其注册进去。
1 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 2 { 3 //配置数据库连接 4 optionsBuilder.UseOracle( 5 "DATA SOURCE=LOCALHOST:1521/ORACLE1;USER ID= 你的ID ;PASSWORD=你的密码;" 6 , b => b.UseOracleSQLCompatibility("12")); 7 //配置Sql输出控制台 8 var loggerFactory = new LoggerFactory(); 9 loggerFactory.AddProvider(new EFLoggerProvider()); 10 optionsBuilder.UseLoggerFactory(loggerFactory); 11 12 base.OnConfiguring(optionsBuilder); 13 }
由于是演示Demo,我们在开发ASP.NET Core项目时一般都是使用依赖注入来注册。如果使用依赖注入的写法,可以这样写。
1 services.AddDomainContext(builder => 2 { 3 //配置数据库连接 4 builder.UseOracle( 5 "DATA SOURCE=LOCALHOST:1521/ORACLE1;USER ID= 你的ID ;PASSWORD=你的密码;" 6 , b => b.UseOracleSQLCompatibility(version)); 7 //配置Sql输出控制台 8 var loggerFactory = new LoggerFactory(); 9 loggerFactory.AddProvider(new EFLoggerProvider()); 10 builder.UseLoggerFactory(loggerFactory); 11 });
查询数据
在执行查询时,需要注意的有两点,一是Table映射的实体类的属性名称如果不显式指定的话需要使用全大写的形式,不然会报错“ORA-00904:标识符无效”。因为生成的Sql中是将所有的字段名都用双引号修饰的,而Oracle中存储字段名默认都是采用全大写的形式的。
所以如果我们不想将实体类所有的属性名称写成大写,就需要显式的指定映射的column的名称。可以使用DataAnnotations的特性标注所有属性,或者在Context中指定列映射。另外如果执行查询时需要指定Schema,也像下面这样配置。
方法1:
1 protected override void OnModelCreating(ModelBuilder modelBuilder) 2 { 3 //判断当前数据库是Oracle 4 if (this.Database.IsOracle()) 5 { //如果需要,手动添加Schema名称,如果是默认或者表前不需要Schema名就可以不用配置 6 modelBuilder.HasDefaultSchema("XUHY"); 7 } 8 modelBuilder.Entity<Employee>(entity => 9 { 10 entity.ToTable("EMPLOYEE"); 11 entity.Property(e => e.Id).IsRequired();12 //列映射 13 entity.Property(e => e.Id).HasColumnName("ID"); 14 entity.Property(e => e.BirthDay).HasColumnName("BIRTHDAY"); 15 entity.Property(e => e.Department).HasColumnName("DEPARTMENT"); 16 entity.Property(e => e.EmployeeNo).HasColumnName("EMPLOYEENO"); 17 entity.Property(e => e.IsValid).HasColumnName("ISVALID"); 18 entity.Property(e => e.Name).HasColumnName("NAME"); 19 }); 20 base.OnModelCreating(modelBuilder); 21 }
方法2:
1 [Table("EMPLOYEE ")] //指定数据库对应表名 2 public class Employee 3 { 4 [Key] //主键 5 [Column("ID")] //指定数据库对应表栏位名称 6 public long Id { get; set; } 7 [Column("EMPLOYEENO")] 8 public int EmployeeNo { get; set; } 9 [Column("NAME")] 10 public string Name { get; set; } 11 [Column("BIRTHDAY")] 12 public DateTime BirthDay { get; set; } 13 [Column("DEPARTMENT")] 14 public string Department { get; set; } 15 [Column("ISVALID")] 16 public bool IsValid { get; set; } 17 }
经过上面的这些配置,便可以正常的执行数据的查询了。
1 await using var context = new MyContext(options); 2 var employeeCollection = await context.Employee.AsNoTracking().ToListAsync(); 3 //var employeeCollection = await context.Employee.OrderBy(e => e.EmployeeNo).AsNoTracking().Skip(0).Take(5).ToListAsync();//分页 4 foreach (var employee in employeeCollection) 5 { 6 Console.WriteLine($"{employee.Name} | {employee.EmployeeNo} | {employee.Department}"); 7 } 8 9 Console.ReadLine();
插入数据
插入数据倒是和其他数据库差别不大,但是由于Oracle天生是没有自增Id的,如果我们设计的表的主键需要采用自增键就需要使用Sequence代替。在这种情况下就需要为实体类的主键指定Sequence的名称了。
1 modelBuilder.Entity<Employee>(entity => 2 { 3 entity.ToTable("EMPLOYEE"); 4 entity.Property(e => e.Id).IsRequired(); 5 //!!!指定需要关联的序列名称!!! 6 entity.Property(e => e.Id).UseHiLo("SEQ_EMPLOYEE_ID"); 7 //列映射 8 entity.Property(e => e.Id).HasColumnName("ID"); 9 entity.Property(e => e.BirthDay).HasColumnName("BIRTHDAY"); 10 entity.Property(e => e.Department).HasColumnName("DEPARTMENT"); 11 entity.Property(e => e.EmployeeNo).HasColumnName("EMPLOYEENO"); 12 entity.Property(e => e.IsValid).HasColumnName("ISVALID"); 13 entity.Property(e => e.Name).HasColumnName("NAME"); 14 });
随后便可以执行插入数据了。
1 var employee = new Employee() 2 { 3 EmployeeNo = 6, 4 Name = "老周", 5 Department = "d6", 6 BirthDay = DateTime.Now.AddYears(-40), 7 IsValid = true 8 }; 9 await using var context = new MyContext(options); 10 var res = await context.Employee.AddAsync(employee); 11 await context.SaveChangesAsync(); 12 Console.WriteLine("添加成功"); 13 Console.ReadLine();
以上就是简单总结的使用EF Core操作Oracle数据库时的一些比较特别的地方,当然本人仍在探索实践中,如有新发现也会抽空更新上来。