数据库模型

    目录

    介绍

    传统关系型数据库(MySQL)的模型,日常增删改查完全够用,支持复合主键、联合主键。

    需要注意的是,imi 的模型与传统 TP 等框架中的模型概念有些差别。

    imi 的模型类里一般不写逻辑代码,模型类的一个对象就代表一条记录,并且所有字段都需要有值(除非你不定义指定的字段)。

    模型定义

    喜闻乐见的对命名空间、类名无要求,只要按照规定写注解即可!

    Entity 注解为定义实体类

    Table 注解为定义数据表

    Column 注解为定义字段

    DDL 定义表结构的 SQL 语句

    建议使用模型生成工具:链接

    具体定义看下面代码:

    namespace Test;
    
    use Imi\Model\Model;
    use Imi\Model\Annotation\Table;
    use Imi\Model\Annotation\Column;
    use Imi\Model\Annotation\Entity;
    
    #[
        // 定义为实体
        Entity,
        // 指定实体为test,复合主键id和a
        Table(name: 'test', id: ['id', 'a'])
    ]
    class Test extends Model
    {
        /**
         * ID
         * 字段id,类型int,长度10,是主键,第0个主键,是自增字段
         * @var int
         */
        #[Column(name: 'id', type: 'int', length: 10, isPrimaryKey: true, primaryKeyIndex: 0, isAutoIncrement: true)]
        protected $id;
    
        /**
         * aaa
         * @var string
         */
        #[Column(name: 'a', type: 'string', length: 255, isPrimaryKey: true, primaryKeyIndex: 1)]
        protected $a;
    
        /**
         * bbb
         * @var string
         */
        #[Column(name: 'b', type: 'string', length: 255)]
        protected $b;
    
        /**
         * ccc
         * @var string
         */
        #[Column(name: 'c', type: 'string', length: 255)]
        protected $c;
    
        /**
         * Get iD
         *
         * @return  int
         */
        public function getId()
        {
            return $this->id;
        }
    
        /**
         * Set iD
         *
         * @param  int  $id  ID
         *
         * @return  self
         */
        public function setId(int $id)
        {
            $this->id = $id;
    
            return $this;
        }
    
        /**
         * Get aaa
         *
         * @return  string
         */
        public function getA()
        {
            return $this->a;
        }
    
        /**
         * Set aaa
         *
         * @param  string  $a  aaa
         *
         * @return  self
         */
        public function setA(string $a)
        {
            $this->a = $a;
    
            return $this;
        }
    
        /**
         * Get bbb
         *
         * @return  string
         */
        public function getB()
        {
            return $this->b;
        }
    
        /**
         * Set bbb
         *
         * @param  string  $b  bbb
         *
         * @return  self
         */
        public function setB(string $b)
        {
            $this->b = $b;
    
            return $this;
        }
    
        /**
         * Get ccc
         *
         * @return  string
         */
        public function getC()
        {
            return $this->c;
        }
    
        /**
         * Set ccc
         *
         * @param  string  $c  ccc
         *
         * @return  self
         */
        public function setC(string $c)
        {
            $this->c = $c;
    
            return $this;
        }
    }

    需要使用注解将表、字段属性全部标注。并且写上getset方法,可以使用模型生成工具生成。

    虚拟字段

    模型中可以加入虚拟字段,通过注解#[Column(virtual: true)],虚拟字段不参与数据库操作。

    模型注解

    Entity

    写在类上,定义类为实体模型类

    用法:

    #[Entity]

    序列化时不使用驼峰命名,使用原本的字段名:

    #[Entity(false)]

    将模型设为非 bean 类:

    #[Entity(camel: true, bean: false)]
    非 bean 类性能更好,但无法用 AOP 切入类,事件也不生效,一般模型建议使用非 bean 类模式。

    启用增量更新:(默认不启用)

    #[Entity(incrUpdate: true)]

    Table

    写在类上,定义数据表

    用法:

    #[Table('tb_user')]

    指定数据库连接池:

    #[Table(name: 'tb_user', dbPoolName: '指定数据库连接池名')]

    指定主键:

    #[Table(name: 'tb_user', id: 'id')]

    指定多个主键:

    #[Table(name: 'tb_user', id: ['id1', 'id2'])]

    DDL

    写在类上,定义表结构

    用法:

    /**
     * ArticleBase
     * @property int $id
     * @property string $title
     * @property string $content
     * @property string $time
     */
    #[
        Entity,
        Table(name: 'tb_article', id: ['id']),
        DDL(sql='CREATE TABLE `tb_article` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `title` varchar(255) NOT NULL,
      `content` mediumtext NOT NULL,
      `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT')
    ]
    abstract class ArticleBase extends Model
    {
    }

    JsonEncode

    设定 JSON 序列化时的配置

    写在类上可以让模型类中所有 Json 字段生效。

    写在属性上,可以覆盖写在类上的注解。

    不使用 Unicode 编码转换中文:#[JsonEncode(JSON_UNESCAPED_UNICODE)]

    完整参数:#[JsonEncode(flags: \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE, depth: 512)]

    参数含义同 json_encode()

    JsonDecode

    设定 JSON 反序列化时的配置

    写在类上可以让模型类中所有 Json 字段生效。

    写在属性上,可以覆盖写在类上的注解。

    完整参数:#[JsonDecode(associative: true, depth: 512, flags: 0, wrap: \Imi\Util\LazyArrayObject::class, arrayWrap: false)]

    associativedepthflags 参数含义同 json_decode()

    参数说明:

    $wrap 反序列化数据的包装,如果是对象或者数组时有效。支持类名、函数名。设为空字符串时,反序列化为数组。

    $arrayWrap 设为 true 时,属性值类型为数组,使用 wrap 对数组成员进行包装。

    类名:

    // 将此类的对象作为属性值
    class WrapClass
    {
        /**
         * @param mixed $data json_decode() 结果
         */
        public function __construct($data)
        {
        }
    }

    函数名:

    /**
     * @param mixed $data json_decode() 结果
     * @return mixed
     */
    function demoWrap($data)
    {
        return $data; // 返回值作为属性值
    }

    Column

    写在属性上,定义字段列

    #[Column(name: "字段名", type: "字段类型", length: "长度", nullable: "是否允许为空true/false", accuracy: "精度,小数位后几位", default: "默认值", isPrimaryKey: "是否为主键true/false", primaryKeyIndex: "联合主键中的第几个,从0开始", isAutoIncrement: "是否为自增字段true/false", virtual: "虚拟字段,不参与数据库操作true/false", updateTime: true, createTime: true, reference: "引用字段名,作为引用字段的别名使用,拥有同等的读写能力,需要将virtual设为true", unsigned: false)]

    当你指定type=json时,写入数据库时自动json_encode,从数据实例化到对象时自动json_decode

    当你指定type=list并且设置了listSeparator分割符时,写入数据库时自动implode,从数据实例化到对象时自动explode

    updateTimesave/update 模型时是否将当前时间写入该字段。
    MySQL 支持:date/time/datetime/timestamp/year/int/bigint
    PostgreSQL 支持:date/time/timetz/timestamp/timestamptz/int/int2/int4/int8
    设为true时,当字段为 int 类型,写入秒级时间戳。当字段为 bigint 类型,写入毫秒级时间戳。
    设为int值时,当字段为 bigint 类型,写入值=time() * updateTime。比如传1是存秒级时间戳,传1000是毫秒级时间戳,以此类推。

    createTimesave/insert 模型时是否将当前时间写入该字段,save时表有自增ID主键才支持
    MySQL 支持:date/time/datetime/timestamp/year/int/bigint
    PostgreSQL 支持:date/time/timetz/timestamp/timestamptz/int/int2/int4/int8
    设为true时,当字段为 int 类型,写入秒级时间戳;当字段为 bigint 类型,写入毫秒级时间戳。
    设为int值时,当字段为 bigint 类型,写入值=time() * updateTime。比如传1是存秒级时间戳,传1000是毫秒级时间戳,以此类推。

    Sql

    为虚拟字段定义 SQL 语句,模型查询时自动带上改字段。

    imi 的模型思想就是要为查询结果建模,而不是每次查询时候手动指定字段。

    Sql("SQL 语句")

    如果 Column 注解定义了 name 属性,则将 name 作为字段别名;如果未定义,则使用属性名称作为别名。

    示例:

    <?php
    
    namespace Imi\Test\Component\Model;
    
    use Imi\Bean\Annotation\Inherit;
    use Imi\Model\Annotation\Column;
    use Imi\Model\Annotation\Serializables;
    use Imi\Model\Annotation\Sql;
    use Imi\Test\Component\Model\Base\MemberBase;
    
    /**
     * Member.
     */
    #[
        Inherit,
        Serializables(mode: 'deny', fields: ['password'])
    ]
    class MemberWithSqlField extends MemberBase
    {
        /**
         * @var int
         */
        #[
            Column(name: 'a', virtual: true),
            Sql(sql: '1+1')
        ]
        public $test1;
    
        /**
         * @var int
         */
        #[
            Column(virtual: true),
            Sql(sql: '2+2')
        ]
        public $test2;
    
        /**
         * Set the value of test1.
         *
         * @param int $test1
         *
         * @return self
         */
        public function setTest1(int $test1)
        {
            $this->test1 = $test1;
    
            return $this;
        }
    
        /**
         * Get the value of test1.
         *
         * @return int
         */
        public function getTest1()
        {
            return $this->test1;
        }
    
        /**
         * Set the value of test2.
         *
         * @param int $test2
         *
         * @return self
         */
        public function setTest2(int $test2)
        {
            $this->test2 = $test2;
    
            return $this;
        }
    
        /**
         * Get the value of test2.
         *
         * @return int
         */
        public function getTest2()
        {
            return $this->test2;
        }
    }

    查询时的 SQL 语句:

    select `tb_member`.*,(1+1) as `a`,(2+2) as `test2` from `tb_member` where `id`=:id limit 1

    JsonNotNull

    写在属性上,无参数。

    当字段值不为 null 时才序列化到 json

    Serializable

    写在属性上,序列化注解

    用法:

    禁止参与序列化(toArray()json_encode()不包含该字段):

    Serializable(false)

    Serializables

    写在类上,批量设置序列化注解,优先级低于针对属性单独设置的Serializable注解

    用法:

    白名单(序列化后只显示id、name字段):

    #[Serializables(mode: "allow", fields: ["id", "name"])]

    黑名单(序列化后,排除id、name字段)

    #[Serializables(mode: "deny", fields: ["id", "name"])]

    ExtractProperty

    写在属性上,提取字段中的属性到当前模型

    用法:

    提取该属性中的userId值到当前模型中的userId

    #[ExtractProperty("userId")]

    提取该属性中的userId值到当前模型中的userId2

    #[ExtractProperty(fieldName: "userId", alias: "userId2")]

    支持多级提取到当前模型中的userId2

    #[
        ExtractProperty(fieldName: 'ex.userId', alias: 'userId2')
    ]
    protected $xxx = [
        'ex'    =>    [
            'userId'    =>    123,
        ],
    ];

    Id

    写在属性上,声明主键,并且支持 ID 生成器(发号器)功能。

    用法:

    声明主键:

    Id

    声明主键,指定顺序:

    #[Id(0)]

    #[Id(index: 0)]

    ID 生成器:

    #[Id(generator: 生成器类::class)]

    ID 生成器指定参数:

    #[Id(generator: 生成器类::class, generatorOptions: ['key' => 'value'])]

    非主键字段使用 ID 生成器:

    #[Id(index: false, generator: 生成器类::class)]

    UUID 发号器

    使用前请先安装 uuid 扩展:http://pecl.php.net/package/uuid

    或安装这个包:composer require symfony/polyfill-uuid

    时间算法:

    #[Id(generator: \Imi\Model\IdGenerator\UUIDGenerator::class)]

    #[Id(generator: \Imi\Model\IdGenerator\UUIDGenerator::class, generatorOptions: ["type" => \Imi\Model\IdGenerator\UUIDGeneratorType::Time])]

    随机算法:#[Id(generator: \Imi\Model\IdGenerator\UUIDGenerator::class, generatorOptions: ["type" => \Imi\Model\IdGenerator\UUIDGeneratorType::Random])]

    MD5、Sha1:

    使用方法基本一样,差别就是 generatorOptions.type 值不同。

    #[Id(index: false, generator: \Imi\Model\IdGenerator\UUIDGenerator::class, generatorOptions: ["type" => \Imi\Model\IdGenerator\UUIDGeneratorType::Md5, "ns" => "99e4edaf-8363-466e-bddf-7254db57675c", "nameField" => "名称字段名,该字段值必须唯一,否则会重复"])]

    ns 必须是一个合法的 UUID

    #[Id(index: false, generator: \Imi\Model\IdGenerator\UUIDGenerator::class, generatorOptions: ["type" => \Imi\Model\IdGenerator\UUIDGeneratorType::Sha1, "nsField" => "命名空间字段名", "name" => "直接指定名称"])]

    注意:nsname 的值如果一样,生成的 UUID 也会一样!!!

    雪花算法发号器

    使用前请先安装 imi-snowflake:composer require imiphp/imi-snowflake

    #[Id(index: false, generator: \Imi\Snowflake\Model\SnowflakeGenerator::class, generatorOptions: ["name" => "雪花算法配置名称"])]

    雪花算法配置请参考:链接

    自定义发号器

    <?php
    
    declare(strict_types=1);
    
    namespace app\Model;
    
    use Imi\Model\BaseModel;
    use Imi\Model\IdGenerator\Contract\IIdGenerator;
    
    class MyGenerator implements IIdGenerator
    {
        /**
         * @return mixed
         */
        public function generate(?BaseModel $model, array $options = [])
        {
            // $options是 Id() 注解中 generatorOptions 参数传入的数组
    
            // $model 是模型对象,也可能为 null,做好容错,或者直接抛异常也可
    
            // 返回值可以是任何类型
            return 0;
        }
    }