Android Kotlin:Kotlin数据类与密封类

一、开篇痛点:样板代码地狱与状态爆炸

在Android开发中,业务建模是最基础也最容易出错的工作。想象一下你正在开发一个电商应用,需要定义网络请求返回的商品详情DTO,以及订单状态流转:

java 复制代码
// Java时代的噩梦 - 商品DTO
public class ProductDTO {
    private String id;
    private String name;
    private BigDecimal price;
    private List<String> tags;
    
    // 构造函数、getter、setter、equals、hashCode、toString...
    // 50行样板代码,手抖写错一个equals就导致比价逻辑bug
    
    public ProductDTO(String id, String name, BigDecimal price, List<String> tags) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.tags = tags;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductDTO that = (ProductDTO) o;
        return Objects.equals(id, that.id) && 
               Objects.equals(name, that.name) && 
               Objects.equals(price, that.price);
    }
    
    // hashCode, toString, getters, setters... 
    // 每加一个字段就要改5个地方,维护成本极高
}

// 状态管理:用int常量+ Object混合数据,类型不安全
public static final int STATE_LOADING = 0;
public static final int STATE_SUCCESS = 1;
public static final int STATE_ERROR = 2;

public class OrderViewModel {
    private MutableLiveData<Integer> state = new MutableLiveData<>();
    private MutableLiveData<String> errorMessage = new MutableLiveData<>();
    private MutableLiveData<Order> orderData = new MutableLiveData<>();
    
    // 问题:状态与数据分离,无法保证一致性;Integer状态值可以随意传错
    public void loadOrder(String orderId) {
        state.setValue(STATE_LOADING);
        repository.getOrder(orderId, new Callback() {
            @Override
            public void onResponse(Order order) {
                state.setValue(STATE_SUCCESS);
                orderData.setValue(order);
            }
            @Override
            public void onFailure(String error) {
                state.setValue(STATE_ERROR);
                errorMessage.setValue(error);
            }
        });
    }
}

这段代码的问题显而易见:DTO维护成本极高 ,任何字段变更都需要同步修改equals、hashCode等多个方法;状态管理缺乏类型安全 ,状态和数据的对应关系全靠人工约定,容易出错;空安全无法保障,Java代码中随处可见的null检查或遗漏的NPE。

二、Kotlin解法:数据类与密封类的优雅重构

Kotlin通过data classsealed class显著改变了这一局面。让我们逐步重构:

2.1 数据类重构DTO

kotlin 复制代码
// 文件路径: com.example.kotlin.dsl.model.Product.kt
package com.example.kotlin.dsl.model

import java.math.BigDecimal

/**
 * 商品数据传输对象
 * @param id 商品唯一标识
 * @param name 商品名称,非空
 * @param price 价格,使用BigDecimal避免浮点精度问题
 * @param tags 标签列表,可为空但默认为空列表
 */
data class Product(
    val id: String,
    val name: String,
    val price: BigDecimal,
    val tags: List<String> = emptyList() // 默认参数减少重载需求
)

// 使用示例
val product = Product(
    id = "SKU-001",
    name = "Kotlin Programming",
    price = BigDecimal("59.99")
)

// copy()实现不可变性更新,适用于MVVM状态流转
val discountedProduct = product.copy(
    price = product.price.multiply(BigDecimal("0.8"))
)

对比分析

  • 空安全 :Kotlin强制标记可空类型,val name: String默认非空,编译器阻止null传入
  • 不可变性 :使用val声明只读属性,配合copy()实现函数式更新,避免并发修改风险
  • 自动生成 :compiler自动生成equals()hashCode()toString()componentN()函数

2.2 密封类重构状态管理

kotlin 复制代码
// 文件路径: com.example.kotlin.dsl.state.OrderState.kt
package com.example.kotlin.dsl.state

import com.example.kotlin.dsl.model.Order

/**
 * 订单加载状态的密封类层次结构
 * 所有子类必须在此文件中定义,保证穷尽性检查
 */
sealed class OrderState {
    // object单例表示无数据状态
    object Idle : OrderState()
    object Loading : OrderState()
    
    // data class携带成功数据
    data class Success(val order: Order, val timestamp: Long = System.currentTimeMillis()) : OrderState()
    
    // data class携带错误信息,支持重试
    data class Error(
        val message: String, 
        val exception: Throwable? = null,
        val canRetry: Boolean = true
    ) : OrderState()
}

// ViewModel中的使用
class OrderViewModel(private val repository: OrderRepository) : ViewModel() {
    
    // StateFlow使用密封类作为状态载体,类型安全且响应式
    private val _uiState = MutableStateFlow<OrderState>(OrderState.Idle)
    val uiState: StateFlow<OrderState> = _uiState.asStateFlow()
    
    fun loadOrder(orderId: String) {
        viewModelScope.launch {
            _uiState.value = OrderState.Loading
            try {
                val order = repository.fetchOrder(orderId) // suspend函数
                _uiState.value = OrderState.Success(order)
            } catch (e: Exception) {
                _uiState.value = OrderState.Error(
                    message = "加载订单失败",
                    exception = e,
                    canRetry = e !is IllegalArgumentException // 参数错误不重试
                )
            }
        }
    }
}

核心优势

  • 类型安全:编译器知道OrderState只有4种可能,switch-case无需default分支
  • 数据绑定:Success和Error携带不同数据,无需像Java那样维护多个LiveData
  • 穷尽检查:when表达式必须处理所有子类,新增状态后编译器强制要求补充处理

三、原理深挖:编译器魔法与JVM实现

3.1 数据类的字节码生成

Kotlin编译器对data class的处理并非简单的POJO。以Product类为例,编译后生成:

java 复制代码
// 反编译后的近似Java代码
public final class Product {
    private final String id;
    private final String name;
    private final BigDecimal price;
    private final List<String> tags;
    
    // 构造函数、getter...
    
    @NotNull
    public final String component1() { return this.id; } // 解构支持
    
    @NotNull
    public final Product copy(@NotNull String id, @NotNull String name, 
                             @NotNull BigDecimal price, @NotNull List<String> tags) {
        return new Product(id, name, price, tags);
    }
    
    @NotNull
    public String toString() { return "Product(id=" + this.id + ...)"; }
    
    public int hashCode() { /* 基于所有属性的哈希计算 */ }
    
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Product)) return false;
        // 逐个比较属性,对集合类型调用equals
        return Objects.equals(this.id, ((Product)other).id) && ...;
    }
}

性能考量

  • 内存布局:与普通类相同,无额外字段开销
  • copy()开销:浅拷贝操作,仅复制引用,时间复杂度O(1);但需注意集合类型的深拷贝问题(见第五节)
  • 解构成本componentN()函数是是普通方法,但实现非常简单,通常会被JIT优化,实际开销可以忽略。

3.2 密封类的编译器策略

密封类的核心机制在于编译期类型封闭

kotlin 复制代码
// 编译器在ModuleMetadata中记录所有子类信息
// 生成的字节码包含PermittedSubclasses属性(Java 17+)或Kotlin Metadata注解
public abstract class OrderState {
    private OrderState() {} // 私有构造函数阻止外部继承
    
    // 静态内部类/对象实现子类型
    public static final class Loading extends OrderState { ... }
}

when表达式穷尽性检查 : Kotlin编译器通过分析密封类的子类型封闭集合,在编译期验证when表达式是否覆盖所有分支。如果新增子类未处理,编译报错而非运行时异常。这比Java 17的switch pattern matching更严格,后者在跨模块边界时可能失去穷尽性保证。

与Java对比

  • Java的sealed class需要显式permits子句,且子类必须同一模块
  • Kotlin 1.5+放宽限制,允许同一包内不同文件定义子类,但仍保持编译期封闭性
  • 性能无差异:密封类在JVM层面就是普通abstract class,判断分支时使用的是instanceof,JIT会优化为直接跳转

四、Android实战场景

场景1:RecyclerView多类型Adapter(密封类+数据类)

电商应用常见的混合列表:标题、商品卡片、分隔线、加载更多。

kotlin 复制代码
// com.example.kotlin.dsl.ui.FeedAdapter.kt
package com.example.kotlin.dsl.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlin.dsl.databinding.*
import com.example.kotlin.dsl.model.Product

/**
 * 列表项类型的密封类定义
 */
sealed class FeedItem {
    abstract val id: String // 用于DiffUtil比较
    
    data class Header(val title: String, val subtitle: String? = null) : FeedItem() {
        override val id: String = "header_$title"
    }
    
    data class ProductCard(val product: Product, val isNewArrival: Boolean = false) : FeedItem() {
        override val id: String = product.id
    }
    
    data class Divider(val heightDp: Int = 8) : FeedItem() {
        override val id: String = "divider_$heightDp"
    }
    
    object LoadingMore : FeedItem() {
        override val id: String = "loading_more"
    }
}

class FeedAdapter(
    private val onProductClick: (Product) -> Unit
) : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {

    // 根据item类型返回对应ViewHolder,编译器强制处理所有子类
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            TYPE_HEADER -> HeaderViewHolder(ItemHeaderBinding.inflate(inflater, parent, false))
            TYPE_PRODUCT -> ProductViewHolder(ItemProductBinding.inflate(inflater, parent, false), onProductClick)
            TYPE_DIVIDER -> DividerViewHolder(ItemDividerBinding.inflate(inflater, parent, false))
            TYPE_LOADING -> LoadingViewHolder(ItemLoadingBinding.inflate(inflater, parent, false))
            else -> throw IllegalArgumentException("Unknown view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // 使用类型安全的smart cast,无需手动强转
        when (val item = getItem(position)) {
            is FeedItem.Header -> (holder as HeaderViewHolder).bind(item)
            is FeedItem.ProductCard -> (holder as ProductViewHolder).bind(item)
            is FeedItem.Divider -> (holder as DividerViewHolder).bind(item)
            is FeedItem.LoadingMore -> { /* 无需绑定数据 */ }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is FeedItem.Header -> TYPE_HEADER
            is FeedItem.ProductCard -> TYPE_PRODUCT
            is FeedItem.Divider -> TYPE_DIVIDER
            is FeedItem.LoadingMore -> TYPE_LOADING
        }
    }

    companion object {
        private const val TYPE_HEADER = 0
        private const val TYPE_PRODUCT = 1
        private const val TYPE_DIVIDER = 2
        private const val TYPE_LOADING = 3
    }

    // ViewHolder实现类...
    class HeaderViewHolder(private val binding: ItemHeaderBinding) : 
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: FeedItem.Header) {
            binding.titleText.text = item.title
            binding.subtitleText.text = item.subtitle ?: ""
        }
    }
    
    class ProductViewHolder(
        private val binding: ItemProductBinding,
        private val onClick: (Product) -> Unit
    ) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: FeedItem.ProductCard) {
            binding.productName.text = item.product.name
            binding.priceText.text = "¥${item.product.price}"
            binding.root.setOnClickListener { onClick(item.product) }
        }
    }
    
    class DividerViewHolder(private val binding: ItemDividerBinding) : 
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: FeedItem.Divider) {
            binding.divider.layoutParams.height = 
                (item.heightDp * binding.root.context.resources.displayMetrics.density).toInt()
        }
    }
    
    class LoadingViewHolder(binding: ItemLoadingBinding) : 
        RecyclerView.ViewHolder(binding.root)

    class FeedDiffCallback : DiffUtil.ItemCallback<FeedItem>() {
        override fun areItemsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean {
            return oldItem.id == newItem.id
        }
        
        override fun areContentsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean {
            return oldItem == newItem // 利用data class自动生成的equals
        }
    }
}

最佳实践

  • 使用object定义无数据状态(如LoadingMore),单例节省内存
  • abstract val id配合DiffUtil实现高效列表更新
  • onBindViewHolder中使用smart cast,Kotlin编译器自动推断类型,无需强制转换

场景2:ViewModel状态管理(密封类+StateFlow)

复杂表单页面的状态流转:

kotlin 复制代码
// com.example.kotlin.dsl.viewmodel.FormViewModel.kt
package com.example.kotlin.dsl.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

data class FormData(
    val username: String = "",
    val email: String = "",
    val age: Int? = null,
    val avatarUrl: String? = null
)

sealed class FormState {
    object Idle : FormState()
    data class Editing(val data: FormData, val isValid: Boolean = false) : FormState()
    data class Submitting(val data: FormData) : FormState()
    data class Success(val userId: String) : FormState()
    data class Error(val message: String, val fieldErrors: Map<String, String> = emptyMap()) : FormState()
}

class FormViewModel(private val api: UserApi) : ViewModel() {
    
    private val _state = MutableStateFlow<FormState>(FormState.Idle)
    val state: StateFlow<FormState> = _state.asStateFlow()
    
    // 暴露当前表单数据,方便UI回显
    val currentFormData: FormData
        get() = when (val s = _state.value) {
            is FormState.Editing -> s.data
            is FormState.Submitting -> s.data
            else -> FormData() // Idle/Success/Error状态下返回空表单
        }
    
    fun updateField(field: String, value: String) {
        val current = currentFormData
        val newData = when (field) {
            "username" -> current.copy(username = value)
            "email" -> current.copy(email = value)
            "age" -> current.copy(age = value.toIntOrNull())
            else -> current
        }
        
        // 实时验证逻辑
        val isValid = newData.username.isNotBlank() && 
                     newData.email.contains("@") &&
                     newData.age != null && newData.age > 0
        
        _state.value = FormState.Editing(newData, isValid)
    }
    
    fun submit() {
        val currentState = _state.value
        if (currentState !is FormState.Editing || !currentState.isValid) return
        
        viewModelScope.launch {
            _state.value = FormState.Submitting(currentState.data)
            try {
                val response = api.createUser(
                    username = currentState.data.username,
                    email = currentState.data.email,
                    age = currentState.data.age!!
                )
                _state.value = FormState.Success(response.userId)
            } catch (e: ValidationException) {
                _state.value = FormState.Error(
                    message = "表单验证失败",
                    fieldErrors = e.fieldErrors
                )
            } catch (e: NetworkException) {
                _state.value = FormState.Error(
                    message = "网络错误,请稍后重试",
                    fieldErrors = emptyMap()
                )
            }
        }
    }
    
    fun reset() {
        _state.value = FormState.Idle
    }
}

// UI层使用(Compose示例)
@Composable
fun FormScreen(viewModel: FormViewModel) {
    val state by viewModel.state.collectAsState()
    
    // 穷尽性处理确保所有状态都有UI对应
    when (val s = state) {
        is FormState.Idle -> IdleView(onStart = { viewModel.updateField("username", "") })
        is FormState.Editing -> FormEditingView(
            data = s.data,
            isValid = s.isValid,
            onFieldChange = viewModel::updateField,
            onSubmit = viewModel::submit
        )
        is FormState.Submitting -> LoadingView(data = s.data)
        is FormState.Success -> SuccessView(userId = s.userId, onReset = viewModel::reset)
        is FormState.Error -> ErrorView(
            message = s.message,
            fieldErrors = s.fieldErrors,
            onRetry = viewModel::submit,
            onReset = viewModel::reset
        )
    }
}

注意事项

  • 状态转换逻辑集中管理,避免UI层直接修改状态
  • Submitting携带data用于展示"正在提交:xxx"的友好提示
  • 使用StateFlow确保配置变更(如旋转屏幕)时状态不丢失

场景3:Repository层网络状态封装(密封类+数据类)

统一处理网络请求的Loading/Error/Success状态:

kotlin 复制代码
// com.example.kotlin.dsl.repository.Result.kt
package com.example.kotlin.dsl.repository

/**
 * 通用网络结果封装
 * @param T 成功时返回的数据类型
 */
sealed class Result<out T> {
    data class Success<T>(val data: T, val fromCache: Boolean = false) : Result<T>()
    data class Error(val exception: Throwable, val code: Int? = null) : Result<Nothing>()
    object Loading : Result<Nothing>()
    
    // 扩展函数:方便处理结果
    fun getOrNull(): T? = (this as? Success<T>)?.data
    fun exceptionOrNull(): Throwable? = (this as? Error)?.exception
    val isSuccess: Boolean get() = this is Success
    val isLoading: Boolean get() = this is Loading
}

// Repository实现
class ProductRepository(private val api: ProductApi, private val dao: ProductDao) {
    
    fun getProducts(categoryId: String): Flow<Result<List<Product>>> = flow {
        emit(Result.Loading)
        
        try {
            // 先尝试缓存
            val cached = dao.getByCategory(categoryId)
            if (cached.isNotEmpty()) {
                emit(Result.Success(cached, fromCache = true))
            }
            
            // 网络请求
            val remote = api.fetchProducts(categoryId)
            dao.insertAll(remote) // 更新缓存
            emit(Result.Success(remote, fromCache = false))
        } catch (e: Exception) {
            emit(Result.Error(e, code = (e as? HttpException)?.code()))
        }
    }
    
    // 使用示例:在ViewModel中收集
    fun loadProducts(categoryId: String) {
        viewModelScope.launch {
            repository.getProducts(categoryId)
                .onStart { /* 可选的额外初始化 */ }
                .collect { result ->
                    when (result) {
                        is Result.Loading -> _uiState.value = UiState.Loading
                        is Result.Success -> {
                            val message = if (result.fromCache) "已同步离线数据" else "数据已更新"
                            _uiState.value = UiState.Success(result.data, message)
                        }
                        is Result.Error -> {
                            val msg = when (result.code) {
                                404 -> "分类不存在"
                                500 -> "服务器错误"
                                else -> result.exception.message ?: "未知错误"
                            }
                            _uiState.value = UiState.Error(msg, canRetry = result.code != 404)
                        }
                    }
                }
        }
    }
}

最佳实践

  • 使用泛型密封类Result<T>统一所有网络请求返回类型
  • Nothing类型用于无数据的Loading/Error状态,确保类型安全
  • 扩展函数封装常用操作,避免重复的类型判断

五、踩坑指南:反模式与正确实践

坑点1:Data Class的浅拷贝陷阱(mutable collection引用共享)

错误代码

kotlin 复制代码
data class UserProfile(
    val userId: String,
    val tags: MutableList<String> // 可变集合作为属性
)

val profile = UserProfile("U001", mutableListOf("VIP", "Active"))
val backup = profile.copy() // 浅拷贝,tags引用相同

backup.tags.add("Suspended") // 意外修改了profile的tags!
println(profile.tags) // [VIP, Active, Suspended] - 原始数据被污染

正确做法

kotlin 复制代码
// 方案1:使用不可变集合(推荐)
data class UserProfile(
    val userId: String,
    val tags: List<String> = emptyList() // 声明为List(只读接口)
)

// 需要修改时创建新集合
val newProfile = profile.copy(
    tags = profile.tags + "Suspended" // 返回新列表,原列表不变
)

// 方案2:防御性拷贝(必须暴露可变集合时)
data class UserProfile private constructor(
    val userId: String,
    private val _tags: MutableList<String>
) {
    val tags: List<String> get() = _tags.toList() // 每次返回副本
    
    companion object {
        fun create(userId: String, tags: List<String>) = 
            UserProfile(userId, tags.toMutableList())
    }
    
    // 自定义深拷贝方法
    fun deepCopy(userId: String = this.userId, tags: List<String> = this.tags.toList()) = 
        create(userId, tags)
}

核心原则 :数据类的设计哲学是不可变性 。一旦在primary constructor中放入可变对象(MutableList、Array等),copy()equals()的行为将违背预期。

坑点2:Sealed Class跨模块使用的限制

错误认知:认为密封类可以在不同模块间自由扩展。

实际情况

kotlin 复制代码
// Module A (core模块)
sealed class NetworkState

// Module B (feature模块) - 编译错误!
data class CustomError : NetworkState() // 不允许,密封类子类必须在同一包内

解决方案

kotlin 复制代码
// 使用抽象类+内部密封类组合,允许部分开放
// Module A
abstract class NetworkState {
    abstract val timestamp: Long
    
    // 核心状态密封
    sealed class Core : NetworkState() {
        data class Loading(override val timestamp: Long) : Core()
        data class Error(val message: String, override val timestamp: Long) : Core()
    }
    
    // 开放给子模块扩展的接口
    interface Extendable
}

// Module B - 可以扩展抽象类,但失去when穷尽性检查
data class CustomState(val data: String, override val timestamp: Long) : 
    NetworkState(), NetworkState.Extendable

建议 :若需跨模块扩展,考虑使用sealed interface(Kotlin 1.5+)配合编译插件,或放弃穷尽性检查改用抽象类。

坑点3:过度使用Data Class作为领域模型

错误场景

kotlin 复制代码
// 将包含业务逻辑的实体强行定义为data class
data class Order(
    val id: String,
    val items: List<OrderItem>,
    val status: OrderStatus
) {
    // 严重错误:在data class中放业务逻辑
    fun calculateTotal(): BigDecimal = 
        items.fold(BigDecimal.ZERO) { sum, item -> 
            sum + item.price * item.quantity.toBigDecimal()
        }
    
    fun applyDiscount(rule: DiscountRule): Order = 
        copy(items = items.map { rule.apply(it) }) // 业务逻辑污染数据类
}

正确分层

kotlin 复制代码
// 纯数据类,仅承载状态
data class Order(
    val id: String,
    val items: List<OrderItem>,
    val status: OrderStatus,
    val totalAmount: BigDecimal, // 计算结果作为字段存储
    val discountApplied: BigDecimal = BigDecimal.ZERO
)

// 领域服务处理业务逻辑
class OrderService {
    fun calculateTotal(order: Order): BigDecimal = 
        order.items.sumOf { it.price * it.quantity.toBigDecimal() }
    
    fun applyDiscount(order: Order, rule: DiscountRule): Order {
        val discount = rule.calculate(order)
        return order.copy(
            totalAmount = order.totalAmount - discount,
            discountApplied = discount
        )
    }
}

设计准则

  • Data Class:DTO、VO、状态载体、配置项(无行为,只有数据)
  • 普通Class/Sealed Class:包含业务逻辑的领域实体、需要继承层次的状态机
  • 数据类不应知晓存储逻辑(Room的@Entity除外,那是框架侵入)

总结:Kotlin的数据类与密封类通过编译期代码生成和类型封闭,将Android业务建模从"防御式编程"(防御null、防御类型错误、防御忘记处理分支)转变为"声明式编程"。掌握其字节码原理与不可变性设计哲学,方能避免浅拷贝陷阱,构建出类型安全、高可维护的Android架构。

相关推荐
恋猫de小郭2 小时前
你的蓝牙设备可能正在泄漏你的隐私? Bluehood 如何追踪附近设备并做隐私分析
android·前端·ios
私人珍藏库3 小时前
[Android] 卫星地图 共生地球 v1.1.22
android·app·工具·软件·多功能
冰珊孤雪3 小时前
Android Studio Panda革命性升级:内存诊断、构建标准化与AI调试全解析
android·前端
_李小白4 小时前
【OSG学习笔记】Day 23: ClipNode(动态裁剪)
android·笔记·学习
Eagsen CEO4 小时前
如何让 Gemini 在 Android Studio 中顺利工作
android·ide·android studio
博.闻广见4 小时前
19-Compose开发-LazyColumn
kotlin·composer
ywf12155 小时前
FlinkCDC实战:将 MySQL 数据同步至 ES
android·mysql·elasticsearch
鹏程十八少6 小时前
9. Android Shadow插件化如何解决资源冲突问题和实现tinker热修复资源(源码分析4)
android·前端·面试
gechunlian886 小时前
MySQL - Navicat自动备份MySQL数据
android·数据库·mysql