FMDB源码阅读(—)

Wesley13
• 阅读 575

阅读目录

  1. 前言
  2. FMDB的最基本流程(结合上面例子)
  3. 总结
  4. 参考文章 【原】FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园

回到顶部

  1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于FMDB组件我是一点都没用过。好在FMDB源码中的main.m文件提供了大量的示例,况且网上也有很多最佳实践的例子,我就不在这献丑了。我们先从一个最简单的FMDB的例子开始:

复制代码 // 找到用户目录下的Documents文件夹位置 NSString* docsdir = [NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; // 将user.sqlite放到Documents文件夹下,并生成user.sqlite的绝对路径 NSString* dbpath = [docsdir stringByAppendingPathComponent:@"user.sqlite"]; // 根据user.sqlite的绝对路径获取到一个FMDatabase对象,其实就是一个封装了的SQLite数据库对象 FMDatabase* db = [FMDatabase databaseWithPath:dbpath]; // 打开该数据库 [db open]; // 执行SQL语句 - select * from people FMResultSet *rs = [db executeQuery :@"select * from people"]; // 利用next函数,循环输出结果 while ([rs next]) { NSLog(@"%@ %@", [rs stringForColumn:@"firstname"], [rs stringForColumn:@"lastname"]); } // 关闭该数据库 [db close]; 复制代码 很简单是吧,甚至我觉得上面我写的注释都多余了。确实,FMDB说白了就是对SQLite数据库的C/C++接口进行了一层封装,当然功能也更为强大,比如多线程操作,另外FMDB接口要比原生的SQLite接口简洁很多。下面我们就上面的例子研究下FMDB的基本流程。

回到顶部 2. FMDB的最基本流程(结合上面例子) 我们先看看上面代码中我用蓝色粗体高亮的部分,研究下其具体实现。

2.1 + [FMDatabase databaseWithPath:] 复制代码 // 核心其实还是调用了+[FMDataBase initWithPath:]函数,下面会详解

  • (instancetype)databaseWithPath:(NSString*)aPath { // FMDBReturnAutoReleased是为了让FMDB兼容MRC和ARC,具体细节看下其宏定义就明白了 return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]); }

/** 初始化一个FMDataBase对象 根据path(aPath)来创建一个SQLite数据库。对应的aPath参数有三种情形:

  1. 数据库文件路径:不为空字符串,不为nil。如果该文件路径不存在,那么SQLite会给你新建一个
  2. 空字符串@"":将在外存临时给你创建一个空的数据库,并且如果该数据库连接释放,那么对应数据库会自动删除
  3. nil:会在内存中创建数据库,随着该数据库连接的释放,也会释放该数据库。 */
  • (instancetype)initWithPath:(NSString*)aPath { // SQLite支持三种线程模式,sqlite3_threadsafe()函数的返回值可以确定编译时指定的线程模式。 // 三种模式分别为1.单线程模式 2.多线程模式 3.串行模式 其中对于单线程模式,sqlite3_threadsafe()返回false // 对于另外两个模式,则返回true。这是因为单线程模式下没有进行互斥(mutex),所以多线程下是不安全的 assert(sqlite3_threadsafe());
    self = [super init]; // 很多属性后面再提。不过这里值得注意的是_db居然赋值为nil,也就是说真正构建_db不是在initWithPath:这个函数中,这里透露下,其实作者是将构建部分代码放到了open函数中if (self) { _databasePath = [aPath copy]; _openResultSets = [[NSMutableSet alloc] init]; _db = nil; _logsErrors = YES; _crashOnErrors = NO; _maxBusyRetryTimeInterval = 2; }

    return self; } 复制代码 2.2 - [FMDatabase open] 上面提到过+ [FMDatabase databaseWithPath:]和- [FMDatabase initWithPath:]本质上只是给了数据库一个名字,并没有真实创建或者获取数据库。这里的open函数才是真正获取到数据库,其本质上也就是调用SQLite的C/C++接口 – sqlite3_open()。

sqlite3_open(const char *filename, sqlite3 **ppDb)

该例程打开一个指向 SQLite 数据库文件的连接,返回一个用于其他 SQLite 程序的数据库连接对象。

如果 filename 参数是 NULL 或 ':memory:',那么 sqlite3_open() 将会在 RAM 中创建一个内存数据库,这只会在 session 的有效时间内持续。

如果文件名 filename 不为 NULL,那么 sqlite3_open() 将使用这个参数值尝试打开数据库文件。如果该名称的文件不存在,sqlite3_open() 将创建一个新的命名为该名称的数据库文件并打开。

复制代码

  • (BOOL)open { if (_db) { return YES; }

    int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db ); if(err != SQLITE_OK) { NSLog(@"error opening!: %d", err); return NO; } // 若_maxBusyRetryTimeInterval大于0,那么就调用setMaxBusyRetryTimeInterval:函数 // setMaxBusyRetryTimeInterval:函数主要是调用sqlite3_busy_handler来处理其他线程已经在操作数据库的情况,默认_maxBusyRetryTimeInterval为2。 // 具体该参数有什么用,下面在FMDBDatabaseBusyHandler函数中会详解。 if (_maxBusyRetryTimeInterval > 0.0) { [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; }

    return YES; }

  • (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {

    _maxBusyRetryTimeInterval = timeout;

    if (!_db) { return; } // 处理的handler设置为FMDBDatabaseBusyHandler这个函数 if (timeout > 0) { sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self)); } else { // 不使用任何busy handler处理 sqlite3_busy_handler(_db, nil, nil); } } 复制代码 这里需要提一下sqlite3_busy_handler这个函数:

int sqlite3_busy_handler(sqlite3*, int()(void,int), void*);

第一个参数是告知哪个数据库需要设置busy handler。

第二个参数是其实就是回调函数(busy handler)了,当你调用该回调函数时,需传递给它的一个void*的参数的拷贝,也即sqlite3_busy_handler的第三个参数;另一个需要传给回调函数的int参数是表示这次锁事件,该回调函数被调用的次数。如果回调函数返回0时,将不再尝试再次访问数据库而返回SQLITE_BUSY或者SQLITE_IOERR_BLOCKED。如果回调函数返回非0, 将会不断尝试操作数据库。

总结:程序运行过程中,如果有其他进程或者线程在读写数据库,那么sqlite3_busy_handler会不断调用回调函数,直到其他进程或者线程释放锁。获得锁之后,不会再调用回调函数,从而向下执行,进行数据库操作。该函数是在获取不到锁的时候,以执行回调函数的次数来进行延迟,等待其他进程或者线程操作数据库结束,从而获得锁操作数据库。

大家也看出来了,sqlite3_busy_handler函数的关键就是这个回调函数了,此处作者定义的是一个名叫FMDBDatabaseBusyHandler的函数作为其busy handler。

复制代码 // 注意:appledoc(生成文档的软件)中,对于有具体实现的C函数,比如下面这个函数, // 是有bug的。所以你在生成文档时,忽略.m文件。

// 该函数就是简单调用sqlite3_sleep来挂起进程 static int FMDBDatabaseBusyHandler(void *f, int count) { FMDatabase self = (__bridge FMDatabase)f; // 如果count为0,表示的第一次执行回调函数 // 初始化self->_startBusyRetryTime,供后面计算delta使用 if (count == 0) { self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate]; return 1; } // 使用delta变量控制执行回调函数的次数,每次挂起50~100ms // 所以maxBusyRetryTimeInterval的作用就在这体现出来了 // 当挂起的时长大于maxBusyRetryTimeInterval,就返回0,并停止执行该回调函数了 NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);

if (delta < [self maxBusyRetryTimeInterval]) {
     // 使用sqlite3_sleep每次当前线程挂起50~100ms
    int requestedSleepInMillseconds = (int) arc4random_uniform(50) + 50;
    int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds); 
    // 如果实际挂起的时长与想要挂起的时长不一致,可能是因为构建SQLite时没将HAVE_USLEEP置为1
    if (actualSleepInMilliseconds != requestedSleepInMillseconds) {
        NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds);
    }
    return 1;
}

return 0;

} 复制代码 2.3 - [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:](重点) 为什么不讲 - [FMDatabase executeQuery:]?因为- [FMDatabase executeQuery:]等等类似的函数,最终都是对- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]的简单封装。该函数比较关键,主要是针对查询的sql语句。

复制代码

  • (FMResultSet *)executeQuery:(NSString )sql withArgumentsInArray:(NSArray)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { // 判断当前是否存在数据库以供操作 if (![self databaseExists]) { return 0x00; } // 如果当前线程已经在使用数据库了,那就输出正在使用的警告 if (_isExecutingStatement) { [self warnInUse]; return 0x00; }

    _isExecutingStatement = YES;

    int rc = 0x00; sqlite3_stmt *pStmt = 0x00; // sqlite的prepared语句类型 FMStatement *statement = 0x00; // 对sqlite3_stmt的简单封装,在实际应用中,你不应直接操作FMStatement对象 FMResultSet *rs = 0x00; // FMResultSet对象是用来获取最终查询结果的 // 需要追踪sql执行状态的话,输出执行状态 if (_traceExecution && sql) { NSLog(@"%@ executeQuery: %@", self, sql); } // 调用sql语句之前,首先要将sql字符串预处理一下,转化为SQLite可用的prepared语句(预处理语句) // 使用sqlite3_prepare_v2来生成sql对应的prepare语句(即pStmt)代价很大 // 所以建议使用缓存机制来减少对sqlite3_prepare_v2的使用 if (_shouldCacheStatements) { // 获取到缓存中的prepared语句 statement = [self cachedStatementForQuery:sql]; pStmt = statement ? [statement statement] : 0x00; // prepared语句可以被重置(调用sqlite3_reset函数),然后可以重新绑定参数以便重新执行。 [statement reset]; } // 如果缓存中没有sql对应的prepared语句,那么只能使用sqlite3_prepare_v2函数进行预处理 if (!pStmt) {

      rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
      // 如果生成prepared语句出错,那么就根据是否需要打印错误信息(_logsErrors)以及是否遇到错误直接中止程序执行(_crashOnErrors)来执行出错处理。
      // 最后调用sqlite3_finalize函数释放所有的内部资源和sqlite3_stmt数据结构,有效删除prepared语句。
      if (SQLITE_OK != rc) {
          if (_logsErrors) {
              NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
              NSLog(@"DB Query: %@", sql);
              NSLog(@"DB Path: %@", _databasePath);
          }
    
          if (_crashOnErrors) {
              NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
            // abort()函数表示中止程序执行,直接从调用的地方跳出。
              abort();
          }
    
          sqlite3_finalize(pStmt);
          _isExecutingStatement = NO;
          return nil;
      }
    

    }

    id obj; int idx = 0; // 获取到pStmt中需要绑定的参数个数 int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)

    // 举一个使用dictionaryArgs的例子

/**

   NSMutableDictionary 

*dictionaryArgs = [NSMutableDictionary dictionary]; [dictionaryArgs setObject:@"Text1" forKey:@"a"]; [db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; // 注意类似:AAA前面有冒号的就是参数 // 其他的参数形式如:"?", "?NNN", ":AAA", "$AAA", 或 "@AAA" */ if (dictionaryArgs) {

    for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
        
        // 在每个dictionaryKey之前加上冒号,比如上面的a -> :a,方便获取参数在prepared语句中的索引
        NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
        // 查看执行状况
        if (_traceExecution) {
            NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
        }
        
        // 在prepared语句中查找对应parameterName的参数索引值namedIdx
        int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
        
        FMDBRelease(parameterName);
         // 可以利用索引namedIdx获取对应参数,再使用bindObject:函数将dictionaryArgs保存的value绑定给对应参数
        if (namedIdx > 0) {
            [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
            // 使用这个idx来判断sql中的所有参数值是否都绑定上了
            idx++;
        }
        else {
            NSLog(@"Could not find index for %@", dictionaryKey);
        }
    }
}
else {
    
    while (idx < queryCount) {
        // 使用arrayArgs的例子
        /**
         [db executeQuery:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]];
         */
        if (arrayArgs && idx < (int)[arrayArgs count]) {
            obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
        }
    // 使用args的例子,使用args其实就是调用- (FMResultSet *)executeQuery:(NSString*)sql, ...;
    /**
      FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];
     */
        else if (args) {
            obj = va_arg(args, id);
        }
        else {
            break;
        }
        
        if (_traceExecution) {
            if ([obj isKindOfClass:[NSData class]]) {
                NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
            }
            else {
                NSLog(@"obj: %@", obj);
            }
        }
        
        idx++;
        // 绑定参数值
        [self bindObject:obj toColumn:idx inStatement:pStmt];
    }
}
// 如果绑定的参数数目不对,认为出错,并释放资源
if (idx != queryCount) {
    NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
    sqlite3_finalize(pStmt);
    _isExecutingStatement = NO;
    return nil;
}

FMDBRetain(statement); // to balance the release below
// statement不为空,进行缓存
if (!statement) {
    statement = [[FMStatement alloc] init];
    [statement setStatement:pStmt];
    // 使用sql作为key来缓存statement(即sql对应的prepare语句)
    if (_shouldCacheStatements && sql) {
        [self setCachedStatement:statement forQuery:sql];
    }
}

// 根据statement和self(FMDatabase对象)构建一个FMResultSet对象,此函数中仅仅是构建该对象,还没使用next等函数获取查询结果
// 注意FMResultSet中含有以下成员(除了最后一个,其他成员均在此处初始化过了)
/**
  @interface FMResultSet : NSObject {
       FMDatabase          *_parentDB; // 表示该对象查询的数据库,主要是为了能在FMResultSet自己的函数中索引到正在操作的FMDatabase对象
       FMStatement         *_statement; // prepared语句

       NSString            *_query; // 对应的sql查询语句
       NSMutableDictionary *_columnNameToIndexMap; 
   }
 */
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
[rs setQuery:sql];
// 将此时的FMResultSet对象添加_openResultSets,主要是为了调试
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[_openResultSets addObject:openResultSet];
// 并设置statement的使用数目useCount加1,暂时不清楚此成员有何作用,感觉也是用于调试
[statement setUseCount:[statement useCount] + 1];

FMDBRelease(statement);
// 生成statement的操作已经结束
_isExecutingStatement = NO;

return rs;

} 复制代码 2.4 - [FMResultSet nextWithError:]

  • [FMResultSet next]函数其实就是对nextWithError:的简单封装。作用就是从我们上一步open中获取到的FMResultSet对象中读取查询后结果的每一行,交给用户自己处理。读取每一行的方法(即next)其实就是封装了sqlite3_step函数。而nextWithError:主要封装了对sqlite3_step函数返回结果的处理。

int sqlite3_step(sqlite3_stmt*);

sqlite3_prepare函数将SQL命令字符串解析并转换为一系列的命令字节码,这些字节码最终被传送到SQlite3的虚拟数据库引擎(VDBE: Virtual Database Engine)中执行,完成这项工作的是sqlite3_step函数。比如一个SELECT查询操作,sqlite3_step函数的每次调用都会返回结果集中的其中一行,直到再没有有效数据行了。每次调用sqlite3_step函数如果返回SQLITE_ROW,代表获得了有效数据行,可以通过sqlite3_column函数提取某列的值。如果调用sqlite3_step函数返回SQLITE_DONE,则代表prepared语句已经执行到终点了,没有有效数据了。很多命令第一次调用sqlite3_step函数就会返回SQLITE_DONE,因为这些SQL命令不会返回数据。对于INSERT,UPDATE,DELETE命令,会返回它们所修改的行号——一个单行单列的值。

复制代码 // 返回YES表示从数据库中获取到了下一行数据

  • (BOOL)nextWithError:(NSError **)outErr { // 尝试步进到下一行 int rc = sqlite3_step([_statement statement]);

    // 对返回结果rc进行处理

    /** SQLITE_BUSY 数据库文件有锁 SQLITE_LOCKED 数据库中的某张表有锁 SQLITE_DONE sqlite3_step()执行完毕 SQLITE_ROW sqlite3_step()获取到下一行数据 SQLITE_ERROR 一般用于没有特别指定错误码的错误,就是说函数在执行过程中发生了错误,但无法知道错误发生的原因。 SQLITE_MISUSE 没有正确使用SQLite接口,比如一条语句在sqlite3_step函数执行之后,没有被重置之前,再次给其绑定参数,这时bind函数就会返回SQLITE_MISUSE。 */
    if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { NSLog(@"%s:%d Database busy (%@)", FUNCTION, LINE, [_parentDB databasePath]); NSLog(@"Database busy"); if (outErr) { // lastError使用sqlite3_errcode获取到错误码,封装成NSError对象返回 *outErr = [_parentDB lastError]; } } else if (SQLITE_DONE == rc || SQLITE_ROW == rc) { // all is well, let's return. } else if (SQLITE_ERROR == rc) { // sqliteHandle就是获取到对应FMDatabase对象,然后使用sqlite3_errmsg来获取错误码的字符串 NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); if (outErr) { *outErr = [_parentDB lastError]; } } else if (SQLITE_MISUSE == rc) { // uh oh. NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); if (outErr) { if (_parentDB) { outErr = [_parentDB lastError]; } else { // 如果next和nextWithError函数是在当前的FMResultSet关闭之后调用的 // 这时输出的错误信息应该是parentDB不存在 NSDictionary errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey]; *outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage]; }

      }
    

    } else { // wtf? NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); if (outErr) { *outErr = [_parentDB lastError]; } }

    // 如果不是读取下一行数据,那么就关闭数据库 if (rc != SQLITE_ROW) { [self close]; }

    return (rc == SQLITE_ROW); } 复制代码 2.5 - [FMDatabase close] 与open函数成对调用。主要还是封装了sqlite_close函数。

复制代码

  • (BOOL)close { // 清除缓存的prepared语句,下面会详解 [self clearCachedStatements]; // 关闭所有打开的FMResultSet对象,目前看来这个_openResultSets大概也是用来调试的 [self closeOpenResultSets];

    if (!_db) { return YES; }

    int rc; BOOL retry; BOOL triedFinalizingOpenStatements = NO;

    do { retry = NO; // 调用sqlite3_close来尝试关闭数据库 rc = sqlite3_close(_db); //如果当前数据库上锁,那么就先尝试重新关闭(置retry为YES) // 同时还尝试释放数据库中的prepared语句资源 if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { if (!triedFinalizingOpenStatements) { triedFinalizingOpenStatements = YES; sqlite3_stmt *pStmt; // sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt )表示从数据库pDb中对应的pStmt语句开始一个个往下找出相应prepared语句,如果pStmt为nil,那么就从pDb的第一个prepared语句开始。 // 此处迭代找到数据库中所有prepared语句,释放其资源。 while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) { NSLog(@"Closing leaked statement"); sqlite3_finalize(pStmt); retry = YES; } } } // 关闭出错,输出错误码 else if (SQLITE_OK != rc) { NSLog(@"error closing!: %d", rc); } } while (retry);

    _db = nil; return YES; }

// _cachedStatements是用来缓存prepared语句的,所以清空_cachedStatements就是将每个缓存的prepared语句释放 // 具体实现就是使用下面那个close函数,close函数中调用了sqlite_finalize函数释放资源

  • (void)clearCachedStatements {

    for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) { // makeObjectsPerformSelector会并发执行同一件事,所以效率比for循环一个个执行要快很多 [statements makeObjectsPerformSelector:@selector(close)]; }

    [_cachedStatements removeAllObjects]; } // 注意:此为FMResultSet的close函数

  • (void)close { if (_statement) { sqlite3_finalize(_statement); _statement = 0x00; }

    _inUse = NO; }

// 清除_openResultSets

  • (void)closeOpenResultSets { //Copy the set so we don't get mutation errors NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]); // 迭代关闭_openResultSets中的FMResultSet对象 for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) { FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue]; // 清除FMResultSet的操作 [rs setParentDB:nil]; [rs close];

      [_openResultSets removeObject:rsInWrappedInATastyValueMeal];
    

    } } 复制代码 回到顶部

  1. 总结 本文结合一个基本的FMDB使用案例,介绍了FMDB基本的运作流程和内部实现。总的来说,FMDB就是对SQLite的封装,所以学习FMDB根本还是在学习SQLite数据库操作。
点赞
收藏
评论区
推荐文章
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中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写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进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这