简单来说:JDBC是一种开放标准的跨编程语言、跨数据库类型编程API。各类型数据库产品厂商都会按它的标准要求来提供针对自身产品的JDBC驱动程序。最主要的这是一套成熟的工具,在编程人员中使用很普及。既然我们的目标是开发一套标准的大数据系统集成API,那么采用JDBC系统数据接口可以沿用业内丰富的技术资源、覆盖更多类型用户的编程需要,以及降低使用门槛。对于scala编程语言来讲,ScalikeJDBC是一套最合适满足我们下面开发要求的工具库,因为它可以用最简单易用的方式来实现JDBC的基本功能。

JDBC的基本组件和工作原理比较简单:核心组件就connection、statement(preparedstatement)两个对象。connection提供与数据库的连接以及数据处理的运行环境,statement是connection的一个属性,包含了可运行的SQL语句及对它们的各种运算方法。下面我们就用ScalikeJDBC官网上的例子来示范说明ScalikeJDBC的应用:

import scalikejdbc._
object JDBCIntro extends App {
//standard java procedure to register driver
Class.forName("org.h2.Driver")
//default connectionPool named 'default
//ConnectionPool.singleton("jdbc:h2:mem:hello", "user", "pass")
//or ConnectionPool.add('default,"jdbc:h2:mem:hello", "user", "pass")
//db name is 'h2mem
ConnectionPool.add('h2mem,"jdbc:h2:mem:hello", "user", "pass")

首先,登记jdbc驱动程序及准备连接池connection-pool。注意,ScalikeJDBC提供了固定名为'default的默认连接池。所以ConnectionPool.singleton == ConnectionPool.add('default。在'default连接池上的操作都无需提供名称,如:(DB==NamedDB('default)),(AutoSession==NamedAutoSession('default))等。上面的代码示范了手工方式的连接池设置。如果应用需要替换增加数据库或调整数据库参数必须通过修改代码来实现。实用的程序应该使用配置文件的方式进行数据库设置可以轻松随意调整数据库参数,我们会在下一章介绍配置文件使用方法。

跟着是一条SQL语句的构建和运算:

  //construct SQL object
val createSQL: SQL[Nothing,NoExtractor] =SQL("""
create table members (
id bigint primary key auto_increment,
name varchar() not null,
description varchar(),
birthday date,
created_at timestamp not null
)""") //implicit val session = AutoSession 'default
//run this SQL
createSQL.execute().apply()(NamedAutoSession('h2mem)) //autoCommit

通过SQL("...")方法构建SQL[A,E]类型的实例,用execute()方法设定不同的运算方式,然后用apply()(implicit s:DBSession)来实际运算SQL,DBSession其实就是个connection实例。相关SQL[A,E]的类型定义如下:

/**
* SQL abstraction.
*
* @param statement SQL template
* @param rawParameters parameters
* @param f extractor function
* @tparam A return type
*/
abstract class SQL[A, E <: WithExtractor](
val statement: String,
private[scalikejdbc] val rawParameters: Seq[Any]
)(f: WrappedResultSet => A)
extends Extractor[A] {...} /**
* Extractor
*/
private[scalikejdbc] trait Extractor[A] {
def extractor: (WrappedResultSet) => A
}

SQL("...")调用了apply构建方法:

object SQL {
...
def apply[A](sql: String): SQL[A, NoExtractor] = new SQLToTraversableImpl[A, NoExtractor](sql, Seq.empty)(noExtractor[A](
ErrorMessage.THIS_IS_A_BUG
))
} /**
* SQL which execute java.sql.Statement#executeQuery() and returns the result as scala.collection.Traversable value.
*
* @param statement SQL template
* @param rawParameters parameters
* @param extractor extractor function
* @tparam A return type
*/
class SQLToTraversableImpl[A, E <: WithExtractor](
override val statement: String, override val rawParameters: Seq[Any]
)(
override val extractor: WrappedResultSet => A
)
extends SQL[A, E](statement, rawParameters)(extractor)
with SQLToTraversable[A, E] { override protected def withParameters(params: Seq[Any]): SQLToResult[A, E, Traversable] = {
new SQLToTraversableImpl[A, E](statement, params)(extractor)
} override protected def withStatementAndParameters(state: String, params: Seq[Any]): SQLToResult[A, E, Traversable] = {
new SQLToTraversableImpl[A, E](state, params)(extractor)
} override protected def withExtractor[B](f: WrappedResultSet => B): SQLToResult[B, HasExtractor, Traversable] = {
new SQLToTraversableImpl[B, HasExtractor](statement, rawParameters)(f)
} }

SQLToTraversableImpl类型提供了SQL语句参数组合方法并构建SQLToResult类型:

trait SQLToResult[A, E <: WithExtractor, C[_]] extends SQL[A, E] with Extractor[A] {
import GeneralizedTypeConstraintsForWithExtractor._ def result[AA](f: WrappedResultSet => AA, session: DBSession): C[AA]
val statement: String
private[scalikejdbc] val rawParameters: Seq[Any]
def apply()(
implicit
session: DBSession,
context: ConnectionPoolContext = NoConnectionPoolContext,
hasExtractor: ThisSQL =:= SQLWithExtractor
): C[A] = {
val attributesSwitcher = createDBSessionAttributesSwitcher()
val f: DBSession => C[A] = s => result[A](extractor, DBSessionWrapper(s, attributesSwitcher))
// format: OFF
session match {
case AutoSession | ReadOnlyAutoSession => DB.readOnly(f)
case NamedAutoSession(name, _) => NamedDB(name, session.settings).readOnly(f)
case ReadOnlyNamedAutoSession(name, _) => NamedDB(name, session.settings).readOnly(f)
case _ => f(session)
}
// format: ON
}
}

这个SQLToResult的apply方法就是SQL-readonly-query的运算方法。

SQL[A,E]中类型参数A是运算SQL返回值类型,E是一种函数类型:(WrappedResultSet => A),用来把返回结果转换成A类型值。SQL类型还提供了bind,bindByName方法来绑定SQL参数。SQL是个泛式的类型,除默认转换成Query型的SQLToResult类型外还可以转换成execute、update、batch等等其它SQL类型:

/**
* Binds parameters for batch
*
* @param parameters parameters
* @return SQL for batch
*/
def batch(parameters: Seq[Any]*): SQLBatch = {
new SQLBatch(statement, parameters, tags)
}
/**
* Binds parameters for largeBatch
*
* @param parameters parameters
* @return SQL for batch
*/
def largeBatch(parameters: Seq[Any]*): SQLLargeBatch =
new SQLLargeBatch(statement, parameters, tags)
/**
* Binds parameters for batch
*
* @param parameters parameters
* @return SQL for batch
*/
def batchAndReturnGeneratedKey(parameters: Seq[Any]*): SQLBatchWithGeneratedKey = {
new SQLBatchWithGeneratedKey(statement, parameters, tags)(None)
}
/**
* Binds parameters for batch
*
* @param generatedKeyName generated key name
* @param parameters parameters
* @return SQL for batch
*/
def batchAndReturnGeneratedKey(generatedKeyName: String, parameters: Seq[Any]*): SQLBatchWithGeneratedKey = {
new SQLBatchWithGeneratedKey(statement, parameters, tags)(Some(generatedKeyName))
}
/**
* Binds parameters for batch
*
* @param parameters parameters
* @return SQL for batch
*/
def batchByName(parameters: Seq[(Symbol, Any)]*): SQLBatch = {
val names = SQLTemplateParser.extractAllParameters(statement)
val sqlWithPlaceHolders = SQLTemplateParser.convertToSQLWithPlaceHolders(statement)
val _sql = validateAndConvertToNormalStatement(statement, sqlWithPlaceHolders, names, _settings, parameters.headOption.getOrElse(Seq.empty))._1
val _parameters: Seq[Seq[Any]] = parameters.map { p =>
validateAndConvertToNormalStatement(statement, sqlWithPlaceHolders, names, _settings, p)._2
}
new SQLBatch(_sql, _parameters, tags)
}
/**
* Set execution type as execute
*
* @return SQL instance
*/
def execute(): SQLExecution = {
new SQLExecution(statement, rawParameters, tags)((stmt: PreparedStatement) => {})((stmt: PreparedStatement) => {})
}
/**
* Set execution type as execute with filters
*
* @param before before filter
* @param after after filter
* @return SQL instance
*/
def executeWithFilters(before: (PreparedStatement) => Unit, after: (PreparedStatement) => Unit): SQLExecution = {
new SQLExecution(statement, rawParameters, tags)(before)(after)
}
/**
* Set execution type as executeUpdate
*
* @return SQL instance
*/
def executeUpdate(): SQLUpdate = update()
/**
* Set execution type as executeUpdate with filters
*
* @param before before filter
* @param after after filter
* @return SQL instance
*/
def executeUpdateWithFilters(before: (PreparedStatement) => Unit, after: (PreparedStatement) => Unit): SQLUpdate = {
updateWithFilters(before, after)
}
/**
* Set execution type as executeUpdate
*
* @return SQL instance
*/
def update(): SQLUpdate = {
new SQLUpdate(statement, rawParameters, tags)((stmt: PreparedStatement) => {})((stmt: PreparedStatement) => {})
}
/**
* Set execution type as executeUpdate with filters
*
* @param before before filter
* @param after after filter
* @return SQL instance
*/
def updateWithFilters(before: (PreparedStatement) => Unit, after: (PreparedStatement) => Unit): SQLUpdate = {
new SQLUpdate(statement, rawParameters, tags)(before)(after)
}
/**
* Set execution type as `executeLargeUpdate`
*
* @return SQL instance
*/
def largeUpdate(): SQLLargeUpdate =
new SQLLargeUpdate(statement, rawParameters, tags)(_ => {})(_ => {})
/**
* Set execution type as `executeLargeUpdate` with filters
*
* @param before before filter
* @param after after filter
* @return SQL instance
*/
def largeUpdateWithFilters(before: PreparedStatement => Unit, after: PreparedStatement => Unit): SQLLargeUpdate =
new SQLLargeUpdate(statement, rawParameters, tags)(before)(after)
/**
* Set execution type as updateAndReturnGeneratedKey
*
* @return SQL instance
*/
def updateAndReturnGeneratedKey(): SQLUpdateWithGeneratedKey = {
updateAndReturnGeneratedKey()
}
def updateAndReturnGeneratedKey(name: String): SQLUpdateWithGeneratedKey = {
new SQLUpdateWithGeneratedKey(statement, rawParameters, this.tags)(name)
}
def updateAndReturnGeneratedKey(index: Int): SQLUpdateWithGeneratedKey = {
new SQLUpdateWithGeneratedKey(statement, rawParameters, this.tags)(index)
}

以上每种SQL类型都是一种特别的SQL运算方式,即它们都有自己独特的apply()函数如:

/**
* SQL which execute java.sql.Statement#execute().
*
* @param statement SQL template
* @param parameters parameters
* @param before before filter
* @param after after filter
*/
class SQLExecution(val statement: String, val parameters: Seq[Any], val tags: Seq[String] = Nil)(
val before: (PreparedStatement) => Unit
)(
val after: (PreparedStatement) => Unit
) { def apply()(implicit session: DBSession): Boolean = {
val attributesSwitcher = new DBSessionAttributesSwitcher(SQL("").tags(tags: _*))
val f: DBSession => Boolean = DBSessionWrapper(_, attributesSwitcher).executeWithFilters(before, after, statement, parameters: _*)
// format: OFF
session match {
case AutoSession => DB.autoCommit(f)
case NamedAutoSession(name, _) => NamedDB(name, session.settings).autoCommit(f)
case ReadOnlyAutoSession => DB.readOnly(f)
case ReadOnlyNamedAutoSession(name, _) => NamedDB(name, session.settings).readOnly(f)
case _ => f(session)
}
// format: ON
} } /**
* SQL which execute java.sql.Statement#executeUpdate().
*
* @param statement SQL template
* @param parameters parameters
* @param before before filter
* @param after after filter
*/
class SQLUpdate(val statement: String, val parameters: Seq[Any], val tags: Seq[String] = Nil)(
val before: (PreparedStatement) => Unit
)(
val after: (PreparedStatement) => Unit
) { def apply()(implicit session: DBSession): Int = {
val attributesSwitcher = new DBSessionAttributesSwitcher(SQL("").tags(tags: _*))
session match {
case AutoSession =>
DB.autoCommit(DBSessionWrapper(_, attributesSwitcher).updateWithFilters(before, after, statement, parameters: _*))
case NamedAutoSession(name, _) =>
NamedDB(name, session.settings).autoCommit(DBSessionWrapper(_, attributesSwitcher).updateWithFilters(before, after, statement, parameters: _*))
case ReadOnlyAutoSession =>
DB.readOnly(DBSessionWrapper(_, attributesSwitcher).updateWithFilters(before, after, statement, parameters: _*))
case ReadOnlyNamedAutoSession(name, _) =>
NamedDB(name, session.settings).readOnly(DBSessionWrapper(_, attributesSwitcher).updateWithFilters(before, after, statement, parameters: _*))
case _ =>
DBSessionWrapper(session, attributesSwitcher).updateWithFilters(before, after, statement, parameters: _*)
}
} }

我们也可以把多个SQL语句共同放在一个session-block里:

 import org.joda.time._
// DB autoCommit == NamedDB('default) autoCommit
NamedDB('h2mem) autoCommit { implicit session =>
val insertSql: SQL[Nothing,NoExtractor] =
SQL("insert into members (name, birthday, created_at) values (?, ?, ?)")
val createdAt: DateTime = DateTime.now insertSql.bind("Alice", Some(new LocalDate("1980-01-01")), createdAt).update.apply() //commit
insertSql.bind("Bob", None, createdAt).update.apply() //commit
}

这是种批次更新SQL:使用了共同PreparedStatement模版,然后绑定不同的参数进行运算。上面的代码是一种loan-pattern编程模式。除autoCommit还有readOnly,localTx等:

  /**
* Provides auto-commit session block.
* @param execution block
* @tparam A return type
* @return result value
*/
def autoCommit[A](execution: DBSession => A): A = {
if (autoCloseEnabled) using(conn)(_ => execution(autoCommitSession()))
else execution(autoCommitSession())
} /**
* Provides read-only session block.
* @param execution block
* @tparam A return type
* @return result value
*/
def readOnly[A](execution: DBSession => A): A = {
if (autoCloseEnabled) using(conn)(_ => execution(readOnlySession()))
else execution(readOnlySession())
} /**
* Provides local-tx session block.
* @param execution block
* @tparam A return type
* @return result value
*/
def localTx[A](execution: DBSession => A)(implicit boundary: TxBoundary[A] = defaultTxBoundary[A]): A = {
val doClose = if (autoCloseEnabled) () => conn.close() else () => ()
val tx = newTx
begin(tx)
val txResult = try {
rollbackIfThrowable[A] {
val session = DBSession(
conn = conn,
tx = Option(tx),
connectionAttributes = connectionAttributes,
settings = this.settingsProvider
)
val result: A = execution(session)
boundary.finishTx(result, tx)
}
} catch {
case e: Throwable => doClose(); throw e
}
boundary.closeConnection(txResult, doClose)
}

在autoCommit域中每一次SQL运算都自动提交事务,不支持交易回滚rollback。在下面的localTx示范里,任何运算出现异常都会导致整体事务的回滚并抛出异常:

  //data model
case class Member(
id: Long,
name: String,
description: Option[String] = None,
birthday: Option[LocalDate] = None,
createdAt: DateTime) def create(name: String, birthday: Option[LocalDate], remarks: Option[String])(implicit session: DBSession): Member = {
val insertSQL: SQL[Nothing,NoExtractor] =
sql"""insert into members (name, birthday, description, created_at)
values (${name}, ${birthday}, ${remarks}, ${DateTime.now})"""
val id: Long = insertSQL.updateAndReturnGeneratedKey.apply()
Member(id, name, remarks, birthday,DateTime.now)
} val users = List(
("John",new LocalDate("2008-03-01"),"youngest user"),
("Susan",new LocalDate("2000-11-03"),"middle aged user"),
("Peter",new LocalDate("1983-01-21"),"oldest user"),
)
import scala.util._ //Try
import scalikejdbc.TxBoundary.Try._
val result: Try[List[Member]] =
NamedDB('h2mem) localTx { implicit session =>
Try {
val members: List[Member] = users.map { person =>
create(person._1, Some(person._2), Some(person._3))
}
throw new RuntimeException("fail test. boooommmm!!!!!")
members
}
} result match {
case Success(mlist) => println(s"batch added members: $mlist")
case Failure(err) => println(s"${err.getMessage}")
}

在上面这段代码中localTx block中的所有运算是包嵌在Try{}里的,即交易界限transaction-boundary,任何异常都被框定在这个界限里。以上的例子中不会抛出异常,返回结果包括了运算的所有状态。

我们也可以用case class或其它类型来模拟数据行类型以实现数据行的强类型操作:

 //data row converter
val toMember = (rs: WrappedResultSet) => Member(
id = rs.long("id"),
name = rs.string("name"),
description = rs.stringOpt("description"),
birthday = rs.jodaLocalDateOpt("birthday"),
createdAt = rs.jodaDateTime("created_at")
) val selectSQL: SQL[Member,HasExtractor] = sql"""select * from members""".map(toMember)
val members: List[Member] = NamedDB('h2mem) readOnly { implicit session =>
selectSQL.list.apply()
} println(s"all members: $members")
NamedDB('h2mem).close()

上面的代码中使用了sql"""...""",这是标准的scala-string-interpolation,是构建SQL[A,E]的另一种方法。

下面是本次讨论的示范源代码:

import scalikejdbc._
object JDBCIntro extends App {
//standard java procedure to register driver
Class.forName("org.h2.Driver")
//must have a db named 'default
//ConnectionPool.singleton("jdbc:h2:mem:hello", "user", "pass")
//==ConnectionPool.add('default,"jdbc:h2:mem:hello", "user", "pass")
//db name is 'h2mem
ConnectionPool.add('h2mem,"jdbc:h2:mem:hello", "user", "pass")
//construct SQL object
val createSQL: SQL[Nothing,NoExtractor] =SQL("""
create table members (
id bigint primary key auto_increment,
name varchar() not null,
description varchar(),
birthday date,
created_at timestamp not null
)""") //implicit val session = AutoSession 'default
//run this SQL
createSQL.execute().apply()(NamedAutoSession('h2mem)) //autoCommit import org.joda.time._
// DB autoCommit == NamedDB('default) autoCommit
NamedDB('h2mem) autoCommit { implicit session =>
val insertSql: SQL[Nothing,NoExtractor] =
SQL("insert into members (name, birthday, created_at) values (?, ?, ?)")
val createdAt: DateTime = DateTime.now
insertSql.bind("Alice", Some(new LocalDate("1980-01-01")), createdAt).update.apply() //commit
insertSql.bind("Bob", None, createdAt).update.apply() //commit
} //data model
case class Member(
id: Long,
name: String,
description: Option[String] = None,
birthday: Option[LocalDate] = None,
createdAt: DateTime) def create(name: String, birthday: Option[LocalDate], remarks: Option[String])(implicit session: DBSession): Member = {
val insertSQL: SQL[Nothing,NoExtractor] =
sql"""insert into members (name, birthday, description, created_at)
values (${name}, ${birthday}, ${remarks}, ${DateTime.now})"""
val id: Long = insertSQL.updateAndReturnGeneratedKey.apply()
Member(id, name, remarks, birthday,DateTime.now)
} val users = List(
("John",new LocalDate("2008-03-01"),"youngest user"),
("Susan",new LocalDate("2000-11-03"),"middle aged user"),
("Peter",new LocalDate("1983-01-21"),"oldest user"),
)
import scala.util._ //Try
import scalikejdbc.TxBoundary.Try._
val result: Try[List[Member]] =
NamedDB('h2mem) localTx { implicit session =>
Try {
val members: List[Member] = users.map { person =>
create(person._1, Some(person._2), Some(person._3))
}
throw new RuntimeException("fail test. boooommmm!!!!!")
members
}
} result match {
case Success(mlist) => println(s"batch added members: $mlist")
case Failure(err) => println(s"${err.getMessage}")
} //data row converter
val toMember = (rs: WrappedResultSet) => Member(
id = rs.long("id"),
name = rs.string("name"),
description = rs.stringOpt("description"),
birthday = rs.jodaLocalDateOpt("birthday"),
createdAt = rs.jodaDateTime("created_at")
) val selectSQL: SQL[Member,HasExtractor] = sql"""select * from members""".map(toMember)
val members: List[Member] = NamedDB('h2mem) readOnly { implicit session =>
selectSQL.list.apply()
} println(s"all members: $members")
NamedDB('h2mem).close() }

SDP(1):ScalikeJDBC-基本操作介绍的更多相关文章

  1. PyQuery基本操作介绍

    PyQuery基本操作介绍 PyQuery为Python提供一个类似于jQuery对HTML的操作方式,可以使用jQuery的语法对html文档进行查询操作. 本文以百度首页为例来介绍PyQuery的 ...

  2. vim编辑器基本操作介绍

    vim编辑器基本操作介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 可能很多小伙伴都听说过vi编辑器或是vim编辑器.它们是Unix和Linux世界最流行的编辑器之一,他们的特 ...

  3. SpringBoot中Redis的set、map、list、value、实体类等基本操作介绍

    今天给大家介绍一下SpringBoot中Redis的set.map.list.value等基本操作的具体使用方法 上一节中给大家介绍了如何在SpringBoot中搭建Redis缓存数据库,这一节就针对 ...

  4. Activiti工作流(一)之基本操作介绍

    工作流的概念: 工作流(Workflow),就是"业务过程的部分或整体在计算机应用环境下的自动化",它主要解决的是"使在多个参与者之间按照某种预定义的规则传递文档.信息或 ...

  5. 基于MyBatis3.0.6的基本操作介绍

    每 一 个 MyBatis 的 应 用 程 序 都 以 一 个 SqlSessionFactory 对 象 的 实 例 为 核 心 .SqlSessionFactory本身是由SqlSessionFa ...

  6. tensorflow基本操作介绍

    1.tensorflow的基本运作 为了快速的熟悉TensorFlow编程,下面从一段简单的代码开始: import tensorflow as tf #定义‘符号’变量,也称为占位符 a = tf. ...

  7. JDBC基本操作介绍

    一 .JDBC主要的API介绍 JDBC为开发人员提供了一套标准的API,都是由JAVA语言编写的类和接口.用于连接数据库和执行SQL语句.JDBC也是JAVA核心类库的一部分,位于Java.sql包 ...

  8. Jquery EasyUI的添加,修改,删除,查询等基本操作介绍

    http://www.jb51.net/article/42016.htm 初识Jquery EasyUI看了一些博主用其开发出来的项目,页面很炫,感觉功能挺强大,效果也挺不错,最近一直想系统学习一套 ...

  9. 【HBase】表模型和基本操作介绍

    目录 HBase表模型 常用Shell操作 HBase表模型 创建一个hbase表,必须要有表名和列族名 列族 -- columnFamily,就是上图中的Column Family1 userInf ...

  10. redis基本操作介绍

    一.字符串 单个设置:set key value,如果key不存在则设置键值对,如果存在则修改 批量设置:mset key1 value1 [key2 value2] 单个获取:get key,如果k ...

随机推荐

  1. android的ADK下载地址

    把下面所有的包下载到temp目录下进行安装. 用代理http://ppdaili.com/https://dl-ssl.google.com/android/repository/repository ...

  2. 新手了解.Nat

    1.Net平台 -->.Net平台 -->.Net FrameWrok框架 2.C#编程语言 -->编程语言:计算机能听懂的语言 -->使用.Net平台  C#是在.NEt平台 ...

  3. Xampp配置本地域名及常见错误解决

    本地域名配置 1.计算机-->C盘-->Windows-->System32-->drivers-->etc-->hosts 127.0.0.1       loc ...

  4. > library('ggplot2') Error in loadNamespace(i, c(lib.loc, .libPaths()), versionCheck = vI[[i]]) : 不存在叫‘colorspace’这个名字的程辑包

    > library('ggplot2')Error in loadNamespace(i, c(lib.loc, .libPaths()), versionCheck = vI[[i]]) : ...

  5. php 理解

    <?php class t { var $num; var $dynamic_function; public function dynamic_function() { $func = $th ...

  6. java inputstream to string

    https://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string 过千赞的答案

  7. 基于Docker的ELK日志平台搭建

    1.安装Docker Docker可简单理解为一个轻量级的虚拟机.Docker对进程进行封装隔离,隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器.Docker和传统虚拟化方式的不同.传统虚拟 ...

  8. JavaScript的DOM编程--05--获取文本节点

    获取文本节点: 1). 步骤: 元素节点 --> 获取元素节点的子节点 2). 若元素节点只有文本节点一个子节点, 例如 <li id="bj" name=" ...

  9. Android手机通过APN设置上网的方法

    今天一个朋友问了我关于android网络设置的问题,感觉还是挺有趣,特分享如下: 他是在香港买了一款LGP500的手机,系统是android系统,但是回来之后不能上网,于是在网上搜了很多资料,但是设置 ...

  10. Yii 框架学习--03 多应用多模块

    本文以YII 2.0.7为例. 概述 首先看看多应用和多模块的特点: 多应用的特点: 独立配置文件 独立域名 多模块的特点: 统一配置文件 统一域名 那么,实际该怎么决定使用多应用还是多模块呢? 对于 ...