JPA 详解
简介
Java Persistence API(JPA)是将Java对象和关系型数据库对象映射起来规范。实现这个规范后开发者可以使用相同的代码可以在任意的数据库中执行CRUD操作,实现的框架不仅仅是处理和数据库交换的代码(JDBC),同时也会将数据库中的数据和Java对象映射起来,无需手动进行转换。此教程基于JAP2.1。 JPA 主要包含的组件:
- 实体: 对于当前JPA的规范,实体就是POJO。
- 对象关系信息:应用开发者必须提供数据库表数据和Java对象之间的对应关系
- JPQL: JPA的目的是抽象具体的数据库,框架仍然提供了类SQL的方式处理特殊的方法
项目实战
首先创建项目:
mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=jpa
添加依赖:
<properties>
<jee.version>7.0</jee.version>
<h2.version>1.3.176</h2.version>
<hibernate.version>4.3.8.Final</hibernate.version>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>${jee.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
</dependencies>
EntityManager 和 Persistence Unit
配置 persistence.xml信息,文件保存在src/main/resource/META-INF下。
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
<persistence-unit name="PersistenceUnit" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="connection.driver_class" value="org.h2.Driver"/>
<property name="hibernate.connection.url" value="jdbc:h2:~/jpa"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="create"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
事务类型设置的是RESOURCE_LOCAL,表示事务由应用自己控制,如果使用了容器提供的事务可以设置为JTA。 provider 设置为org.hibernate.ejb.HibernatePersistence 表示使用Hibernate实现的JPA。 之后的设置就是设置JPA连接数据库的基本信息。
public class Main {
private static final Logger LOGGER = Logger.getLogger("JPA");
public static void main(String[] args) {
Main main = new Main();
main.run();
}
public void run() {
EntityManagerFactory factory = null;
EntityManager entityManager = null;
try {
factory = Persistence.createEntityManagerFactory("PersistenceUnit");
entityManager = factory.createEntityManager();
persistPerson(entityManager);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
e.printStackTrace();
} finally {
if (entityManager != null) {
entityManager.close();
}
if (factory != null) {
factory.close();
}
}
}
...
JPA中所有的操作基本都由EntityManager完成。为了获取EntityManger,需要创建EntityManagerFactory实例。一个应用尽需要一个EntityManagerFactory。
事务
现在来实现上面代码的persistPersion()方法,以为我们选择的是事务类型是本地事务,所有事务要有应用控制,存储一个对象
private void persistPerson(EntityManager entityManager) {
EntityTransaction transaction = entityManager.getTransaction();
try {
transaction.begin();
Person person = new Person();
person.setFirstName("Homer");
person.setLastName("Simpson");
entityManager.persist(person);
transaction.commit();
} catch (Exception e) {
if (transaction.isActive()) {
transaction.rollback();
}
}
}
表
上述的Person对象的定义:表名为T_PERSION
@Entity
@Table(name = "T_PERSON")
public class Person {
private Long id;
private String firstName;
private String lastName;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "FIRST_NAME")
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@Column(name = "LAST_NAME")
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
如果表明和类名相同,那么@Table是可以省略的,但是仍然推荐加上。 注解@Column 是用来映射Java对象和表中的列的,及时不加注解,JPA仍然会映射,除非其使用注解@Transient修饰,则不会被映射。关于@Column的使用
@Colunm(name="FIRST_NAME", length=100, nullable = false, unique = false)
上述注解的意思就是映射表中列名为FIRST_NAME的列,长度100字符,不能空,不唯一,当试图插入null值是会抛出异常并会滚事务。 对于@Id和@GeneratedValue是告诉JAP,这个值是主键并且其值是由数据库自动生成的。 上述例子,@Column是修饰getter的同样可以直接修饰字段。
@Entity
@Table(name = "T_PERSON")
public class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
...
这两种方法几乎没什么区别,唯一的区别在于如果注解修饰字段子类无法重写其注解。 另一个需要注意的是需要在一个实体的层次上使用一种注解方式。可以在JPA的整个项目混用注解字段或者方法,但是在一个实体和它的子类中需要确保使用的是同一种注解方式。如果要修改子类的注解方式,可以使用 @Access注解改变
@Entity
@Table(name = "T_GEEK")
@Access(AccessType.PROPERTY)
public class Geek extends Person {
...
继承
对于Geek类,其直接继承与Person
@Entity
@Table(name = "T_GEEK")
public class Geek extends Person {
private String favouriteProgrammingLanguage;
private List<Project> projects = new ArrayList<Project>();
@Column(name = "FAV_PROG_LANG")
public String getFavouriteProgrammingLanguage() {
return favouriteProgrammingLanguage;
}
public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
}
...
}
Hibernate 会重新创建T_PERSON表
Hibernate: create table T_PERSON (DTYPE varchar(31) not null, id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (id))
保存Geek
private void persistGeek(EntityManager entityManager) {
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
Geek geek = new Geek();
geek.setFirstName("Gavin");
geek.setLastName("Coffee");
geek.setFavouriteProgrammingLanguage("Java");
entityManager.persist(geek);
geek = new Geek();
geek.setFirstName("Thomas");
geek.setLastName("Micro");
geek.setFavouriteProgrammingLanguage("C#");
entityManager.persist(geek);
geek = new Geek();
geek.setFirstName("Christian");
geek.setLastName("Cup");
geek.setFavouriteProgrammingLanguage("Java");
entityManager.persist(geek);
transaction.commit();
}
T_PERSION保存的数据
sql> select * from t_person;
DTYPE | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
Person | 1 | Homer | Simpson | null
Geek | 2 | Gavin | Coffee | Java
Geek | 3 | Thomas | Micro | C#
Geek | 4 | Christian | Cup | Java
DTYPE表示了不同类型的PERSON,对于Person,其FAV_PROG_LANG是null。 如果不喜欢使用字符串类型的鉴别器,可以设置@DiscriminatorColumn来表示不同的类型
@DiscriminatroColumn(name="PERSION_TYPE",discriminatorType=DiscriminatroType.INTEGER)
存储结果:
sql> select * from t_person;
PERSON_TYPE | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
-1907849355 | 1 | Homer | Simpson | null
2215460 | 2 | Gavin | Coffee | Java
2215460 | 3 | Thomas | Micro | C#
2215460 | 4 | Christian | Cup | Java
并不是所有情况下都想把信息存储在一张表中,可以使用@Inheritance选择不同的存储策略,对于这种一共有三种选择:
- SINGLE_TABLE: 这个策略就是把所有的字段映射到一张表中
- JOINERD: 对每个实体创建一张表。每个表只包含其映射的对象的信息,加载一个实体的时候,通过join的方式获取所有的信息,虽然降低了存储空间,但是
- TABLE_PER_CLASS: 所有的表中都会包含全部信息。 使用JOIN的方式: @Inheritance(strategy=InheritanceType.JOINED) Hibernate会创建两张表: Hibernate: create table T_GEEK (FAV_PROG_LANG varchar(255), id bigint not null, primary key (id)) Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id)) 其获取值: sql> select * from t_person; ID | FIRST_NAME | LAST_NAME 1 | Homer | Simpson 2 | Gavin | Coffee 3 | Thomas | Micro 4 | Christian | Cup (4 rows, 12 ms) sql> select * from t_geek; FAV_PROG_LANG | ID Java | 2 C# | 3 Java | 4 (3 rows, 7 ms) 使用SQL获取完整的值: select person0_.id as id1_2_, person0_.FIRST_NAME as FIRST_NA2_2_, person0_.LAST_NAME as LAST_NAM3_2_, person0_1_.FAV_PROG_LANG as FAV_PROG1_1_, case when person0_1_.id is not null then 1 when person0_.id is not null then 0 end as clazz_ from T_PERSON person0_ left outer join T_GEEK person0_1_ on person0_.id=person0_1_.id 使用JPA实现如下: TypedQuery<Person> query = entityManager.createQuery("from Person", Person.class); List<Person> resultList = query.getResultList(); for (Person person : resultList) { StringBuilder sb = new StringBuilder(); sb.append(person.getFirstName()).append(" ").append(person.getLastName()); if (person instanceof Geek) { Geek geek = (Geek)person; sb.append(" ").append(geek.getFavouriteProgrammingLanguage()); } LOGGER.info(sb.toString()); } 关系 表,实体之间的关系主要有,1对1,1对多,多对多,嵌入,集合。 1对1 @Entity @Table(name = "T_ID_CARD") public class IdCard { private Long id; private String idNumber; private Date issueDate; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Column(name = "ID_NUMBER") public String getIdNumber() { return idNumber; } public void setIdNumber(String idNumber) { this.idNumber = idNumber; } @Column(name = "ISSUE_DATE") @Temporal(TemporalType.TIMESTAMP) public Date getIssueDate() { return issueDate; } public void setIssueDate(Date issueDate) { this.issueDate = issueDate; } } @Temporal告诉JPA如何将其序列化保存到数据库中,可以选择DATE,TIME,TIMESTAMP 然后设置JPA,每个Persion都有一个IdCard @Entity @Table(name = "T_PERSON") public class Person { ... private IdCard idCard; ... @OneToOne @JoinColumn(name = "ID_CARD_ID") public IdCard getIdCard() { return idCard; } 创建表的语句: ```SQL create table T_ID_CARD ( id bigint generated by default as identity, ID_NUMBER varchar(255), ISSUE_DATE timestamp, primary key (id) )
create table T_PERSON ( id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), ID_CARD_ID bigint, primary key (id) )
@OneToOne有一个重要特性就是可以设置何时获取IdCard。
```Java
@OneToOne(fetch = FetchType.EAGER)
默认值EAGER表示每次获取Person都要获取IdCard
@OneToOne(fetch = FetchType.LAZY)
表示只有在需要IdCard的时候才会去获取 其获取SQL如下:
Hibernate:
select
person0_.id as id1_3_,
person0_.FIRST_NAME as FIRST_NA2_3_,
person0_.ID_CARD_ID as ID_CARD_4_3_,
person0_.LAST_NAME as LAST_NAM3_3_,
person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
case
when person0_1_.id is not null then 1
when person0_.id is not null then 0
end as clazz_
from
T_PERSON person0_
left outer join
T_GEEK person0_1_
on person0_.id=person0_1_.id
Hibernate:
select
idcard0_.id as id1_2_0_,
idcard0_.ID_NUMBER as ID_NUMBE2_2_0_,
idcard0_.ISSUE_DATE as ISSUE_DA3_2_0_
from
T_ID_CARD idcard0_
where
idcard0_.id=?
1对多
@Entity
@Table(name = "T_PHONE")
public class Phone {
private Long id;
private String number;
private Person person;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "NUMBER")
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PERSON_ID")
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
对于Person
private List<Phone> phones = new ArrayList<>();
...
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
public List<Phone> getPhones() {
return phones;
}
@OneToMany 的mappedBy 表示使用Phone中的person字段来关联。其fetch设置为LAZY模式,因为每次获取Person的时候不一定需要其Phone,如果需要可以通过如下方式获取:
TypedQuery<Person> query = entityManager.createQuery("from Person p left join fetch p.phones", Person.class);
对应的SQL如下:
select
person0_.id as id1_3_0_,
phones1_.id as id1_4_1_,
person0_.FIRST_NAME as FIRST_NA2_3_0_,
person0_.ID_CARD_ID as ID_CARD_4_3_0_,
person0_.LAST_NAME as LAST_NAM3_3_0_,
person0_1_.FAV_PROG_LANG as FAV_PROG1_1_0_,
case
when person0_1_.id is not null then 1
when person0_.id is not null then 0
end as clazz_0_,
phones1_.NUMBER as NUMBER2_4_1_,
phones1_.PERSON_ID as PERSON_I3_4_1_,
phones1_.PERSON_ID as PERSON_I3_3_0__,
phones1_.id as id1_4_0__
from
T_PERSON person0_
left outer join
T_GEEK person0_1_
on person0_.id=person0_1_.id
left outer join
T_PHONE phones1_
on person0_.id=phones1_.PERSON_ID
多对多
@Entity
@Table(name = "T_PROJECT")
public class Project {
private Long id;
private String title;
private List<Geek> geeks = new ArrayList<Geek>();
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "TITLE")
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@ManyToMany(mappedBy="projects")
public List<Geek> getGeeks() {
return geeks;
}
public void setGeeks(List<Geek> geeks) {
this.geeks = geeks;
}
}
通过@ManyToMany中的mappedBy得知,需要在Geek中的字段projects做多对多关系 Geek类:
private List<Project> projects = new ArrayList<>();
...
@ManyToMany
@JoinTable(
name="T_GEEK_PROJECT",
joinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")},
inverseJoinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")})
public List<Project> getProjects() {
return projects;
}
在Geek类中提供了@JoinTable明确其和哪个表做join,joinColunms和inverseJoinConlumns则表示如何做JOIN。 如果是在Project中实现是同样的,只需要将joinColumns和inverseJoinColumn换一下即可 Project类
@ManyToMany
@JoinTable(
name="T_GEEK_PROJECT",
joinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")},
inverseJoinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")})
public List<Geek> getGeeks() {
return geeks;
}
Geek类:
@ManyToMany(mappedBy="geeks")
public List<Project> getProjects() {
return projects;
}
获取如下:
List<Geek> resultList = entityManager.createQuery("from Geek g where g.favouriteProgrammingLanguage = :fpl", Geek.class).setParameter("fpl", "Java").getResultList();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
Project project = new Project();
project.setTitle("Java Project");
for (Geek geek : resultList) {
project.getGeeks().add(geek);
geek.getProjects().add(project);
}
entityManager.persist(project);
transaction.commit();
嵌入元素/嵌入的集合
如果想要在Java中更加细粒度的控制其model可以使用嵌入模式。例如Project有startDate 和 endDate,可以创建Period类以便重用开始和结束时间。
@Embeddable
public class Period {
private Date startDate;
private Date endDate;
@Column(name ="START_DATE")
public Date getStartDate() {
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
@Column(name ="END_DATE")
public Date getEndDate() {
return endDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
}
在Project中加入:
private Period projectPeriod;
@Embedded
public Period getProjectPeriod() {
return projectPeriod;
}
public void setProjectPeriod(Period projectPeriod) {
this.projectPeriod = projectPeriod;
}
Project表结构:
create table T_PROJECT (
id bigint generated by default as identity,
END_DATE timestamp,
START_DATE timestamp,
projectType varchar(255),
TITLE varchar(255),
primary key (id)
)
查询:
entityManager.createQuery("from Project p where p.projectPeriod.startDate = :startDate", Project.class).setParameter("startDate", createDate(1, 1, 2015));
数据类型和转换
SQL类型 |
java类型 |
---|---|
VARCHAR (CHAR, VARCHAR2, CLOB, TEXT) |
String (char, char[]) |
NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE) |
Number (BigDecimal, BigInteger, Integer, Double, Long, Float, Short, Byte) |
NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE) |
int, long, float, double, short, byte |
VARBINARY (BINARY, BLOB) |
byte[] |
BOOLEAN (BIT, SMALLINT, INT, NUMBER) |
boolean(Boolean) |
TIMESTAMP (DATE, DATETIME) |
java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar |
NUMERIC (VARCHAR, CHAR) |
java.lang.Enum |
VARBINARY (BINARY, BLOB) |
java.util.Serializable |
例如使用enum
@Entity
@Table(name = "T_PROJECT")
public class Project {
...
private ProjectType projectType;
public enum ProjectType {
FIXED, TIME_AND_MATERIAL
}
...
@Enumerated(EnumType.ORDINAL)
public ProjectType getProjectType() {
return projectType;
}
public void setProjectType(ProjectType projectType) {
this.projectType = projectType;
}
}
通过使用@Enumerated 将enum和数据库的字段进行映射,EnumType.ORDINAL 表示使用数字表示enum并保存到数据中。
sql> select * from t_project;
ID | PROJECTTYPE | TITLE
1 | 1 | Java Project
(1 row, 2 ms)
如果使用 EnumType.String 则表示使用String保存,调用enum的方法name()获取其名字。
sql> select * from t_project;
ID | PROJECTTYPE | TITLE
1 | TIME_AND_MATERIAL | Java Project
(1 row, 2 ms)
如果没有满足需要的转换器,可以自己构建
@Converter
public class BooleanConverter implements AttributeConverter<Boolean, Integer> {
@Override
public Integer convertToDatabaseColumn(Boolean aBoolean) {
if (Boolean.TRUE.equals(aBoolean)) {
return 1;
} else {
return -1;
}
}
@Override
public Boolean convertToEntityAttribute(Integer value) {
if (value == null) {
return Boolean.FALSE;
} else {
if (value == 1) {
return Boolean.TRUE;
} else {
return Boolean.FALSE;
}
}
}
}
使用
private boolean valid;
...
@Column(name = "VALID")
@Convert(converter = BooleanConverter.class)
public boolean isValid() {
return valid;
}
public void setValid(boolean valid) {
this.valid = valid;
}
结果如下:
sql> select * from t_id_card;
ID | ID_NUMBER | ISSUE_DATE | VALID
1 | 4711 | 2015-02-04 16:43:30.233 | -1
标准API
目前为止一直使用JPQL来查询数据库,现在通过使用JPQL提供的标准的API来查询。 例如:
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> personRoot = query.from(Person.class);
query.where(builder.equal(personRoot.get("firstName"), "Homer"));
List<Person> resultList = entityManager.createQuery(query).getResultList();
首先通过EntityManger创建CriteriaBuilder。通过CruiteriaBuilder创建CriteriaQuery。提供一个函数获取where语句
query.where(builder.equal(personRoot.get("firstName"), "Homer"));
对于多where语句
query.where(builder.and(
builder.equal(personRoot.get("firstName"), "Homer"),
builder.equal(personRoot.get("lastName"), "Simpson")));
CriteriaQuery定义了一下子句和选项:
- distinct() 清除重复
- from() 设置查询的表
- select() 表示select语句
- multiselect() select的列表
- where() where子句
- orderBy() 确定排序顺序
- groupBy() 分组结果
- having() having子句
- subquery() 子查询
序列
对于@GeneratedValue提供了三种策略:
- TABLE: JPA创建另一个表来提供序号
- SEQUENCE: 如果数据库支持SEQUENCE,使用SEQUENCE方式创建序号
- IDENTITY: 数据库提供标识列使用标识列提供序号
例如使用TABLE
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "TABLE_GENERATOR")
@TableGenerator(name = "TABLE_GENERATOR", table="T_SEQUENCES", pkColumnName = "SEQ_NAME", valueColumnName = "SEQ_VALUE", pkColumnValue = "PHONE")
public Long getId() {
return id;
}
SQL
sql> select * from t_sequences;
SEQ_NAME | SEQ_VALUE
PHONE | 1
使用SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "S_PROJECT")
@SequenceGenerator(name = "S_PROJECT", sequenceName = "S_PROJECT", allocationSize = 100)
public Long getId() {
return id;
}
通过@SequenceGenerator JPA获取其SEQUENCE,名字为S_PROJECT,分配的大小(100)。 使用IDENTITY
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
表:
create table T_ID_CARD (
id bigint generated by default as identity,
ID_NUMBER varchar(255),
ISSUE_DATE timestamp,
VALID integer,
primary key (id)
)
- 采用一个自创的"验证框架"实现对数据实体的验证[扩展篇]
- 采用一个自创的"验证框架"实现对数据实体的验证[改进篇]
- Flash XSS检测脚本的简单实现
- 采用一个自创的"验证框架"实现对数据实体的验证[设计篇]
- 采用一个自创的"验证框架"实现对数据实体的验证[编程篇]
- 谈谈你最熟悉的System.DateTime[上篇]
- 12步轻松搞定Python装饰器
- 实用小工具,教你轻松转化Python通用数据格式
- 数据工程师常用的几个小工具(附python源代码)
- R语言的三种聚类方法
- 技能 | R语言的igraph画社交关系图示例
- 魔兽世界中招:一条命令行就能劫持你的游戏!
- R语言 apply函数家族详解
- 基于R语言的梯度推进算法介绍
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PyQt5 技巧篇-增加一个类级变量,类级变量的设置方法,类级"常量"设置方法
- PyQt5 技巧篇-按钮隐藏并保留位置,设置按钮的可见度,设置按钮透明度
- PyQt5 技巧篇-复选框绑定行内容,全选、清空、展示选中的内容功能实现演示,设置复选框选中,检查复选框选中状态
- PyQt5 技巧篇-QWidget、Dialog界面固定大小设置
- 力扣:地下城游戏,手把手教你做困难题
- RN布局
- 学会MySQL主从复制读写分离,看这篇就够了
- Canal+Kafka实现MySQL与Redis数据同步
- 超详细canal入门,看这篇就够了
- 详细讲解!RabbitMQ防止数据丢失
- 详细讲解!从秒杀聊到ZooKeeper分布式锁
- 正确使用 wait/notify/notify方法以及源码解析
- 秒杀商品超卖事故:Redis分布式锁请慎用!
- 3D开发是一个生态,ThingJS支持js,css,json,html外部资源引用
- 手把手教你实现xxl-job分布式任务调度平台搭建