IOS数据存储 之WCDB (一)

article/2025/10/9 11:22:30

IOS数据存储 之WCDB (一)

  • 1. WCDB 简介
    • 1.1 使用WCDB框架3大优势
    • 1.2 WCDB 的一些基础概念
      • 1.2.1 类字段绑定(ORM)
      • 1.2.2 WINQ(WCDB语言集成查询)
        • 1.2.2.1 字段映射与运算符
        • 1.2.2.2 字段组合
        • 1.2.2.3 AllProperties
        • 1.2.2.4 AnyProperty
      • 1.2.3 加密
      • 1.2.4 全局监控
        • 1.2.4.1 WCTError
        • 1.2.4.2 获取错误
      • 1.2.5 损坏修复
      • 1.2.6 性能监控
        • 1.2.6.1 操作耗时
        • 1.2.6.2 监控耗时
        • 1.2.6.3 SQL执行监控
  • 2. WCDB OC版本
    • 2.1 WCDB OC版本 增删改查(CRUD)
      • 2.1.1 增
      • 2.1.2 删
      • 2.1.3 改
      • 2.1.4 查
    • 2.2 事务 (Transaction)
  • 3. WCDB Swift版本
    • 3.1 WCDB.swift安装
    • 3.2 WCDB.swift使用
  • 4. 数据库从FMDB迁移到WCDB
    • 4.1 为什么要迁移到WCDB?
      • 4.1.1 WCDB 对比FMDB的优势一:高效
      • 4.1.2 WCDB 对比FMDB的优势一:易用
      • 4.1.3 WCDB 对比FMDB的优势一:完整
    • 4.2 FMDB迁移
      • 4.2.1 安装
      • 4.2.2 创建数据库
      • 4.2.3 打开数据库
      • 4.2.4 建表与ORM
      • 4.2.5 数据库升级
      • 4.2.6 访问数据库
        • 4.2.6.1 查询
        • 4.2.6.2 插入
        • 4.2.6.3 修改
        • 4.2.6.4 删除
      • 4.2.7 条件语句
        • 4.2.7.1 改写条件语句
          • 4.2.7.1.1 部分查询
          • 4.2.7.1.2 自增插入
          • 4.2.7.1.3 数值更新
          • 4.2.7.1.4 部分删除
      • 4.2.8 特殊语句和核心层接口
        • 4.2.8.1 执行WINQ
        • 4.2.8.2 获取WINQ运行结果
      • 4.2.9 事务
        • 4.2.9.1 便捷事务接口
      • 4.2.10 多重语句和批处理
      • 4.2.11 线程安全与并发
      • 4.2.12 配置
      • 4.2.13 关闭数据库
        • 4.2.13.1 回收对象
        • 4.2.13.2 手动关闭数据库
      • 4.2.14 隔离Objective-C++代码
        • 4.2.14.1 WCTTableCoding文件模版

1. WCDB 简介

  • WCDB是腾讯开发的,微信中使用的DB开源框架:

引用官方说法:“WCDB是一个易用、高效、完整的移动数据库框架,它基于 SQLite 和 SQLCipher 开发。”

1.1 使用WCDB框架3大优势

  • 易用性
  1. one line of code 是它坚持的原则,大多数操作只需要一行代码即可完成.
  2. 使用WINQ 语句查询,不用为拼接SQL语句而烦恼了,模型绑定映射也是按照规定模板去实现方便快捷。
  • 高效性
  1. 和fmdb做对比

WCDB.swift和fmdb做对比WCDB.swift和fmdb做对比

  • 完整性
  1. 支持基于SQLCipher 加密
  2. 持全文搜索
  3. 支持反注入,可以避免第三方从输入框注入 SQL,进行预期之外的恶意操作。
  4. 用户不用手动管理数据库字段版本,升级方便自动.
  5. 提供数据库修复工具。

1.2 WCDB 的一些基础概念

1.2.1 类字段绑定(ORM)

  • ORM定义:

在WCDB内,ORM(Object Relational Mapping)是指

  1. 将一个ObjC的类,映射到数据库的表和索引;
  2. 将类的property,映射到数据库表的字段;
  3. 这一过程。通过ORM,可以达到直接通过Object进行数据库操作,省去拼装过程的目的。
  • WCDB通过内建的宏实现ORM的功能。如下:
//Message.h
@interface Message : NSObject@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;
@property(assign) int unused; //You can only define the properties you need@end
//Message.mm
#import "Message.h"
@implementation MessageWCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE(Message, content)
WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE(Message, modifiedTime)WCDB_PRIMARY(Message, localID)WCDB_INDEX(Message, "_index", createTime)@end
//Message+WCTTableCoding.h
#import "Message.h"
#import <WCDB/WCDB.h>@interface Message (WCTTableCoding) <WCTTableCoding>WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)
WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)@end
  • 将一个已有的ObjC类进行ORM绑定的过程如下:
  1. 定义该类遵循WCTTableCoding协议。可以在类声明上定义,也可以通过文件模版在category内定义。
  2. 使用WCDB_PROPERTY宏在头文件声明需要绑定到数据库表的字段。
  3. 使用WCDB_IMPLEMENTATIO宏在类文件定义绑定到数据库表的类。
  4. 使用WCDB_SYNTHESIZE宏在类文件定义需要绑定到数据库表的字段。
  • 简单几行代码,就完成了将类和需要的字段绑定到数据库表的过程。这三个宏在名称和使用习惯上,也都和定义一个ObjC类相似,以此便于记忆。
  • 除此之外,WCDB还提供了许多可选的宏,用于定义数据库索引、约束等,如:

WCDB_PRIMARY用于定义主键
WCDB_INDEX用于定义索引
WCDB_UNIQUE用于定义唯一约束
WCDB_NOT_NULL用于定义非空约束

  • 定义完成后,只需要调用createTableAndIndexesOfName:withClass:接口,即可创建表和索引。
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
/*CREATE TABLE messsage (localID INTEGER PRIMARY KEY,content TEXT,createTime BLOB,modifiedTime BLOB)*/
BOOL result = [database createTableAndIndexesOfName:@"message"withClass:Message.class];
  • 接口会根据ORM的定义,创建对应表和索引。

1.2.2 WINQ(WCDB语言集成查询)

  • WINQ简介
  1. WINQ(WCDB Integrated Query,音’wink’),是将自然查询的SQL集成到WCDB框架中的技术,基于C++实现。
  2. 传统的SQL语句,通常是开发者拼接字符串完成。这种方式不仅繁琐、易错,而且出错后很难定位到问题所在。同时也容易给SQL注入留下可乘之机。
  3. 而WINQ将查询语言集成到了C++中,可以通过类似函数调用的方式来写SQL查询。借用IDE的代码提示和编译器的语法检查,达到易用、纠错的效果。
  4. WINQ的使用上接近于C函数调用。对于熟悉SQL的开发者,无须特别学习即可立刻上手使用.
  • WINQ原理:请参考官方文档:WINQ原理

1.2.2.1 字段映射与运算符

  • 对于一个已绑定ORM的类,可以通过className.propertyName的方式,获得数据库内字段的映射,以此书写SQL的条件、排序、过滤等等所有语句。如下是几个例子:
/*SELECT MAX(createTime), MIN(createTime)FROM messageWHERE localID>0 AND content IS NOT NULL*/
[database getObjectsOnResults:{Message.createTime.max(), Message.createTime.min()}fromTable:@"message"where:Message.localID > 0 && Message.content.isNotNull()];
/*SELECT DISTINCT localIDFROM messageORDER BY modifiedTime ASCLIMIT 10*/
[database getObjectsOnResults:Message.localID.distinct()fromTable:@"message"orderBy:Message.modifiedTime.order(WCTOrderedAscending)limit:10];
/*DELETE FROM messageWHERE localID BETWEEN 10 AND 20 OR content LIKE 'Hello%'*/
[database deleteObjectsFromtable:@"message"where:Message.local.between(10, 20) || Message.content.like("Hello%")];
  • 由于WINQ通过接口调用实现SQL查询,因此在书写过程中会有IDE的代码提示和编译器的语法检查,从而提升开发效率,避免写错。
  • WINQ的接口包括但不限于:

一元操作符:+、-、!等
二元操作符:||、&&、+、-、*、/、|、&、<<、>>、<、<=、==、!=、>、>=等
范围比较:IN、BETWEEN等
字符串匹配:LIKE、GLOB、MATCH、REGEXP等
聚合函数:AVG、COUNT、MAX、MIN、SUM等

  • 凡是SQLite支持的语法规则,WINQ基本都有其对应的接口。且接口名称与SQLite的语法规则基本保持一致。

1.2.2.2 字段组合

  • 多个字段映射可通过大括号{}进行组合,如:
/*SELECT localID, contentFROM message*/
[database getAllObjectsOnResults:{Message.localID, Message.content}fromTable:@"message"];
/*SELECT *FROM messageORDER BY createTime ASC, localID DESC*/
[database getObjectsOfClass:Message.class fromTable:@"message" orderBy:{Message.createTime.order(WCTOrderedAscending),  Message.localID.order(WCTOrderedDescending)}];

1.2.2.3 AllProperties

  • 在上述的组合中,大括号{}的语法实质是C++中列表std::list的隐式初始化。而className.AllProperties则用于获取类定义的所有字段映射的列表,如:
/*SELECT localID, content, createTime, modifiedTimeFROM message
*/
[database getAllObjectsOnResults:Message.AllPropertiesfromTable:@"message"];

1.2.2.4 AnyProperty

  • className.AnyProperty用于指代SQL中的*,如:
/*SELECT count(*)FROM message*/[database getOneValueOnResult:Message.AnyProperty.count()fromTable:@"message"];

1.2.3 加密

  • WCDB提供基于sqlcipher的数据库加密功能,如下:
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
NSData *password = [@"MyPassword" dataUsingEncoding:NSASCIIStringEncoding];
[database setCipherKey:password];

1.2.4 全局监控

  • WCDB提供了对错误和性能的全局监控,可用于调试错误和性能。

  • WCDB可以对所有错误进行统一的监控,也可以获取某个特定操作的错误信息。所有错误都以WCTError的形式出现。

1.2.4.1 WCTError

  • WCTError继承自NSError,包含了WCDB错误的所有信息,以供调试或发现问题。
    type表示错误的类型,不同类型的错误其错误码和拥有的信息不同。其对应关系如下
type描述相关参考代码
SQLite表示该错误来自SQLite接口请参考rescode
SystemCall表示该错误来自系统调用请参考errno
Core表示该错误来自WCDB Core层请参考源码的error.hpp
Interface表示该错误来自WCDB Interface层请参考源码的error.hpp
Abort表示中断,该错误一般是开发错误,应该在发布前修复
Warning表示警告,建议修复
SQLiteGlobal表示该信息来自SQLite的log接口,一般只作为debug log请参考rescode
  • 其他错误信息通过infoForKey接口获得,包括:
type描述相关参考代码
Tag正在操作的数据库的tag
Operation正在进行的操作请参考源码的error.hpp
Extended CodeSQLite的扩展码请参考rescode
Message错误信息
SQL发生错误时正在执行的SQL
Path发生错误时正在操作的文件的路径

1.2.4.2 获取错误

  • 由于便捷接口的设计原则是易用,因此不提供获取错误的方式。错误处理需使用链式接口
WCTSelect *select = [database prepareSelectObjectsOfClass:Message.classfromTable:@"message"];
NSArray<Message *> *objects = [[[select where:Message.localID > 0] orderBy:Message.createTime.order()] limit:10].allObjects;
WCTError *error = select.error;
  • 开发者也可以注册全局的错误接口,以调试、上报、打log, 代码如下:
//Error Monitor
[WCTStatistics SetGlobalErrorReport:^(WCTError *error) {NSLog(@"[WCDB]%@", error);
}];
//Performance Monitor
[WCTStatistics SetGlobalPerformanceTrace:^(WCTTag tag, NSDictionary<NSString *, NSNumber *> *sqls, NSInteger cost) {NSLog(@"Database with tag:%d", tag);NSLog(@"Run :");[sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sqls, NSNumber *count, BOOL *) {NSLog(@"SQL %@ %@ times", sqls, count);}];NSLog(@"Total cost %lld nanoseconds", cost);
}];
//SQL Execution Monitor
[WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {NSLog(@"SQL: %@", sql);
}];

1.2.5 损坏修复

  • WCDB内建了修复工具,以应对数据库损坏,无法使用的情况。
  • 开发者需要在数据库未损坏时,对数据库元信息定时进行备份,如下:
NSData *backupPassword = [@"MyBackupPassword" dataUsingEncoding:NSASCIIStringEncoding];
[database backupWithCipher:backupPassword];
  • 当检测到数据库损坏,即WCTErrortypeWCTErrorTypeSQLitecode为11或26(SQLITE_CORRUPTSQLITE_NOTADB)时,可以进行修复。
//Since recovering is a long time operation, you'd better call it in sub-thread.
[view startLoading];
dispatch_async(DISPATCH_QUEUE_PRIORITY_BACKGROUND, ^{WCTDatabase *recover = [[WCTDatabase alloc] initWithPath:recoverPath];NSData *password = [@"MyPassword" dataUsingEncoding:NSASCIIStringEncoding];NSData *backupPassword = [@"MyBackupPassword" dataUsingEncoding:NSASCIIStringEncoding];int pageSize = 4096;//Default to 4096 on iOS and 1024 on macOS.[database close:^{[recover recoverFromPath:path withPageSize:pageSize backupCipher:cipher databaseCipher:password];}];[view stopLoading];
});

1.2.6 性能监控

  • WCDB支持获取单次操作的耗时,也支持对单个DB或全局注册统一接口监控性能。
  • 所有性能监控都会有少量的性能损坏,请根据需求开启.

1.2.6.1 操作耗时

  • 由于便捷接口的设计原则是易用,因此不提供获取错误的方式。操作耗时需使用链式接口。

  • 首先安通过setStatisticsEnabled:打开耗时监控

WCTSelect *select = [database prepareSelectObjectsOfClass:Message.classfromTable:@"message"];
[select setStatisticsEnabled:YES];//You should call this before all other operations
  • 在操作执行完成后,通过cost接口获取耗时
NSArray<Message *> *objects = [[[select where:Message.localID > 0] orderBy:Message.createTime.order()] limit:10].allObjects;
NSLog(@"%f", select.cost);//You should call this after all other operations

1.2.6.2 监控耗时

  • WCDB支持对所有SQL操作进行全局监控,也支持监控单个特定的数据库.

所有监控的返回数据都相同,包括三个数据:

  1. Tag,执行操作的数据库的tag
  2. sqls,执行的SQL和对应的次数。
    对于非事务操作,则为单条SQL
    对于事务操作,则为该次事务所执行的所有SQL和每个sql执行的次数
  3. cost,耗时
  • 全局监控:

    监控所有db的数据库操作耗时,该接口需要在所有db打开、操作之前调用。

[WCTStatistics SetGlobalTrace:^(WCTTag tag, NSDictionary<NSString *, NSNumber *> *sqls, NSInteger cost) {NSLog(@"Tag: %d", tag);[sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sql, NSNumber *count, BOOL *) {NSLog(@"SQL: %@ Count: %d", sql, count.intValue);}];NSLog(@"Total cost %ld nanoseconds", (long) cost);
}];
  • 特定数据库监控

对于特定的数据库,该接口会覆盖全局监控的注册。

[db setTrace:^(WCTTag tag, NSDictionary<NSString *, NSNumber *> *sqls, NSInteger cost) {NSLog(@"Tag: %d", tag);[sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sql, NSNumber *count, BOOL *) {NSLog(@"SQL: %@ Count: %d", sql, count.intValue);}];NSLog(@"Total cost %ld nanoseconds", (long) cost);
}];
  • 操作耗时监控耗时的区别
  1. 操作耗时cost返回的耗时为浮点数的秒,监控耗时的cost返回的耗时为整型的纳秒。
  2. 监控耗时仅包括SQL在SQLite层面的耗时,包括SQL的编译、I/O等。而操作耗时除以上之外,还包括了WCDB层面对类封装等产生的耗时

1.2.6.3 SQL执行监控

  • WCDB可以监控所有SQL的执行,以确定代码符合预期
//SQL Execution Monitor
[WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {NSLog(@"SQL: %@", sql);
}];

2. WCDB OC版本

  • 得益于ORM的定义,WCDB可以直接进行通过object进行增删改查(CRUD)操作。开发者可以通过WCTDatabaseWCTTable两个类进行一般的增删改查操作。

2.1 WCDB OC版本 增删改查(CRUD)

2.1.1 增

//插入
Message *message = [[Message alloc] init];
message.localID = 1;
message.content = @"Hello, WCDB!";
message.createTime = [NSDate date];
message.modifiedTime = [NSDate date];
/*INSERT INTO message(localID, content, createTime, modifiedTime) VALUES(1, "Hello, WCDB!", 1496396165, 1496396165);*/
BOOL result = [database insertObject:messageinto:@"message"];

2.1.2 删

//删除
//DELETE FROM message WHERE localID>0;
BOOL result = [database deleteObjectsFromTable:@"message"where:Message.localID > 0];

2.1.3 改

//修改
//UPDATE message SET content="Hello, Wechat!";
Message *message = [[Message alloc] init];
message.content = @"Hello, Wechat!";
BOOL result = [database updateRowsInTable:@"message"onProperties:Message.contentwithObject:message];

2.1.4 查

//查询
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [database getObjectsOfClass:Message.classfromTable:@"message"                                                         orderBy:Message.localID.order()];
  • WCTTable相当于预设了表名和类名的WCTDatabase对象,接口和WCTDatabase基本一致。
WCTTable *table = [database getTableOfName:@"message"withClass:Message.class];
//查询
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [table getObjectsOrderBy:Message.localID.order()];

2.2 事务 (Transaction)

  • WCDB内可通过两种方式执行事务,一是runTransaction:接口,如下:
BOOL commited = [database runTransaction:^BOOL {[database insertObject:message into:@"message"];return YES; //return YES to commit transaction and return NO to rollback transaction.
}];
  • 这种方式要求数据库操作在一个BLOCK内完成,简单易用。
  • 另一种方式则是获取WCTTransaction对象,如下:
WCTTransaction *transaction = [database getTransaction];
BOOL result = [transaction begin];
[transaction insertObject:message into:@"message"];
result = [transaction commit];
if (!result) {[transaction rollback];NSLog(@"%@", [transaction error]);
}
  • WCTTransaction对象可以在类或函数间传递,因此这种方式也更具灵活性。

3. WCDB Swift版本

3.1 WCDB.swift安装

  • 安装要求:

Swift 4.0 及以上
Xcode 9.0 及以上

  • 腾讯官方文档:WCDB.swift安装与兼容性
  • pod 安装:
 pod 'WCDB.swift'

3.2 WCDB.swift使用

  • 模型绑定,直接用wcdb提供的模板
class Sample: TableCodable {var identifier: Int? = nilvar description: String? = nilenum CodingKeys: String, CodingTableKey {typealias Root = Samplestatic let objectRelationalMapping = TableBinding(CodingKeys.self)case identifiercase description}
}
  • 数据库创建以及操作单独写了个单例类 HMDataBaseManager.swift
import Foundation
import WCDBSwiftstruct HMDataBasePath {let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,.userDomainMask,true).last! + "/HMDB/HMDB.db"
}class HMDataBaseManager: NSObject {static let share = HMDataBaseManager()let dataBasePath = URL(fileURLWithPath: HMDataBasePath().dbPath)var dataBase: Database?private override init() {super.init()dataBase = createDb()}///创建dbprivate func createDb() -> Database {debugPrint("数据库路径==\(dataBasePath.absoluteString)")return Database(withFileURL: dataBasePath)}///创建表func createTable<T: TableDecodable>(table: String, of ttype:T.Type) -> Void {do {try dataBase?.create(table: table, of:ttype)} catch let error {debugPrint("create table error \(error.localizedDescription)")}}///插入func insertToDb<T: TableEncodable>(objects: [T] ,intoTable table: String) -> Void {do {try dataBase?.insert(objects: objects, intoTable: table)} catch let error {debugPrint(" insert obj error \(error.localizedDescription)")}}///修改func updateToDb<T: TableEncodable>(table: String, on propertys:[PropertyConvertible],with object:T,where condition: Condition? = nil) -> Void{do {try dataBase?.update(table: table, on: propertys, with: object,where: condition)} catch let error {debugPrint(" update obj error \(error.localizedDescription)")}}///删除func deleteFromDb(fromTable: String, where condition: Condition? = nil) -> Void {do {try dataBase?.delete(fromTable: fromTable, where:condition)} catch let error {debugPrint("delete error \(error.localizedDescription)")}}///查询func qureyFromDb<T: TableDecodable>(fromTable: String, cls cName: T.Type, where condition: Condition? = nil, orderBy orderList:[OrderBy]? = nil) -> [T]? {do {let allObjects: [T] = try (dataBase?.getObjects(fromTable: fromTable, where:condition, orderBy:orderList))!debugPrint("\(allObjects)");return allObjects} catch let error {debugPrint("no data find \(error.localizedDescription)")}return nil}///删除数据表func dropTable(table: String) -> Void {do {try dataBase?.drop(table: table)} catch let error {debugPrint("drop table error \(error)")}}/// 删除所有与该数据库相关的文件func removeDbFile() -> Void {do {try dataBase?.close(onClosed: {try dataBase?.removeFiles()})} catch let error {debugPrint("not close db \(error)")}}
}
  • 比较复杂的查询可以使用 prepareSelect 查询接口
//查询所有站点并按字母排序去重func qureyAllStations(cityId: Int) -> [StationModel]{var stationArray = [StationModel]()do {let selectPrep = try HMDataBaseManager.share.dataBase?.prepareSelect(on: StationModel.Properties.all, fromTable: String(describing: StationModel.self)).where(StationModel.Properties.cityid == cityId).group(by: StationModel.Properties.statid).order(by: StationModel.Properties.statpname.asOrder(by: .ascending))stationArray = try selectPrep?.allObjects() ?? []} catch let error {debugPrint("\(error)")}return stationArray}

4. 数据库从FMDB迁移到WCDB

4.1 为什么要迁移到WCDB?

  1. WCDB依托于微信上亿用户的实际场景,解决了许多在开发和线上遇到的共性问题,在性能、易用性、功能完整性以及兼容性上都有较好的表现。并且,开发者可以平滑地从FMDB升级到WCDB。
  2. WCDB有三大优势:更高效,更易用,功能更完整,具体优势比较上面已经对比过了。

4.1.1 WCDB 对比FMDB的优势一:高效

  • WCDB在并发、ORM以及SQLite源码都做了许多针对性的优化,使得在写入、多线程并发、初始化等方面比FMDB有30%-280%的性能提升。
    WCDB 对比FMDB的优势一:高效

4.1.2 WCDB 对比FMDB的优势一:易用

  • WCDB通过WINQ和ORM,使得从拼接SQL、获取数据、拼装Object的整个过程,只需要一行代码即可完成。

  • FMDB代码,实现一个查询需要一堆胶水代码:

/*FMDB Code*/
FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"];
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
while ([resultSet next]) {Message *message = [[Message alloc] init];message.localID = [resultSet intForColumnIndex:0];message.content = [resultSet stringForColumnIndex:1];message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];[messages addObject:message];
}
  • WCDB只需要一行代码:
/*WCDB Code*/
NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];

4.1.3 WCDB 对比FMDB的优势一:完整

  • FMDB只是简单对Sqlite3的封装,没有提供错误统计,性能统计,损坏修复,反注入,加密等功能。
  • 相反WCDB提供了一套完整的功能。

错误统计
性能统计
损坏修复
反注入
加密

4.2 FMDB迁移

4.2.1 安装

  • 首先在工程的配置Build Phases->Link Binary With Libraries中,将FMDB以及SQLite的库移出工程。
  • 然后参考安装教程选择适合方式链入WCDB的库。

4.2.2 创建数据库

  • WCTDatabase通过指定路径进行创建。同时,该接口会自动创建路径中未创建的目录。
NSString* path = @"intermediate/directory/will/be/created/automatically/wcdb";
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
  • 临时数据库可以创建在iOS/macOS的临时目录上。
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
  • WCDB暂不支持创建内存数据库。由于移动平台的磁盘介质大多为SSD,其性能与纯内存操作差别不大。同时内存数据库会占用大量内存,从而导致FOOM。

4.2.3 打开数据库

  • WCDB会在第一次访问数据库时,自动打开数据库,不需要开发者主动操作。
  • canOpen接口可用于测试数据库能否正常打开,isOpened接口可用于测试数据库是否已打开。
if (![wcdb canOpen]) {NSLog(@"open failed");
}if ([wcdb isOpened]) {NSLog(@"database is already opened");
}

4.2.4 建表与ORM

  • FMDB不支持ORM,而WCDB可以通过绑定类与表绑定起来,从而大幅度减少代码量。
  • 对于在FMDB已经定义的类:
//Message.h
@interface Message : NSObject@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;@end
  • 对于在FMDB已经定义的表:
FMDatabase* fmdb = [[FMDatabase alloc] initWithPath:path];
[fmdb executeUpdate:@"CREATE TABLE message(localID INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, createTime INTEGER, db_modifiedTime INTEGER)"];
[fmdb executeUpdate:@"CREATE INDEX message_index ON message(createTime)"];
  • 可以将其建模为:
//Message.h
@interface Message : NSObject <WCTTableCoding>@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)
WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)@end//Message.mm
@implementation MessageWCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE(Message, content)
WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime")WCDB_PRIMARY_AUTO_INCREMENT(Message, localID)
WCDB_INDEX(Message, "_index", createTime)@end

其中:

  1. WCDB_IMPLEMENTATION(className)用于定义进行绑定的类
  2. WCDB_PROPERTY(propertyName)WCDB_SYNTHESIZE(className, propertyName)用于声明和定义字段。
  3. WCDB_SYNTHESIZE(className, propertyName)默认使用属性名作为数据库表的字段名。对于属性名与字段名不同的情况,可以使用WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)进行映射。
    对于在FMDB已经创建的表,若属性名与字段名不同,则可以用WCDB_SYNTHESIZE_COLUMN宏进行映射,如例子中的db_modifiedTime字段
  4. WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)用于定义主键且自增。
  5. WCDB_INDEX(className, indexNameSubfix, propertyName)用于定义索引。
  • 定义完成后,调用createTableAndIndexesOfName:withClass:即可完成创建。
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
[wcdb createTableAndIndexesOfName:@"message" withClass:Message.class]

注:该接口使用的是IF NOT EXISTS的SQL,因此可以用重复调用。不需要在每次调用前判断表或索引是否已经存在。

4.2.5 数据库升级

createTableAndIndexesOfName:withClass:会根据ORM的定义,创建表或索引。
当定义发生变化时,该接口也会对应的增加字段或索引。
因此,该接口可用于数据库表的升级。

  • 定义模型
//Message.h
@interface Message : NSObject <WCTTableCoding>@property int localID;
@property(assign) const char *newContent;
//@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;
@property(retain) NSDate *newProperty;WCDB_PROPERTY(localID)
WCDB_PROPERTY(newContent)
//WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)
WCDB_PROPERTY(newProperty)@end//Message.mm
@implementation MessageWCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE_COLUMN(Message, newContent, "content")
//WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime")
WCDB_SYNTHESIZE(Message, newProperty)WCDB_PRIMARY_AUTO_INCREMENT(Message, localID)
WCDB_INDEX(Message, "_index", createTime)
WCDB_UNIQUE(Message, modifiedTime)
WCDB_INDEX(Message, "_newIndex", newProperty)@end
  • 新建表
WCTDatabase* db = [[WCTDatabase alloc] initWithPath:path];
[db createTableAndIndexesOfName:@"message" withClass:Message.class]
  • 删除字段

如例子中的createTime字段,删除字段只需直接将ORM中的定义删除即可。

注:由于SQLite不支持删除字段,因此该操作只是将对应字段忽略。

  • 增加字段

如例子中的newProperty字段,增加字段只需直接在ORM定义出添加,并再次调用createTableAndIndexesOfName:withClass:

  • 修改字段

如例子中的newContent字段,字段类型可以直接修改,但需要确保新类型与旧类型兼容;字段名称则需要通过WCDB_SYNTHESIZE_COLUMN(className, proeprtyName, columnName)重新映射到旧字段。

注:由于SQLite不支持修改字段名,因此该操作只是将新的属性映射到原来的字段名。

  • 增加约束

如例子中的WCDB_UNIQUE(Message, modifiedTime),新的约束只需直接在ORM中添加,并再次调用createTableAndIndexesOfName:withClass:

  • 增加索引

如例子中的WCDB_INDEX(Message, "_newIndex", newProperty),新的索引只需直接在ORM添加,并再次调用createTableAndIndexesOfName:withClass:

4.2.6 访问数据库

  • 得益于ORM的定义,开发者无需使用类似intForColumnIndex:的接口手动组装Object。以下是增删查改的代码示例。

4.2.6.1 查询

  • FMDB代码
/*FMDB Code*/
FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"];
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
while ([resultSet next]) {Message *message = [[Message alloc] init];message.localID = [resultSet intForColumnIndex:0];message.content = [resultSet stringForColumnIndex:1];message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];[messages addObject:message];
}
  • WCDB代码
NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];

4.2.6.2 插入

  • FMDB代码
/*FMDB Code*/
[fmdb executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)];
  • WCDB代码
[wcdb insertObject:message into:@"message"];

4.2.6.3 修改

  • FMDB代码
/*FMDB Code*/
[fmdb executeUpdate:@"UPDATE message SET modifiedTime=?", @(message.modifiedTime.timeIntervalSince1970)];
  • WCDB代码
[wcdb updateAllRowsInTable:@"message" onProperties:Message.modifiedTime withObject:message];

4.2.6.4 删除

  • FMDB代码
/*FMDB Code*/
[fmdb executeUpdate:@"DELETE FROM message"];
  • WCDB代码
[wcdb deleteAllObjects];

4.2.7 条件语句

  • WCDB通过WINQ完成条件语句,以减轻了拼装SQL的繁琐,并提供一系列优化和反注入等特性。
  • 以下是SQLWINQ之间转换的一些例子。
类型SQL示例WINQ示例
排序ORDER BY localID ASCMessage.localID.order(WCTOrderedAscending)
多字段排序ORDER BY localID ASC, content DESC{Message.localID.order(WCTOrderedAscending), Message.content.order(WCTOrderedDescending)}
聚合函数MAX(localID)Message.localID.max()
条件语句localID==2 AND content IS NOT NULLMessage.localID==2&&Message.content.isNotNull()
多个字段组合localID, content{Message.localID, Message.content}
*COUNT(*)Message.AnyProperty.count()
所有ORM定义的字段(localID, content, createTime, modifiedTime)Message.AllProperties
指定tablemyTable.localIDMessage.localID.inTable(“myTable”)

4.2.7.1 改写条件语句

  • 了解了WINQ,就可以完成更复杂的增删查改操作了。
4.2.7.1.1 部分查询
  • FMDB代码
/*FMDB Code*/
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
FMResultSet* resultSet = [fmdb executeQuery:@"SELECT localID, createTime FROM message WHERE localID>=1 OR modified!=createTime"];
while (resultSet && [resultSet next]) {Message *message = [[Message alloc] init];message.localID = [resultSet intForColumnIndex:0];message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];[messages addObject:message];
}
  • WCDB代码
NSArray *messages = [wcdb getObjectsOnResults:{Message.localID, Message.createTime} fromTable:@"message"where:Message.localID>1||Message.modifiedTime!=Message.createTime];
4.2.7.1.2 自增插入
  • FMDB代码
/*FMDB Code*/
[fmdb executeUpdate:@"INSERT INTO message(localID, content) VALUES(?, ?)", nil, message.content];
  • WCDB代码
message.isAutoIncrement = YES;
[wcdb insertObject:message onProperties:{Message.localID, Message.content} into:@"message"];
4.2.7.1.3 数值更新
  • FMDB代码
/*FMDB Code*/
[fmdb executeUpdate:@"UPDATE message SET modifiedTime=? WHERE localID==?", @([NSDate date].timeIntervalSince1970), @(1)];
  • WCDB代码
[wcdb updateRowsInTable:@"message" onProperty:Message.modifiedTime withValue:[NSDate date]where:Message.localID==1];
4.2.7.1.4 部分删除
  • FMDB代码
/*FMDB Code*/
[fmdb executeUpdate:@"DELETE FROM message WHERE localID>0 AND content IS NULL LIMIT ?", @(1)];
  • WCDB代码
[wcdb deleteObjectsFromTable:@"messsage" where:Message.localID>0&&Message.content!=nillimit:1];

4.2.8 特殊语句和核心层接口

  • WCDBObjC层接口封装了绝大部分场景下适用的增删查改语句。但SQL千变万化,接口层不可能覆盖全部场景。对于这种情况,可以通过WINQ的核心层接口进行调用。
  • 对于SQL:EXPLAIN QUERY PLAN CREATE TABLE message(localID INTEGER)
  1. 找到其对应的sql-stmt,然后通过以WCDB::Statement开头的类进行调用。如例子中,其对应的sql-stmt为WCDB::StatementExplain和WCDB::StatementCreateTable。
  2. 获取字段映射。对于已经定义ORM的字段,可以通过className.propertyName获取,如:Message.localID。对于未定义ORM的字段,可以通过WCDB::Column columnName(“columnName”)创建,如 WCDB::Column localID(“localID”).
  3. 根据Statement内的定义,按照与SQL同名的函数调用获得完整的WINQ语句。如例子中,其对应的WINQ语句为:
WCDB::ColumnDefList columnDefList = {WCTSampleORM.identifier.def(WCTColumnTypeInteger32, true)};
WCDB::StatementExplain statementExplain = WCDB::StatementExplain().explainQueryPlan(WCDB::StatementCreateTable().create("message", columnDefList));

4.2.8.1 执行WINQ

通过exec:执行WINQ statement

[wcdb exec:statement];

4.2.8.2 获取WINQ运行结果

通过prepare:运行WINQ statement,获得WCTStatement,并以此获取返回值。

WCTStatement *statement = [wcdb prepare:statementExplain];
if (statement && [statement step]) {for (int i = 0; i < [statement getCount]; ++i) {NSString *columnName = [statement getNameAtIndex:i];WCTValue *value = [statement getValueAtIndex:i];NSLog(@"%@:%@", columnName, value);}
}
  1. 通过getDescription()打印log,调试确保SQL正确
NSLog(@"SQL: %s", statementExplain.getDescription().c_str());

4.2.9 事务

  • WCDB的基础事务接口与FMDB的接口类似。

  • FMDB代码

/*FMDB Code*/
BOOL result = [fmdb beginTransaction];
if (!result) {//failed
}
//do sth...
if (![fmdb commit]) {//failed[fmdb rollback];
}
  • WCDB代码
/*WCDB Code*/
BOOL result = [wcdb beginTransaction];
if (!result) {//failed
}
//do sth...
if (![wcdb commitTransaction]) {[wcdb rollbackTransaction];
}

4.2.9.1 便捷事务接口

  • runTransaction:接口会在commit失败时自动rollback事务。开发者也可以在BLOCK结束时返回YESNO来决定commitrollback事务,以此减少代码量。
[wcdb runTransaction:^BOOL{//do sth...return result;//YES to commit transaction and NO to rollback transaction
}];

4.2.10 多重语句和批处理

  • WCDB不支持多重语句。多个语句需拆分单独写。

  • WCDB对于涉及批量操作的接口,都有内置的事务。如createTableAndIndexesOfName:withClass:insertObjects:into:等,这类接口通常不止执行一条SQL,因此WCDB会自动嵌入事务,以提高性能。

4.2.11 线程安全与并发

  • FMDB通过FMDatabasePool完成多线程任务。

  • 而对于WCDBWCTDatabaseWCTTableWCTTransaction的所有SQL操作接口都是线程安全,并且自动管理并发的。

  • WCDB的连接池会根据数据库访问所在的线程、是否处于事务、并发状态等,自动分发合适的SQLite连接进行操作,并在完成后回收以供下一次再利用。

  • 因此,开发者既不需要使用一个新的类来完成多线程任务,也不需要过多关注线程安全的问题。同时,还能获得更高的性能表现。

  • FMDB代码

/*FMDB Code*/
//thread-1 read
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{[fmdbPool inDatabase:^(FMDatabase *_Nonnull db) {NSMutableArray *messages = [[NSMutableArray alloc] init];FMResultSet *resultSet = [db executeQuery:@"SELECT * FROM message"];while ([resultSet next]) {Message *message = [[Message alloc] init];message.localID = [resultSet intForColumnIndex:0];message.content = [resultSet stringForColumnIndex:1];message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];[messages addObject:message];}//...}];
});
//thread-2 write
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{[fmdbPool inDatabase:^(FMDatabase *_Nonnull db) {[db beginTransaction]for (Message *message in messages) {[db executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)];}if (![db commit]) {[db rollback];}}];
});
  • WCDB代码
/*WCDB Code*/
//thread-1 read
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{NSArray *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];//...
});
//thread-2 write
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{[wcdb insertObjects:messages into:@"message"];
});

4.2.12 配置

  • 在使用数据库时,通常会对其设置一些默认的配置,如cache_sizejournal_mode等。

  • FMDB通过FMDatabasePoolDelegate进行配置,但其只能在SQLite Handle创建时进行配置。对于已经产生的SQLite handle,很难再次更改配置。

  • WCDB可以随时灵活地对其设置或变更。

  • FMDB代码

/*FMDB Code*/
- (BOOL)databasePool:(FMDatabasePool *)pool shouldAddDatabaseToPool:(FMDatabase *)database
{FMResultSet* resultSet = [database executeQuery:@"PRAGMA cache_size=-2000"];[result next];
}
  • WCDB代码
/*WCDB Code*/
[wcdb setConfig:^BOOL(std::shared_ptr<WCDB::Handle> handle, WCDB::Error &error) {return handle->exec(WCDB::StatementPragma().pragma(WCDB::Pragma::CacheSize, -2000));
} forName:@"CacheSizeConfig"]'

4.2.13 关闭数据库

  • 关闭数据库通常有两种场景:
  1. 数据库使用结束,回收对象。
  2. 数据库进行某些操作,需要临时关闭数据库。如移动、复制数据库文件。

4.2.13.1 回收对象

  • 对于这种情况,开发者无需手动操作。WCDB会自动管理这个过程。对于某一路径的数据库,WCDB会在所有对其的引用释放时,自动关闭数据库,并回收资源。
  • 对于iOS平台,当内存不足时,WCDB会自动关闭空闲的SQLite连接,以节省内存。开发者也可以手动调用[db purgeFreeHandles]对清理单个数据库的空闲SQLite连接。或调用[WCTDatabase PurgeFreeHandlesInAllDatabases]清理所有数据库的空闲SQLite连接。

4.2.13.2 手动关闭数据库

  • 无论是WCDB的多线程管理,还是FMDBFMDatabasePool,都存在多线程关闭数据库的问题。即,当一个线程希望关闭数据库时,另一个线程还在继续执行操作。

  • 而某些特殊的操作需要确保数据库完全关闭,例如移动、重命名、删除数据库等文件层面的操作。

  • 例如,若在A线程进行插入操作的执行过程中,B线程尝试复制数据库,则复制后的新数据库很可能是一个损坏的数据库。

  • 因此,WCDB提供了close:接口确保完全关闭数据库,并阻塞其他线程的访问。

[wcdb close:^(){//do something on this closed database}];

4.2.14 隔离Objective-C++代码

  • WCDB基于WINQ,引入了Objective-C++代码,因此对于所有引入WCDB的源文件,都需要将其后缀.m改为.mm。为减少影响范围,可以通过Objective-Ccategory特性将其隔离,达到只在model层使用Objective-C++编译,而不影响controllerview
  • 对于已有类WCTSampleAdvance
//WCTSampleAdvance.h
#import <Foundation/Foundation.h>
#import "WCTSampleColumnCoding.h"@interface WCTSampleAdvance : NSObject@property(nonatomic, assign) int intValue;
@property(nonatomic, retain) WCTSampleColumnCoding *columnCoding;@end//WCTSampleAdvance.mm
@implementation WCTSampleAdvance@end
  • 可以创建WCTSampleAdvance (WCTTableCoding)专门用于定义ORM
  • 为简化定义代码,WCDB同样提供了文件模版.

4.2.14.1 WCTTableCoding文件模版

  • 为了简化定义,WCDB同样提供了Xcode文件模版来创建WCTTableCodingcategory
  1. 首先需要安装文件模版。

安装脚本集成在WCDB的编译脚本中,只需编译一次WCDB,就会自动安装文件模版。
也可以手动运行cd path-to-your-wcdb-dir/objc/templates; sh install.sh;手动安装 文件模版。

  1. 安装完成后重启Xcode,选择新建文件,滚到窗口底部,即可看到对应的文件模版.

  2. 选择WCTTableCoding ,输入需要实现WCTTableCoding的类

  3. 这里以WCTSampleAdvance为例,Xcode会自动创建WCTSampleAdvance+WCTTableCoding.h文件模版:

#import "WCTSampleAdvance.h"
#import <WCDB/WCDB.h>@interface WCTSampleAdvance (WCTTableCoding) <WCTTableCoding>WCDB_PROPERTY(<#property1 #>)
WCDB_PROPERTY(<#property2 #>)
WCDB_PROPERTY(<#property3 #>)
WCDB_PROPERTY(<#property4 #>)
WCDB_PROPERTY(<#... #>)@end
  1. 加上类的ORM实现即可。
//WCTSampleAdvance.h
#import <Foundation/Foundation.h>
#import "WCTSampleColumnCoding.h"@interface WCTSampleAdvance : NSObject@property(nonatomic, assign) int intValue;
@property(nonatomic, retain) WCTSampleColumnCoding *columnCoding;@end//WCTSampleAdvance.mm
@implementation WCTSampleAdvanceWCDB_IMPLEMENTATION(WCTSampleAdvance)
WCDB_SYNTHESIZE(WCTSampleAdvance, intValue)
WCDB_SYNTHESIZE(WCTSampleAdvance, columnCoding)WCDB_PRIMARY_ASC_AUTO_INCREMENT(WCTSampleAdvance, intValue)@end//WCTSampleAdvance+WCTTableCoding.h
#import "WCTSampleAdvance.h"
#import <WCDB/WCDB.h>@interface WCTSampleAdvance (WCTTableCoding) <WCTTableCoding>WCDB_PROPERTY(intValue)
WCDB_PROPERTY(columnCoding)@end
  1. 此时,原来的WCTSampleAdvance.h中不包含任何C++的代码。因此,其他文件对其引用时,不需要修改文件名后缀。只有Model层需要使用WCDB接口的类,才需要包含WCTSampleAdvance+WCTTableCoding.h,并修改文件名后缀为.mm。

http://chatgpt.dhexx.cn/article/CMYm1WnW.shtml

相关文章

iOS开发 数据存储之WCDB的介绍

一.介绍 WCDB是一个高效、完整、易用的移动数据库框架,基于SQLCipher,支持iOS,macOS和Android 二.基本特性 易用,WCDB支持一句代码即可将数据取出并组合为object WINQ(WCDB语言集成查询):通过WINQ,开发者无须为了拼接SQL的字符串而写一大坨胶水代码ORM(Object Relational Ma…

开源微信小程序自助建站系统源码 含精美的多行业模板和搭建教程

分享一个微信小程序自助建站系统源码&#xff0c;含各行各业的小程序模板和搭建教程&#xff0c;可一键切换模板&#xff0c;自由DIY&#xff0c;搭建属于你自己的小程序。 特色功能一览&#xff1a; 11、支持创建多个小程序&#xff01;&#xff08;没有数量限制&#xff0c;后…

强大易用的开源建站工具Halo

最近无意间看到别人的博客外观非常美观&#xff0c;便萌生了偷师学艺的想法…所以就看到看了Halo这个开源的建站项目,其实使用起来非常简单&#xff0c;但是想要做一个类似的开源建站工具&#xff0c;谈何容易 访问官网 https://halo.run/ 使用docker部署 搜索镜像halo do…

14个免费好用的建站工具

有时候&#xff0c;我们想建立一个独立网站的时候&#xff0c;苦于自己技能不够&#xff0c;而迟迟没有行动&#xff0c;其实&#xff0c;我们真正的去构建一个独立网站的时候&#xff0c;我们并不需要多复杂的技术。我们也不一定要成为非常专业的程序员&#xff0c;因为现在&a…

推荐一款免费开源的建站系统 - AnqiCMS

安企内容管理系统(AnqiCMS)&#xff0c;是一款使用 GoLang 开发的企业站内容管理系统&#xff0c;它部署简单&#xff0c;软件安全&#xff0c;界面优雅&#xff0c;小巧&#xff0c;执行速度飞快&#xff0c;使用 AnqiCMS 搭建的网站可以防止众多安全问题发生。AnqiCMS 的设计…

介绍一款开源、高性价比的在线教育建站系统

今天给大家介绍一款开源在线教育建站系统——edusoho&#xff0c;项目是用PHP开发&#xff0c;所以基本上会搭建php站点就可以完成本次的搭建。 先看看安装之后的登录界面。 去官网下载源码 笔者下载企培开源版&#xff1a;edusoho-ct-21.4.5.zip 系统说明 1.系统&#xff1…

国内好用的五款开源建站系统

推荐5款优秀的开源建站系统,都有免费版本,有需要可以去试试。蝉知 蝉知系统是一款开源的的企业营销自助建站系统。它专为企业营销设计,伪静态网址、关键词、语义化结构,内置流量统计。 蝉知功能全面,文章发布、会员管理、论坛评论、产品展示等,并内置商城系统,商品、订…

免费开源的建站程序大全,不会编程也可以自助搭建网站了哦

想建网站又不会编程的小伙伴有福啦&#xff0c;本期推荐一些开源的cms建站程序&#xff0c;不需要写后端的任何逻辑代码&#xff0c;轻轻松松就可以建立自己的网站了&#xff0c;当然&#xff0c;要想网站有自己的个性&#xff0c;模版还是需要自己写的&#xff0c;只需要会简单…

绝了!小说建站项目完整开源

超棒的开源小说文学建站 CMS 系统&#xff0c;作为面试项目有牌面儿&#xff01; 编程导航开源仓库&#xff1a;https://github.com/liyupi/code-nav 大家好&#xff0c;我是鱼皮&#xff0c;今天给大家推荐一个优秀的开源 Java 全栈项目。 小说精品屋&#xff0c;是一套非常完…

最新首发自助建站系统源码,傻瓜式一键建站系统源码,高度开源支持专业在线自助建站服务平台软件

一佰互联,巅云门户自助建站系统v8建站平台版&#xff0c;历经3年不断打磨终于上线了。专业PS级大师级高端响应式智能建站平台软件&#xff0c;只为网络公司而生&#xff0c;采用国内知名开源php框架,Thinkphp6vue.js前端数据响应系统,实现了在线自助开通网站&#xff0c;企业站…

四大免费开源建站系统

原文&#xff1a;四大免费开源建站系统 - 知乎 第一&#xff1a; WordPress WordPress的主流客户是企业/个人的官网。一家公司不一定会在网上卖东西&#xff0c;但一定会需要一个官网。用WordPress做官网可谓是性价比最优选择。如果没有预算&#xff0c;你可以自己买几十美金的…

有哪些免费好用的开源建站程序/系统,推荐下?

我推荐WordPress建站程序&#xff0c;学习入门门槛很低&#xff0c;全世界近三分之一的网站都是采用wordpress,所以没有理由不去学习它。 我从一名小白变为wordpress建站老手&#xff0c;对比过很多建站程序&#xff0c;还是觉得wordpress能帮我节省时间和精力&#xff0c;让我…

开源自助建站系统源码完整源码+搭建教程 傻瓜式一键建站系统源码

一键傻瓜式自助建站系统源码&#xff0c;目前包含七百多套完整网站模板&#xff0c;全部都是响应式网站模板&#xff0c;傻瓜一键自助建站。开发组合PHPmysql&#xff0c;功能强大。 一键自助建站系统源码带安装教程&#xff0c;源码下载&#xff1a;春哥技术博客获取。自助建站…

五款开源免费的建站系统推荐

最近研究了下开源的建站系统&#xff0c;推荐5款国内的吧&#xff0c;都有免费版本&#xff0c;有需要可以去试试。 ECTouch ECTouch是一款开源免费的移动商城网店系统。能够帮助企业和个人快速构建手机移动商城并减少二次开发带来的成本。 ECTouch采用PHPMYSQL方式运行&…

Excel如何按照指定顺序排列

需求&#xff1a;要求按照指定的code出相应的value值&#xff0c;后台可能查出来的code对应的值无法与给定的顺序匹配上&#xff0c;当然可以用代码解决&#xff0c;但是如果想偷懒可以直接用Excel强大的自定义排序解决。 Step1&#xff1a;在Excel中自定义排序&#xff1a;点击…

(19)写一个函数,用”起泡法“对输入的10数字符按由小到大顺序排列

用”起泡法“对输入的10数字符按由小到大顺序排列 起泡法&#xff1a;即每一次将相邻两个数进行比较&#xff0c;若符合条件则交换两个数的值。每进行一趟排序&#xff0c;最大的一个数变成最后一个数。以此类推&#xff0c;直至整个数组按照由小到大排列。 举例&#xff1a;…

G2Plot 折线图表错乱,不按顺序排列

问题描述&#xff1a; 问题&#xff1a;G2Plot 折线图表错乱&#xff0c;不按顺序排列 BUG如图&#xff1a; 原因分析&#xff1a; 问题分析&#xff1a; 在仔细检查了参数值时&#xff0c;分析没有问题的图表的 value 值类型为Number&#xff0c;出问题的是String类型。 有…

输入五个整数,将他们从大到小顺序排列

思路: 将5个整数做成列表,运用 sorted函数将列表元素的值从小到大排序,然后利用切片反转列表元素. a int(input(">> ")) b int(input(">> ")) c int(input(">> ")) d int(input(">> ")) e int(input(&…

matplotlib中柱状图Y轴坐标不按顺序排列问题

背景&#xff1a; 昨天偶然做一个爬取数据&#xff0c;做成直方图的小练习发现&#xff0c;最后出来的图标Y轴并不是按顺序排列&#xff0c;按照老规矩&#xff0c;百度一下&#xff0c;找到了原因&#xff0c;一个低级的问题 import matplotlib.pyplot as plt from matplotl…

R语言ggplot画条形图按照想要的顺序排列

加载所需要的包 library(ggplot2) library(dplyr)创建数据集 Year <- c(rep("2012" , 3) , rep("2013" , 3) , rep("2014" , 3) , rep("2015" , 3) ) Legend <- rep(c("A" , "X" , "E") , 4) C…