知識圖譜學習筆記(1)


知識圖譜學習筆記第一部分,包含RDF介紹,以及Jena RDF API使用

知識圖譜的基石:RDF

RDF(Resource Description Framework),即資源描述框架,其本質是一個數據模型(Data Model)。它提供了一個統一的標准,用於描述實體/資源。簡單來說,就是表示事物的一種方法和手段。
enter description here

RDF序列化方法

RDF序列化的方式主要有:RDF/XML,N-Triples,Turtle,RDFa,JSON-LD等幾種。

  1. RDF/XML,顧名思義,就是用XML的格式來表示RDF數據
  2. N-Triples,即用多個三元組來表示RDF數據集,是最直觀的表示方法。在文件中,每一行表示一個三元組,方便機器解析和處理。開放領域知識圖譜DBpedia通常是用這種格式來發布數據的。
  3. Turtle, ['tɝtl] 應該是使用得最多的一種RDF序列化方式了。它比RDF/XML緊湊,且可讀性比N-Triples好。
  4. RDFa,即“The Resource Description Framework in Attributes”,是HTML5的一個擴展,在不改變任何顯示效果的情況下,讓網站構建者能夠在頁面中標記實體,像人物、地點、時間、評論等等
  5. JSON-LD,即“JSON for Linking Data”,用鍵值對的方式來存儲RDF數據

Example1 N-Triples:

<http://www.kg.com/person/1> <http://www.kg.com/ontology/chineseName> "羅納爾多·路易斯·納薩里奧·德·利馬"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/career> "足球運動員"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/fullName> "Ronaldo Luís Nazário de Lima"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/birthDate> "1976-09-18"^^date.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/height> "180"^^int.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/weight> "98"^^int.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/nationality> "巴西"^^string.
<http://www.kg.com/person/1> <http://www.kg.com/ontology/hasBirthPlace> <http://www.kg.com/place/10086>.
<http://www.kg.com/place/10086> <http://www.kg.com/ontology/address> "里約熱內盧"^^string.
<http://www.kg.com/place/10086> <http://www.kg.com/ontology/coordinate> "-22.908333, -43.196389"^^string.

Example2 Turtle:

@prefix person: <http://www.kg.com/person/> .
@prefix place: <http://www.kg.com/place/> .
@prefix : <http://www.kg.com/ontology/> .

person:1 :chineseName "羅納爾多·路易斯·納薩里奧·德·利馬"^^string.
person:1 :career "足球運動員"^^string.
person:1 :fullName "Ronaldo Luís Nazário de Lima"^^string.
person:1 :birthDate "1976-09-18"^^date.
person:1 :height "180"^^int. 
person:1 :weight "98"^^int.
person:1 :nationality "巴西"^^string. 
person:1 :hasBirthPlace place:10086.
place:10086 :address "里約熱內盧"^^string.
place:10086 :coordinate "-22.908333, -43.196389"^^string.

RDF的表達能力

RDF的表達能力有限,無法區分類和對象,也無法定義和描述類的關系/屬性。RDF是對具體事物的描述,缺乏抽象能力,無法對同一個類別的事物進行定義和描述。就以羅納爾多這個知識圖為例,RDF能夠表達羅納爾多和里約熱內盧這兩個實體具有哪些屬性,以及它們之間的關系。但如果我們想定義羅納爾多是人,里約熱內盧是地點,並且人具有哪些屬性,地點具有哪些屬性,人和地點之間存在哪些關系,這個時候RDF就表示無能為力了。

RDFS/OWL

RDFS/OWL本質上是一些預定義詞匯(vocabulary)構成的集合,用於對RDF進行類似的類定義及其屬性的定義。

RDFS/OWL序列化方式和RDF沒什么不同,其實在表現形式上,它們就是RDF。其常用的方式主要是RDF/XML,Turtle。另外,通常我們用小寫開頭的單詞或詞組來表示屬性,大寫開頭的表示類。數據屬性(data property,實體和literal字面量的關系)通常由名詞組成,而對象數據(object property,實體和實體之間的關系)通常由動詞(has,is之類的)加名詞組成。剩下的部分符合駝峰命名法。

輕量級的模式語言——RDFS

RDFS,即“Resource Description Framework Schema”,是最基礎的模式語言。還是以羅納爾多知識圖為例,我們在概念、抽象層面對RDF數據進行定義。下面的RDFS定義了人和地點這兩個類,及每個類包含的屬性。

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <http://www.kg.com/ontology/> .

### 這里我們用詞匯rdfs:Class定義了“人”和“地點”這兩個類。
:Person rdf:type rdfs:Class.
:Place rdf:type rdfs:Class.

### rdfs當中不區分數據屬性和對象屬性,詞匯rdf:Property定義了屬性,即RDF的“邊”。

:chineseName rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:career rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:fullName rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:birthDate rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:date .

:height rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:int .

:weight rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:int .

:nationality rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:hasBirthPlace rdf:type rdf:Property;
		rdfs:domain :Person;
		rdfs:range :Place .

:address rdf:type rdf:Property;
		rdfs:domain :Place;
		rdfs:range xsd:string .

:coordinate rdf:type rdf:Property;
		rdfs:domain :Place;
		rdfs:range xsd:string .

RDFS幾個比較重要,常用的詞匯:

  1. rdfs:Class. 用於定義類
  2. rdfs:domain. 用於表示該屬性屬於哪個類別
  3. rdfs:range. 用於描述該屬性的取值類型
  4. rdfs:subClassOf. 用於描述該類的父類
  5. rdfs:subProperty. 用於描述該屬性的父屬性

enter description here

Data層是我們用RDF對羅納爾多知識圖的具體描述,Vocabulary是我們自己定義的一些詞匯(類別,屬性),RDF(S)則是預定義詞匯。從下到上是一個具體到抽象的過程。圖中我們用紅色圓角矩形表示類,綠色字體表示rdf:type,rdfs:domain,rdfs:range三種預定義詞匯,虛線表示rdf:type這種所屬關系。

RDFS的擴展——OWL

RDFS本質上是RDF詞匯的一個擴展。后來人們發現RDFS的表達能力還是相當有限,因此提出了OWL。我們也可以把OWL當做是RDFS的一個擴展,其添加了額外的預定義詞匯。

OWL,即“Web Ontology Language”,語義網技術棧的核心之一。OWL有兩個主要的功能:

  1. 提供快速、靈活的數據建模能力。
  2. 高效的自動推理。

用OWL對羅納爾多知識圖進行語義層的描述:

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <http://www.kg.com/ontology/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .

### 這里我們用詞匯owl:Class定義了“人”和“地點”這兩個類。
:Person rdf:type owl:Class.
:Place rdf:type owl:Class.

### owl區分數據屬性和對象屬性(對象屬性表示實體和實體之間的關系)。詞匯owl:DatatypeProperty定義了數據屬性,owl:ObjectProperty定義了對象屬性。
:chineseName rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:career rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:fullName rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:birthDate rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:date .

:height rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:int .

:weight rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:int .

:nationality rdf:type owl:DatatypeProperty;
		rdfs:domain :Person;
		rdfs:range xsd:string .

:hasBirthPlace rdf:type owl:ObjectProperty;
		rdfs:domain :Person;
		rdfs:range :Place .

:address rdf:type owl:DatatypeProperty;
		rdfs:domain :Place;
		rdfs:range xsd:string .

:coordinate rdf:type owl:DatatypeProperty;
		rdfs:domain :Place;
		rdfs:range xsd:string .

schema層的描述語言換為OWL后,層次圖表示為:
enter description here

owl區分數據屬性和對象屬性(對象屬性表示實體和實體之間的關系)。詞匯owl:DatatypeProperty定義了數據屬性,owl:ObjectProperty定義了對象屬性。

上圖中,數據屬性用青色表示,對象屬性由藍色表示。

描述屬性特征的詞匯

  1. owl:TransitiveProperty. 表示該屬性具有傳遞性質。例如,我們定義“位於”是具有傳遞性的屬性,若A位於B,B位於C,那么A肯定位於C。
  2. owl:SymmetricProperty. 表示該屬性具有對稱性。例如,我們定義“認識”是具有對稱性的屬性,若A認識B,那么B肯定認識A。
  3. owl:FunctionalProperty. 表示該屬性取值的唯一性。 例如,我們定義“母親”是具有唯一性的屬性,若A的母親是B,在其他地方我們得知A的母親是C,那么B和C指的是同一個人。
  4. owl:inverseOf. 定義某個屬性的相反關系。例如,定義“父母”的相反關系是“子女”,若A是B的父母,那么B肯定是A的子女。

本體映射詞匯(Ontology Mapping)

  1. owl:equivalentClass. 表示某個類和另一個類是相同的。
  2. owl:equivalentProperty. 表示某個屬性和另一個屬性是相同的。
  3. owl:sameAs. 表示兩個實體是同一個實體。

RDFS,OWL推理的推理機(reasoner)

RDFS同樣支持推理,由於缺乏豐富的表達能力,推理能力也不強。舉個例子,我們用RDFS定義人和動物兩個類,另外,定義人是動物的一個子類。此時推理機能夠推斷出一個實體若是人,那么它也是動物。OWL當然支持這種基本的推理,除此之外,憑借其強大的表達能力,我們能進行更有實際意義的推理。想象一個場景,我們有一個龐大數據庫存儲人物的親屬關系。里面很多關系都是單向的,比如,其只保存了A的父親(母親)是B,但B的子女字段里面沒有A,可以推理得到B的子女A。

enter description here

RDF查詢語言SPARQL

SPARQL即SPARQL Protocol and RDF Query Language的遞歸縮寫,專門用於訪問和操作RDF數據,是語義網的核心技術之一。W3C的RDF數據存取小組(RDF Data Access Working Group, RDAWG)對其進行了標准化。在2008年,SPARQL 1.0成為W3C官方所推薦的標准。2013年發布了SPARQL 1.1。相對第一個版本,其支持RDF圖的更新,提供更強大的查詢,比如:子查詢、聚合操作(像我們常用的count)等等。

由兩個部分組成:協議和查詢語言。

  1. 查詢語言很好理解,就像SQL用於查詢關系數據庫中的數據,XQuery用於查詢XML數據,SPARQL用於查詢RDF數據。
  2. 協議是指我們可以通過HTTP協議在客戶端和SPARQL服務器(SPARQL endpoint)之間傳輸查詢和結果,這也是和其他查詢語言最大的區別。

一個SPARQL查詢本質上是一個帶有變量的RDF圖,以我們之前提到的羅納爾多RDF數據為例:

<http://www.kg.com/person/1> <http://www.kg.com/ontology/chineseName> "羅納爾多·路易斯·納薩里奧·德·利馬"^^string.

查詢SPARQL

<http://www.kg.com/person/1> <http://www.kg.com/ontology/chineseName> ?x.

SPARQL查詢是基於圖匹配的思想。我們把上述的查詢與RDF圖進行匹配,找到符合該匹配模式的所有子圖,最后得到變量的值。就上面這個例子而言,在RDF圖中找到匹配的子圖后,將"羅納爾多·路易斯·納薩里奧·德·利馬"和“?x”綁定,我們就得到最后的結果。簡而言之,SPARQL查詢分為三個步驟:

  1. 構建查詢圖模式,表現形式就是帶有變量的RDF。
  2. 匹配,匹配到符合指定圖模式的子圖。
  3. 綁定,將結果綁定到查詢圖模式對應的變量上。

舉例

如何查詢所有數據

PREFIX : <http://www.kgdemo.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX xsd: <XML Schema>
PREFIX vocab: <http://localhost:2020/resource/vocab/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX map: <http://localhost:2020/resource/#>
PREFIX db: <http://localhost:2020/resource/>

SELECT * WHERE {
  ?s ?p ?o
}

SPARQL的部分關鍵詞:

  1. SELECT, 指定我們要查詢的變量。在這里我們查詢所有的變量,用*代替。
  2. WHERE,指定我們要查詢的圖模式。含義上和SQL的WHERE沒有區別。
  3. FROM,指定查詢的RDF數據集。我們這里只有一個圖,因此省去了FROM關鍵詞。 PREFIX,用於IRI的縮寫。

“周星馳出演了哪些電影”:

PREFIX : <http://www.kgdemo.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX xsd: <XML Schema>
PREFIX vocab: <http://localhost:2020/resource/vocab/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX map: <http://localhost:2020/resource/#>
PREFIX db: <http://localhost:2020/resource/>

SELECT ?n WHERE {
  ?s rdf:type :Person.
  ?s :personName '周星馳'.
  ?s :hasActedIn ?o.
  ?o :movieTitle ?n
}

使用Jena 構建知識圖譜

Jena是Apache基金會旗下的開源Java框架,用於構建Semantic Web 和 Linked Data 應用。

下面簡要的介紹下API,要使用jena,可以下載jar包或者使用maven(推薦),建議測試時下面的都加上:

	<dependency>
		<groupId>org.apache.jena</groupId>
		<artifactId>apache-jena-libs</artifactId>
		<type>pom</type>
		<version>3.7.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.jena</groupId>
		<artifactId>jena-sdb</artifactId>
		<version>3.7.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.jena</groupId>
		<artifactId>jena-base</artifactId>
		<version>3.7.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.jena</groupId>
		<artifactId>jena-fuseki-embedded</artifactId>
		<version>3.7.0</version> <!-- Set the version -->
	</dependency>

	<!-- https://mvnrepository.com/artifact/org.apache.jena/jena-arq -->
	<dependency>
		<groupId>org.apache.jena</groupId>
		<artifactId>jena-arq</artifactId>
		<version>3.7.0</version>
	</dependency>

Jena RDF API

首先,三元組(triple)組成的圖稱之為Model,這個圖里的Node可以是resources(實體)、literals(文本)或者blank nodes。

一個三元組,在jena里稱之為Statement,一個 statement 包含三部分::

  • the subject :實體
  • the predicate :屬性
  • the object : 值

創建Model

// URI 定義
static String personURI    = "http://somewhere/JohnSmith";
static String fullName     = "John Smith";

// 創建一個空模型(KG)
Model model = ModelFactory.createDefaultModel();

// 創建一個resource(一個subject)
Resource johnSmith = model.createResource(personURI);

// 添加屬性,這里的value是一個literals(文本)
 johnSmith.addProperty(VCARD.FN, fullName);

當然,你還可以使用鏈式API,為resource添加多個Property

// create the resource
//   and add the properties cascading style
Resource johnSmith
  = model.createResource(personURI)
		 .addProperty(VCARD.FN, fullName)
		 .addProperty(VCARD.N,
					  model.createResource()
						   .addProperty(VCARD.Given, givenName)
						   .addProperty(VCARD.Family, familyName));

遍歷Model

使用model.listStatements遍歷statements,返回一個迭代器,使用hasNext判斷是否還有數據,通過getSubject,getPredicate,getObject 獲取三元組信息。

// list the statements in the Model
StmtIterator iter = model.listStatements();

// print out the predicate, subject and object of each statement
while (iter.hasNext()) {
    Statement stmt      = iter.nextStatement();  // get next statement
    Resource  subject   = stmt.getSubject();     // get the subject
    Property  predicate = stmt.getPredicate();   // get the predicate
    RDFNode   object    = stmt.getObject();      // get the object
    System.out.print(subject.toString());
    System.out.print(" " + predicate.toString() + " ");
    if (object instanceof Resource) {
       System.out.print(object.toString());
    } else {
        // object is a literal
        System.out.print(" \"" + object.toString() + "\"");
    }
    System.out.println(" .");
} 

運行結果:

http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#N 80aeb72e-ef9c-4879-807d-62daf3c13b72 .
http://somewhere/JohnSmith http://www.w3.org/2001/vcard-rdf/3.0#FN  "John Smith" .
80aeb72e-ef9c-4879-807d-62daf3c13b72 http://www.w3.org/2001/vcard-rdf/3.0#Family  "Smith" .
80aeb72e-ef9c-4879-807d-62daf3c13b72 http://www.w3.org/2001/vcard-rdf/3.0#Given  "John" .

保存為 RDF文件

可以使用model.write方便的把Model保存為rdf文件,write默認保存為XML格式

// now write the model in XML form to a file
model.write(System.out);
<rdf:RDF
  xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
  xmlns:vcard='http://www.w3.org/2001/vcard-rdf/3.0#'
 >
  <rdf:Description rdf:about='http://somewhere/JohnSmith'>
    <vcard:FN>John Smith</vcard:FN>
    <vcard:N rdf:nodeID="A0"/>
  </rdf:Description>
  <rdf:Description rdf:nodeID="A0">
    <vcard:Given>John</vcard:Given>
    <vcard:Family>Smith</vcard:Family>
  </rdf:Description>
</rdf:RDF>

write還提供重載版本write( OutputStream out, String lang ),lang可以為"RDF/XML-ABBREV", "N-TRIPLE", "TURTLE", (and "TTL") and "N3"
我們來保存為常見的TURTLE:

model.write(System.out, "TURTLE");

結果:

<http://somewhere/JohnSmith>
		<http://www.w3.org/2001/vcard-rdf/3.0#FN>
				"John Smith" ;
		<http://www.w3.org/2001/vcard-rdf/3.0#N>
				[ <http://www.w3.org/2001/vcard-rdf/3.0#Family>
						  "Smith" ;
				  <http://www.w3.org/2001/vcard-rdf/3.0#Given>
						  "John"
				] .

jena還提供prefix功能,我們可以指定prefix來簡化turtle,下面的代碼將指定prefix,並保存到文件1.rdf里:

	model.setNsPrefix( "vCard", "http://www.w3.org/2001/vcard-rdf/3.0#" );
		model.setNsPrefix( "rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#" );
		try {
			model.write(new FileOutputStream("1.rdf"),"TURTLE");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

結果:

@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix vCard: <http://www.w3.org/2001/vcard-rdf/3.0#> .

<http://somewhere/JohnSmith>
		vCard:FN  "John Smith" ;
		vCard:N   [ vCard:Family  "Smith" ;
					vCard:Given   "John"
				  ] .

讀取rdf

Mode的read(Reader reader, String base)方法,提供 讀取RDF文件的功能:

    static final String inputFileName  = "1.rdf";
                              
    public static void main (String args[]) {
        // create an empty model
        Model model = ModelFactory.createDefaultModel();

        InputStream in = FileManager.get().open( inputFileName );
        if (in == null) {
            throw new IllegalArgumentException( "File: " + inputFileName + " not found");
        }
        
        // read the RDF/XML file
        model.read(in, "","TURTLE");
                    
        // write it to standard out
        model.write(System.out);            
    }

注意,read的時候,默認是讀取XML,如果是其他格式,需要指定lang。

從模型讀取Resouce

一個resouce都有一個唯一的URI,我們可以通過URI來獲取對應的Resouce:
函數原型:

	/**
		Return a Resource instance with the given URI in this model. <i>This method
		behaves identically to <code>createResource(String)</code></i> and exists as
		legacy: createResource is now capable of, and allowed to, reuse existing objects.
	<p>
		Subsequent operations on the returned object may modify this model.
	   @return a resource instance
	   @param uri the URI of the resource
	*/
	Resource getResource(String uri) ;

獲取到Resouce后,通過getRequiredProperty獲取屬性,如果一個屬性包含多個值,可以使用listProperties獲取。

 static final String inputFileName = "1.rdf";
    static final String johnSmithURI = "http://somewhere/JohnSmith";
    
    public static void main (String args[]) {
        // create an empty model
        Model model = ModelFactory.createDefaultModel();
       
        // use the FileManager to find the input file
        InputStream in = FileManager.get().open(inputFileName);
        if (in == null) {
            throw new IllegalArgumentException( "File: " + inputFileName + " not found");
        }
        
        // read the RDF/XML file
        model.read(new InputStreamReader(in), "");
        
        // retrieve the Adam Smith vcard resource from the model
        Resource vcard = model.getResource(johnSmithURI);

        // retrieve the value of the N property
        Resource name = (Resource) vcard.getRequiredProperty(VCARD.N)
                                        .getObject();
        // retrieve the given name property
        String fullName = vcard.getRequiredProperty(VCARD.FN)
                               .getString();
        // add two nick name properties to vcard
        vcard.addProperty(VCARD.NICKNAME, "Smithy")
             .addProperty(VCARD.NICKNAME, "Adman");
        
        // set up the output
        System.out.println("The nicknames of \"" + fullName + "\" are:");
        // list the nicknames
        StmtIterator iter = vcard.listProperties(VCARD.NICKNAME);
        while (iter.hasNext()) {
            System.out.println("    " + iter.nextStatement().getObject()
                                            .toString());
        }

        try {
            model.write(new FileOutputStream("1.rdf"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

查詢模型

可以通過listResourcesWithProperty查詢包含Property的數據:

    ResIterator iter = model.listResourcesWithProperty(VCARD.FN);
        if (iter.hasNext()) {
            System.out.println("The database contains vcards for:");
            while (iter.hasNext()) {
                System.out.println("  " + iter.nextResource()
                                              .getRequiredProperty(VCARD.FN)
                                              .getString() );
            }
        } else {
            System.out.println("No vcards were found in the database");
        }        

通過listStatements(SimpleSelector)查詢Statement:

        // select all the resources with a VCARD.FN property
        // whose value ends with "Smith"
        StmtIterator iter = model.listStatements(
            new 
                SimpleSelector(null, VCARD.FN, (RDFNode) null) {
                    @Override
                    public boolean selects(Statement s) {
                            return s.getString().endsWith("Smith");
                    }
                });
        if (iter.hasNext()) {
            System.out.println("The database contains vcards for:");
            while (iter.hasNext()) {
                System.out.println("  " + iter.nextStatement()
                                              .getString());
            }
        } else {
            System.out.println("No Smith's were found in the database");
        }     

模型合並

可以通過union合並兩個模型:

enter description here
enter description here

合並后:
enter description here

來源


作者:Jadepeng
出處:jqpeng的技術記事本--http://www.cnblogs.com/xiaoqi
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM