首页 > 代码库 > 应用开发之Linq和EF

应用开发之Linq和EF

上一章笔者对于WinForm开发过程用到的几个知识点做了讲解。笔者们可以以此为开端进行学习。而本章我们来讲一个跟ORM思想有关的知识点。在讲之前让我们想一下关于JAVA的hibernate知识点。hibernate也是ORM框架。记得hibernate里面有一个叫HQL。先不管HQL的好与坏。主要是明白HQL的目地是什么。ORM的思想就是为了让用户在操作数据的时候用上面向对象的思想来看,而不是二维数据了。所以HQL笔者认为就是一个面向对象思想的SQL语句。那么为什么笔者要讲到HQL呢?事实上笔者认为Linq有一点跟他类似。如果项目架构是三层的话,就是让业务层的开发人员不用在看二维数据了。就连SQL语句都是面向对象思想形式来操作了。而EF(Entity Framework)可以说就是hibernate。即是可以理解为Linq的数据源。但是HQL要在hibernate上面才能有效果。Linq却可以不用EF。

Linq语法

.NET对于Linq知识的分类让笔者有时候觉得很无力。为什么呢?最早的时候笔者认为Linq知识点分三大块。分别为Linq to SQL、Linq to Entity、Linq to Database。随着对Linq使用的增加却发现还有Linq to Xml 、Linq to Excel等。笔者想读者们是不是看出门道来了。可以说.NET在设计Linq的时候,应该是有充分的想过将来扩展的问题。当然这不是本章的目标。笔者在开发过程中最常用的就是Linq to SQL和 Linq to Entity。另外还有一个叫Linq to Object.对于Linq to Object笔者一直认为就是Linq to Entity。笔者的意思是指他们的知识该应放在一块。好了。先笔者讲一下关于Linq to SQL。

对于Linq to SQL来讲,只要学习SQL语法的人都不用担心很容易就上手。记得笔者学习的时候,一看我去不就HQL的另一种形态吗?当然 HQL可不是Linq还是要学习一下的。讲那么多没有用。用列子才是最好的。

一、建立EF环境。先建一个项目,然后通过NUGET来获得对应的EF的DLL。对于NUGET是什么。相信看过《Java进击C#——项目开发环境》的人应该可以了解到。选择“引用”右击》管理Nuget程序包。

技术分享

相信看了上面的图片的时候,我们已经发现了EntityFramework了吧。点击“安装”就可以了。这个时候项目就会多出一个叫packages.config文件。这里面记录着当前安装的dll信息。同时物理目录里面会多出一个文件夹packages来存在这些dll。

技术分享

我们看到引用里面多出了关于EF的引用dll。这个时候我们就可以做EF的事情了。

二、新建EF上下文。EF有一个很重要的类。可以说是学习EF的核心点。这个类就是DbContext。笔者新建一类叫AomiContext继承他。如下

public class AomiContext : DbContext
{

}

DbContext类有几个构造函数。笔者这里讲一个常用的吧。如下

 public DbContext(string nameOrConnectionString);

就是个构造函数意思就是传一个连接字符串或是配置文件的连接字符的配置名。记得上一节中讲的App.config了吧。没有错就是要用到他。看一下笔者写的内容吧。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="aomi" connectionString="Data Source=.;Initial Catalog=Ado;Persist Security Info=True;User ID=sa;Password=123" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="http://www.mamicode.com/v11.0" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
</configuration>

上面的connectionStrings部分是笔者自己写的。其他是自动生成的。.NET自己有一个配置连接字符串的节点。我们就是在这个节点上写入自己的连接就可以了。

<add name="aomi" connectionString="Data Source=.;Initial Catalog=Ado;Persist Security Info=True;User ID=sa;Password=123" providerName="System.Data.SqlClient"/>

好了。接下来就是把AomiContext类修改一下。让他跟对应的连接字符串的配置发生关系。如下

 public class AomiContext : DbContext
    {
        public AomiContext()
            : base("Aomi")
        { }
    }

看到红色部分的代码了吧。把Aomi就是对应上面配置add节点的name的值。这个时候EF会自己去配置文件里面去找。

三、建立表和类的映射。

对应数据库的表:

CREATE TABLE [dbo].[Catalogs](
    [ID] [int] NOT NULL,
    [CatalogName] [nvarchar](50) NULL,
    [CatalogCode] [nvarchar](50) NULL,
 CONSTRAINT [PK_Catalogs] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

对应数据库的数据

INSERT [dbo].[Catalogs] ([ID], [CatalogName], [CatalogCode]) VALUES (1, N‘小吃‘, N‘c0001‘)
INSERT [dbo].[Catalogs] ([ID], [CatalogName], [CatalogCode]) VALUES (2, N‘计算机‘, N‘c0002‘)

笔者建一个类用于跟数据库里面的表相对应。这个时候要记得属性要跟表里里面的列名一样子才行。代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LinqExample
{
    public class Catalogs
    {
        public int ID { set; get; }

        public string CatalogName { set; get; }
  
        public string CatalogCode { set; get; }
    }
}

有了对应的类之后,还不行。我们还要修改一下AomiContext类。这样子就可以通过AomiContext类来访问对应的类对象了。即是数据了。如下

    public class AomiContext : DbContext
    {
        public AomiContext()
            : base("Aomi")
        { }

        public IDbSet<Catalogs> Catalogs { set; get; }
    }

四、执行EF。

 class Program
    {
        static void Main(string[] args)
        {
            using (AomiContext ac = new AomiContext())
            {
                IQueryable<Catalogs> queryable = from c in ac.Catalogs select c;
                List<Catalogs> catalogList = queryable.ToList();

                foreach (Catalogs catalog in catalogList)
                {
                    Console.WriteLine(string.Format("ID:{0} CatalogName:{1}", catalog.ID, catalog.CatalogName));
                }
            }

            Console.ReadKey();
        }
    }

执行结果:

技术分享

从上面的例子我们可以看到用了from c in ac.Catalogs select c;来获得对应的数据。这便是linq to sql。简章讲他是一个面向对象的SQL语句。SQL语句是以select开头,结尾不确定。而linq to sql一般是以from开头,以select结尾。表示从哪一个数据源开始,最后要以什么样子返回。可是我们可以看到他返回是一个IQueryable<T>类型。事实这个时候他并没有去执行获得数据。可以解理为他现在只是去组装SQL语句。只到queryable.ToList();才是去执行获得数据。为了学习上的方便笔者又加几个关键字让大家看一下。

using (AomiContext ac = new AomiContext())
            {
                IQueryable<Catalogs> queryable = from c in ac.Catalogs where c.CatalogName.Contains("吃") orderby c.CatalogCode ascending select c;
                List<Catalogs> catalogList = queryable.ToList();

                foreach (Catalogs catalog in catalogList)
                {
                    Console.WriteLine(string.Format("ID:{0} CatalogName:{1}", catalog.ID, catalog.CatalogName));
                }
            }

看完了linq to sql之后,让我看一下关于linq to entity又是什么东东呢?可是这样子讲linq的语法都相像。只是用法和写法不一样子而以。把上面的例子变一变吧。

 using (AomiContext ac = new AomiContext())
            {
                //IQueryable<Catalogs> queryable = from c in ac.Catalogs where c.CatalogName.Contains("吃") orderby c.CatalogCode ascending select c;
                IQueryable<Catalogs> queryable = ac.Catalogs.Where(t => t.CatalogName.Contains("吃")).OrderBy(t => t.CatalogCode).Select(c=> c);
                List<Catalogs> catalogList = queryable.ToList();
                foreach (Catalogs catalog in catalogList)
                {
                    Console.WriteLine(string.Format("ID:{0} CatalogName:{1}", catalog.ID, catalog.CatalogName));
                }
            }

看样子笔者不用多说也明白。就是变成了对应的关键字方法而以。没有错。就是这样子。这个时候笔者就可以这样子认为不管是Linq to entity还是Linq to sql都必须要有对应的数据源。这里EF就是为他们提供数据源的。他们俩个对应都是返回IQueryable<T>类型。只是Linq to entity是用方法。而Linq to sql更多像SQL语句。

好了。让我们看一下关于linq to object吧。可以这样了讲吧——不管是Linq to sql还是linq to entity他们俩个都离不开linq to object。linq to object是专对内存中的数据进行处理。我们可以看到上面例子中有出现一段queryable.ToList()。如果笔者说ToList()是linq to object会不会有人喷我。为什么笔者说他是linq to object呢?主要是ToList()是对于IEnumerable<T>进行静态扩展的。IEnumerable<T>一般都是用于数组和集合。位于内存中的。而上面都是专对于IQueryable<T>类型的。好了。如果你实在觉得笔者分的不对的话,那就是不要分了。都为Linq语法就行了。linq还提供了一些比较常用的方法。

First:返回第一个数据。没有数据就出生异常。同时也可以传入第一个数据的条件作为参数。如queryable.First(t => t.CatalogName.Contains("吃"));。

FirstOrDefault:同样子返回第一个数据,没有数据的话,就返回NULL。同时也可以传入第一个数据的条件作为参数。

Last:同理获得最后一方法。用法同上一样子。

LastOrDefault:同上一样子。跟FirstOrDefault用法一样子。

Skip:给定一个数字,那么数字前面都不会取出来,后以的才取出来。一般都跟Take方法一起用来作分页功能。

Take:表示要返回的数量。你可以理解为SQL语句中的TOP关键字。

让笔者举个linq to object的列子吧。

List<string> src = http://www.mamicode.com/new List();"a1");
src.Add("b2");
src.Add("c4");
src.Add("d5");
string value = http://www.mamicode.com/src.First(t => t.StartsWith("a"));
Console.WriteLine(value);

注意:笔者是这样子分的。一般静态扩展IQueryable<TSource> 的方法属于linq to entity。而静态扩展IEnumerable<TSource>则为linq to object。俩者很像。只是linq to entity必须要有数据源。linq to object一般是处理内存数据。

Entity Framework

Entity Framework做为ORM框架之一。所以ORM框架必须有的东西他多有。学习Entity Framework就必须知道他有什么知识点。Entity Framework根据开发模式的不同分为Code First、Model First和Database First。让笔者用土一点的说法来讲吧。

Code First模式:就是通过写代码来生成对应的数据库和表。

Model First模式:事实上跟Code First有一点像。只是他用了.NET的一个叫xxx.edmx的文件来操作而以。通过他来生成对应的数据库和表。

Database First模式:却跟前面俩个相反。先建数据库和表在生成对的类。即是代码。

我们现在要学习Entity Framework。笔者个人意见读者们最好选择Code First模式来学习。为什么。不管是Model First模式还是Database First模式大部分都是软件工具帮你生成对应的代码。所以很多东西我们根本看不到。而Code First模式就是要开发人员手把手的写了。记得笔者在使用hibernate的时候。并没有说只做一边的事情。一般都是数据库的表建完之后。还是要去写对应的映射配置文件(xxx.hbm.xml)。好一点就自己写一个代码生成器。Entity Framework意图就是帮开发人员做掉一边的工作。不过这也是笔者不喜欢的。正因为这样子Entity Framework多出了一个知识点那就是数据迁移。我们都知道在开发的过程中。可能会因为当初表没有设计好。突然发现需要增加一个字段。这样个时候Entity Framework就要做很多事情。假设我们用的是Code First吧。我们在代码中的类增加一个属性。这个时候Entity Framework就是要去判断哪些属性是旧的。哪些属性是修改的。哪些属性是新增加的。然后Entity Framework在更新数据库。就是Entity Framework的数据迁移。

从上面的讲解中我们知道EF想帮我做了另一半的事情,所以就必须对数据库操作才行。那么就是存在对数据库设置,对表设置,对数据操作。以下全是在在Code First模式下的讲说。

1、EF对数据库的设置。执行代码的时候,EF会去判断是否存在对应的数据库。而对数据库进行操作。是创建还是删除在创建。还是更新呢?主要看你设置对应的数据库操作的模式。那么EF为我们提供了三个。当然我们可以自己写一个。三个类都在System.Data.Entity命名空间下。分别是CreateDatabaseIfNotExists、DropCreateDatabaseAlways、DropCreateDatabaseIfModelChanges。文英好的人都能看得懂是什么一会事。

 public AomiContext()
            : base("Aomi")
        {
            Database.SetInitializer<AomiContext>(new CreateDatabaseIfNotExists<AomiContext>());
        }

注意:关于Database.SetInitializer方法的赋值是可以放在别的地方。但一定要执行EF之前。

2、EF对表的设置。这些设置大部是关于表和表与表之间的关系如何体现在类和类与类之间的关系。下面笔者做了一个简单的映射例子。

 public class AomiContext : DbContext
    {
        public AomiContext()
            : base("Aomi")
        { }

        public IDbSet<Catalogs> Catalogs { set; get; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Configurations.Add(new CatalogsMap());

        }
    }

下面CatalogsMap类就相当于映射配置文件(xxx.hbm.xml)。其中包括一对一,一对多,多对多之类的关系也是在这里配置。有一点要注意的是上面红色代码。即是把映射关系加入EF配置里面

 public class CatalogsMap : EntityTypeConfiguration<Catalogs>
    {
        public CatalogsMap()
        {
            this.HasKey(t => t.ID);
            this.Property(t => t.CatalogName).HasColumnName("CatalogName");
            this.Property(t => t.CatalogCode).HasColumnName("CatalogCode");
        }
    }

3、对数据的操作。对数据的操作一般就是增删改查了。

增加:

using (AomiContext ac = new AomiContext())
{
      Catalogs catalogs = new Catalogs();
      catalogs.ID = 6;
      catalogs.CatalogName = "商品";
      catalogs.CatalogCode = "s0001";
      ac.Catalogs.Add(catalogs);
                
      ac.SaveChanges();
}

事实上面的代码是没有问题。可是执行的时候却会发生错误。为什么呢?笔者也不是清楚什么原因。查找没有问题。可是在增加却会出问题。让我看一下异常吧。这里笔者只复制出一部分。

       InnerException: System.Data.SqlClient.SqlException
            _HResult=-2146232060
            _message=不能将值 NULL 插入列 ‘ID‘,表 ‘Ado.dbo.Catalogs‘;列不允许有 Null 值。INSERT 失败。
语句已终止。
            HResult=-2146232060
            IsTransient=false
            Message=不能将值 NULL 插入列 ‘ID‘,表 ‘Ado.dbo.Catalogs‘;列不允许有 Null 值。INSERT 失败。
语句已终止。
            Source=.Net SqlClient Data Provider
            ErrorCode=-2146232060
            _doNotReconnect=false

他说我的ID没有设置值,可是我设置了。笔者想你们一定会认为是没有设置标识。也就是自动增长。不是这样子的。笔者本来就是没有想过要自动增长啊。那么为什么会错呢?事实笔者也是以这样子的角度去想的。会不会EF默认就认为ID是自动增长。因为ID是int类型的。又是主键。所以我就在映射配置里面加了一段代码。如下

 public class CatalogsMap : EntityTypeConfiguration<Catalogs>
    {
        public CatalogsMap()
        {
            this.HasKey(t => t.ID);
            this.Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
            this.Property(t => t.CatalogName).HasColumnName("CatalogName");
            this.Property(t => t.CatalogCode).HasColumnName("CatalogCode");
        }
    }

红色部分就是增加的代码。这个时候就不会出错了。那么这段代码是什么意思。就是告诉EF这里的ID只是普通的。没有别的设置。

修改:

using (AomiContext ac = new AomiContext())
{
     Catalogs updateCatalogs = ac.Catalogs.FirstOrDefault(t => t.ID == 4);
     updateCatalogs.CatalogName = "纸类";
                
     ac.SaveChanges();
}

删除:

using (AomiContext ac = new AomiContext())
{
 Catalogs deleteCatalogs = ac.Catalogs.FirstOrDefault(t => t.ID == 1);
 ac.Catalogs.Remove(deleteCatalogs);
                
 ac.SaveChanges();
}

笔者在做EF的增删改的时候,我心里面一直在想俩个问题?

第一:EF并没有像Hibernate那样子。有增加的方法和更新的方法。他只有一种概念那就是数据有没有发生改变。根据改变数据来更新数据库的数据。

第二:Hibernate在处理对象的时候。会用到对象的三种状态。这三种状态在不同的书里面有不同的叫法。笔者一般喜欢叫他们为普通状、持久状、游离状。可是如果把Hibernate这个知识放在EF这边来的话,也不是说不可以。只是觉得这个时候有一点怪。EF这边并没有类似相关的说明。可是笔者还是觉得有必要用他放在EF这边。为什么呢?先让我们看一下情况吧。如果我们把上面的ID变成了标识。即为自动增长。在增加的时候就没有必要去设置这个值。那么增加成功之后我们要如果去获得对应的ID值呢?难道在获取一遍吗?显然不是。如下。

class Program
    {
        static void Main(string[] args)
        {
            using (AomiContext ac = new AomiContext())
            {
                Catalogs catalogs = new Catalogs();
                catalogs.ID = 0;
                catalogs.CatalogName = "商品";
                catalogs.CatalogCode = "s0001";
                ac.Catalogs.Add(catalogs);

                ac.SaveChanges();

                Console.WriteLine("ID:" + catalogs.ID);
            }

            Console.ReadKey();
        }
    }

笔者把上面的数据全部删除掉。并且把Catalogs表的列ID修改为标识。即是自动增长。在新建Catalogs对象的时候把ID设置为0。然后我们在看一下增加成功之后ID是不是还是为0。但是记得把CatalogsMap类里面的映射配置修改一下。修改如下。

 this.Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

好了,让我们看一下结果值是多少。如下。我们看到是7。为什么不是0。笔者的SQL Server的标识是从6开始的。所以这边是7。如果你们一开始的话。是1。但决对不是0。为什么?就是因为对象变成了持久状了。

技术分享

普通状:就是正常用关键字new来创建。

持久状:就是通过EF之后,比如增加。这个时候对象跟数据库同步。

游离状:关闭EF之后的对象。不过笔者认为EF没有这种状态。因为Hibernate有唤醒这个功能。

本章总结

本章主要讲到关于Linq和EF的知识点。Linq的一些入门用法和EF的基本知识点。当然,有关EF的类与类之间的关系和数据迁移笔者笔者必没有说。希望读者们自行查看。

应用开发之Linq和EF