SpringBoot24 SpringDataJPA环境搭建、实体类注解、关联查询
1 版本说明
JDK:1.8
MAVEN:3.5
SpringBoot:2.0.4
IDEA:旗舰版207.2
MySQL:5.5
2 SpringDataJPA环境搭建(SpringBoot版本)
2.1 创建一个SrpingBoot项目
需要引入的依赖如下图所示
2.2 配置数据库相关
》创建一个mysql数据库testdemo
》在testdemo中创建一个student表
/*
Navicat MySQL Data Transfer Source Server : mysql5.4
Source Server Version : 50540
Source Host : localhost:3306
Source Database : testdemo Target Server Type : MYSQL
Target Server Version : 50540
File Encoding : 65001 Date: 2018-08-13 21:13:51
*/ SET FOREIGN_KEY_CHECKS=0; -- ----------------------------
-- Table structure for `student`
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(50) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`age` int(10) NOT NULL,
`address` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4;
student.sql
》在springboot项目中配置数据库信息
spring:
datasource:
url: jdbc:mysql://127.0.0.1/testdemo?characterEncoding=utf-8&useSSL=false
username: root
password: 182838 jpa:
properties:
hibernate:
format_sql: true
show_sql: true
application.yml
2.2.1 利用IDEA连接数据
2.2.2 利用IDEA自动生成实体类
package cn.xiangxu.jpa_demo03.domain.domain_do; import javax.persistence.*; /**
* @author 王杨帅
* @create 2018-08-13 21:36
* @desc
**/
@Entity
@Table(name = "student", schema = "testdemo", catalog = "")
public class StudentDO {
private int id;
private String name;
private int age;
private String address; @Id
@Column(name = "id")
public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} @Basic
@Column(name = "name")
public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Basic
@Column(name = "age")
public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} @Basic
@Column(name = "address")
public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address;
} @Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false; StudentDO studentDO = (StudentDO) object; if (id != studentDO.id) return false;
if (age != studentDO.age) return false;
if (name != null ? !name.equals(studentDO.name) : studentDO.name != null) return false;
if (address != null ? !address.equals(studentDO.address) : studentDO.address != null) return false; return true;
} @Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
result = 31 * result + (address != null ? address.hashCode() : 0);
return result;
}
}
StudentDO.java
2.3 创建持久层
技巧01:持久层接口只需要继承 JpaRepository 接口即可
package cn.xiangxu.jpa_demo03.domain.repository; import cn.xiangxu.jpa_demo03.domain.domain_do.StudentDO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; /**
* @author 王杨帅
* @create 2018-08-13 21:53
* @desc 学生持久层接口
**/
public interface StudentRepository extends JpaRepository<StudentDO, Integer> { }
StudentRepository.java
技巧02:需要使用持久层实例时,只需要进行依赖注入即可
技巧03:持久层接口上不用写 @Repository ,因为继承了 JpaRepository 接口后就已经是一个被Spring容器管理的持久层Bean啦
技巧04:Repository相关接口
2.4 测试持久层
技巧01:继承 JpaRepository 接口后就可以有很过方法可以使用
技巧02:可以直接使用的原因是:拦截 + JDK动态代理
package cn.xiangxu.jpa_demo03.domain.repository; import cn.xiangxu.jpa_demo03.domain.domain_do.StudentDO;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; import java.util.List; import static org.junit.Assert.*; @RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class StudentRepositoryTest { @Autowired
private StudentRepository studentRepository; @Test
public void findall() {
List<StudentDO> all = studentRepository.findAll();
System.out.println(all);
} }
技巧03:jpa支持自定义SQL【借助@Query注解实现】
3 Projections 对查询结果的扩展
3.1 返回类型建模
Spring JPA 对 Projections 的扩展的支持,个人觉得这是个非常好的东西,从字面意思上理解就是映射,指的是和 DB 的查询结果的字段映射关系。一般情况下,我们是返回的字段和 DB 的查询结果的字段是一一对应的,但有的时候,需要返回一些指定的字段,不需要全部返回,或者返回一些复合型的字段,还得自己写逻辑。Spring Data 正是考虑到了这一点,允许对专用返回类型进行建模,以便更有选择地将部分视图对象。(声明:来自gitChat)
3.1.1 需求
只需要查询学生的姓名和年龄信息,其余信息不进行查询
3.1.2 思路
声明一个接口,包含我们要返回的属性的方法即可
3.1.3 编程实现
》根据实体类中属性的get方法创建接口
技巧01:接口中的方法就是需要获取的字段在实体类中对应的get方法
package cn.xiangxu.jpa_demo03.domain.domain_do; /**
* @author 王杨帅
* @create 2018-08-13 22:16
* @desc
**/
public interface StudentBaseInfo { String getName();
Integer getAge(); }
StudentBaseInfo.java
》修改持久层接口中的方法
技巧01:返回值用StudentBaseInfo类型
package cn.xiangxu.jpa_demo02.repository; import cn.xiangxu.jpa_demo02.domain.domain_do.StudentBaseInfo;
import cn.xiangxu.jpa_demo02.domain.domain_do.StudentDO;
import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; /**
* @author 王杨帅
* @create 2018-08-13 10:00
* @desc
**/
public interface StudentRepository extends JpaRepository<StudentDO, Integer> { /**
* 根据姓名和年龄
* @param name 姓名
* @param age 年龄
* @return
*/
List<StudentDO> findByNameAndAge(String name, Integer age); /**
* 根据年龄段查询【PS: 开区间】
* @param start 最小值
* @param end 最大值
* @return
*/
List<StudentDO> findByAgeBetween(Integer start, Integer end); /**
* 小于给定年龄【PS: 开区间】
* @param age 年龄
* @return
*/
List<StudentDO> findByAgeLessThan(Integer age); /**
* 小于给定年龄【PS: 闭区间】
* @param age 年龄
* @return
*/
List<StudentDO> findByAgeLessThanEqual(Integer age); List<StudentDO> findByAgeGreaterThan(Integer age); List<StudentDO> findByAgeGreaterThanEqual(Integer age); List<StudentDO> findByName(String name); List<StudentDO> findByNameEquals(String name); List<StudentDO> findByNameIs(String name); List<StudentDO> findByAgeAfter(Integer age); List<StudentDO> findByAgeBefore(Integer age); List<StudentDO> findByAgeAfterOrAgeEquals(Integer age, Integer age2); // List<StudentDO> findByAddress(String address); List<StudentBaseInfo> findByAddress(String address); }
StudentRepository.java
》测试类
package cn.xiangxu.jpa_demo03.domain.repository; import cn.xiangxu.jpa_demo03.domain.domain_do.StudentBaseInfo;
import cn.xiangxu.jpa_demo03.domain.domain_do.StudentDO;
import cn.xiangxu.jpa_demo03.repository.StudentRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class StudentRepositoryTest { @Autowired
private StudentRepository studentRepository; @Test
public void findall() {
List<StudentDO> all = studentRepository.findAll();
System.out.println(all);
} @Test
public void findByName() {
List<StudentBaseInfo> byNameIs = studentRepository.findByNameIs("杨玉林");
System.out.println(byNameIs);
} }
》跳坑01:通过这种方式获取到的数据是一个Map对象,如何转化成一个实体类列表呢
》填坑01:创建一个实体类,这个实体类的字段要和创建的接口中方法对应实体类类的字段名保持一致【PS:本博文就继续使用之前的实体类】,将查询到的数据转化成流,然后利用peek这个中间操作进行转化
package cn.xiangxu.jpa_demo03.domain.repository; import cn.xiangxu.jpa_demo03.domain.domain_do.StudentBaseInfo;
import cn.xiangxu.jpa_demo03.domain.domain_do.StudentDO;
import cn.xiangxu.jpa_demo03.repository.StudentRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; import java.util.ArrayList;
import java.util.List; @RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class StudentRepositoryTest { @Autowired
private StudentRepository studentRepository; @Test
public void findall() {
List<StudentDO> all = studentRepository.findAll();
System.out.println(all);
} @Test
public void findByName() {
List<StudentBaseInfo> byNameIs = studentRepository.findByNameIs("杨玉林");
System.out.println(byNameIs);
} @Test
public void findBYName02() {
List<StudentBaseInfo> byNameIs = studentRepository.findByNameIs("王杨帅");
List<StudentDO> studentDOList = new ArrayList<>();
byNameIs.stream()
.peek(
i -> {
StudentDO studentDO = new StudentDO();
BeanUtils.copyProperties(i, studentDO);
studentDOList.add(studentDO);
}
)
.forEach(
System.out::println
);
System.out.println(studentDOList);
} }
》转化后的结果为
4 实体类注解
待更新...... 2018年8月14日20:21:24
5 关联查询
说明:本博是利用原生的SQL进行关联查询
5.1 需求
现有两张数据库表:
product -> 存放产品信息
provider -> 存放供应商信息
product 和 provider 的关系时多对一的关系,即:一个供应商可能和多个产品对应
5.2 思路
知己利用SQL的关联查询实现
5.3 编程实现
5.3.1 数据表
技巧01:product中的provider_id字段和provider的id字段作为关联查询的桥梁
技巧02:理论上说,product中的provider_id字段应该设置成外键,但是为了开发方便,此处不进行设置【PS: 开发人员知道这是外键即可】
/*
Navicat MySQL Data Transfer Source Server : mysql5.4
Source Server Version : 50540
Source Host : localhost:3306
Source Database : testdemo Target Server Type : MYSQL
Target Server Version : 50540
File Encoding : 65001 Date: 2018-08-14 20:25:59
*/ SET FOREIGN_KEY_CHECKS=0; -- ----------------------------
-- Table structure for `product`
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_name` varchar(255) NOT NULL,
`product_price` double NOT NULL,
`provider_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4; -- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO `product` VALUES ('', '天友纯牛奶', '', '');
INSERT INTO `product` VALUES ('', '天友核桃花生奶', '', '');
INSERT INTO `product` VALUES ('', '福特福克斯', '', '');
INSERT INTO `product` VALUES ('', '福特嘉年华', '', ''); -- ----------------------------
-- Table structure for `provider`
-- ----------------------------
DROP TABLE IF EXISTS `provider`;
CREATE TABLE `provider` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`provider_name` varchar(255) NOT NULL,
`provider_phone` varchar(255) NOT NULL,
`provider_address` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; -- ----------------------------
-- Records of provider
-- ----------------------------
INSERT INTO `provider` VALUES ('', '天友乳业', '', '重庆市大足区');
INSERT INTO `provider` VALUES ('', '长安制造', '', '重庆市渝北区');
5.3.2 创建实体类
技巧01:利用IDEA自动生成
技巧02:ProductInfoDTO这个实体类是用来封装产品信息和供应商信息的
package cn.xiangxu.jpa_demo04.domain.domain_do; import lombok.ToString; import javax.persistence.*; /**
* @author 王杨帅
* @create 2018-08-14 19:54
* @desc
**/
@Entity
@ToString
@Table(name = "product", schema = "testdemo", catalog = "")
public class ProductDO {
private int id;
private String productName;
private double productPrice;
private int providerId; @Id
@Column(name = "id")
public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} @Basic
@Column(name = "product_name")
public String getProductName() {
return productName;
} public void setProductName(String productName) {
this.productName = productName;
} @Basic
@Column(name = "product_price")
public double getProductPrice() {
return productPrice;
} public void setProductPrice(double productPrice) {
this.productPrice = productPrice;
} @Basic
@Column(name = "provider_id")
public int getProviderId() {
return providerId;
} public void setProviderId(int providerId) {
this.providerId = providerId;
} @Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false; ProductDO productDO = (ProductDO) object; if (id != productDO.id) return false;
if (Double.compare(productDO.productPrice, productPrice) != 0) return false;
if (providerId != productDO.providerId) return false;
if (productName != null ? !productName.equals(productDO.productName) : productDO.productName != null)
return false; return true;
} @Override
public int hashCode() {
int result;
long temp;
result = id;
result = 31 * result + (productName != null ? productName.hashCode() : 0);
temp = Double.doubleToLongBits(productPrice);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + providerId;
return result;
}
}
ProductDO.java
package cn.xiangxu.jpa_demo04.domain.domain_do; import lombok.ToString; import javax.persistence.*; /**
* @author 王杨帅
* @create 2018-08-14 19:54
* @desc
**/
@Entity
@ToString
@Table(name = "provider", schema = "testdemo", catalog = "")
public class ProviderDO {
private int id;
private String providerName;
private String providerPhone;
private String providerAddress; @Id
@Column(name = "id")
public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} @Basic
@Column(name = "provider_name")
public String getProviderName() {
return providerName;
} public void setProviderName(String providerName) {
this.providerName = providerName;
} @Basic
@Column(name = "provider_phone")
public String getProviderPhone() {
return providerPhone;
} public void setProviderPhone(String providerPhone) {
this.providerPhone = providerPhone;
} @Basic
@Column(name = "provider_address")
public String getProviderAddress() {
return providerAddress;
} public void setProviderAddress(String providerAddress) {
this.providerAddress = providerAddress;
} @Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false; ProviderDO that = (ProviderDO) object; if (id != that.id) return false;
if (providerName != null ? !providerName.equals(that.providerName) : that.providerName != null) return false;
if (providerPhone != null ? !providerPhone.equals(that.providerPhone) : that.providerPhone != null)
return false;
if (providerAddress != null ? !providerAddress.equals(that.providerAddress) : that.providerAddress != null)
return false; return true;
} @Override
public int hashCode() {
int result = id;
result = 31 * result + (providerName != null ? providerName.hashCode() : 0);
result = 31 * result + (providerPhone != null ? providerPhone.hashCode() : 0);
result = 31 * result + (providerAddress != null ? providerAddress.hashCode() : 0);
return result;
}
}
ProviderDO.java
package cn.xiangxu.jpa_demo04.domain.domain_do; import lombok.Builder;
import lombok.Data; /**
* @author 王杨帅
* @create 2018-08-14 20:03
* @desc
**/
@Data
@Builder public class ProductInfoDTO { private int id;
private String productName;
private double productPrice;
private int providerId;
private String providerName;
private String providerPhone;
private String providerAddress; }
ProductInfoDTO.java
5.3.3 创建持久层接口
技巧01:这里使用@Query进行原生的SQL查询,所以直接在ProductDAO中就可以查询出产品和供应商的信息
package cn.xiangxu.jpa_demo04.repository; import cn.xiangxu.jpa_demo04.domain.domain_do.ProductDO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import java.util.List;
import java.util.Map; /**
* @author 王杨帅
* @create 2018-08-14 19:55
* @desc
**/
public interface ProductDAO extends JpaRepository<ProductDO, Integer> { @Query(
nativeQuery = true,
value = "SELECT product_name, product_price, provider_name, provider_phone, provider_address\n" +
"FROM product\n" +
"JOIN provider \n" +
"ON product.provider_id = provider.id\n" +
"WHERE product.provider_id = ?1"
)
List<Map<String, Object>> findTest(Integer providerId); }
ProductDAO.java
5.3.4 测试
技巧01:多表关联查询时获取到数据是Map类型的,需要进行一层转化;本博文用的是阿里巴巴的 fastjson 进行转化
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.46</version>
</dependency>
坑01:fastjson 转化时 map 的 key 要和 实体类的属性 保持一致,不一致时只有通过本办法实现了
package cn.xiangxu.jpa_demo04.repository; import cn.xiangxu.jpa_demo04.JpaDemo04ApplicationTests;
import cn.xiangxu.jpa_demo04.domain.domain_do.DoctorInfoDetail;
import cn.xiangxu.jpa_demo04.domain.domain_do.ProductDO;
import cn.xiangxu.jpa_demo04.domain.domain_do.ProductInfoDTO;
import com.alibaba.fastjson.JSONObject;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import java.util.ArrayList;
import java.util.List;
import java.util.Map; @Component
public class ProductDaoTest extends JpaDemo04ApplicationTests { @Autowired
private ProductDAO productDAO; @Test
public void test01() {
List<ProductDO> all = productDAO.findAll();
System.out.println(all);
} @Test
public void test02() {
List<Map<String, Object>> test = productDAO.findTest(2);
System.out.println(test); List<ProductInfoDTO> productInfoDTOS = new ArrayList<>(); for (Integer i = 0; i < test.size(); i++) {
Map<String, Object> info = test.get(i);
// ProductInfoDTO productInfoDTO = JSONObject.parseObject(JSONObject.toJSONString(test.get(i)), ProductInfoDTO.class);
ProductInfoDTO productInfoDTO = ProductInfoDTO
.builder()
.productName((String)info.get("product_name"))
.productPrice((Double)info.get("product_price"))
.providerName((String)info.get("provider_name"))
.providerPhone((String)info.get("provider_phone"))
.providerAddress((String)info.get("provider_address"))
.build();
System.out.println(productInfoDTO); productInfoDTOS.add(productInfoDTO);
} System.out.println(productInfoDTOS); } }
SpringBoot24 SpringDataJPA环境搭建、实体类注解、关联查询的更多相关文章
- spring+hibernate实体类注解详解(非原创) + cascade属性取值
@Entity //继承策略.另一个类继承本类,那么本类里的属性应用到另一个类中 @Inheritance(strategy = InheritanceType.JOINED ) @Table(nam ...
- 实体类注解错误:Could not determine type for: java.util.List
今天配置实体类注解时,出现以下错误: Caused by: org.hibernate.MappingException: Could not determine type for: java.uti ...
- JPA实体类注解、springboot测试类、lombok的使用
前提准备: 搭建一个springboot项目,详情请参见其它博客:点击前往 1 引入相关依赖 web.mysql.jpa.lombok <?xml version="1.0" ...
- Hibernate实体类注解
常用的hibernate annotation标签如下: @Entity --注释声明该类为持久类.将一个Javabean类声明为一 个实体的数据库表映射类,最好实现序列化.此时,默认情况下,所有的类 ...
- HIbernate实体类注解配置
一.类级别注解 1.@Entity(name="EntityName") 必须 name为可选,对应数据库中一的个表 2.@Table(name="",cata ...
- Hibernate自动生成实体类注解(转)
常用的hibernate annotation标签如下: @Entity --注释声明该类为持久类.将一个Javabean类声明为一 个实体的数据库表映射类,最好实现序列化.此时,默认情况下,所有的类 ...
- Hibernate实体类注解解释
Hibernate注解1.@Entity(name="EntityName")必须,name为可选,对应数据库中一的个表2.@Table(name="",cat ...
- spring+hibernate 实体类注解问题
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.Ann ...
- IntelliJ IDEA 2017版 spring-boot2.0.2 搭建 JPA springboot DataSource JPA环境搭建,JPA注解@ManyToOne使用详情;JPA外键设置
一.数据库原型 数据库模型如图所示,而现在需要根据数据库模型,建立对应的实体类,这在项目重构老数据库,采用新的框架重构上应该是比较常见的. 数据库脚本如下: CREATE TABLE `bomsub` ...
随机推荐
- Ubuntu 中 java 环境 (sunjdk) 的配置 (附详细说明)
暑假以来为了鼓捣双系统废了很大的劲儿,本来一股脑想装 CentOS,无奈怎么处理分区引导都不能成功地与 Win8 共存,最终用 Ubuntu 一句 "检测到系统上有 Windows Boot ...
- 【vs2013】使用VS2013打包程序
如何用 VS 2013 打包 程序? 摘自:http://www.zhihu.com/question/25415940 更多请见摘自. 答案就在这里,想要你的exe独立运行在XP中:1.将平台工具集 ...
- boost 部分编译
完整编译boost库需要很长时间,而且我们不一定会用到所有的库. 那么如何只编译只需要的库呢? 解压boost源码,进入解压后的目录 ./bootstrap.sh生成bjam ./bjam --bui ...
- C#网络编程(接收文件) - Part.5
这篇文章将完成 Part.4 中剩余的部分,它们本来是一篇完整的文章,但是因为上一篇比较长,合并起来页数太多,浏览起来可能会比较不方便,我就将它拆为两篇了,本文便是它的后半部分.我们继续进行上一篇没有 ...
- css关系选择符
<!Doctype html> <html> <head> <meta http-equiv="Content-Type" content ...
- PCB 锣板和半孔工艺的差别
PCB 锣板和半孔工艺的差别 PCB 在做模块时会用到半孔工艺,但是由于半孔是特殊工艺. 需要加费用,打板时费还不低. 下面这个图是锣板和半孔工艺的差别. https://www.amobbs.com ...
- Javascript 原型链资料收集
Javascript 原型链资料收集 先收集,后理解. 理解JavaScript的原型链和继承 https://blog.oyanglul.us/javascript/understand-proto ...
- Linux 中断下半部
为什么使用中断下半部? 中断执行的原则是要以最快的速度执行完,而且期间不能延时和休眠! 可是现实中,中断中可能没办法很快的处理完需要做的事,或者必须用到延时和休眠,因此引入了中断下半部. 中断中处理紧 ...
- Android Binder机制中的异步回调
“Binder通信是同步而不是异步的”,但是在实际使用时,是设计成客户端同步而服务端异步. 看看Framwork层的各service类java源码便会知道,在客户端调用服务端的各种方法时,通常会传递一 ...
- Py修行路 python基础 (四)运算 copy
字符串的格式化 在字符串中插入 %s ,作为占位符,后边儿再定义插入变量. 算术运算 % 取模 判断奇偶数 / 除法 有小于号 // 取整除 返回整数部分 逻辑运算 and or not ' ...