zoukankan      html  css  js  c++  java
  • 浅谈Slick(4)- Slick301:我的Slick开发项目设置

      前面几篇介绍里尝试了一些Slick的功能和使用方式,看来基本可以满足用scala语言进行数据库操作编程的要求,而且有些代码可以通过函数式编程模式来实现。我想,如果把Slick当作数据库操作编程主要方式的话,可能需要先制定一套比较规范的模式来应付日常开发(也要考虑团队开发)、测试和维护。首先从项目结构来说,我发现由Intellij-Idea IDE界面直接产生的SBT项目结构已经比较理想了。在src/main/resources是scala项目获取配置文件的默认目录、我们可以按照需要在src/main/scala下增加代码子目录(package)及在src/main/test下摆放测试代码。配置文件application.conf、logback.xml是放在src/main/resources下的。application.conf是Slick的配置文件,logback.xml是跟踪器logback(log4j)的配置文件。Slick把jdbc api集成到scala编程语言里,能够支持多种数据库。也就是说Slick提供了多种数据库的驱动api。Slick支持在配置文件application.conf里配置数据库功能模式,这样我们就可以在正式部署软件时才通过修订application.conf里的配置来决定具体的数据库种类和参数。当然前提是我们的程序代码不能依赖任何特别的数据库api。我们从表结构设定开始,先看看上篇Slick101里的例子:

     1 package com.datatech.learn.slick101
     2 import slick.driver.H2Driver.api._
     3 object slick101 {
     4 
     5 /* ----- schema  */
     6   //表字段对应模版
     7   case class AlbumModel (id: Long
     8                    ,title: String
     9                    ,year: Option[Int]
    10                    ,artist: String
    11                    )
    12   //表结构: 定义字段类型, * 代表结果集字段
    13   class AlbumTable(tag: Tag) extends Table[AlbumModel](tag, "ALBUMS") {
    14     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
    15     def title = column[String]("TITLE")
    16     def year = column[Option[Int]]("YEAR")
    17     def artist = column[String]("ARTIST",O.Default("Unknown"))
    18     def * = (id,title,year,artist) <> (AlbumModel.tupled, AlbumModel.unapply)
    19   }
    20   //库表实例
    21   val albums = TableQuery[AlbumTable]

    我们可以看到这段代码依赖了slick.driver.H2Driver.api,是专门针对H2 Database的了。我们可以用依赖注入(dependency injection, IOC)来解决这个依赖问题。先试试用最传统的依赖注入方式:传入参数来注入这个数据库驱动依赖,把代码放在src/main/scala/model/TableDefs.scala里:

     1 package com.bayakala.learn.slick301.model
     2 import slick.driver.JdbcProfile
     3 class TableDefs(val dbDriver: JdbcProfile) {
     4   import dbDriver.api._
     5   case class Supplier(id: Long
     6                       , name: String
     7                       , contact: Option[String]
     8                       , website: Option[String])
     9   final class Suppliers(tag: Tag) extends Table[Supplier](tag,"SUPPLERS") {
    10     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
    11     def name = column[String]("NAME")
    12     def contact = column[Option[String]]("CONTACT")
    13     def website = column[Option[String]]("WEBSITE")
    14     def * = (id, name, contact, website) <> (Supplier.tupled,Supplier.unapply)
    15     def nidx = index("NM_IDX",name,unique = true)
    16   }
    17   val suppliers = TableQuery[Suppliers]
    18 
    19   case class Coffee(id: Long
    20                      ,name: String
    21                      ,supid: Long
    22                      ,price: Double
    23                      ,sales: Int)
    24   final class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") {
    25     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
    26     def name = column[String]("NAME")
    27     def supid = column[Long]("SUPID")
    28     def price = column[Double]("PRICE",O.Default(0.0))
    29     def sales = column[Int]("SALES",O.Default(0))
    30     def * = (id,name,supid,price,sales) <> (Coffee.tupled, Coffee.unapply)
    31     def fk_sup = foreignKey("FK_SUP",supid,suppliers)(_.id,onDelete = ForeignKeyAction.Restrict,onUpdate = ForeignKeyAction.Cascade)
    32     def supidx = index("SUP_IDX",supid,unique = false)
    33     def nidx = index("NM_IDX",name,unique = true)
    34   }
    35   val coffees = TableQuery[Coffees]
    36 
    37 }

    注意我们是把JdbcProfile作为参数注入了class TableDefs里。如果TableDefs经常需要作为其它类的父类继承的话,设计成trait能更加灵活的进行类型混合(type mixing)。这样的需求可以用cake pattern方式进行依赖注入。我们在需要src/main/scala/config/AppConfig.scala里定义依赖界面trait DBConfig:

    1 package com.bayakala.learn.slick301.config
    2 import slick.driver.JdbcProfile
    3 trait DBConfig {
    4   val jdbcDriver: JdbcProfile
    5   import jdbcDriver.api._
    6   val db: Database
    7 }

    后面我们可以通过实现多种DBConfig实例方式来构建开发、测试、部署等数据库环境。为了方便示范,我们设计几个基本的Query Action,放在src/main/scala/access/DAOs.scala里,用cake pattern注入依赖DBConfig:

     1 package com.bayakala.learn.slick301.access
     2 import com.bayakala.learn.slick301.config
     3 import com.bayakala.learn.slick301.config.DBConfig
     4 import com.bayakala.learn.slick301.model.TableDefs
     5 trait DAOs { dbconf: DBConfig =>
     6   import jdbcDriver.api._
     7   //注入依赖
     8   val tables = new TableDefs(dbconf.jdbcDriver)
     9   import tables._
    10   //suppliers queries
    11   val createSupplierTable = suppliers.schema.create
    12   val allSuppliers = suppliers.result
    13   def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String])
    14   = suppliers += Supplier(id,name,address,website)
    15   def insertSupbyName(n: String) = suppliers.map(_.name) += n
    16   //coffees queries
    17   val createCoffeeTable = coffees.schema.create
    18   val allCoffees = coffees.result
    19   def insertCoffee(c: (Long,String,Long,Double,Int)) =
    20     coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5)
    21 
    22 }

    dbconf: DBConfig => 的意思是在进行DAOs的实例化时必须混入(mixing)DBConfig类。

    以上两个代码文件TableDefs.scala和DAOs.scala在注入依赖后都能够顺利通过编译了。

    我们在src/main/scala/main/Main.scala里测试运算DAOs里的query action:

     1 package com.bayakala.learn.slick301.main
     2 import com.bayakala.learn.slick301.config.DBConfig
     3 import com.bayakala.learn.slick301.access.DAOs
     4 
     5 import scala.concurrent.{Await, Future}
     6 import scala.util.{Failure, Success}
     7 import scala.concurrent.duration._
     8 import scala.concurrent.ExecutionContext.Implicits.global
     9 import slick.backend.DatabaseConfig
    10 import slick.driver.{H2Driver, JdbcProfile}
    11 object Main {
    12  
    13   object Actions extends DAOs with DBConfig {
    14     override lazy val jdbcDriver: JdbcProfile = H2Driver
    15     val dbConf: DatabaseConfig[H2Driver] = DatabaseConfig.forConfig("h2")
    16     override val db = dbConf.db
    17   }
    18   import Actions._
    19   
    20   def main(args: Array[String]) = {
    21     val res = db.run(createSupplierTable).andThen {
    22       case Success(_) => println("supplier table created")
    23       case Failure(_) => println("unable to create supplier table")
    24     }
    25     Await.ready(res, 3 seconds)
    26 
    27     val res2 = db.run(insertSupbyName("Acme Coffee Co."))
    28     Await.ready(res2, 3 seconds)
    29 
    30     Await.ready(db.run(allSuppliers), 10 seconds).foreach(println)
    31 
    32     val res10 = db.run(createCoffeeTable).andThen {
    33       case Success(_) => println("coffee table created")
    34       case Failure(_) => println("unable to create coffee table")
    35     }
    36     Await.ready(res10, 3 seconds)
    37 
    38     val res11 = db.run(insertCoffee((101,"Columbia",1,158.0,0)))
    39     Await.ready(res11, 3 seconds)
    40 
    41     Await.ready(db.run(allCoffees), 10 seconds).foreach(println)
    42 
    43   }
    44 
    45 }

    Actions是DAOs的实例。我们看到必须把DBConfig混入(mixin)。但是我们构建的数据库又变成了专门针对H2的api了,这样的话每次变动数据库对象我们就必须重新编译Main.scala,不符合上面我们提到的要求。我们可以把目标数据库放到application.conf里,然后在Main.scala里用typesafe-config实时根据application.conf里的设置确定数据库参数。src/main/resources/application.conf内容如下:

     1 app = {
     2   dbconfig = h2
     3 }
     4 
     5 h2 {
     6   driver = "slick.driver.H2Driver$"
     7   db {
     8     url = "jdbc:h2:~/slickdemo;mv_store=false"
     9     driver = "org.h2.Driver"
    10     connectionPool = HikariCP
    11     numThreads = 10
    12     maxConnections = 12
    13     minConnections = 4
    14     keepAliveConnection = true
    15   }
    16 }
    17 
    18 h2mem = {
    19   url = "jdbc:h2:mem:slickdemo"
    20   driver = org.h2.Driver
    21   connectionPool = disabled
    22   keepAliveConnection = true
    23 }
    24 
    25 mysql {
    26   driver = "slick.driver.MySQLDriver$"
    27   db {
    28     url = "jdbc:mysql://localhost/slickdemo"
    29     driver = com.mysql.jdbc.Driver
    30     keepAliveConnection = true
    31     user="root"
    32     password="123"
    33     numThreads=10
    34     maxConnections = 12
    35     minConnections = 4
    36   }
    37 }
    38 
    39 mysqldb = {
    40   dataSourceClass = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
    41   properties {
    42     user = "root"
    43     password = "123"
    44     databaseName = "slickdemo"
    45     serverName = "localhost"
    46   }
    47   numThreads = 10
    48   maxConnections = 12
    49   minConnections = 4
    50 }
    51 
    52 postgres {
    53   driver = "slick.driver.PostgresDriver$"
    54   db {
    55     url = "jdbc:postgresql://127.0.0.1/slickdemo"
    56     driver = "org.postgresql.Driver"
    57     connectionPool = HikariCP
    58     user = "slick"
    59     password = "123"
    60     numThreads = 10
    61     maxConnections = 12
    62     minConnections = 4
    63   }
    64 }
    65 
    66 postgressdb = {
    67   dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
    68   properties = {
    69     databaseName = "slickdemo"
    70     user = "slick"
    71     password = "123"
    72   }
    73   connectionPool = HikariCP
    74   numThreads = 10
    75   maxConnections = 12
    76   minConnections = 4
    77 }
    78 
    79 mssql {
    80   driver = "com.typesafe.slick.driver.ms.SQLServerDriver$"
    81   db {
    82     url = "jdbc:sqlserver://host:port"
    83     driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
    84     connectionTimeout = 30 second
    85     connectionPool = HikariCP
    86     user = "slick"
    87     password = "123"
    88     numThreads = 10
    89     maxConnections = 12
    90     minConnections = 4
    91     keepAliveConnection = true
    92   }
    93 }
    94 
    95 tsql {
    96   driver = "slick.driver.H2Driver$"
    97   db = ${h2mem}
    98 }

    现在application.conf里除了数据库配置外又加了个app配置。我们在Main.scala里实例化DAOs时可以用typesafe-config读取app.dbconfig值后设定jdbcDriver和db:

     1   object Actions extends DAOs with DBConfig {
     2     import slick.util.ClassLoaderUtil
     3     import scala.util.control.NonFatal
     4     import com.typesafe.config.ConfigFactory
     5 
     6     def getDbConfig: String =
     7         ConfigFactory.load().getString("app.dbconfig")
     8 
     9     def getDbDriver(path: String): JdbcProfile = {
    10       val config = ConfigFactory.load()
    11       val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver")
    12       val untypedP = try {
    13         if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null)
    14         else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance()
    15       } catch {
    16         case NonFatal(ex) =>
    17           throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex)
    18       }
    19       untypedP.asInstanceOf[JdbcProfile]
    20     }
    21     
    22     override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig)
    23     val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig)
    24     override val db = dbConf.db
    25   }

    现在我们只需要改变application.conf里的app.dbconfig就可以转换目标数据库参数了。实际上,除了数据库配置,我们还可以在application.conf里进行其它类型的配置。然后用typesafe-config实时读取。如果不想在application.conf进行数据库之外的配置,可以把其它配置放在任何文件里,然后用ConfigFactory.load(path)来读取。

    另外,在软件开发过程中跟踪除错也是很重要的。我们可以用logback来跟踪Slick、HikariCP等库的运行状态。logback配置在src/main/resources/logback.xml:

     1 <?xml version="1.0" encoding="UTF-8"?>
     2 
     3 <configuration>
     4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
     5         <encoder>
     6             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
     7         </encoder>
     8     </appender>
     9 
    10     <logger name="application" level="DEBUG"/>
    11     <logger name="com.zaxxer.hikari" level="DEBUG"/>
    12     <logger name="slick" level="DEBUG"/>
    13 
    14     <root level="DEBUG">
    15         <appender-ref ref="STDOUT"/>
    16     </root>
    17 </configuration>

    DEBUG值可以显示最详细的状态信息。

    好了,我把这次示范代码提供在下面:

    build.sbt:

     1 name := "learn-slick301"
     2 
     3 version := "1.0"
     4 
     5 scalaVersion := "2.11.8"
     6 
     7 libraryDependencies ++= Seq(
     8   "com.typesafe.slick" %% "slick" % "3.1.1",
     9   "com.h2database" % "h2" % "1.4.191",
    10   "com.typesafe.slick" %% "slick-hikaricp" % "3.1.1",
    11   "ch.qos.logback" % "logback-classic" % "1.1.7",
    12   "org.typelevel" %% "cats" % "0.7.2"
    13 
    14 )

    src/main/resources/

    application.conf:

     1 app = {
     2   dbconfig = h2
     3 }
     4 
     5 h2 {
     6   driver = "slick.driver.H2Driver$"
     7   db {
     8     url = "jdbc:h2:~/slickdemo;mv_store=false"
     9     driver = "org.h2.Driver"
    10     connectionPool = HikariCP
    11     numThreads = 10
    12     maxConnections = 12
    13     minConnections = 4
    14     keepAliveConnection = true
    15   }
    16 }
    17 
    18 h2mem = {
    19   url = "jdbc:h2:mem:slickdemo"
    20   driver = org.h2.Driver
    21   connectionPool = disabled
    22   keepAliveConnection = true
    23 }
    24 
    25 mysql {
    26   driver = "slick.driver.MySQLDriver$"
    27   db {
    28     url = "jdbc:mysql://localhost/slickdemo"
    29     driver = com.mysql.jdbc.Driver
    30     keepAliveConnection = true
    31     user="root"
    32     password="123"
    33     numThreads=10
    34     maxConnections = 12
    35     minConnections = 4
    36   }
    37 }
    38 
    39 mysqldb = {
    40   dataSourceClass = "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
    41   properties {
    42     user = "root"
    43     password = "123"
    44     databaseName = "slickdemo"
    45     serverName = "localhost"
    46   }
    47   numThreads = 10
    48   maxConnections = 12
    49   minConnections = 4
    50 }
    51 
    52 postgres {
    53   driver = "slick.driver.PostgresDriver$"
    54   db {
    55     url = "jdbc:postgresql://127.0.0.1/slickdemo"
    56     driver = "org.postgresql.Driver"
    57     connectionPool = HikariCP
    58     user = "slick"
    59     password = "123"
    60     numThreads = 10
    61     maxConnections = 12
    62     minConnections = 4
    63   }
    64 }
    65 
    66 postgressdb = {
    67   dataSourceClass = "org.postgresql.ds.PGSimpleDataSource"
    68   properties = {
    69     databaseName = "slickdemo"
    70     user = "slick"
    71     password = "123"
    72   }
    73   connectionPool = HikariCP
    74   numThreads = 10
    75   maxConnections = 12
    76   minConnections = 4
    77 }
    78 
    79 mssql {
    80   driver = "com.typesafe.slick.driver.ms.SQLServerDriver$"
    81   db {
    82     url = "jdbc:sqlserver://host:port"
    83     driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
    84     connectionTimeout = 30 second
    85     connectionPool = HikariCP
    86     user = "slick"
    87     password = "123"
    88     numThreads = 10
    89     maxConnections = 12
    90     minConnections = 4
    91     keepAliveConnection = true
    92   }
    93 }
    94 
    95 tsql {
    96   driver = "slick.driver.H2Driver$"
    97   db = ${h2mem}
    98 }

    logback.xml:

     1 <?xml version="1.0" encoding="UTF-8"?>
     2 
     3 <configuration>
     4     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
     5         <encoder>
     6             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
     7         </encoder>
     8     </appender>
     9 
    10     <logger name="application" level="DEBUG"/>
    11     <logger name="com.zaxxer.hikari" level="DEBUG"/>
    12     <logger name="slick" level="DEBUG"/>
    13 
    14     <root level="DEBUG">
    15         <appender-ref ref="STDOUT"/>
    16     </root>
    17 </configuration>

    src/main/scala/config/AppConfig.scala:

    1 package com.bayakala.learn.slick301.config
    2 import slick.driver.JdbcProfile
    3 trait DBConfig {
    4   val jdbcDriver: JdbcProfile
    5   import jdbcDriver.api._
    6   val db: Database
    7 }

    src/main/scala/model/TableDefs.scala:

     1 package com.bayakala.learn.slick301.model
     2 import slick.driver.JdbcProfile
     3 class TableDefs(val dbDriver: JdbcProfile) {
     4   import dbDriver.api._
     5   case class Supplier(id: Long
     6                       , name: String
     7                       , contact: Option[String]
     8                       , website: Option[String])
     9   final class Suppliers(tag: Tag) extends Table[Supplier](tag,"SUPPLERS") {
    10     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
    11     def name = column[String]("NAME")
    12     def contact = column[Option[String]]("CONTACT")
    13     def website = column[Option[String]]("WEBSITE")
    14     def * = (id, name, contact, website) <> (Supplier.tupled,Supplier.unapply)
    15     def nidx = index("NM_IDX",name,unique = true)
    16   }
    17   val suppliers = TableQuery[Suppliers]
    18 
    19   case class Coffee(id: Long
    20                      ,name: String
    21                      ,supid: Long
    22                      ,price: Double
    23                      ,sales: Int)
    24   final class Coffees(tag: Tag) extends Table[Coffee](tag, "COFFEES") {
    25     def id = column[Long]("ID",O.AutoInc,O.PrimaryKey)
    26     def name = column[String]("NAME")
    27     def supid = column[Long]("SUPID")
    28     def price = column[Double]("PRICE",O.Default(0.0))
    29     def sales = column[Int]("SALES",O.Default(0))
    30     def * = (id,name,supid,price,sales) <> (Coffee.tupled, Coffee.unapply)
    31     def fk_sup = foreignKey("FK_SUP",supid,suppliers)(_.id,onDelete = ForeignKeyAction.Restrict,onUpdate = ForeignKeyAction.Cascade)
    32     def supidx = index("SUP_IDX",supid,unique = false)
    33     def nidx = index("NM_IDX",name,unique = true)
    34   }
    35   val coffees = TableQuery[Coffees]
    36 
    37 }

    src/main/scala/access/DAOs.scala:

     1 package com.bayakala.learn.slick301.access
     2 import com.bayakala.learn.slick301.config
     3 import com.bayakala.learn.slick301.config.DBConfig
     4 import com.bayakala.learn.slick301.model.TableDefs
     5 trait DAOs { dbconf: DBConfig =>
     6   import jdbcDriver.api._
     7   //注入依赖
     8   val tables = new TableDefs(dbconf.jdbcDriver)
     9   import tables._
    10   //suppliers queries
    11   val createSupplierTable = suppliers.schema.create
    12   val allSuppliers = suppliers.result
    13   def insertSupplier(id:Long,name:String,address:Option[String],website:Option[String])
    14   = suppliers += Supplier(id,name,address,website)
    15   def insertSupbyName(n: String) = suppliers.map(_.name) += n
    16   //coffees queries
    17   val createCoffeeTable = coffees.schema.create
    18   val allCoffees = coffees.result
    19   def insertCoffee(c: (Long,String,Long,Double,Int)) =
    20     coffees += Coffee(id=c._1, name=c._2,supid=c._3,price=c._4,sales=c._5)
    21 
    22 }

    src/main/scala/main/Main.scala:

     1 package com.bayakala.learn.slick301.main
     2 import com.bayakala.learn.slick301.config.DBConfig
     3 import com.bayakala.learn.slick301.access.DAOs
     4 
     5 import scala.concurrent.Await
     6 import scala.util.{Failure, Success}
     7 import scala.concurrent.duration._
     8 import scala.concurrent.ExecutionContext.Implicits.global
     9 import slick.backend.DatabaseConfig
    10 import slick.driver.JdbcProfile
    11 
    12 object Main {
    13 
    14   object Actions extends DAOs with DBConfig {
    15     import slick.SlickException
    16     import slick.util.ClassLoaderUtil
    17     import scala.util.control.NonFatal
    18     import com.typesafe.config.ConfigFactory
    19 
    20     def getDbConfig: String =
    21         ConfigFactory.load().getString("app.dbconfig")
    22 
    23     def getDbDriver(path: String): JdbcProfile = {
    24       val config = ConfigFactory.load()
    25       val n = config.getString((if (path.isEmpty) "" else path + ".") + "driver")
    26       val untypedP = try {
    27         if (n.endsWith("$")) ClassLoaderUtil.defaultClassLoader.loadClass(n).getField("MODULE$").get(null)
    28         else ClassLoaderUtil.defaultClassLoader.loadClass(n).newInstance()
    29       } catch {
    30         case NonFatal(ex) =>
    31           throw new SlickException(s"""Error getting instance of Slick driver "$n"""", ex)
    32       }
    33       untypedP.asInstanceOf[JdbcProfile]
    34     }
    35 
    36     override lazy val jdbcDriver: JdbcProfile = getDbDriver(getDbConfig)
    37     val dbConf: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(getDbConfig)
    38     override val db = dbConf.db
    39   }
    40   import Actions._
    41 
    42 
    43   def main(args: Array[String]) = {
    44 
    45     val res = db.run(createSupplierTable).andThen {
    46       case Success(_) => println("supplier table created")
    47       case Failure(_) => println("unable to create supplier table")
    48     }
    49     Await.ready(res, 3 seconds)
    50 
    51     val res2 = db.run(insertSupbyName("Acme Coffee Co."))
    52     Await.ready(res2, 3 seconds)
    53 
    54     Await.ready(db.run(allSuppliers), 10 seconds).foreach(println)
    55 
    56     val res10 = db.run(createCoffeeTable).andThen {
    57       case Success(_) => println("coffee table created")
    58       case Failure(_) => println("unable to create coffee table")
    59     }
    60     Await.ready(res10, 3 seconds)
    61 
    62     val res11 = db.run(insertCoffee((101,"Columbia",1,158.0,0)))
    63     Await.ready(res11, 3 seconds)
    64 
    65     Await.ready(db.run(allCoffees), 10 seconds).foreach(println)
    66 
    67   }

     

  • 相关阅读:
    HBase 文件读写过程描述
    Kafka 部署指南-好久没有更新博客了
    《Python高性能编程》——列表、元组、集合、字典特性及创建过程
    Ansible常用功能
    vim内替换文件内容
    线程队列-queue
    Python多进程
    python多线程知识-实用实例
    夜间模式的实现
    本地通知的实现
  • 原文地址:https://www.cnblogs.com/tiger-xc/p/5930326.html
Copyright © 2011-2022 走看看