entitywrapper like怎么查询不为数据库的某一列不为null

Entity Framework 查询使用集成查询,简称LINQ。LINQ是一个查询框架,并不限于Entity Framework使用,同样不限于数据库。LINQ Provider 负责将LINQ查询翻译成对数据的查询,然后返回查询结果。Entity Framework的LINQ Provider是LINQ to Entities,它将LINQ查询翻译成目标数据库的SQL查询语句。
除了LINQ,Entity Framework还支持基于文本的查询Entity SQL,简称ESQL。ESQL通常使用在需要动态构造查询的情况下。由于ESQL不常用,没有直接暴露在DbContext API。如果需要使用ESQL,需要使用IObjectContextAdapter接口访问ObjectContext API。
一、用到的Model
本篇文章使用BAGA模型,BAGA模型的完整代码可以点击下载,下面给出两个主要实体的代码。
Destination实体:
[Table("Locations", Schema = "baga")]
public class Destination
public Destination()
this.Lodgings = new List&Lodging&();
[Column("LocationID")]
public int DestinationId { }
[Required, Column("LocationName")]
[MaxLength(200)]
public string Name { }
public string Country { }
[MaxLength(500)]
public string Description { }
[Column(TypeName = "image")]
public byte[] Photo { }
public string TravelWarnings { }
public string ClimateInfo { }
public virtual List&Lodging& Lodgings { }
Lodging实体:
public class Lodging
public int LodgingId { }
[Required]
[MaxLength(200)]
[MinLength(10)]
public string Name { }
public string Owner { }
public decimal MilesFromNearestAirport { }
[Column("destination_id")]
public int DestinationId { }
public Destination Destination { }
public List&InternetSpecial& InternetSpecials { }
public Nullable&int& PrimaryContactId { }
[InverseProperty("PrimaryContactFor")]
[ForeignKey("PrimaryContactId")]
public Person PrimaryContact { }
public Nullable&int& SecondaryContactId { }
[InverseProperty("SecondaryContactFor")]
[ForeignKey("SecondaryContactId")]
public Person SecondaryContact { }
二、查询所有数据
查询所有的数据有以下两种方式:
private static void PrintAllDestinations()
using (var ctx = new BreakAwayContext())
foreach (var destination in ctx.Destinations)
Console.WriteLine(destination.Name);
private static void PrintAllDestinations()
using (var ctx = new BreakAwayContext())
var destinations = ctx.Destinations.ToList();
foreach (var destination in destinations)
Console.WriteLine(destination.Name);
首先,上面两种方式生成的SQL是相同的,下面说一下它们之间的区别:
第一种情况,当应用程序请求第一条结果时,查询语句发送到数据库,也就是在foreach第一次循环期间,但是EF没有一次返回所有的数据。这是查询保持激活状态,当需要数据时就从数据库中读取。这种情况下需要注意的是,每次对DbSet的内容进行遍历都会查询数据库。如果不停地从数据库中查询相同的数据,就会出现明显的性能问题,避免上述问题的方法是使用第二种方式。
第二种情况通过使用ToList()一次从数据库中查询得到所有的数据存放到内存中,不管反复查询多少次,都是在内存中进行的操作,因此不存在上述提到的性能问题。
另外,发送到数据库中的查询是查找DbSet中的项目,遍历DbSet只能得到存在于数据库中的项目,任何停留在内存还没保存到数据库中的项目都不会出现在查询的结果中。
上面最后一句话用程序表达就是下面的程序不会打印出名字为&杭州&的目的地,因为它还没有保存到数据库中。
using (var ctx = new BreakAwayContext())
ctx.Destinations.Add(new Destination()
Name = "杭州",
Country = "中国"
var destinations = ctx.Destinations.ToList();
foreach (var destination in destinations)
Console.WriteLine(destination.Name);
三、查找单个对象
查询单个对象最常见的情况就是根据主键来查询。为此,DbContext API提供了Find方法。如果Find根据提供的主键值找到相应的对象就将它返回,如果不存在就返回Null。
Find不仅查询数据库,而且还查询新添加的没有保存到数据库中的对象。Find使用以下规则查找对象:
1.在内存中查找从数据库中加载或附加到上下文的已存在的实体。
2.查找新添加的还没有保存到数据库中的对象。
3.在数据库中查找还没有加载到内存中的实体。
看下面的程序:
static void FindDestination()
Console.WriteLine("请输入目的地的Id:");
var id = int.Parse(Console.ReadLine());
using (var ctx = new BreakAwayContext())
ctx.Destinations.Add(new Destination()
DestinationId = 3,
Name = "杭州",
Country = "中国"
var destination = ctx.Destinations.Find(id);
if (destination == null)
Console.WriteLine("目的地不存在!");
Console.WriteLine(destination.Name);
在上面的程序中,DestinationId的值是由数据库自动生成的,为了演示Find可以查找没有保存到数据库的对象,我手动分配了一个值:3 给它,数据库中也存在DestinationId为3的记录。运行上面的程序,输入3,结果如下图所示:
下面看一下Single方法。
Single方法适用于不根据主键进行查询或查询时加载相关实体的情况。
static void FindSpain()
using (var ctx = new BreakAwayContext())
var spain = ctx.Destinations.Single(t =& t.Name == "西班牙");
Console.WriteLine(spain.Description);
如果Single查询没有返回结果或返回的结果多于一个就会抛出异常。
SingleOrDefault方法
SingleOrDefault和Single的区别是如果查询没有结果返回,那么SingleOrDefault返回null,Single则抛出异常。但是如果返回的结果多于一个,那么两者都会抛出异常。
First和FirstOrDefault方法
如果不关心是否有多个结果,仅仅取得第一条,就是用First或FirstOrDefault方法。
如果查询没有结果返回,First会抛出异常,FirstOrDefault返回null。
四、查询本地数据(内存中的数据)
前边提到了Find方法,它在从数据库中查询数据之前会先查询内存中的数据,不足之处是它只能根据主键来查询,但是我们会经常性的对已经存在于内存中的数据进行复杂查询并且能被DbContext追踪。
这样做的原因主要可能是,当需要的数据已经加载到内存中了,要避免发送多个查询到数据库。前面使用ToList()方法将查询的结果复制到了List中,这种方法在同一个代码块中使用起来没有问题,但是在应用程序的多个地方使用的话就要到处传递这个List了,显然这样使用起来有点乱。举个例子:当应用程序加载时,从数据库中加载所有的Destinations,应用程序不同的地方都会对它进行不同的查询,有的地方可能显示所有的Destinations,有的地方可能根据Name排序,有的地方可能根据Country条件过滤查询。
另一个原因就是希望查询的结果中包含新添加的数据(未保存到数据库中的数据)。ToList()方法总是发送查询到数据库,也就是说没有保存到数据库中的数据不能包含在查询结果中。
上面提到的两种情况,使用本地查询都能解决。查询内存中的数据使用Local属性,Local属性返回所有从数据库中加载的数据以及新添加的数据(未保存到数据库)。任何被标记为Deleted,但是还没有从数据库中删除的数据都会被过滤掉。
下面看个例子,先了解一下本地查询:
static void GetLocalDestinationCount()
using (var ctx = new BreakAwayContext())
int count = ctx.Destinations.Local.C
Console.WriteLine("内存中Destination的个数:" + count);
foreach (var destination in ctx.Destinations)
count = ctx.Destinations.Local.C
Console.WriteLine("内存中Destination的个数:" + count);
运行结果如下图所示:
使用Load方法加载数据到内存
前边的例子使用foreach循环将数据加载到内存中,仅仅是加载数据,这样多少有点不称职,而且有那么一点点不清楚代码是干什么用的。幸好DbContext提供了Load方法,上面的例子使用Load方法如下:
static void GetLocalDestinationCount()
using (var ctx = new BreakAwayContext())
int count = ctx.Destinations.Local.C
Console.WriteLine("内存中Destination的个数:" + count);
ctx.Destinations.Load();
count = ctx.Destinations.Local.C
Console.WriteLine("内存中Destination的个数:" + count);
Load方法是在IQueryable&T&上的扩展方法,因此可以加载指定条件的数据到内存中。方法如下所示:
ctx.Destinations.Where(t=&t.Country=="中国").Load();
上面的代码是加载Destination在中国的数据到内存中。
使用ObservableCollection
Local属性返回的类型是ObservableCollection&TEntity&,这种类型的集合,无论往集合中添加还是移除对象,都允许订阅通知。熟悉WPF的,想必对ObservableCollection&TEntity&已经非常了解了。
当Local的内容变化时,Local会激发CollectionChanged事件,包括以下情况:通过查询从数据库中加载数据,将一个新的对象添加到DbContext,或已经存在内存的对象被标记为删除。
下面看个例子:
static void ListenToLocalChanges()
using (var ctx = new BreakAwayContext())
ctx.Destinations.Local
.CollectionChanged += (sender, args) =&
//添加到Local中的项
if (args.NewItems != null)
foreach (Destination item in args.NewItems)
Console.WriteLine("Added: " + item.Name);
//从Local中移除的项
if (args.OldItems != null)
foreach (Destination item in args.OldItems)
Console.WriteLine("Removed: " + item.Name);
//加载国家在中国的Destination到Local中
ctx.Destinations.Where(t =& t.Country == "中国").Load();
//查询Id为3的Destination,也添加到了Local中
var destination = ctx.Destinations.Find(3);
if (destination != null)
//从Local中移除Id为3的Destination
ctx.Destinations.Remove(destination);
int count = ctx.Destinations.Local.C
Console.WriteLine("内存中Destination的个数:" + count);
上面的代码注册了CollectionChanged事件,新添加到Local中或从Local中的项都打印出来。
运行结果如下图所示:
五、加载关联数据
加载关联的数据有3种方法:延迟加载、预先加载、显示加载。先来看延迟加载。
延迟加载在程序中是最显而易见的,例如,加载了一个Destination,如果想使用Destination的Lodgings属性,EF会自动发送查询到数据库来加载位于这个Destination的所有Lodgings。
EF的延迟加载使用的是动态代理,下面是它的工作过程:
当EF返回查询的结果时,它会创建类的实例并使用从数据库返回的数据进行填充。EF具有在运行时动态创建派生自POCO类的新类的能力。新类充当POCO类的代理,称为动态代理。当属性被访问时,它会重写POCO类的导航属性并包含一些从数据库获取数据的逻辑。
使用动态代理完成延迟加载,需要满足一定的规则。如果不能满足规则,EF就不能创建类的动态代理,只能返回不能进行延迟加载的POCO类的实例。下面是要满足的规则:
1.POCO类必须是public的且不能为sealed。
2.需要延迟加载的导航属性必须标记为virtual,以便EF可以重写导航属性加入延迟加载的逻辑。
延迟加载的缺点
不合理的使用延迟加载会导致大量的查询发送到数据库。例如,加载50个Destination,然后访问每个Destination的Lodgings属性,这样就会有51条查询语句发送到数据库。合理的做法是使用一条查询语句加载所有的数据,这也正是预先加载要做的。
关闭延迟加载
关闭延迟加载可以去掉导航属性的virtual标记,还可以将DbContext.Configuration.LazyLoadingEnabled属性设置false,设置false之后即使导航属性标记为virtual,延迟加载也不会起作用。
预先加载关联的数据需要你告诉EF关联什么数据,然后EF在产生的SQL语句中使用Join,一条语句加载所有的数据。告诉EF关联的数据使用Include方法。看下面的例子:
static void TestEagerLoading()
using (var ctx = new BreakAwayContext())
//var allDestinations = ctx.Destinations.Include("Lodgings");
//使用Include的泛型方法,需要引进System.Data.Entity命名空间
var allDestinations = ctx.Destinations.Include(t =& t.Lodgings);
foreach (var destination in allDestinations)
Console.WriteLine(destination.Name);
foreach (var lodging in destination.Lodgings)
Console.WriteLine(" - " + lodging.Name);
运行结果如下图:
可以在单个查询中包含多个关联的数据集合,比如说可以在查询Lodgings时,关联PrimaryContact和Photo,代码如下:
ctx.Lodgings.Include(t =& t.PrimaryContact.Photo);
PrimaryContact是Person类,Photo是PersonPhoto类,具体可以下载本文的源码查看。
还有一种情况就是,查询Destinations,要关联Lodgings,同时关联每个Lodging的PrimaryContact,代码如下:
ctx.Destinations.Include(t =& t.Lodgings.Select(l =& l.PrimaryContact));
在一个查询中,还可以包含多次Include,例如,查询Lodgings时,既想包含PrimaryContact,还想包含SecondaryContact,代码如下所示:
ctx.Lodgings.Include(t =& t.PrimaryContact).Include(t =& t.SecondaryContact);
预先加载的缺点
使用预先加载,有一件事情一定要牢记就是越少的查询并不总是好的。减少查询的数量是以查询的简单性为代价的。包含的关联数据越多,发送到数据库查询中关联的数量也就会越多,结果就是查询变得更慢和更复杂。如果只是需要关联少量的数据,多个简单的查询常常要比复杂的查询要快。
显示加载在加载关联数据上和延迟加载相似,都是在主数据加载完后,再加载关联的数据。与延迟加载不同的是,它不会自动发生,需要手动调用方法加载数据。
下面可能是你会选择显示加载而不选择延迟加载的原因:
1.不需要标记导航属性为virtual。这一改变对一些人来说没有意义,而对另外一些人来说数据访问技术需要改变POCO类非常不理想,使用显示加载则不需要改变POCO类。
2.使用已有的类库,导航属性没有标记为virtual。
3.显示加载允许知道查询什么时候发送到数据库。延迟加载会潜在的产生很多查询,而显示加载何时何地运行查询都是非常明显的。
显示加载使用DbContext.Entry方法。一旦拥有了给定实体的entry,就可以使用Collection和Reference方法在导航属性上获取信息和执行操作。一个可行的操作就是Load方法,它发送一个查询到数据加载导航属性的内容。
下面看一下代码,加载集合导航属性:
static void TestExplicitLoading()
using (var ctx = new BreakAwayContext())
var hongkong = ctx.Destinations.Single(t =& t.Name == "香港");
ctx.Entry(china).Collection(d =& d.Lodgings).Load();
Console.WriteLine("香港住宿:");
foreach (var lodging in hongkong.Lodgings)
Console.WriteLine(lodging.Name);
加载引用导航属性代码:
var lodging = ctx.Lodgings.First();
ctx.Entry(lodging).Reference(l =& l.PrimaryContact).Load();
检查导航属性是否加载:
static void TestIsLoaded()
using (var ctx = new BreakAwayContext())
var hongkong = ctx.Destinations.Single(t =& t.Name == "香港");
var entry = ctx.Entry(hongkong);
Console.WriteLine(
"加载前: {0}",
entry.Collection(d =& d.Lodgings).IsLoaded);
entry.Collection(d =& d.Lodgings).Load();
Console.WriteLine(
"加载后: {0}",
entry.Collection(d =& d.Lodgings).IsLoaded);
查询集合导航属性的内容
到目前为止,已经看到了加载集合导航属性的整个内容。如果想筛选导航属性的内容,可以先加载到内存中进行操作,但是,如果只想加载导航属性内容的一个子集或者只求导航属性内容的个数抑或是一些其他计算,在数据库中计算比加载所有的数据到内存中更有意义。
使上述问题变得更有意义的是Query方法。假设想查找所有位于法国且离最近机场距离小于10里的Lodgings。
下面看一下代码:
static void QueryLodgingDistance()
using (var ctx = new BreakAwayContext())
var france = ctx.Destinations.First(t =& t.Country == "法国");
var lessTenMilesLodgings = ctx.Entry(france)
.Collection(t =& t.Lodgings)
.Where(t =& t.MilesFromNearestAirport & 10);
foreach (var lodging in lessTenMilesLodgings)
Console.WriteLine(lodging.Name);
由上图可以看到生成的SQL,where条件里包含了MilesFromNearestAirport&10,如果写成如下形式:
var lessTenMilesLodgings = france.Lodgings.Where(t =& t.MilesFromNearestAirport & 10);
会将DestinationId为3的Lodging都加在到内存中,然后在内存中筛选小于10里的数据。
通过文档可以发现Query()返回的是IQueryable&T&类型的。求集合导航属性内容的个数,只需要在Query()方法使用Count()扩展方法即可,代码如下:
var count = ctx.Entry(france)
.Collection(t =& t.Lodgings)
显示加载导航属性内容的子集
Query方法和Load方法可以组合使用。比如,可能只想加载位于法国且名字中包含&Martinique&的Lodgings,代码如下:
ctx.Entry(france)
.Collection(t =& t.Lodgings)
.Where(t =& t.Name.Contains("Martinique"))
注意,调用Load方法不会清除已经存在于导航属性中的任何对象。如果已经加载了位于法国且Name中包含&Martinique&的Lodgings,然后又加载了包含&Miquelon&的Lodgings,那么Lodgings导航属性将包含Martinique和Miquelon。
六、结束语
本文使用的完整源码和数据库到这里下载:
系列的其他文章。
如果遇到问题,可以加群:& 进行讨论。
外欢迎大家访问,网址是。
阅读(...) 评论().NET(79)
我们在使用Entity Framework的时候经常会把数据库中的某一个视图映射为EF的实体,但是如果数据库视图中的列没有包含表的主键列,EF会报出警告说视图没有主键,导致视图映射为实体失败,错误如下:
表/视图“{0}”未定义主键,无法推断有效的主键。已排除该表/视图。要使用该实体,您将需要检查架构,添加正确的键并对它取消注释。
English translation: The table/view '{0}' does not have a primary key defined and no valid primary key could be inferred. This table/view has been excluded. To use the entity you will need to review your schema, add the correct keys and uncomment it.
这时候我们需要在视图里面用sql的isnull函数生成一列值不能为null的列就行了。
比如假设现在我们的数据库中本来有一个视图叫V_Customer,它返回的列中没有包含查询表dbo.CustomersBoughtCarsSurvey的主键列,其Sql脚本如下:
CREATE VIEW [dbo].[V_Customer]
Name, Age, Sex, Nation, City
dbo.CustomersBoughtCarsSurvey
那么这个视图是无法映射为Entity Framework的实体的,现在我们修改视图增加一个自动生成GUID值的列叫Id,并且使用ISNULL函数确保该列值不为空,脚本如下:
CREATE VIEW [dbo].[V_Customer]
ISNULL(NEWID(), 'd1e57ca7-6eee-495a-be13-73d5e7d51f36') AS Id, Name, Age, Sex, Nation, City
dbo.CustomersBoughtCarsSurvey
我们可以看到现在在视图中多了一列id,且是not&null的,现在Entity Framework就可以将上面这个视图映射为实体了,并且自动将列Id映射为了实体键
所以如果要将数据库的一个视图映射为Entity Framework的实体,一定要确保该视图中有一列值是唯一的且是not&null的就可以了!
另外要注意sql的isnull函数第二个参数一定要设置为一个常量视图才认为这列是not&null的,比如本例中我们在视图中自定义的列Id是ISNULL(NEWID(), 'd1e57ca7-6eee-495a-be13-73d5e7d51f36') AS Id,isnull函数的第二个参数是写的一个GUID的常量值'd1e57ca7-6eee-495a-be13-73d5e7d51f36',这样SqlServer就会判定列Id肯定不会是null,所以视图定义中最后才会显示列Id是not&null的。
但是如果你将isnull第二个参数定义为一个不确定的值比如一个函数像这样ISNULL(NEWID(), NEWID()) AS Id那么最后SqlServer会认为即便列Id的值使用了isnull函数但是列Id还是有可能为null,所以列Id最后在视图定义中还是显示可以为null。最后该视图还是无法映射为Entity Framework的实体。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:56713次
排名:千里之外
原创:10篇
转载:107篇
评论:12条
(1)(1)(1)(2)(1)(3)(1)(7)(5)(9)(1)(10)(14)(19)(22)(20)11164人阅读
android(151)
java(116)
由于第二章是整个文档的核心,内容也很多,所以分次翻译。下一章的内容会继续本章接着翻译。
-------------------------------------------------------------------------------------
2&如何使用
这一章进入到更多详细地使用的各种功能。
2.1&配置你的
为了配置你的使其持久化,你需要做下面几步:
①添加注解到你每个需要持久化的类的顶部。你也可以用。
②添加注解到你需要持久化的字段的上面。你也可以使用和其他的。
③为每个添加一个无参的构造器,并且构造器在包内是可见的。
2.1.1&添加注解
自从开始,注解是有效的特殊代码标识,它提供信息包括类、方法或成员变量。把指定的类和成员变量存入数据库,既支持它自己的注解(和&)也支持很多来自包中标准的注解。注解是配置你的最简单的方式,当然你也可以使用代码或者(是个框架,更多信息一下)对进行配置。
用注解,对每个你想要持久化到数据库的类你都需要添加注解到public&class&这一行的正上方。每个被这些注解标记过的都将持久化到它自己的数据库表中。例如:
@DatabaseTable(tableName = &accounts&)
public class Account {
@DatabaseTable注解可以有个可选的参数,也就是这个类对于的表名。如果没有特别指出,那么这个类名将默认作为表名。使用示例中的的对象都将会作为一条记录持久化到数据库名为的表中。这将会在实例化内部的时候使用到。
除此之外,对于每个你需要添加注解到的成员变量上,这个类是需要持久化到数据库的。每个成员变量都将被作为数据库中记录的一个字段进行持久化。示例:
@DatabaseTable(tableName = &accounts&)
public class Account {
@DatabaseField(id = true)
@DatabaseField(canBeNull = false)
①字段,它是一个字符串并且是数据库中记录的唯一标识(主键)。在上面的示例中,表的每条记录都有两个字段:
②字段,它也是一个字符串,它不能为。
@DatabaseField注解可以用下面的一些成员:(对常用的字段进行翻译,其他的参考原文)
常用的注解
columnName
数据库的列名。如果你没有设置这个成员名,会用标准的形式代替它。
字段的数据类型。通常情况下,数据类型是从类的成员变量获取的,并不需要进行特殊指出。它相当于是的数据类型。
defaultValue
当我们在表中创建新的记录时的一个字段的默认值。默认情况下是没有这个值的。
字段的宽度,主要用于字符串字段。默认是,意味着采用默认的数据类型和具体的数据库的默认情况。对于字符串以为在个字符即使有些数据库并不支持。
字段是否能被分配值。默认是。如果你设置成,那么你每次在数据库中插入数据是都必须为这个字段提供值。
这个字段是否是,默认是。在一个中只有一个成变量可以有这个值。字段是一条记录的唯一标识而且是必需的,只有和&其中之一。
generatedId
字段是否自动增加。默认为。类中的一个成员变量设置了这个值,它告诉数据库每添加一条新记录都自动增加。当一个有的对象被创建时使用方法,数据库将为记录生成一个,它会被返回并且被方法设置进对象。
generatedIdSequence
序列编号的名字,这个值在生成的时候会被使用。和相似,但是你能够指定使用的序列名称。默认是没有的。一个中只有一个成员变量可以设置这个值。这仅仅在数据库需要序列生成时才需要它。如果你选择使用代替它,那么代码将自动增加序列名。
throwIfNull
unknownEnumName
uniqueIndexName
allowGeneratedIdInsert
foreignAutoRefresh
columnDefinition
uniqueIndex
uniqueCombo
uniqueIndexName
maxForeignAutoRefreshLevel
foreignColumnName
foreignAutoCreate
2.1.2&使用注解
取代使用注解,你可以使用来自包的更多的标准注解。取代注解,你可以使用注解。示例:
@Entity(name = &accounts&)
public class Account {
@Entity注解有个可选的参数,它用于指定表名。如果没有指定,类名将是默认的表名。
在每个成员变量中取代使用注解,你可以用注解:&示例:
下面这些注解和字段都支持:
用于关联的数据库表的名字。如果没有设置那么类名将被作为表名。
用作表字段的名字。如果没有设置那么变量名将作为字段名。
数据库表字段的长度。可能只有应用在字符串并且只被某些数据库类型支持。默认是。
设置成,那么这个字段允许插入值。
&添加一个约束,它在表中必须是唯一的。
@GeneratedValue
用于定义一个自动增长的值,它只能用于添加到注解。
@OneToOne&
成员变量使用这些注解后会被认为是外键字段。&没有实现多个或一个关系,它也不能使用任何注解成员。它只能使用这些注解的两者之一来表明它是一个外键。
@ManyToOne
@JoinColumn
设置成员变量的列名(字段名)。
设置成,那么这个字段允许插入值。
使用它将会把这些类型的成员转化成版本成员。
如果Column注解在成员变量上用了一个未知的类型,那么它将被认为是序列化类型字段并且这个对象需要实现。
2.1.3&添加无参构造器
在你给添加了注解字段后,你也需要添加一个无参的包内可见的构造器。当一个对象在查询中被返回时,使用反射机制构造一个对象并且构造器需要被调用。所以你最终的示例拥有注解和构造器的类应该像这样:
@DatabaseTable(tableName = &accounts&)
public class Account {
@DatabaseField(id = true)
@DatabaseField(canBeNull = false)
Account() {
// all persisted classes must define a no-arg constructor
// with at least package visibility
2.2&持久化数据类型&
下面这些类型能够被持久化到数据库。数据库具体编码帮助类型和数据库具体持有类型的相互转化。
(具体的类型相互转化在此就不作介绍了,参见原文)。
2.3&连接源
注意:关于连接源,用户应该参见手册中详细文档。
为了使用数据库和对象,你需要配置调用数据源和调用连接源。连接源是连接物理数据库的一个工厂。这里是创建简单、单连接的代码示例:
// single connection source example for a database URI
ConnectionSource connectionSource =
new JdbcConnectionSource(&jdbc:h2:mem:account&);
这包中也包括了类,它是一个相对简单的连接池的实现。一个数据库连接已经释放而成为关闭,之后向他们添加内部列表他们都会拒绝。只有在没有休眠的可用的连接时,新的连接才需要创建。也是同步的并且可以用于多线程中。在连接关闭前它可以设置空闲连接的最大数和存活的最长时间。
// pooled connection source
JdbcPooledConnectionSource connectionSource =
new JdbcPooledConnectionSource(&jdbc:h2:mem:account&);
// only keep the connections open for 5 minutes
connectionSource.setMaxConnectionAgeMillis(5 * 60 * 1000);
JdbcPooledConnectionSource也有一个一直存活的线程,它偶尔一下池中空闲的每个连接,目的是为了确认他们是有效的,关闭的那个就不再有效。在你从池中取得连接之前你也可以测试连接是否有效。
// change the check-every milliseconds from 30 seconds to 60
connectionSource.setCheckConnectionsEveryMillis(60 * 1000);
// for extra protection, enable the testing of connections
// right before they are handed to the user
connectionSource.setTestBeforeGet(true);
有很多其他额外的数据,他们能够被使用,包括很多更强大甚至高性能的池连接管理器。你可以用你自己直接封装的类来例举说明。
// basic Apache data source
BasicDataSource dataSource = new BasicDataSource();
String databaseUrl = &jdbc:h2:mem:account&;
dataSource.setUrl(databaseUrl);
// we wrap it in the DataSourceConnectionSource
ConnectionSource connectionSource =
new DataSourceConnectionSource(dataSource, databaseUrl);
当你用时,你想调用方法来关闭一些底层的连接。推荐像下面这样的模式。
JdbcConnectionSource connectionSource =
new JdbcPooledConnectionSource(&jdbc:h2:mem:account&);
// work with the data-source and DAOs
} finally {
connectionSource.close();
很不幸,接口没有关闭方法,所以你使用你必须关闭底层数据源,通过操作上的()方法。
一旦在你的类中有注解并且定义了你的,你就需要创建一个(),它是一个拥有数据库操作句柄的单一持久化类。每个都有两个泛型参数:①我们用持久化的类,②字段,它用于确定数据库具体的记录。如果你的类没有字段,你可以放入或者作为第二个参数。例如,在上面的类,成员变量是字段,所以类是。
创建最简单的方式是使用类的静态方法。示例:
Dao&Account, String& accountDao =
DaoManager.createDao(connectionSource, Account.class);
Dao&Order, Integer& orderDao =
DaoManager.createDao(connectionSource, Order.class);
注意:你需要使用方法创建你自己的类,所以如果内置功能是需要他们,他们可以被再次利用并且不能再次生成。创建会有昂贵的操作代价并且很多设备有资源限制(比如移动设备应用),尽可能重复使用。&
如果你想更好的类层次的机构或者你需要添加附加的方法套你的中,你应该考虑定义一个接口,它继承自接口。这个接口不是必需的,但是他说一种好的模式,这样你的代码在关联持久化的时候会更少。接下来是一个相当于本手册前面章节类的接口的示例:
/** Account DAO which has a String id (Account.name) */
public interface AccountDao extends Dao&Account, String& {
// empty wrapper, you can add additional DAO methods here
然后在实现中,你需要扩展基类。这里是个实现你的接口的示例。
/** JDBC implementation of the AccountDao interface. */
public class AccountDaoImpl extends BaseDaoImpl&Account, String&
implements AccountDao {
public AccountDaoImpl(ConnectionSource connectionSource)
throws SQLException {
super(connectionSource, Account.class);
那就是你需要定义你的类。如果有特殊的操作需要并且基类没有提供的方法,你可以自由添加更多方法到你的接口和添加到你的实现中。
注意:如果你正在使用一个定制的,然后确保添加参数到你自己定制的类的注解。这会被用于内部实例化。
2.5&支持的数据库
ORMLite支持下面的数据库。这些数据库中的某些数据库有具体需要遵守的文档。(下面给出支持的数据库,具体文档参见官方文档)
支持的数据库
Android&SQLite
Microsoft&SQL&Server
这样你有一个注解对象被持久化,添加一个无参构造器,创建你的并且定义你的类。你已经开始持久化和查询你的数据库对象了。你需要下载并且添加文件到你的中,如果你想让这个示例跑起来的话。下面是整合的代码:
// h2 by default but change to match your database
String databaseUrl = &jdbc:h2:mem:account&;
JdbcConnectionSource connectionSource =
new JdbcConnectionSource(databaseUrl);
// instantiate the dao with the connection source
AccountDaoImpl accountDao = new AccountDaoImpl(connectionSource);
// if you need to create the 'accounts' table make this call
TableUtils.createTable(connectionSource, Account.class);
// create an instance of Account
Account account = new Account(&Jim Coakley&);
// persist the account object to the database
accountDao.create(account);
// destroy the data source which should close underlying connections
connectionSource.destroy();
2.7&表和创建
有几个提供的工具,可以帮助你为存入数据库的类创建表和。
2.7.1&TableUtils类
TableUtils类提供了一些静态方法用以辅助创建和删除表,同样也提供了schema申明。(下面例举出静态方法名,详细说明参见官网)
静态方法原型
createTable(ConnectionSource,&Class)
createTableIfNotExists(ConnectionSource,&Class)
createTable(ConnectionSource,&DatabaseTableConfig)
createTableIfNotExists(ConnectionSource,&DatabaseTableConfig)
dropTable(ConnectionSource,&Class,&boolean&ignoreErrors)
dropTable(ConnectionSource,&DatabaseTableConfig,&boolean&ignoreErrors)
getCreateTableStatements(ConnectionSource,&Class)
getCreateTableStatements(ConnectionSource,&DatabaseTableConfig)
clearTable(ConnectionSource,&Class)
clearTable(ConnectionSource,&DatabaseTableConfig)
2.7.2&TableCreator类
TableCreator这个类虽然是为使用框架设计的,但是在其他的配置方面是很有用的。它配置和被程序使用的列表。
如果系统属性设置成值,他会自动创建和这些相关的表。如果系统属性设置成值,它也会自动删除创建的表。这在测试的时候特别有用:你开始使用取得最新的测试数据库,但是实际生产过程中你需要手动改变一个具体的。你可以在你运行测试脚本的时候设置系统属性,但是在运行实际脚本时要关闭它。
List&Dao&?, ?&& daoList = new ArrayList&Dao&?, ?&&();
daoList.add(accountDao);
TableCreator creator =
new TableCreator(connectionSource, daoList);
// create the tables if the right system property is set
creator.maybeCreateTables();
// later, we may want to drop the tables that were created
creator.maybeDropTables();
&2.8&唯一标识符字段
数据库中的记录通过定义为唯一的特殊字段成为唯一标识。记录不是必须有唯一标识字段当时很多操作(更新、删除、刷新)都需要一个唯一标识字段。这个标识要么用户提供要么数据库自动生成。标识字段有表中唯一的值并且如果你用根据查询、删除、刷新或者更新指定行的时候他们必须存在。为了配置一个成员变量作为标识成员,你应该使用下面三个设置之一(而且必须使用一个):。
2.8.1&成员变量使用
用我们的类的示例,字符串变量被标记有。这意味着变量是这个对象的标识字段。每个存入数据库都必须有一个唯一的变量值,比如你不能有两行的都是“”。
public class Account {
@DatabaseField(id = true)
当你使用利用具体的值查询时,你将使用标识成员定位数据库中
Account account = accountDao.queryForId(&John Smith&);
if (account == null) {
// the name &John Smith& does not match any rows
&注意:如果你需要改变对象字段的值,那么你必须使用方法,它获得当前对象的旧值和新值。
2.8.2&成员变量使用
你可以配置一个或的变量作为生成的标识字段。每条记录的号都是数据库自动生成的。
public class Order {
@DatabaseField(generatedId = true)
private int
传递一个对象去创建和存储到数据库时,数据库返回一个生成的值并且设置给对象。在大部分数据库类型中向表中插入一条新记录时生成的值从开始,每次增长。
// build our order object without an id
Order order = new Order(&Jim Sanders&, 12.34);
orderDao.create(order);
System.out.println(&Order id & + order.getId() +
& was persisted to the database&);
// query for the order with an id of 1372
order = orderDao.queryForId(1372);
if (order == null) {
// none of the order rows have an id of 1372
在上面的代码示例中,一个构造用和两个属性。当把它传给的方法时,变量没有设置。它保存到数据库之后,会把生成的设置给变量并且()方法在方法返回后被调用是有有效的。
注意:其他特殊变量类型也是可以生成的,比如。你可以使用变量进行设置,允许向表中插入拥有已经设置过或没有设置过的对象。根据数据库类型,你可能不能改变自动生成字段的值。
2.8.3&成员变量使用
一些数据库使用一种被称为序列号生成器的东西来提供生成的值。如果你把用在这些数据库上,序列名将会被自动生成。如果这样,你需要设置序列名来匹配已经存在的,你可以使用序列名的值。
public class Order {
@DatabaseField(generatedIdSequence = &order_id_seq&)
private int
在上面的示例中,虽然值再次自动生成,但是仍然使用序列名:。如果你使用的数据库不支持序列,那么这将会抛出一个异常。
注意:根据数据库类型,你不能改变自动生成字段的值。
2.9&DAO&的使用
下面通过使用方法简单完成数据库操作:
①创建并且持久化对象到数据库。
插入一条和对象相关的记录到数据库中。
Account account = new Account();
account.name = &Jim Coakley&;
accountDao.create(account);
②查询它的字段
如果对象有个成员变量通过注解定义的,我们可以通过它的在数据库中查找一个对象。
Account account = accountDao.queryForId(name);
if (account == null) {
account not found handling ...
③更新与对象相关的数据库记录
如果你在内存中改变一个对象的成员变量,你必须调用把它持久化到数据库。这需要一个字段。
account.password = &_secret&;
accountDao.update(account);
④当数据库有改变,刷新对象
如果一些与内存中对象相关的数据库实体发生了改变,你就需要刷新来得到最新的存储对象。这需要一个字段。
accountDao.refresh(account);
⑤从数据库中删除数据
从数据库删除与对象关联的记录。一旦对象从数据库删除,你可以继续使用内存中的对象但是任何的更新或者刷新都很可能失败。这需要一个字段。
accountDao.delete(account);
⑥遍历表中所有记录
DAO也有迭代器,所以你可以简单的执行数据库中所有的记录。
// page through all of the accounts in the database
for (Account account : accountDao) {
System.out.println(account.getName());
&注意:你必须遍历迭代器所有的条目来关闭底层的对象。如果你没有通过循环走所有的途径,那么不知道关闭底层对象,并且一个到数据库的连接可能泄露,如果更迟一些垃圾回收器才获得它,那么它将被迫关闭,这会在你的代码中产出漏洞。使用下面的模板包住迭代器。
例如,下面是一个不好的循环模板。
for (Account account : accountDao) {
if (account.getName().equals(&Bob Smith&)) {
// you can't return, break, or throw from here
&如果一个异常仍出循环这种照样会发生,所以如果这样的话循环就不应该被使用。这也是一个用迟加载收集的一个案例。
⑦直接使用迭代器
你也可以直接使用迭代器,因为用循环并不是最佳选择。这种方式允许你使用更好的模板。
CloseableIterator&Account& iterator =
accountDao.closeableIterator();
while (iterator.hasNext()) {
Account account = iterator.next();
System.out.println(account.getName());
} finally {
// close it at the end to close underlying SQL statement
iterator.close();
你也可以使用,它允许你在中使用而一直使用循环。
CloseableWrappedIterable&Account& wrappedIterable =
accountDao.getWrappedIterable();
for (Account account : wrappedIterable) {
} finally {
wrappedIterable.close();
2.10&索引成员
在你的数据类中提供了一些多种成员索引有限的支持。首先,它重点指明任何已经被标记成的成员变量已经被编入索引。一个成员变量不需要添加额外构建的索引并且如果他们被指定的话那么数据库会产生错误。
添加一个索引到没有的成员变量,你需要添加布尔域到注解。这将会在表被创建时创建一个非唯一的索引给成员变量并且如果表被删除那么将删除索引。索引用于帮助优化查询并且在查询媒介中数据量大的表时显著优化了查询时间。
public class Account {
@DatabaseField(id = true)
// this indexes the city field so queries on city
// will go faster for large tables
@DatabaseField(index = true)
这个例子在表中创建一个索引。如果你想用不同的名字,你可以使用,用允许你指定的索引名来替换othername成员。
@DatabaseField(indexName = &account_citystate_idx&)
@DatabaseField(indexName = &account_citystate_idx&)
这个示例会为和成员变量都创建一个索引。注意,通过本身查询是没有优化的,只有在和多关键字查询时才会被优化。有些数据库,它可能更好的创建一个单一字段索引在每个字段上而且如果你用和多关键字查询时它会让数据库同时使用两个索引。对于另一些数据库,推荐在多个成员变量上创建一个索引。你可能需要尝试使用命令来查明你的数据库是怎么使用你的索引的。
创建一个唯一的索引,和成员变量仅仅在注解中有效。这些操作和上面的设置一样但是将会不用创建唯一索引来确保没有任何两条记录的索引有相同的值。
2.11&发出原生语句
在大量实例中,使用定义的功能操作数据库可能还不够。由于这个原因,允许你发出查找、更新、执行等数据库原生语句给数据库。
2.11.1&发出原生查找
通过接口的内置方法并且类没有提供操作所有查询类型的能力。比如,聚合查询(sum,count,avg等等)不能当做一个对象进行操作,因为每个查询有不同的结果列表。为了这样的查询操作,你可以使用中的方法发出原生的数据库查询。这些方法返回一个对象,它表示一个结果是一个字符串数组,对象数组或者用户映射对象。查看关于的文档有更多如何使用它的详解说明,或者看看下面的示例。
// find out how many orders account-id #10 has
GenericRawResults&String[]& rawResults =
orderDao.queryRaw(
&select count(*) from orders where account_id = 10&);
// there should be 1 result
List&String[]& results = rawResults.getResults();
// the results array should have 1 value
String[] resultArray = results.get(0);
// this should print the number of orders that have this account-id
System.out.println(&Account-id 10 has & + resultArray[0] + & orders&);
你甚至可以使用构建原生的查询,如果你喜欢使用方法的话。
QueryBuilder&Account, Integer& qb = accountDao.queryBuilder();
qb.where().ge(&orderCount&, 10);
results = accountDao.queryRaw(qb.prepareStatementString());
如果你想以参数的形式使用原生查询,那么你应该像这样的:
QueryBuilder&Account, Integer& qb = accountDao.queryBuilder();
// we specify a SelectArg here to generate a ? in statement string below
qb.where().ge(&orderCount&, new SelectArg());
// the 10 at the end is an optional argument to fulfill SelectArg above
results = accountDao.queryRaw(qb.prepareStatementString(), 10);
如果你想以聚合的方式使用或者是其他原生、自定义的参数那么像下面这样做。因为只有一个结果输出你可以使用方法:
QueryBuilder&Account, Integer& qb = accountDao.queryBuilder();
// select 2 aggregate functions as the return
qb.selectRaw(&MIN(orderCount)&, &MAX(orderCount)&);
// the results will contain 2 string values for the min and max
results = accountDao.queryRaw(qb.prepareStatementString());
String[] values = results.getFirstResult();
对于有大量的结果集,你可以考虑使用利用数据库分页的对象的方法。示例:
// return the orders with the sum of their amounts per account
GenericRawResults&String[]& rawResults =
orderDao.queryRaw(
&select account_id,sum(amount) from orders group by account_id&);
// page through the results
for (String[] resultArray : rawResults) {
System.out.println(&Account-id & + resultArray[0] + & has &
+ resultArray[1] + & total orders&);
rawResults.close();
如果你传进去的结果字段类型有些字段不能合适的映射到字符串,你也可以以形式返回字段。例如:
// return the orders with the sum of their amounts per account
GenericRawResults&Object[]& rawResults =
orderDao.queryRaw(
&select account_id,sum(amount) from orders group by account_id&,
new DataType[] { DataType.LONG, DataType.INTEGER });
// page through the results
for (Object[] resultArray : rawResults) {
System.out.println(&Account-id & + resultArray[0] + & has &
+ resultArray[1] + & total orders&);
rawResults.close();
注意:能返回在不同的表中的字段,这依赖于数据库类型。
为了保证数组数据类型和返回的列匹配,你必须具体地指定字段并且不能用中的&。
你也可以通过在对象传一个你自己的对象来映射结果集。这将调用对象和一个字符串数组的映射并且它把字符串转化为对象。例如:
// return the orders with the sum of their amounts per account
GenericRawResults&Foo& rawResults =
orderDao.queryRaw(
&select account_id,sum(amount) from orders group by account_id&,
new RawRowMapper&Foo&() {
public Foo mapRow(String[] columnNames,
String[] resultColumns) {
return new Foo(Long.parseLong(resultColumns[0]),
Integer.parseInt(resultColumns[1]));
// page through the results
for (Foo foo : rawResults) {
System.out.println(&Account-id & + foo.accountId + & has &
+ foo.totalOrders + & total orders&);
rawResults.close();
注意:查询和结果字符串可以是非常具体的数据库类型。比如:
1、某一数据库需要一些字段名指定成大写,另一些需要指定成小写。
2、你必须引用你的字段名或表明,如果他们是关键字的话。
3、结果集字段名也可以是大写或者是小写。
4、Select&*&可以根据数据库类型返回表中不同的字段。
注意:就像其他的迭代器,你将需要确定循环遍历所以结果集后有自动关闭的申明。你也可以调用方法来确保迭代器和其他相关数据库连接被关闭。
2.11.2&发出原生更新语句
如果给你的功能不够灵活的话,你也可以发出数据的原生更新语句。更新的语句必须包含关键字、、&。例如:
fooDao.updateRaw(&INSERT INTO accountlog (account_id, total) &
+ &VALUES ((SELECT account_id,sum(amount) FROM accounts))
2.11.3&发出原生的执行语句
如果给你的功能不够灵活的话,你也可以发出数据的原生更新语句。例如:
fooDao.executeRaw(&ALTER TABLE accountlog DROP COLUMN partner&);
2.12&外部对象字段
ORMLite支持对象的概念,一个或多个与对象相关的字段被持久化到同一数据库的另一张表中。比如,如果在你的数据库中有一个对象,&并且每个有一个对应的对象,那么这个对象就会有外部字段。有一个外部对象,只有中的字段被持久化到表中的列。例如,这个类可以像这样:
@DatabaseTable(tableName = &orders&)
public class Order {
@DatabaseField(generatedId = true)
private int
@DatabaseField(canBeNull = false, foreign = true)
当表被创建时,有些像下面的将会被生产:
CREATE TABLE `orders`
(`id` INTEGER AUTO_INCREMENT , `account_id` INTEGER,
PRIMARY KEY (`id`));
注意:字段名不是,而是。如果你查询的时候你将会使用这个字段名。你可以在注解中使用成员来设置字段名。
当你用外部对象创建一个字段时,请注意这个外键对象不会为你自动创建。如果你的外部对象有一个数据库提供的,那么你需要在你创建其他引用它的对象之前创建它。例如:
Account account = new Account(&Jim Coakley&);
accountDao.create(account);
// this will create the account object and set any generated ids
// now we can set the account on the order and create it
Order order = new Order(&Jim Sanders&, 12.34);
order.setAccount(account);
orderDao.create(order);
如果你希望一些自动创建的等级,那么你可以使用进行设置。
当你查询一个表时,你将会得到一个对象,这对象拥有一个有它集合的字段。在外部对象中剩下的字段将有默认值(,,等)。如果你想使用中的其他字段,你必须调用类的来得到填充了的对象。比如:
Order order = orderDao.queryForId(orderId);
System.out.println(&Account-id on the order should be set: & +
order.account.id);
// this should print null for order.account.name
System.out.println(&But other fields on the account should not be set: &
+ order.account.name);
// so we refresh the account using the AccountDao
accountDao.refresh(order.getAccount());
System.out.println(&Now the account fields will be set: & +
order.account.name);
你可以通过使用设置拥有一个自动刷新的外部对象。
注意:因为我们使用,所以外部对象需要有一个字段。
你可以用两三种不同的方式查询外部字段。下面实例演示代码,代码是查询所有匹配确定的字段的所有。因为字段是字段,所有你可以通过的字段来进行查询。
// query for all orders that match a certain account
List&Order& results =
orderDao.queryBuilder().where().
eq(&account_id&, account.getName()).query();
或者你可以仅仅让从取得字段。这将演示一个和上面等同的查询:
// ORMLite will extract and use the id field internally
List&Order& results =
orderDao.queryBuilder().where().
eq(&account_id&, account).query();
2.13&外部集合
在本手册前面章节中我们有个类的例子,它有一个到表的外部对象字段。一个外部集合允许你添加表中的集合。每当对象通过的查询或刷新返回时,表和设置在上集合规定了一个单独的查询。所有的在集合中有一个对应的和匹配的外部对象。例如:
public class Account {
@ForeignCollectionField(eager = false)
ForeignCollection&Order&
在上面的示例中,注解标记了成员变量是一个匹配的集合。成员变量的类型必须要么是要么是,没有其他的集合被支持,因为其他集合难以有更多的方法支持。注解支持下面的成员:
maxEagerLevel
columnName
orderColumnName
foreignFieldName
备注:具体成员描述参见官方文档。
记住,当你有个成员变量,集合中的类必须得有一个外部成员。如果有个的外部集合,那么必须有一个外部成员。它是这么要求的,所以能找到匹配具体的。
警告:用集合甚至是()方法导致迭代器跨越数据库。你可能最想只使用集合中的和方法。
注意:就像使用方法类似,迭代器被集合返回,当你用了它那么必须关闭它,因为有链接在数据库底层一直开着。下面的方式关闭操作会执行:那么是你通过迭代器把所有的方式走一遍,那么是你调用()方法。只有会返回一个可以关闭的迭代器。这意味着循环懒加载集合是不好的模式。
在这种情况下外部集合支持add()和remove()方法:如果一个集合想对象被添加和从内部列表删除,并且被调用用来影响表以及和集合。
注意:当你在一个使用了外部集合的对象上调用时,保存在集合中的对象不是自动写到数据库的。可惜在中没有方法可以检测到对象被更新了。如果你更新一个集合中的对象你需要在上调用方法来确保对象被持久化。例如:
for (Order order : account.orders()) {
// if we are changing some field in the order
order.setAmount(123);
// then we need to update it in the database
account.orders.update(order);
2.14&DAO激活对象
另一种模式是:有对象执行和他们自己相关的数据库操作来代替使用。比如,给一个数据对象,你会调用foo.refresh()来代替fooDao.refresh(foo)。默认的模式是使用类,它允许你的数据类有他们自己的层次并且它独立于中的数据库代码。但是,如果你喜欢这种模式的话你可以自由使用类。
要使所有的类能够刷新(更新、删除等等)他们自己,那么需要继承类。例如:
@DatabaseTable(tableName = &accounts&)
public class Account extends BaseDaoEnabled {
@DatabaseField(id = true)
@DatabaseField(canBeNull = false)
首先创建对象,你需要使用对象或者你需要设置相关对象的以便它能自我创建:
account.setDao(accountDao);
account.create();
不过,任何时候一个对象被作为一个查询结果返回,那么已经被设置在继承类的对象上了。
Account account = accountDao.queryForId(name);
account.setPassword(newPassword);
account.update();
这也将会为外部成员工作。
Order order = orderDao.queryForId(orderId);
// load all of the fields from the account
order.getAccount().refresh();
这个文档有最新的操作列表,现在类仅仅可以做:
创建对象,你需要使用或者在对象上调用()。
当数据库中数据发生更新时刷新对象。
你改变了内存中的对象之后把它更新到数据库。
如果你需要更新对象的那么你需要使用这个方法。你不能改变对象的成员然后调用更新方法,因为这样对象会找不到。
从数据库删除。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:798246次
积分:5779
积分:5779
排名:第4484名
原创:18篇
转载:204篇
评论:91条
(1)(1)(1)(1)(2)(1)(1)(2)(2)(5)(7)(1)(1)(5)(6)(19)(8)(4)(4)(4)(7)(2)(3)(6)(13)(5)(5)(7)(19)(52)(17)(13)}

我要回帖

更多关于 httpentitywrapper 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信