FMDB源码阅读(二)

Wesley13
• 阅读 628

随笔 - 31 文章 - 0 评论 - 101 【原】FMDB源码阅读(二)

阅读目录

  1. 前言
  2. executeUpdate:系列函数
  3. executeStatements:系列函数
  4. executeQueryWithFormat:和executeUpdateWithFormat:函数
    • (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt
  5. openWithFlags:系列函数
  6. FMResultSet其他的获取结果方式
  7. FMDB的加解密
  8. 参考文章 【原】FMDB源码阅读(二) 本文转载请注明出处 —— polobymulberry-博客园

回到顶部

  1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比如FMDB的executeUpdate:系列方法、数据库的加解密等等。这次写的就是对FMDatabase和FMResultSet这两个文件的补全内容。每次写这种补全的内容最头疼,内容会很分散,感觉没啥条理。

回到顶部 2. executeUpdate:系列函数 注意除了“SELECT”语句外,其他的SQL语句都需要使用executeUpdate:系列函数,这些SQL语句包括CREATE, UPDATE, INSERT, ALTER, COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM, 和REPLACE等等。

executeUpdate:函数使用例子如下:

BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", @(identifier), name, date, comment ?: [NSNull null]]; if (!success) { NSLog(@"error = %@", [db lastErrorMessage]); } 基本上所有executeUpdate:系列函数都是对- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函数的封装。注意- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函数的具体实现,基本和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]大部分实现是差不多的,关键在于executeQuery是查询语句,所以它需要FMResultSet来保存查询的结果。而executeUpdate是非查询语句,不需要保存查询结果,但需要调用sqlite3_step(pStmt)来执行该SQL语句。这里就不赘述了,详见源码。

回到顶部 3. executeStatements:系列函数 使用executeStatements:函数可以将多个SQL执行语句写在一个字符串中,并执行。具体使用举例如下:

复制代码 NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);" "create table bulktest2 (id integer primary key autoincrement, y text);" "create table bulktest3 (id integer primary key autoincrement, z text);" "insert into bulktest1 (x) values ('XXX');" "insert into bulktest2 (y) values ('YYY');" "insert into bulktest3 (z) values ('ZZZ');";

success = [db executeStatements:sql];

sql = @"select count() as count from bulktest1;" "select count() as count from bulktest2;" "select count(*) as count from bulktest3;";

success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) { NSInteger count = [dictionary[@"count"] integerValue]; XCTAssertEqual(count, 1, @"expected one record for dictionary %@", dictionary); return 0; }]; 复制代码 基本上executeStatements:系列函数最终封装的都是- [FMDatabase executeStatements:withResultBlock:]函数,而此函数又是对sqlite3_exec函数的封装。

sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)

该例程提供了一个执行 SQL 命令的快捷方式,SQL 命令由 sql 参数提供,可以由多个 SQL 命令组成。

在这里,第一个参数 sqlite3 是打开的数据库对象,sqlite_callback 是一个回调,data 作为其第一个参数,errmsg 将被返回用来获取程序生成的任何错误。

sqlite3_exec() 程序解析并执行由 sql 参数所给的每个命令,直到字符串结束或者遇到错误为止。

executeStatements:源码如下(很简单,就不赘述了):

复制代码

  • (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block {

    int rc; char *errmsg = nil;

    rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg);

    if (errmsg && [self logsErrors]) { NSLog(@"Error inserting batch: %s", errmsg); sqlite3_free(errmsg); }

    return (rc == SQLITE_OK); } 复制代码 回到顶部

  1. executeQueryWithFormat:和executeUpdateWithFormat:函数 考虑到如果用户直接调用printf那种形式的字符串(比如“ INSERT INTO myTable (%@) VALUES (%d)”, “age”,25),那么就需要自己将对应字符串处理成相应的SQL语句。恰好executeQuery和executeUpdate系列函数提供了相应的接口:
  • (FMResultSet )executeQueryWithFormat:(NSString)format, ... NS_FORMAT_FUNCTION(1,2);
  • (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); 其实这两个函数和其他executeQuery和executeUpdate系列方法,多的就是一个将format和…转化为可用的SQL语句步骤。其它部分其实本质还是调用- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]。下面仅列出format和…的转化代码:

复制代码 va_list args; // 将args指向format中第一个参数 va_start(args, format);

NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; NSMutableArray *arguments = [NSMutableArray array];

// 使用extractSQL函数将format和args转化为sql和arguments供后面函数使用 [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; // 关闭args,与va_start成对出现 va_end(args); 复制代码 至于extractSQL:这个函数其实就是将(“INSERT INTO myTable (%@) VALUES (%d)”, “age”,25)中的%s和%d这种符号变成”?”,然后将”age”和25加入到arguments中。具体实现如下:

复制代码

  • (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {

    NSUInteger length = [sql length]; unichar last = '\0'; for (NSUInteger i = 0; i < length; ++i) { id arg = nil; /** 使用last和current两个变量(有些还需要next变量,比如%llu)判断当前扫描到的字符串是不是%@、 %c、%s、%d等等。举个例子,如果碰到%s,那么说明我替换的参数其实是一个字符串,所以使用arg = [NSString stringWithUTF8String:]获取到相应的arg作为参数值,至于%s/%c/%llu这些表示什么, 那就属于C语言的范畴,此处就不讨论了。 / // 注意type va_arg(va_list arg_ptr,type)函数是根据传入的type参数决定返回值类型的 // 另外它的作用是获取下一个参数的地址 unichar current = [sql characterAtIndex:i]; unichar add = current; if (last == '%') { switch (current) { case '@': arg = va_arg(args, id); break; case 'c': // warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int' arg = [NSString stringWithFormat:@"%c", va_arg(args, int)]; break; case 's': arg = [NSString stringWithUTF8String:va_arg(args, char)]; break; case 'd': case 'D': case 'i': arg = [NSNumber numberWithInt:va_arg(args, int)]; break; case 'u': case 'U': arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)]; break; // %hi表示short int,%hu表示short unsigned int case 'h': i++; if (i < length && [sql characterAtIndex:i] == 'i') { // warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int' arg = [NSNumber numberWithShort:(short)(va_arg(args, int))]; } else if (i < length && [sql characterAtIndex:i] == 'u') { // warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int' arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))]; } else { i--; } break; // %qi表示long long,%qu表示unsigned long long case 'q': i++; if (i < length && [sql characterAtIndex:i] == 'i') { arg = [NSNumber numberWithLongLong:va_arg(args, long long)]; } else if (i < length && [sql characterAtIndex:i] == 'u') { arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)]; } else { i--; } break; case 'f': arg = [NSNumber numberWithDouble:va_arg(args, double)]; break; // %g原本是根据数据选择合适的方式输出(浮点数还是科学计数法),不过此处是用float类型输出 case 'g': // warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))]; break; case 'l': i++; if (i < length) { unichar next = [sql characterAtIndex:i]; if (next == 'l') { i++; if (i < length && [sql characterAtIndex:i] == 'd') { //%lld arg = [NSNumber numberWithLongLong:va_arg(args, long long)]; } else if (i < length && [sql characterAtIndex:i] == 'u') { //%llu arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)]; } else { i--; } } else if (next == 'd') { //%ld arg = [NSNumber numberWithLong:va_arg(args, long)]; } else if (next == 'u') { //%lu arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)]; } else { i--; } } else { i--; } break; default: // something else that we can't interpret. just pass it on through like normal break; } } else if (current == '%') { // 遇到%,直接跳过。 add = '\0'; } // 如果arg不为空,表示确定arg是参数,那么就使用?替换它,并将其对应参数值arg添加到arguments if (arg != nil) { [cleanedSQL appendString:@"?"]; [arguments addObject:arg]; } // 如果参数格式是%@,但此时arg是空,那么就替换为NULL else if (add == (unichar)'@' && last == (unichar) '%') { [cleanedSQL appendFormat:@"NULL"]; } // 如果不是参数,就用原先字符串替换 else if (add != '\0') { [cleanedSQL appendFormat:@"%C", add]; } last = current; } } 复制代码 回到顶部

    • (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt 上一篇仅仅对该函数进行简单说明,该函数是用来在pStmt中绑定参数值到指定(根据idx)参数上。具体封装的是sqlite3_bind*系列函数。

如果要使用sqlite3_bind_系列函数,需要指定三个参数,一个是正在使用的sqlite_stmt对象,一个是参数索引idx,还有一个就是需要绑定的参数值,此函数解决的关键就是根据obj判断出其类型,然后调用相关的sqlite3_bind_函数,比如obj是int型,那么就调用sqlite3_bind_int函数。又或者obj是NSData类型,那么就调用sqlite_bind_blob函数。具体后面详细解释。

复制代码

  • (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt { // 如果obj为指针为空,那么就使用sqlite3_bind_null给该参数绑定SQL null。 if ((!obj) || ((NSNull *)obj == [NSNull null])) { sqlite3_bind_null(pStmt, idx); }

    // FIXME - someday check the return codes on these binds. else if ([obj isKindOfClass:[NSData class]]) { const void *bytes = [obj bytes]; if (!bytes) { // 如果obj是一个空的NSData对象 // 不要直接将NULL指针作为参数值,否则sqlite会绑定一个NULL指针给参数,而不是一个blob对象(Binary Large Object) bytes = ""; } // SQLITE_STATIC表示传过来参数值的指针是不变的,所以完事后不需要销毁它,与其相对的是 SQLITE_TRANSIENT sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC); } // 如果obj是一个NSDate对象 else if ([obj isKindOfClass:[NSDate class]]) { // 如果你自定义了Date格式,那么就将该NSDate转化为你定义的格式,并绑定到参数上 // 如果没有自定义Date格式,那么默认使用timeIntervalSince1970来计算参数值进行绑定 if (self.hasDateFormatter) sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC); else sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]); } // 如果是NSNumber对象,注意此处判断obj类型的方法 // @encode,@编译器指令之一,返回一个给定类型编码为一种内部表示的字符串(例如,@encode(int) → i),类似于 ANSI C 的 typeof 操作。苹果的 Objective-C 运行时库内部利用类型编码帮助加快消息分发。 else if ([obj isKindOfClass:[NSNumber class]]) {

      if (strcmp([obj objCType], @encode(char)) == 0) {
          sqlite3_bind_int(pStmt, idx, [obj charValue]);
      }
      else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
          sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
      }
      else if (strcmp([obj objCType], @encode(short)) == 0) {
          sqlite3_bind_int(pStmt, idx, [obj shortValue]);
      }
      else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
          sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
      }
      else if (strcmp([obj objCType], @encode(int)) == 0) {
          sqlite3_bind_int(pStmt, idx, [obj intValue]);
      }
      else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
          sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
      }
      else if (strcmp([obj objCType], @encode(long)) == 0) {
          sqlite3_bind_int64(pStmt, idx, [obj longValue]);
      }
      else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
          sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
      }
      else if (strcmp([obj objCType], @encode(long long)) == 0) {
          sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
      }
      else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) {
          sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
      }
      else if (strcmp([obj objCType], @encode(float)) == 0) {
          sqlite3_bind_double(pStmt, idx, [obj floatValue]);
      }
      else if (strcmp([obj objCType], @encode(double)) == 0) {
          sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
      }
      else if (strcmp([obj objCType], @encode(BOOL)) == 0) { // bool使用sqlite3_bind_int来绑定的
          sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
      }
      else {
          sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
      }
    

    } else { sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); } } 复制代码 回到顶部

  1. openWithFlags:系列函数 除了前面提到过的open函数外,FMDB还为我们提供了openWithFlags:系列函数,其本质是封装了sqlite3_open_v2。

int sqlite3_open_v2(
const char filename, / 数据库名称 (UTF-8) */ sqlite3 *ppDb, / 输出: SQLite数据库对象 / int flags, / 标识符 */ const char zVfs / 想要使用的VFS名称 */ ); 对于sqlite3_open和sqlite3_open16函数,如果可能将以可读可写的方式打开数据库,否则以只读的方式打开数据库。如果要打开的数据库文件不存在,就新建一个。对于sqlite3_open_v2函数,情况就要复杂一些了,因为这个v2版本的函数强大就强大在它可以对打开(连接)数据库的方式进行控制,具体是通过它的参数flags来完成。sqlite3_open_v2函数只支持UTF-8编码的SQlite3数据库文件。

如flags设置为SQLITE_OPEN_READONLY,则SQlite3数据库文件以只读的方式打开,如果该数据库文件不存在,则sqlite3_open_v2函数执行失败,返回一个error。如果flags设置为SQLITE_OPEN_READWRITE,则SQlite3数据库文件以可读可写的方式打开,如果该数据库文件本身被操作系统设置为写保护状态,则以只读的方式打开。如果该数据库文件不存在,则sqlite3_open_v2函数执行失败,返回一个error。如果flags设置为SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,则SQlite3数据库文件以可读可写的方式打开,如果该数据库文件不存在则新建一个。这也是sqlite3_open和sqlite3_open16函数的默认行为。除此之外,flags还可以设置为其他标志,具体可以查看SQlite官方文档。

参数zVfs允许客户应用程序命名一个虚拟文件系统(Virtual File System)模块,用来与数据库连接。VFS作为SQlite library和底层存储系统(如某个文件系统)之间的一个抽象层,通常客户应用程序可以简单的给该参数传递一个NULL指针,以使用默认的VFS模块。

对于UTF-8编码的SQlite3数据库文件,推荐使用sqlite3_open_v2函数进行连接,它可以对数据库文件的打开和处理操作进行更多的控制。

回到顶部 7. FMResultSet其他的获取结果方式 之前只提到过FMResultSet的resultSetWithStatement:、close、next函数。其实FMResultSet除了使用next获取查询结果外,还有很多其他的接口可以查询到结果。

一系列的_ForColumn:和_ForColumnIndex:(_表示对应的数据类型)函数都是用来获取查询结果的。这里值得注意的是_ForColumn:函数本质是调用相应的*ForColumnIndex:函数。比如:

  • (int)intForColumn:(NSString*)columnName { return [self intForColumnIndex:[self columnIndexForName:columnName]]; } 上述函数实现内部做了一个转化,就是利用columIndexForName:函数查询到这个columnName对应的索引值。而这个columnIndexForName:本质是根据_columnNameToIndexMap属性获取到列名称(columnName)的对应列号(columnIdx)。_columnNameToIndexMap是一个NSMutableDictionary对象。其中key表示的是指定结果集中对应列的名称,value表示的是指定结果集中对应的列号(columnIdx)。所以我们这里主要看下columnNameToIndexMap的实现:

复制代码

  • (NSMutableDictionary _)columnNameToIndexMap { if (!_columnNameToIndexMap) { // 找出由statement指定的结果集中列的数目 int columnCount = sqlite3_column_count([_statement statement]); _columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount]; int columnIdx = 0; // 将列号和该列对应名称绑定在一起,组成_columnNameToIndexMap for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { [_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx] forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]]; } } return _columnNameToIndexMap; } 复制代码 这时我们再回头看看_ForColumnIndex:函数的实现。它的本质就是调用sqlite3_column_*(*表示对应的数据类型),也就是从statement中获取到对应列号的数据,比如

  • (int)intForColumnIndex:(int)columnIdx { return sqlite3_column_int([_statement statement], columnIdx); } 回到顶部

  1. FMDB的加解密 FMDB中使用- [FMDatabase setKey:]和- [FMDatabase setKeyWithData:]输入数据库密码以求验证用户身份,使用- [FMDatabase rekey:]和- [FMDatabase rekeyWithData:]来给数据库设置密码或者清除密码。这两类函数分别对sqlite3_key和sqlite3_rekey函数进行了封装。

int sqlite3_key( sqlite3 *db, const void *pKey, int nKey)

db 是指定数据库,pKey 是密钥,nKey 是密钥长度。例:sqlite3_key( db, "abc", 3);

sqlite3_key是输入密钥,如果数据库已加密必须先执行此函数并输入正确密钥才能进行操作,如果数据库没有加密,执行此函数后进行数据库操作反而会出现“此数据库已加密或不是一个数据库文件”的错误。

int sqlite3_rekey( sqlite3 *db, const void *pKey, int nKey)

参数同sqlite3_key。 sqlite3_rekey是变更密钥或给没有加密的数据库添加密钥或清空密钥,变更密钥或清空密钥前必须先正确执行 sqlite3_key。在正确执行 sqlite3_rekey 之后在 sqlite3_close 关闭数据库之前可以正常操作数据库,不需要再执行 sqlite3_key。 清空密钥为 sqlite3_rekey( db, NULL, 0)。

复制代码 // 下面的代码比较简单,就不过多解释了。核心就是sqlite3_key和sqlite3_rekey这两个函数 // 使用rekey:和setKey:之前先要将对应NSString对象转化为NSData数据

  • (BOOL)rekey:(NSString*)key { NSData *keyData = [NSData dataWithBytes:(void *)[key UTF8String] length:(NSUInteger)strlen([key UTF8String])];

    return [self rekeyWithData:keyData]; }

  • (BOOL)rekeyWithData:(NSData *)keyData { #ifdef SQLITE_HAS_CODEC if (!keyData) { return NO; }

    int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]);

    if (rc != SQLITE_OK) { NSLog(@"error on rekey: %d", rc); NSLog(@"%@", [self lastErrorMessage]); }

    return (rc == SQLITE_OK); #else #pragma unused(keyData) return NO; #endif }

  • (BOOL)setKey:(NSString*)key { NSData *keyData = [NSData dataWithBytes:[key UTF8String] length:(NSUInteger)strlen([key UTF8String])];

    return [self setKeyWithData:keyData]; }

  • (BOOL)setKeyWithData:(NSData *)keyData { #ifdef SQLITE_HAS_CODEC if (!keyData) { return NO; }

    int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]);

    return (rc == SQLITE_OK); #else #pragma unused(keyData) return NO; #endif }

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这