Feb 1997

Overtime and overdue


  • Home

  • Tags

  • Categories

  • Archives

  • Search

MyBatis:日志

Posted on 2020-01-23 Edited on 2020-04-08

如果一个数据库操作出现了异常,我们需要排错。日志就是最好的助手。
日志在核心配置文件中配置,使用标签<settings>:

1
2
3
<settings>
<setting name="logImpl" value=""/>
</settings>

其中name="logImpl"是固定的,表示日志的具体实现,value的值有SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING

STDOUT_LOGGING

标准化日志

1
2
3
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

Log4j

通过Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等。
我们可以控制每一条日志的输出格式:通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
这些可以通过一个配置文件来灵活地进行配置,而不需要修改里面的代码。

导包

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

log4j.properties

在应用的类路径中创建一个名为 log4j.properties 的文件,文件的具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/xliu.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

配置log4j为日志的实现

1
2
3
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>

简单使用

  1. 导包
    在要使用Log4j的类中,导入包import org.apache.log4j.Logger;
  2. 日志对象,参数为当前类的class
    static Logger logger = Logger.getLogger(UserMapperTest.class);
  3. 日志级别
    1
    2
    3
    logger.info("info:进入了testLog4j");
    logger.debug("debug:进入了testLog4J");
    logger.error("error:进入了testLog4J");

MyBatis:resultMap

Posted on 2020-01-22 Edited on 2020-01-23

问题背景

之前学习所用到的代码相关内容如下:

  • 数据库:表中字段为id, name, pwd
  • 实体类:id, name, pwd
  • UserMapper.xml:

    1
    2
    3
    <select id="getUserById" resultType="User" parameterType="int">
    select * from mybatis.user where id = #{id}
    </select>

    通过这种UserMapper.xml的配置可以实现通过Id取用户信息的需求。
    假设现在我保持其它不变,更改实体类为: id, name, password,重新启动测试。输出内容为:

    1
    User{id=1, name='张三', password='null'}

    可以看到password的输出为空。

    问题分析

    类型处理器会自动转译属性名,因此select * from mybatis.user where id = #{id}这段Sql代码的转译过后应该是select id,name,pwd from mybatis.user where id = #{id}

    解决方案

    别名(不推荐)

    1
    2
    3
    <select id="getUserById" resultType="User" parameterType="int">
    select id,name,pwd as password from mybatis.user where id = #{id}
    </select>

    resultMap结果集映射

    原来的结果是:id name pwd
    实体类是: id name password

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- UserMap是随意取的名字,用来与下面select标签中的resultMap对应 -->
    <resultMap id="UserMap" type="User">
    <!-- column数据库中的字段,property实体类中的属性 -->
    <result column="pwd" property="password"/>
    </resultMap>

    <select id="getUserById" resultMap="UserMap">
    select * from mybatis.user where id = #{id}
    </select>

    ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。简单的语句就是我们原本的代码,直接使用Sql语句就能完成映射。

MyBatis:生命周期和作用域

Posted on 2020-01-22 Edited on 2020-04-11


上图为程序的执行顺序。配置文件config.xml创建SqlSessionFactoryBuilder,后者建造工厂SqlSessionFactory,工厂生产SqlSession,后者拿到Mapper(即Mapper.xml)。

生命周期和作用域是至关重要的,因为错误的使用会导致非常严重的并发问题。

SqlSessionFactoryBuilder

  • 一旦创建了SqlSessionFactory,就不再需要它了
  • 局部变量

    SqlSessionFactory

  • 说白了就是可以想象为:数据库连接池
  • SqlSessionFactory一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或者重新创建另一个实例
  • 因此SqlSessionFactory的最佳作用域是应用作用域
  • 最简单的就是单例模式或者静态单例模式

    SqlSession

  • 连接到连接池的一个请求
  • SqlSession的实例不是线程安全的,因此是不能共享大,所以它的最佳作用域是请求或方法作用域
  • 用完之后需要赶紧关闭,否则资源被占用

更新

MyBatis:映射器

Posted on 2020-01-22 Edited on 2020-04-11

在定义SQL映射语句之前,我们需要告诉MyBatis到哪里去找到这些语句,因此我们需要在配置文件中告诉MyBatis到哪里去寻找映射文件。可以使用相对于类路径的资源引用,或完全限定资源定位符(包括file:///的URL),或类名和包名。
已知目录中,com.xliu.dao包中有:接口UserMapper, UserMapper.xml; 配置文件config.xml在resources目录下。

使用相对于类路径的资源引用(推荐)

1
2
3
<mappers>
<mapper resource="com/xliu/dao/UserMapper.xml"/>
</mappers>

每一个Mapper.xml都需要在Mybatis核心配置文件中注册!

使用完全限定资源定位符(URL)

1
2
3
<mappers>
<mapper class="com.xliu.dao.UserMapper"/>
</mappers>

注意点:

  • 接口和Mapper配置文件必须同名
  • 接口和Mapper配置文件必须在同一包下

    将包内的映射器接口是选全部注册为映射器

    1
    2
    3
    <mappers>
    <package name="com.xliu.dao"/>
    </mappers>

注意点:

  • 接口和Mapper配置文件必须同名
  • 接口和Mapper配置文件必须在同一包下

更新

如果SQL语句写在XML文件里,则使用<mapper resource="com/xliu/dao/UserMapper.xml"/>,如果是用注解实现,例如TeacherMapper.java(接口):

1
2
3
4
public interface TeacherMapper {
@Select("select * from teacher where id=#{tid}")
Teacher getTeacher(@Param("tid") int id);
}

那么使用<mapper class="com.xliu.dao.TeacherMapper"/>

MyBatis:模糊查询

Posted on 2020-01-20

假设我们要查询一个名字里带“张”的用户,sql语句应该这样写:select * from user where name like '%张%'。
在MyBatis里有两种方法

执行时传递通配符

UserMapper.java:

1
2
3
<!-- 添加方法 -->
// 模糊查询
List<User> getUserLike(String value);

UserMapper.xml:

1
2
3
4
<!-- 添加标签 -->
<select id="getUserLike" resultType="com.xliu.pojo.User">
select * from mybatis.user where name like #{value};
</select>

Test.java:

1
2
3
4
5
6
7
8
9
10
@Test
public void getUserLike() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = mapper.getUserLike("%张%");
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}

在Sql拼接中使用通配符

UserMapper.java:

1
2
3
<!-- 添加方法 -->
// 模糊查询
List<User> getUserLike(String value);

UserMapper.xml:

1
2
3
4
<!-- 添加标签 -->
<select id="getUserLike" resultType="com.xliu.pojo.User">
select * from mybatis.user where name like "%"#{value}"%";
</select>

Test.java:

1
2
3
4
5
6
7
8
9
10
@Test
public void getUserLike() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = mapper.getUserLike("张");
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}

对比

两种方法的区别在于通配符”%”到底应该由用户传入(方法一)还是应该在xml中拼接,这样用户只需要输入“张”即可。第二种方法的优点是可以防止Sql注入。

Sql注入

Sql注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
在第一种方法中,用户对于将要执行的sql语句有较高的自由度,因为我们将select * from user where name like之后的内容都交给用户来写,那么用户可能可以执行非法操作。如果我们使用第二种方法,用户只有权限去输入通配符中间的值,大大降低了Sql注入的风险。

MyBatis:使用Map参数类型

Posted on 2020-01-20

上一篇文章中介绍了简单的CRUD语句,其中当我们需要根据Id更新name以及pwd字段时,传入的实际上是一个对象:
测试类中的方法:

1
mapper.updateUser(new User(2,"Liu","password"));

UserMapper.xml:

1
2
3
<update id="updateUser" parameterType="com.xliu.pojo.User">
update mybatis.user set name=#{name}, pwd=#{pwd} where id=#{id};
</update>

想象一下当数据库的字段变得非常多的时候,假设一个用户同时拥有id,name,pwd,address,hobby,phone等多条属性时,再做上述操作传入对象时,我们需要填写所有的字段的值,即使我们的需求仅仅是根据id改用户的密码。
为了解决上述问题,我们可以使用Map作为参数的类型

使用Map参数

已知Map是一个的数据结构,我们可以将我们所需要用到的字段以及值存储进去,包括条件字段以及需要更新的字段。
回归上述情形,假设一条用户数据有很多个字段,而现在的需求是根据用户ID更改用户密码,因此我们只需要将这两个字段以及值存入Map中。
UserMapper.java:

1
2
<!-- 添加一条方法 -->
void updateUser2(Map<String,Object> map);

UserMapper.xml:

1
2
3
4
<!-- 添加一个标签 -->
<update id="updateUser2" parameterType="map">
update mybatis.user set pwd=#{pwd} where id=#{id};
</update>

Test.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void updateUser2() {

SqlSession sqlSession = MybatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
<!-- 创建一个HashMap对象 -->
HashMap<String, Object> map = new HashMap<String, Object>();
<!-- 添加K-V数据 -->
map.put("id", 1);
map.put("pwd", "password");
<!-- 将map作为参数传入方法 -->
mapper.updateUser2(map);
sqlSession.commit();
sqlSession.close();
}

总结

Map传递参数,设置parameterType=”map”后,直接在sql中取出key即可:update mybatis.user set pwd=#{pwd} where id=#{id};。另外,此处#{}中可以用任意的名字,只要在创建map的时候和Key的名字统一就行。

MyBatis:执行SQL语句

Posted on 2020-01-19 Edited on 2020-01-20

我们已经完成工具类的设计,即从SqlSessionFactory中获取SqlSession,那么如何使用SqlSession来执行Sql语句呢?
假设我们现在已经有一张User表,并且里面存了一些信息,现在我想对这张表执行CRUD

传统方法

传统方法中我们需要建立一个Dao包,在Dao包里面有接口以及接口实现类,每增加一个新的接口方法就需要新增一个实现类,实现类类里的方法就是对数据库的操作,增删改查之类的。

Mapper代理方法

在这种方法中,接口类中每增加一个方法,只需要在xml中新增一个标签。
UserMapper.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    package com.xliu.dao;

<!-- User的实体类 -->
import com.xliu.pojo.User;

import java.util.List;

public interface UserMapper {
<!-- 定义一个抽象方法getUser(),返回类型是一个User类型的List -->
List<User> getUser();
// 根据Id查询用户
User getUserById(int id);

void addUser(User user);

void updateUser(User user);

void deleteUser(int id);
}

UserMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xliu.dao.UserMapper">

<select id="getUser" resultType="com.xliu.pojo.User">
select * from mybatis.user
</select>

<select id="getUserById" resultType="com.xliu.pojo.User" parameterType="int">
select * from mybatis.user where id = #{id}
</select>

<insert id="addUser" parameterType="com.xliu.pojo.User">
insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
</insert>

<update id="updateUser" parameterType="com.xliu.pojo.User">
update mybatis.user set name=#{name}, pwd=#{pwd} where id=#{id};
</update>

<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id=#{id};
</delete>
</mapper>

UserMapper.xml文件中,namespace对应接口,select id对应接口(namespace)中的方法名,resultType对应Sql语句执行的返回类型,如果接口方法有输入值的话,则添加属性parameterType。
另外需要注意的是增删改查的SQL语句对应的标签是不同的。

另外一定要记住,在/resources/config.xml中要添加Mapper.xml的映射:
config.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="pwd"/>
</dataSource>
</environment>
</environments>

<!-- 添加Mapper.xml的映射 -->
<mappers>
<mapper resource="com/xliu/dao/UserMapper.xml"/>
</mappers>

</configuration>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.xliu.dao;

import com.xliu.pojo.User;
import com.xliu.utils.MybatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

public class UserMapperTest {
@Test
public void test() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = mapper.getUser();
for (User user : userList) {
System.out.println(user);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
sqlSession.close();
}


}
@Test
public void testById() {
SqlSession sqlSession = MybatisUtil.getSqlSession();

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User userById = mapper.getUserById(1);
System.out.println(userById);
sqlSession.close();
}
@Test
public void addUser() {
SqlSession sqlSession = MybatisUtil.getSqlSession();

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.addUser(new User(4,"hha", "pwd"));

// 增删改需要提交事务
sqlSession.commit();
sqlSession.close();
}

@Test
public void updateUser() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.updateUser(new User(2,"Liu","password"));
sqlSession.commit();
sqlSession.close();
}

@Test
public void deleteUser() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.deleteUser(4);
sqlSession.commit();
sqlSession.close();
}
}

需要记住:执行完一次之后需要sqlSession.close(),增删改的语句需要提交事务sqlSession.commit()

MyBatis:工具类

Posted on 2020-01-19 Edited on 2020-01-20

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。
从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但是也可以使用任意的输入流(InputStream)实例,包括字符串形式的文件路径或者 file:// 的 URL 形式的文件路径来配置。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,可使从 classpath 或其他位置加载资源文件更加容易。

1
2
3
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

以上内容来自MyBatis官方文档

简而言之就是再用MyBatis框架连接数据库时,需要创建SqlSessionFactory。因此为了避免编写重复代码,通常将这一步骤封装成一个工具类。以下是我写的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 首先用一个专门的utils包来装工具类
package com.xliu.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class MybatisUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
// 官方文档上的代码,要注意配置文件的路径
String resource = "config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory= new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
// 以上步骤帮助我们创建了sqlSessionFactory,以下方法获得SqlSession的实例。
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}

SqlSession完全包含了面向数据库执行SQL命令所需的所有方法。可以通过SqlSession实例来直接执行已映射的SQL语句。

Spring Boot学习笔记(一)

Posted on 2020-01-19

Module

  1. 新建一个Module->Spring Initializr
  2. Dependencies: Spring Web, JDBC API, MyBatis Framework, MySQL Driver
  3. pom.xml 文件中已经把依赖配置好了

配置文件

application.properties

1
2
3
4
5
6
7
8
9
10
server.port=8080  // 服务端口

# jdbc
spring.datasource.password=pwd // 数据库密码
spring.datasource.username=root // 数据库用户
spring.datasource.url=jdbc:mysql://localhost:3306/gmall_study?characterEncoding=UTF-8&serverTimezone=GMT-5 // 数据库url记得要在3306后面写数据库的名字,serverTimezone也要手动设置,不然可能会报错

# mybatis
mybatis.mapper-locations=classpath:mapper/*Mapper.xml // 读取mapper目录下所有以Mapper.xml结尾的文件,即映射器文件的位置
mybatis.configuration.map-underscore-to-camel-case=true // MySQL一般定义字段用下划线表示,该配置项就是指将带有下划线的表字段映射为驼峰格式的实体类属性

controller

controller层负责具体的业务模块流程的控制,在此层要调用service层的接口实现业务逻辑

service

服务是一个相对独立的功能模块,主要负责业务逻辑应用设计。首先也要设计接口,然后再设计其实现该接口的类。这样我们就可以在应用中调用service接口进行业务处理。service层业务实现,具体调用到已经定义的Mapper的接口,封装service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性 。

mapper

通常我们在mapper层里面写接口,里面有与数据打交道的方法。SQL语句通常写在mapper文件里面的

bean

bean实体类,映射数据库中的表,里面有表的属性以及get/set方法

Proxy Pattern

Posted on 2020-01-08

Introduction

A real world example can be a cheque or credit card is a proxy for what is in our bank account. It can be used in place of cash, and provides a means of accessing that cash when required. And that’s exactly what the proxy pattern does – “Controls and manage access to the object they are protecting”.

Another example in real life, when we want to book a flight, we sometimes will refer to a thrid party like Tripadvisor other than airline.
In this example, the Tripadvisor is a proxy. Using tripadvisor, we can avoid directly contacting with airline because tripadvisor can sell tickets on behalf of airline. However, different from airline, Tripadvisor may also provide some other service that airline doesn’t have such as hotel coupon.

Code Example

UserService.java:

1
2
3
4
5
6
7
8
package com.xliu.demo02;

public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}

Define a interface that include several database operations.

UserServiceImpl.java:

1
2
3
4
5
6
7
8
9
10
11
package com.xliu.demo02;

public class UserServiceImpl implements UserService {
public void add() { System.out.println("Add a user"); }

public void delete() { System.out.println("Delete a user"); }

public void update() { System.out.println("Update a user"); }

public void query() { System.out.println("Query a user"); }
}

Implement the interface.

UserServiceProxy.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.xliu.demo02;

public class UserService Proxy implements UserService {
UserImpl user = new UserImpl();
public void add() {
log("add");
user.add();
}

public void delete() {
log("delete");
user.delete();
}

public void update() {
log("update");
user.update();
}

public void query() {
log("query");
user.query();
}

public void log(String msg){
System.out.println("Use" +msg+ "method");
}
}

Design a proxy.

Client:

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
Proxy proxy = new Proxy();
proxy.add();
}
}

Using the add() method at Client without interacting with UserServiceImpl but Proxy

Note

Using compositing to call a object of User.

Those methods need to be rewrite in proxy class but using the User object to call instead of repeating the same method body.

In this example I showed that a proxy can not only provide an interface of original method to client, but also do some modification (like adding a log in this example).

Advantage

  • One of the advantages of Proxy pattern is security.
  • This pattern avoids duplication of objects which might be huge size and memory intensive. This in turn increases the performance of the application.

Disadvantage

This pattern introduces another layer of abstraction which sometimes may be an issue if the RealSubject code is accessed by some of the clients directly and some of them might access the Proxy classes. This might cause disparate behaviour.

1…678…12
Feb 1997

Feb 1997

112 posts
4 categories
24 tags
© 2020 Feb 1997